204 lines
7.3 KiB
TypeScript
204 lines
7.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { test } from "node:test";
|
|
import { compileCapabilitySource } from "../src/capability-language/index.js";
|
|
import {
|
|
capabilityFixturePath,
|
|
capabilityFixtureSource,
|
|
fixtureId,
|
|
} from "./fixtures/capability-model.js";
|
|
|
|
test("ANTLR parses and validates a complete capability workspace", () => {
|
|
const result = compileCapabilitySource(
|
|
capabilityFixtureSource,
|
|
capabilityFixturePath,
|
|
);
|
|
assert.equal(result.ok, true);
|
|
if (!result.ok) return;
|
|
assert.equal(result.workspace.workspaceId, fixtureId.workspace);
|
|
assert.equal(result.workspace.id, fixtureId.workspaceRevision);
|
|
assert.equal(result.workspace.interfaceImports.length, 3);
|
|
assert.equal(result.workspace.conformances.length, 4);
|
|
assert.equal("id" in result.workspace.conformances[0]!, false);
|
|
});
|
|
|
|
test("conformances do not accept authored IDs", () => {
|
|
const result = compileCapabilitySource(
|
|
capabilityFixtureSource.replace(
|
|
"conform Project as Named {",
|
|
'conform Project as Named id "conformance:project:named" {',
|
|
),
|
|
"conformance-id.qx",
|
|
);
|
|
assert.equal(result.ok, false);
|
|
if (result.ok) return;
|
|
assert.ok(result.diagnostics.some((entry) => entry.phase === "syntax"));
|
|
});
|
|
|
|
test("the v1 language has no implicit relationship materialization rule", () => {
|
|
const result = compileCapabilitySource(
|
|
capabilityFixtureSource.replace(
|
|
/\n}\s*$/,
|
|
'\n materialize ProjectOwner.owner if absent with Person;\n}\n',
|
|
),
|
|
"materialize.qx",
|
|
);
|
|
assert.equal(result.ok, false);
|
|
if (result.ok) return;
|
|
assert.ok(result.diagnostics.some((entry) => entry.phase === "syntax"));
|
|
});
|
|
|
|
test("forward declarations make source order irrelevant", () => {
|
|
const source = capabilityFixtureSource
|
|
.replace(/ atom Project[^\n]*\n/, "")
|
|
.replace(/ atom Person[^\n]*\n/, "")
|
|
.replace(
|
|
/\n}\s*$/,
|
|
'\n atom Project id "atom:project";\n atom Person id "atom:person";\n}\n',
|
|
);
|
|
assert.equal(compileCapabilitySource(source, "reordered.qx").ok, true);
|
|
});
|
|
|
|
test("interfaces can declare ordinary call operations", () => {
|
|
const source = capabilityFixtureSource.replace(
|
|
" value summary id \"member:summary:summary\" : string {\n get id \"operation:summary:summary:get\";\n }",
|
|
" operation summarize id \"member:summary:summarize\" : string -> string {\n call id \"operation:summary:summarize\";\n }",
|
|
).replace(
|
|
"bind summary.get to package TodoRuntime.summaryGet",
|
|
"bind summarize.call to package TodoRuntime.summaryGet",
|
|
).replace(
|
|
"operation summaryGet id \"export:todo-runtime:summary-get\" : unit -> string",
|
|
"operation summaryGet id \"export:todo-runtime:summary-get\" : string -> string",
|
|
);
|
|
const result = compileCapabilitySource(source, "operation-member.qx");
|
|
assert.equal(result.ok, true);
|
|
if (!result.ok) return;
|
|
const member = result.workspace.interfaceImports
|
|
.find((entry) => entry.displayName === "Summary")?.members[0];
|
|
assert.equal(member?.kind, "operation");
|
|
});
|
|
|
|
test("state defaults accept recursive JSON values", () => {
|
|
const source = capabilityFixtureSource.replace(
|
|
' policy optimistic-register default "Untitled project";',
|
|
` policy optimistic-register default "Untitled project";
|
|
shared state ProjectMetadata id "slot:project:metadata" on Project : message "example.Metadata"
|
|
policy optimistic-register default {"labels":["compiler","runtime"],"score":1.5,"enabled":true,"extra":null};`,
|
|
);
|
|
const result = compileCapabilitySource(source, "json-default.qx");
|
|
assert.equal(result.ok, true);
|
|
if (!result.ok) return;
|
|
const metadata = result.workspace.sharedAttachments.find(
|
|
(entry) => entry.id === "slot:project:metadata",
|
|
);
|
|
assert.equal(metadata?.kind, "state");
|
|
if (metadata?.kind !== "state") return;
|
|
assert.deepEqual(metadata.defaultValue, {
|
|
labels: ["compiler", "runtime"],
|
|
score: 1.5,
|
|
enabled: true,
|
|
extra: null,
|
|
});
|
|
});
|
|
|
|
test("syntax errors retain source locations", () => {
|
|
const result = compileCapabilitySource(
|
|
capabilityFixtureSource.replace("atom Project", "atom Project ???"),
|
|
"broken.qx",
|
|
);
|
|
assert.equal(result.ok, false);
|
|
if (result.ok) return;
|
|
assert.ok(result.diagnostics.some((entry) =>
|
|
entry.phase === "syntax" && entry.fileName === "broken.qx" && entry.line > 0
|
|
));
|
|
});
|
|
|
|
test("unknown authoring names are lowering errors", () => {
|
|
const result = compileCapabilitySource(
|
|
capabilityFixtureSource.replace(
|
|
"to state ProjectTitle.read",
|
|
"to state NotAState.read",
|
|
),
|
|
"unknown-name.qx",
|
|
);
|
|
assert.equal(result.ok, false);
|
|
if (result.ok) return;
|
|
assert.ok(result.diagnostics.some((entry) =>
|
|
entry.phase === "lowering" &&
|
|
entry.code === "unknown-symbol" &&
|
|
entry.message.includes("NotAState")
|
|
));
|
|
});
|
|
|
|
test("well-formed but invalid programs report semantic paths", () => {
|
|
const result = compileCapabilitySource(
|
|
capabilityFixtureSource.replace(
|
|
"bind name.get to state ProjectTitle.read",
|
|
"bind name.get to state ProjectTitle.write",
|
|
),
|
|
"invalid-binding.qx",
|
|
);
|
|
assert.equal(result.ok, false);
|
|
if (result.ok) return;
|
|
const diagnostic = result.diagnostics.find(
|
|
(entry) => entry.code === "invalid-state-binding",
|
|
);
|
|
assert.ok(diagnostic);
|
|
assert.equal(diagnostic.phase, "validation");
|
|
assert.ok(diagnostic.path?.includes("operationBindings"));
|
|
});
|
|
|
|
test("Web Studio sidecars declare lazy materialization and checked cross-object ports", () => {
|
|
const source = readFileSync(
|
|
resolve(process.cwd(), "test/fixtures/web-studio.capabilities.qx"),
|
|
"utf8",
|
|
);
|
|
const result = compileCapabilitySource(source, "web-studio.capabilities.qx");
|
|
assert.equal(result.ok, true);
|
|
if (!result.ok) return;
|
|
|
|
const host = result.workspace.conformances.find((entry) =>
|
|
entry.atomId === "atom:project" &&
|
|
entry.interfaceRevisionId === "interface:org.quixos.web-studio.has-react-component@1"
|
|
);
|
|
assert.deepEqual(host?.relationshipMaterializations, [{
|
|
memberId: "member:org.quixos.web-studio.has-react-component:component",
|
|
constructorAtomId: "atom:project-component",
|
|
edgeTypeId: "edge:project:component",
|
|
constructedProjectionId: "projection:component:subject",
|
|
}]);
|
|
|
|
const component = result.workspace.conformances.find((entry) =>
|
|
entry.atomId === "atom:project-component"
|
|
);
|
|
const binding = component?.operationBindings[0]?.binding;
|
|
assert.equal(binding?.kind, "package");
|
|
if (binding?.kind !== "package") return;
|
|
assert.deepEqual(binding.dependencies[1]?.binding, {
|
|
kind: "state",
|
|
slotId: "slot:project:name",
|
|
via: {
|
|
edgeTypeId: "edge:project:component",
|
|
projectionId: "projection:component:subject",
|
|
},
|
|
});
|
|
});
|
|
|
|
test("relationship materializers require a constructor from the host atom", () => {
|
|
const source = readFileSync(
|
|
resolve(process.cwd(), "test/fixtures/web-studio.capabilities.qx"),
|
|
"utf8",
|
|
).replace(
|
|
"constructs ProjectComponent : atom-ref<Project>;",
|
|
"constructs ProjectComponent : unit;",
|
|
);
|
|
const result = compileCapabilitySource(source, "invalid-materializer.qx");
|
|
assert.equal(result.ok, false);
|
|
if (result.ok) return;
|
|
assert.ok(result.diagnostics.some((entry) =>
|
|
entry.code === "invalid-relationship-materialization" &&
|
|
entry.message.includes("must accept")
|
|
));
|
|
});
|