Implement checked workspace integration contracts (WI-01)
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js";
|
||||
import {
|
||||
compileExecutionPlan,
|
||||
generateInterfaceClientBindings,
|
||||
publicOperationContract,
|
||||
linkInterfaceOperation,
|
||||
publicQueryContract,
|
||||
linkPackageQuery,
|
||||
} from "../src/execution-world/index.js";
|
||||
import { compileQuery } from "../src/query/compile.js";
|
||||
import { parseControllerCatalog, validateControllers } from "../src/capability-model/controllers.js";
|
||||
const source = { repository: "https://example.test/contracts.git", commit: "a".repeat(40) };
|
||||
const parsed = compileCapabilityResourceSource(
|
||||
`interface Data id "data" revision "data@1" {
|
||||
queryable portable value label id "label" : string { get id "label:get" execution snapshot targets [browser, server]; }
|
||||
operation publish id "publish" : string -> unit { call id "publish:call"; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(parsed.ok && parsed.resource.kind === "interface", JSON.stringify(parsed.diagnostics));
|
||||
const iface = parsed.resource.revision;
|
||||
const environment = { interfaces: new Map([["Data", iface]]), interfaceClosure: [iface] };
|
||||
const compile = async () => {
|
||||
const pkg = compileCapabilityResourceSource(
|
||||
`import interface Data; package Views id "views" revision "views@1" {
|
||||
query Labels id "labels" root workspace Data document "labels.graphql" operation "Labels" { replica; }
|
||||
operation publish id "export:publish" : string -> unit mode call receiver any;
|
||||
}`,
|
||||
{ source, environment },
|
||||
);
|
||||
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
|
||||
const revision = pkg.resource.revision;
|
||||
revision.checkedQueries = [
|
||||
await compileQuery(
|
||||
revision.queries![0]!,
|
||||
[iface],
|
||||
async () => "query Labels { root { objects(first: 10) { entries { node { label } } } } }",
|
||||
),
|
||||
];
|
||||
const result = compileCapabilitySource(
|
||||
`workspace Test id "test" revision "test@1" commit "${source.commit}" {
|
||||
import interface Data; import package Views;
|
||||
atom One id "one"; atom Two id "two";
|
||||
${["One", "Two"]
|
||||
.map(
|
||||
(atom) => `conform ${atom} as Data id "${atom}:data" {
|
||||
private state ${atom}Label id "${atom}:label" on ${atom} : string policy register default "";
|
||||
bind label.get to state ${atom}Label.read;
|
||||
bind publish.call to package Views.publish;
|
||||
}`,
|
||||
)
|
||||
.join("\n")}
|
||||
}`,
|
||||
"workspace.qx",
|
||||
{ ...environment, packages: new Map([["Views", revision]]) },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
return compileExecutionPlan(result.workspace);
|
||||
};
|
||||
|
||||
test("one imported operation contract links to two atom implementations without containing either", async () => {
|
||||
const plan = await compile();
|
||||
const contract = publicOperationContract(iface, iface.members[0]!.operations[0]!);
|
||||
assert.equal(linkInterfaceOperation(plan, "one", contract).atomId, "one");
|
||||
assert.equal(linkInterfaceOperation(plan, "two", contract).atomId, "two");
|
||||
const generated = generateInterfaceClientBindings({ Data: iface });
|
||||
assert.ok(generated.includes("defineInterfaceOperation<null, string>"));
|
||||
for (const privateValue of [plan.digest, "One:label", '"atomId"', '"binding"'])
|
||||
assert.equal(generated.includes(privateValue), false);
|
||||
assert.throws(() => linkInterfaceOperation(plan, "absent", contract), /UNINSTALLED_OPERATION/);
|
||||
for (const change of [{ scope: "class" }, { input: { kind: "string" } }, { execution: { kind: "remote" } }])
|
||||
assert.throws(
|
||||
() => linkInterfaceOperation(plan, "one", { ...contract, ...change } as typeof contract),
|
||||
/CLIENT_CONTRACT_MISMATCH/,
|
||||
);
|
||||
assert.throws(() => linkInterfaceOperation({ ...plan, digest: "bad" }, "one", contract), /INVALID_PLAN/);
|
||||
});
|
||||
|
||||
test("package query handles bind the checked definition, placement and codecs", async () => {
|
||||
const plan = await compile();
|
||||
const contract = publicQueryContract(plan.definition.workspace.packageImports[0]!, "labels");
|
||||
assert.equal(linkPackageQuery(plan, contract).query.checked.declaration.id, "labels");
|
||||
for (const change of [{ placement: "local" }, { definitionDigest: "bad" }, { output: null }])
|
||||
assert.throws(() => linkPackageQuery(plan, { ...contract, ...change } as typeof contract), /CLIENT_QUERY_MISMATCH/);
|
||||
});
|
||||
|
||||
test("controller templates check object capability effects and exact input mapping", () => {
|
||||
const catalog = {
|
||||
schemaVersion: 1,
|
||||
controllers: [
|
||||
{ id: "publish-label", subject: "data@1", snapshot: "label:get", remote: "publish:call", input: "value" },
|
||||
],
|
||||
};
|
||||
assert.equal(parseControllerCatalog(JSON.stringify(catalog), [iface]).length, 1);
|
||||
for (const change of [
|
||||
{ input: "unit" },
|
||||
{ input: "callback" },
|
||||
{ subject: "missing" },
|
||||
{ snapshot: "publish:call" },
|
||||
{ remote: "label:get" },
|
||||
{ url: "https://example.test" },
|
||||
]) {
|
||||
const invalid = { ...catalog, controllers: [{ ...catalog.controllers[0], ...change }] };
|
||||
assert.throws(() => parseControllerCatalog(JSON.stringify(invalid), [iface]), /Controller|controller/);
|
||||
}
|
||||
assert.throws(
|
||||
() =>
|
||||
parseControllerCatalog(
|
||||
JSON.stringify({ ...catalog, controllers: [...catalog.controllers, ...catalog.controllers] }),
|
||||
[iface],
|
||||
),
|
||||
/unique/,
|
||||
);
|
||||
const classIface = structuredClone(iface);
|
||||
classIface.members[1]!.operations[0]!.scope = "class";
|
||||
assert.ok(
|
||||
validateControllers(parseControllerCatalog(JSON.stringify(catalog), [iface]), [classIface], "controllers").length,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,230 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
compileCapabilityResourceSource,
|
||||
compileCapabilitySource,
|
||||
formatQx,
|
||||
parseQx,
|
||||
walkSyntax,
|
||||
} from "../src/capability-language/index.js";
|
||||
import {
|
||||
compileExecutionPlan,
|
||||
checkExecutionWorld,
|
||||
componentArtifactDigest,
|
||||
componentSharedModules,
|
||||
expectedArtifactReceipt,
|
||||
loadExecutionInstallation,
|
||||
clientContractCatalog,
|
||||
type ComponentArtifact,
|
||||
type PackageArtifacts,
|
||||
} from "../src/execution-world/index.js";
|
||||
import { validateWorkspaceRevision } from "../src/capability-model/index.js";
|
||||
|
||||
const source = { repository: "https://example.test/components.git", commit: "a".repeat(40) };
|
||||
const contract = (name: string) => {
|
||||
const result = compileCapabilityResourceSource(
|
||||
`interface ${name} id "interface:${name}" revision "interface:${name}@1" { }`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics));
|
||||
return result.resource.revision;
|
||||
};
|
||||
const data = contract("Data"),
|
||||
other = contract("Other");
|
||||
const interfaces = new Map([
|
||||
["Data", data],
|
||||
["Other", other],
|
||||
]);
|
||||
const componentSource = (body = 'component Card id "component:card" subject Data runtime react;') =>
|
||||
`import interface Data; package UI id "package:ui" revision "package:ui@1" { ${body} }`;
|
||||
const compilePackage = (body?: string) =>
|
||||
compileCapabilityResourceSource(componentSource(body), {
|
||||
source,
|
||||
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
|
||||
});
|
||||
const workspace = (presentation = 'presentation Card id "presentation:card" for Data using UI.Card;') => {
|
||||
const pkg = compilePackage();
|
||||
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
|
||||
return compileCapabilitySource(
|
||||
`workspace Example id "workspace:example" revision "workspace:example@1" commit "${source.commit}" {
|
||||
import interface Data; import interface Other; import package UI;
|
||||
atom One id "atom:one"; atom Two id "atom:two";
|
||||
conform One as Data id "conformance:one:data" { }
|
||||
conform Two as Data id "conformance:two:data" { }
|
||||
${presentation}
|
||||
}`,
|
||||
"workspace.qx",
|
||||
{
|
||||
interfaces,
|
||||
interfaceClosure: [...interfaces.values()],
|
||||
packages: new Map([["UI", pkg.resource.revision]]),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
test("interface-only component declarations serve multiple conforming atoms without capability exports", () => {
|
||||
const result = workspace();
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
const plan = compileExecutionPlan(result.workspace);
|
||||
assert.equal(plan.definition.packages[0]!.exports.length, 0);
|
||||
assert.deepEqual(plan.definition.packages[0]!.components, [
|
||||
{
|
||||
id: "component:card",
|
||||
displayName: "Card",
|
||||
subject: "interface:Data@1",
|
||||
runtime: "react",
|
||||
},
|
||||
]);
|
||||
assert.equal(plan.definition.presentations?.[0]?.componentId, "component:card");
|
||||
assert.equal(plan.definition.workspace.conformances.length, 2);
|
||||
assert.equal(plan.definition.operations.length, 0);
|
||||
});
|
||||
|
||||
test("component declarations survive formatting and syntax inspection", () => {
|
||||
const text = formatQx(componentSource());
|
||||
const parsed = parseQx(text);
|
||||
assert.deepEqual(parsed.diagnostics, []);
|
||||
assert.ok([...walkSyntax(parsed.root)].some((node) => node.kind === "componentDecl"));
|
||||
assert.equal(formatQx(text), text);
|
||||
});
|
||||
|
||||
test("component and presentation contracts reject wrong subjects, duplicates and unavailable runtimes", () => {
|
||||
for (const [input, message] of [
|
||||
['component Card id "component:card" subject Missing runtime react;', /Missing/],
|
||||
['component Card id "component:card" subject Data runtime arduino;', /Unsupported component runtime/],
|
||||
[
|
||||
'component Card id "component:card" subject Data runtime react; component Other id "component:card" subject Data runtime react;',
|
||||
/Duplicate component/,
|
||||
],
|
||||
[
|
||||
'component Card id "component:card" subject Data runtime react; component Card id "component:other" subject Data runtime react;',
|
||||
/Duplicate component/,
|
||||
],
|
||||
] as const) {
|
||||
const result = compilePackage(input);
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(JSON.stringify(result.diagnostics), message);
|
||||
}
|
||||
for (const [input, message] of [
|
||||
['presentation Card id "presentation:card" for Other using UI.Card;', /Component requires/],
|
||||
['presentation Card id "presentation:card" for Data using UI.Missing;', /Unknown component/],
|
||||
[
|
||||
'presentation Card id "presentation:card" for Data using UI.Card; presentation Duplicate id "presentation:card" for Data using UI.Card;',
|
||||
/Duplicate presentation/,
|
||||
],
|
||||
] as const) {
|
||||
const result = workspace(input);
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(JSON.stringify(result.diagnostics), message);
|
||||
}
|
||||
});
|
||||
|
||||
test("model validation rechecks presentation metadata instead of trusting the source parser", () => {
|
||||
const result = workspace();
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
result.workspace.presentations![0]!.componentId = "missing";
|
||||
assert.ok(validateWorkspaceRevision(result.workspace).some((issue) => issue.code === "invalid-presentation"));
|
||||
assert.throws(() => compileExecutionPlan(result.workspace), /no installed package component/);
|
||||
});
|
||||
|
||||
test("checked component-only artifacts install without a service and reject missing or substituted entries", async () => {
|
||||
const result = workspace();
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
const plan = compileExecutionPlan(result.workspace),
|
||||
pkg = plan.definition.packages[0]!;
|
||||
const bytes = new TextEncoder().encode("export const Card = () => null;\n");
|
||||
const paths = [
|
||||
"components/card.mjs",
|
||||
...componentSharedModules.map((_name, i) => `share/quixos/shared/runtime-${i}.mjs`),
|
||||
];
|
||||
const files = paths.map((file) => ({
|
||||
path: file,
|
||||
digest: createHash("sha256").update(bytes).digest("hex"),
|
||||
bytes: bytes.length,
|
||||
mediaType: "text/javascript",
|
||||
}));
|
||||
const body: Omit<ComponentArtifact, "contentDigest"> = {
|
||||
id: "component:card",
|
||||
subject: "interface:Data@1",
|
||||
compatibility: "quixos-react-component-v1",
|
||||
path: paths[0]!,
|
||||
exportName: "Card",
|
||||
files,
|
||||
sharedModules: Object.fromEntries(
|
||||
componentSharedModules.map((name, i) => [name, paths[i + 1]!]),
|
||||
) as ComponentArtifact["sharedModules"],
|
||||
};
|
||||
const manifest: PackageArtifacts = {
|
||||
schemaVersion: 1,
|
||||
packageRevisionId: pkg.revision,
|
||||
bindingDigest: pkg.bindingDigest,
|
||||
sourceDigest: pkg.sourceDigest,
|
||||
versions: plan.definition.versions,
|
||||
implementations: [],
|
||||
components: [{ ...body, contentDigest: componentArtifactDigest(body) }],
|
||||
};
|
||||
const world = await checkExecutionWorld(plan, [manifest], async () => bytes);
|
||||
assert.equal(world.artifacts.length, 1);
|
||||
assert.equal(world.artifacts[0]!.target, "browser-renderer");
|
||||
await assert.rejects(
|
||||
checkExecutionWorld(plan, [{ ...manifest, components: [] }], async () => bytes),
|
||||
/Missing component artifact/,
|
||||
);
|
||||
const substitute = structuredClone(manifest);
|
||||
substitute.components![0]!.subject = "interface:Other@1";
|
||||
const { contentDigest: _digest, ...changed } = substitute.components![0]!;
|
||||
substitute.components![0]!.contentDigest = componentArtifactDigest(changed);
|
||||
await assert.rejects(
|
||||
checkExecutionWorld(plan, [substitute], async () => bytes),
|
||||
/declared subject/,
|
||||
);
|
||||
await assert.rejects(
|
||||
checkExecutionWorld(plan, [manifest], async () => new Uint8Array(bytes.length)),
|
||||
/Artifact bytes/,
|
||||
);
|
||||
const missingShared = structuredClone(manifest);
|
||||
delete (missingShared.components![0]!.sharedModules as Partial<ComponentArtifact["sharedModules"]>).react;
|
||||
await assert.rejects(
|
||||
checkExecutionWorld(plan, [missingShared], async () => bytes),
|
||||
/missing fields/,
|
||||
);
|
||||
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "camino-component-contract-"));
|
||||
try {
|
||||
for (const file of files) {
|
||||
await mkdir(path.dirname(path.join(root, file.path)), { recursive: true });
|
||||
await writeFile(path.join(root, file.path), bytes);
|
||||
}
|
||||
await mkdir(path.join(root, "share/quixos"), { recursive: true });
|
||||
await writeFile(path.join(root, "share/quixos/package-artifacts.json"), JSON.stringify(manifest));
|
||||
const generator = "a".repeat(64);
|
||||
const { receipt, sources } = expectedArtifactReceipt(plan, manifest, generator);
|
||||
for (const [file, content] of sources) {
|
||||
await mkdir(path.dirname(path.join(root, file)), { recursive: true });
|
||||
await writeFile(path.join(root, file), content);
|
||||
}
|
||||
await writeFile(path.join(root, "share/quixos/build-receipt.json"), JSON.stringify(receipt));
|
||||
const installation = await loadExecutionInstallation(plan, { [pkg.revision]: root }, generator);
|
||||
assert.equal(installation.packages[0]!.service, undefined);
|
||||
assert.equal(installation.components[0]!.id, "component:card");
|
||||
assert.ok(installation.routes[installation.components[0]!.url]);
|
||||
assert.equal(JSON.stringify(installation.components).includes(root), false);
|
||||
assert.equal(JSON.stringify(installation.components).includes("slot:"), false);
|
||||
const catalog = clientContractCatalog(plan, installation, [pkg.revision]);
|
||||
assert.equal(catalog.presentations[0]!.component, "component:card");
|
||||
assert.equal(catalog.components[0]!.subject, "interface:Data@1");
|
||||
assert.equal(JSON.stringify(catalog).includes(root), false);
|
||||
assert.equal(JSON.stringify(catalog).includes("bindingSchema"), false);
|
||||
assert.throws(() => clientContractCatalog(plan, installation, ["absent"]), /UNINSTALLED_PACKAGE/);
|
||||
assert.throws(
|
||||
() => clientContractCatalog(plan, { ...installation, plan: "bad" }, [pkg.revision]),
|
||||
/CATALOG_PLAN_MISMATCH/,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user