150 lines
6.7 KiB
TypeScript
150 lines
6.7 KiB
TypeScript
import assert from "node:assert/strict";
|
|||
|
|
import test from "node:test";
|
||
|
|
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js";
|
||
|
|
import { compileWorkspaceRevision, resolveOperationPlan } from "../src/capability-model/validation.js";
|
||
|
|
import { capabilityId, type InterfaceRevision } from "../src/capability-model/types.js";
|
||
|
|
import { compileQuery } from "../src/query/compile.js";
|
||
|
|
import { linkQueries } from "../src/query/link.js";
|
||
|
|
import { compileCapabilityFixture, fixtureId } from "./fixtures/capability-model.js";
|
||
|
|
|
||
|
|
const source = { repository: "https://example.test/forwarding.git", commit: "a".repeat(40) };
|
||
|
|
function fixture(transform = (s: string) => s, generic = false) {
|
||
|
|
const interfaces = new Map<string, InterfaceRevision>();
|
||
|
|
for (const name of ["Owner", "Alias", "Relay"]) {
|
||
|
|
const result = compileCapabilityResourceSource(
|
||
|
|
`interface ${name}${generic && name === "Owner" ? "<value T>" : ""} id "${name}" revision "${name}@1" {
|
||
|
|
queryable value title id "${name}:title" : ${generic && name === "Owner" ? "T" : "string"} {
|
||
|
|
get id "${name}:get"; set id "${name}:set";
|
||
|
|
watch start id "${name}:start" stop id "${name}:stop";
|
||
|
|
}
|
||
|
|
}`,
|
||
|
|
{ source },
|
||
|
|
);
|
||
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||
|
|
if (result.resource.kind !== "interface") throw new Error("interface expected");
|
||
|
|
interfaces.set(name, result.resource.revision);
|
||
|
|
}
|
||
|
|
const forward = (name: string, target: string) => `conform Item as ${name} id "${name}-conformance" {
|
||
|
|
bind title.get to interface ${target}.title.get;
|
||
|
|
bind title.set to interface ${target}.title.set;
|
||
|
|
bind title.watch-start to interface ${target}.title.watch-start;
|
||
|
|
bind title.watch-stop to interface ${target}.title.watch-stop;
|
||
|
|
}`;
|
||
|
|
const text = `workspace Test id "ws" revision "ws@1" commit "${source.commit}" {
|
||
|
|
import interface Owner; import interface Alias; import interface Relay;
|
||
|
|
atom Item id "item";
|
||
|
|
${forward("Relay", "Alias")}
|
||
|
|
${forward("Alias", generic ? "Owner<string>" : "Owner")}
|
||
|
|
conform Item as Owner${generic ? "<string>" : ""} id "owner-conformance" {
|
||
|
|
private state Title id "title" on Item : string policy optimistic-register default "";
|
||
|
|
bind title to state Title;
|
||
|
|
}
|
||
|
|
}`;
|
||
|
|
return { result: compileCapabilitySource(transform(text), "workspace.qx", { interfaces }), interfaces };
|
||
|
|
}
|
||
|
|
|
||
|
|
test("bind chains forward native reads, writes and watches without changing storage ownership", async () => {
|
||
|
|
const { result, interfaces } = fixture();
|
||
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||
|
|
const compiled = compileWorkspaceRevision(result.workspace);
|
||
|
|
assert.ok(compiled.ok);
|
||
|
|
for (const [operation, primitive] of [
|
||
|
|
["get", "read"],
|
||
|
|
["set", "write"],
|
||
|
|
["start", "watch-start"],
|
||
|
|
["stop", "watch-stop"],
|
||
|
|
]) {
|
||
|
|
const resolved = resolveOperationPlan(
|
||
|
|
compiled.plan,
|
||
|
|
capabilityId.atom("item"),
|
||
|
|
capabilityId.interfaceRevision("Relay@1"),
|
||
|
|
capabilityId.operation(`Relay:${operation}`),
|
||
|
|
);
|
||
|
|
assert.equal(resolved?.binding.kind, "state");
|
||
|
|
if (resolved?.kind !== "state") throw new Error("state expected");
|
||
|
|
assert.equal(resolved.binding.primitive, primitive);
|
||
|
|
assert.deepEqual(resolved.attachment.owner, {
|
||
|
|
kind: "conformance",
|
||
|
|
atomId: "item",
|
||
|
|
interfaceRevisionId: "Owner@1",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const pkg = compileCapabilityResourceSource(
|
||
|
|
`import interface Relay;
|
||
|
|
package Views id "views" revision "views@1" { query Rows id "rows" root workspace Relay document "rows.graphql" operation "Rows" {max rows 10; watch;} }`,
|
||
|
|
{ source, environment: { interfaces } },
|
||
|
|
);
|
||
|
|
assert.ok(pkg.ok, JSON.stringify(pkg.diagnostics));
|
||
|
|
if (pkg.resource.kind !== "package") throw new Error("package expected");
|
||
|
|
const revision = pkg.resource.revision;
|
||
|
|
revision.checkedQueries = [
|
||
|
|
await compileQuery(
|
||
|
|
revision.queries![0]!,
|
||
|
|
[...interfaces.values()],
|
||
|
|
async () => `query Rows {root {objects(first: 10) {entries {node {title @live}}}}}`,
|
||
|
|
),
|
||
|
|
];
|
||
|
|
result.workspace.packageImports.push(revision);
|
||
|
|
const linked = linkQueries(result.workspace)[0]!;
|
||
|
|
assert.equal(linked.fields[0]!.binding.kind, "state");
|
||
|
|
assert.equal(linked.attachments[0]!.id, "title");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("forwarding rejects cycles, wrong signatures, missing targets, and direct private access", () => {
|
||
|
|
for (const transform of [
|
||
|
|
(s: string) => s.replaceAll("interface Owner.title", "interface Relay.title"),
|
||
|
|
(s: string) => s.replace("interface Owner.title.get", "interface Owner.title.set"),
|
||
|
|
(s: string) =>
|
||
|
|
s
|
||
|
|
.replace("conform Item as Owner", "conform Other as Owner")
|
||
|
|
.replace('atom Item id "item";', 'atom Item id "item"; atom Other id "other";')
|
||
|
|
.replace("on Item :", "on Other :"),
|
||
|
|
(s: string) => s.replace("interface Owner.title.get", "state Title.read"),
|
||
|
|
(s: string) => s.replace("interface Owner.title.watch-stop", "interface Relay.title.watch-stop"),
|
||
|
|
])
|
||
|
|
assert.equal(fixture(transform).result.ok, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("forwarding closes generic targets and rejects incompatible applications", () => {
|
||
|
|
const good = fixture(undefined, true).result;
|
||
|
|
assert.ok(good.ok, JSON.stringify(good.diagnostics));
|
||
|
|
const bad = fixture(
|
||
|
|
(s) => s.replace("interface Owner<string>.title.get", "interface Owner<int32>.title.get"),
|
||
|
|
true,
|
||
|
|
).result;
|
||
|
|
assert.equal(bad.ok, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("forwarded package actions retain the provider and its dependency ports", () => {
|
||
|
|
const fixture = compileCapabilityFixture();
|
||
|
|
assert.ok(fixture.ok, JSON.stringify(fixture.diagnostics));
|
||
|
|
const ws = fixture.workspace;
|
||
|
|
const target = ws.interfaceImports.find((i) => i.revisionId === fixtureId.summaryV1)!;
|
||
|
|
const alias = {
|
||
|
|
...structuredClone(target),
|
||
|
|
interfaceId: capabilityId.interface("alias"),
|
||
|
|
revisionId: capabilityId.interfaceRevision("alias@1"),
|
||
|
|
};
|
||
|
|
ws.interfaceImports.push(alias);
|
||
|
|
ws.conformances.push({
|
||
|
|
atomId: fixtureId.project,
|
||
|
|
interfaceRevisionId: alias.revisionId,
|
||
|
|
id: capabilityId.conformance("alias-owner"),
|
||
|
|
privateAttachments: [],
|
||
|
|
relationshipMaterializations: [],
|
||
|
|
operationBindings: [
|
||
|
|
{
|
||
|
|
operationId: fixtureId.summaryGet,
|
||
|
|
binding: { kind: "interface", interfaceRevisionId: fixtureId.summaryV1, operationId: fixtureId.summaryGet },
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
const result = compileWorkspaceRevision(ws);
|
||
|
|
assert.ok(result.ok, JSON.stringify(result));
|
||
|
|
const plan = resolveOperationPlan(result.plan, fixtureId.project, alias.revisionId, fixtureId.summaryGet);
|
||
|
|
assert.equal(plan?.kind, "package");
|
||
|
|
if (plan?.kind !== "package") throw new Error("package expected");
|
||
|
|
assert.equal(plan.binding.exportId, fixtureId.summaryGetExport);
|
||
|
|
assert.ok(plan.dependencies.length > 0);
|
||
|
|
});
|