Add local-first execution contracts to the capability language
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
|
||||
|
||||
const source = { repository: "https://example.test/execution.git", commit: "a".repeat(40) };
|
||||
const data = (execution = "snapshot targets [browser, server]") =>
|
||||
compileCapabilityResourceSource(
|
||||
`
|
||||
interface Data id "interface:data" revision "interface:data@1" {
|
||||
queryable portable value label id "member:label" : string {
|
||||
get id "operation:label:get" execution ${execution};
|
||||
}
|
||||
operation refresh id "member:refresh" : unit -> unit { call id "operation:refresh"; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
const program = (selection: string, execution = "snapshot targets [browser, server]") => {
|
||||
const contract = data();
|
||||
assert.ok(contract.ok, JSON.stringify(contract.diagnostics));
|
||||
assert.equal(contract.resource.kind, "interface");
|
||||
if (contract.resource.kind !== "interface") throw new Error("Expected interface");
|
||||
return compileCapabilityResourceSource(
|
||||
`
|
||||
import interface Data;
|
||||
package Reader id "package:reader" revision "package:reader@1" {
|
||||
function label id "export:label" : unit -> string execution ${execution} requires {
|
||||
interface data id "port:data" : Data ${selection};
|
||||
};
|
||||
}`,
|
||||
{
|
||||
source,
|
||||
environment: {
|
||||
interfaces: new Map([["Data", contract.resource.revision]]),
|
||||
interfaceClosure: [contract.resource.revision],
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
test("portable contracts preserve exact selected operations and targets", () => {
|
||||
const result = program("using [label.get]");
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
assert.equal(result.resource.kind, "package");
|
||||
if (result.resource.kind !== "package") return;
|
||||
const entry = result.resource.revision.exports[0]!;
|
||||
assert.deepEqual(entry.execution, { kind: "snapshot", targets: ["browser", "server"] });
|
||||
assert.deepEqual(entry.dependencyPorts[0]!.requirement, {
|
||||
kind: "interface",
|
||||
interfaceRevisionId: "interface:data@1",
|
||||
operations: ["operation:label:get"],
|
||||
});
|
||||
});
|
||||
|
||||
test("portable dependency diagnostics reject remote defaults, missing targets, and broad acquisition", () => {
|
||||
for (const [selection, execution, message] of [
|
||||
["using [refresh.call]", "snapshot targets [browser]", "remote-only"],
|
||||
["using [label.get]", "snapshot targets [arduino]", "arduino"],
|
||||
["", "snapshot targets [browser]", "select"],
|
||||
["using [label.get, label.get]", "snapshot targets [browser]", "duplicates"],
|
||||
["using [label.set]", "snapshot targets [browser]", "Unknown"],
|
||||
]) {
|
||||
const result = program(selection!, execution);
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(JSON.stringify(result.diagnostics), new RegExp(message!));
|
||||
}
|
||||
});
|
||||
|
||||
test("portable query getters and target declarations are checked", () => {
|
||||
assert.equal(data("remote").ok, false);
|
||||
assert.equal(data("transaction targets [browser]").ok, false);
|
||||
assert.equal(data("snapshot targets [browser, browser]").ok, false);
|
||||
});
|
||||
|
||||
test("snapshot exports reject mutable state ports but transactions admit them", () => {
|
||||
const compile = (effect: string) =>
|
||||
compileCapabilityResourceSource(
|
||||
`
|
||||
package Reader id "package:reader" revision "package:reader@1" {
|
||||
function update id "export:update" : string -> unit execution ${effect} targets [browser, server] requires {
|
||||
state storage id "port:value" : string [read, write];
|
||||
};
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
const snapshot = compile("snapshot");
|
||||
assert.equal(snapshot.ok, false);
|
||||
assert.match(JSON.stringify(snapshot.diagnostics), /write primitives/);
|
||||
const transaction = compile("transaction");
|
||||
assert.ok(transaction.ok, JSON.stringify(transaction.diagnostics));
|
||||
});
|
||||
|
||||
test("query placement round-trips and local extents never claim complete membership", async () => {
|
||||
const { compileQuery } = await import("../src/query/compile.js");
|
||||
const { linkQueries } = await import("../src/query/link.js");
|
||||
const { toBinary, fromBinary, fromJson } = await import("@bufbuild/protobuf");
|
||||
const { InstalledQuerySchema } = await import("../src/gen/camino/schema_pb.js");
|
||||
const { formatQx, parseQx, walkSyntax, applySourceEdits } = await import("../src/capability-language/index.js");
|
||||
const contract = data();
|
||||
assert.ok(contract.ok && contract.resource.kind === "interface");
|
||||
const iface = contract.resource.revision;
|
||||
const text = `import interface Data;
|
||||
package Reader id "package:reader" revision "package:reader@1" {
|
||||
query Visible id "visible" root workspace Data document "visible.graphql" operation "Visible" { replica; }
|
||||
query Presentation id "presentation" root workspace Data document "presentation.graphql" operation "Presentation" { local from Visible; }
|
||||
}`;
|
||||
const environment = { interfaces: new Map([["Data", iface]]), interfaceClosure: [iface] };
|
||||
const compiled = compileCapabilityResourceSource(formatQx(text), { source, environment });
|
||||
assert.ok(compiled.ok && compiled.resource.kind === "package", JSON.stringify(compiled.diagnostics));
|
||||
const pkg = compiled.resource.revision;
|
||||
assert.deepEqual(
|
||||
pkg.queries!.map((q) => q.placement),
|
||||
[{ kind: "replica" }, { kind: "local", sourceQueryId: "visible" }],
|
||||
);
|
||||
const parsed = parseQx(text);
|
||||
const clause = [...walkSyntax(parsed.root)].find(
|
||||
(node) => node.kind === "queryClause" && text.slice(node.start, node.end).startsWith("local"),
|
||||
)!;
|
||||
const edited = applySourceEdits(text, [{ ...clause, text: "local from Missing;" }]);
|
||||
assert.equal(compileCapabilityResourceSource(edited, { source, environment }).ok, false);
|
||||
const admission = pkg.queries![0]!,
|
||||
local = pkg.queries![1]!;
|
||||
const document = (name: string, field: string) =>
|
||||
`query ${name} { root { ${field}(first: 10) { entries { node { label } } } } }`;
|
||||
pkg.checkedQueries = [
|
||||
await compileQuery(admission, [iface], async () => document("Visible", "objects")),
|
||||
await compileQuery(local, [iface], async () => document("Presentation", "availableObjects")),
|
||||
];
|
||||
await assert.rejects(
|
||||
compileQuery(local, [iface], async () => document("Presentation", "objects")),
|
||||
/Cannot query field "objects"/,
|
||||
);
|
||||
const remote = structuredClone(iface);
|
||||
delete remote.members[0]!.operations[0]!.execution;
|
||||
await assert.rejects(
|
||||
compileQuery(local, [remote], async () => document("Presentation", "availableObjects")),
|
||||
/browser snapshot getter/,
|
||||
);
|
||||
const { capabilityId } = await import("../src/capability-model/index.js");
|
||||
const linked = linkQueries({
|
||||
id: capabilityId.workspaceRevision("w@1"),
|
||||
workspaceId: capabilityId.workspace("w"),
|
||||
sourceRootCommit: "a".repeat(40),
|
||||
parentRevisionIds: [],
|
||||
atoms: [],
|
||||
sharedAttachments: [],
|
||||
interfaceImports: [iface],
|
||||
packageImports: [pkg],
|
||||
conformances: [],
|
||||
constructors: [],
|
||||
});
|
||||
const wire = fromJson(InstalledQuerySchema, linked[1]!.runtime!);
|
||||
const roundtrip = fromBinary(InstalledQuerySchema, toBinary(InstalledQuerySchema, wire));
|
||||
assert.deepEqual(roundtrip.placement?.kind, { case: "localSourceQueryId", value: "package:reader@1:visible" });
|
||||
});
|
||||
|
||||
test("generated interface ports expose only selected operations", async () => {
|
||||
const { generateTypeScriptBindings } = await import("../src/bindings/index.js");
|
||||
const contract = data();
|
||||
const result = program("using [label.get]");
|
||||
assert.ok(contract.ok && contract.resource.kind === "interface");
|
||||
assert.ok(result.ok && result.resource.kind === "package");
|
||||
const generated = generateTypeScriptBindings(
|
||||
{
|
||||
format: "quixos-bindings",
|
||||
version: 1,
|
||||
interfaces: [contract.resource.revision],
|
||||
packages: [result.resource.revision],
|
||||
},
|
||||
"package:reader@1",
|
||||
);
|
||||
assert.match(generated, /label\.get/);
|
||||
assert.doesNotMatch(generated.split("export type Results")[0]!, /refresh\.call/);
|
||||
assert.doesNotMatch(generated.split("const specs")[1]!, /refresh\.call/);
|
||||
});
|
||||
|
||||
test("workspace validation checks concrete package and storage effects", async () => {
|
||||
const { makeValidCapabilityWorkspace, fixtureId } = await import("./fixtures/capability-model.js");
|
||||
const { validateWorkspaceRevision } = await import("../src/capability-model/index.js");
|
||||
const workspace = makeValidCapabilityWorkspace();
|
||||
const named = workspace.interfaceImports.find((i) => i.revisionId === fixtureId.namedV1)!;
|
||||
const setter = named.members.flatMap((m) => m.operations).find((o) => o.id === fixtureId.namedSet)!;
|
||||
setter.execution = { kind: "snapshot", targets: ["browser"] };
|
||||
assert.ok(validateWorkspaceRevision(workspace).some((i) => i.message.includes("bind write primitives")));
|
||||
setter.execution.kind = "transaction";
|
||||
assert.deepEqual(validateWorkspaceRevision(workspace), []);
|
||||
const summary = workspace.interfaceImports.find((i) => i.revisionId === fixtureId.summaryV1)!;
|
||||
summary.members[0]!.operations[0]!.execution = { kind: "snapshot", targets: ["browser"] };
|
||||
assert.ok(
|
||||
validateWorkspaceRevision(workspace).some(
|
||||
(i) => i.path.includes(fixtureId.summaryGet) && i.message.includes("remote-only"),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("generic selected ports preserve operation identities and execution through specialization", async () => {
|
||||
const { specializePackageExport, capabilityId } = await import("../src/capability-model/index.js");
|
||||
const iface = compileCapabilityResourceSource(
|
||||
`interface Cell<value T> id "cell" revision "cell@1" {
|
||||
value content id "content" : T { get id "content:get" execution snapshot targets [browser, server]; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(iface.ok && iface.resource.kind === "interface", JSON.stringify(iface.diagnostics));
|
||||
const pkg = compileCapabilityResourceSource(
|
||||
`import interface Cell;
|
||||
package P id "p" revision "p@1" {
|
||||
function load<value T> id "read" : unit -> T execution snapshot targets [browser, server] requires {
|
||||
interface cell id "cell" : Cell<T> using [content.get];
|
||||
};
|
||||
}`,
|
||||
{
|
||||
source,
|
||||
environment: {
|
||||
interfaces: new Map([["Cell", iface.resource.revision]]),
|
||||
interfaceClosure: [iface.resource.revision],
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
|
||||
const definition = pkg.resource.revision.genericExports![0]!;
|
||||
const specialized = specializePackageExport(
|
||||
pkg.resource.revision,
|
||||
definition,
|
||||
[{ kind: "value", type: { kind: "scalar", name: "string" } }],
|
||||
{
|
||||
arguments: new Map(),
|
||||
applyInterface: (id, args) => {
|
||||
assert.equal(id, "cell@1");
|
||||
assert.deepEqual(args, [{ kind: "value", type: { kind: "scalar", name: "string" } }]);
|
||||
return capabilityId.interfaceRevision("cell:string");
|
||||
},
|
||||
implementsInterface: () => false,
|
||||
},
|
||||
);
|
||||
assert.deepEqual(specialized.execution, { kind: "snapshot", targets: ["browser", "server"] });
|
||||
assert.deepEqual(specialized.dependencyPorts[0]!.requirement.kind, "interface");
|
||||
const requirement = specialized.dependencyPorts[0]!.requirement;
|
||||
if (requirement.kind === "interface")
|
||||
assert.deepEqual(requirement.operations, [capabilityId.operation("content:get")]);
|
||||
});
|
||||
Reference in New Issue
Block a user