Files

150 lines
6.6 KiB
TypeScript
Raw Permalink Normal View History

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 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}}}}}`,
),
];
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);
});