214 lines
8.8 KiB
TypeScript
214 lines
8.8 KiB
TypeScript
import assert from "node:assert/strict";
|
|||
|
|
import { test } from "node:test";
|
||
|
|
import {
|
||
|
|
compileCapabilityResourceSource,
|
||
|
|
compileCapabilitySource,
|
||
|
|
formatQx,
|
||
|
|
} from "../src/capability-language/index.js";
|
||
|
|
import {
|
||
|
|
validateWorkspaceRevision,
|
||
|
|
compileWorkspaceRevision,
|
||
|
|
computeCapabilityClosure,
|
||
|
|
capabilityId,
|
||
|
|
type InterfaceRevision,
|
||
|
|
} from "../src/capability-model/index.js";
|
||
|
|
import { compileExecutionPlan, checkExecutionWorld } from "../src/execution-world/index.js";
|
||
|
|
|
||
|
|
const source = { repository: "https://example.test/modules.git", commit: "a".repeat(40) };
|
||
|
|
const interfaces = new Map<string, InterfaceRevision>();
|
||
|
|
function define(text: string) {
|
||
|
|
const result = compileCapabilityResourceSource(text, {
|
||
|
|
source,
|
||
|
|
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
|
||
|
|
});
|
||
|
|
assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics));
|
||
|
|
interfaces.set(result.resource.revision.displayName, result.resource.revision);
|
||
|
|
}
|
||
|
|
define('interface Data id "interface:data" revision "interface:data@1" {}');
|
||
|
|
define('interface Other id "interface:other" revision "interface:other@1" {}');
|
||
|
|
define(`import interface Data;
|
||
|
|
interface Row id "interface:row" revision "interface:row@1" {
|
||
|
|
module render id "member:row:render" contract "org.quixos.react.component/1" subject interface Data;
|
||
|
|
}`);
|
||
|
|
define(`interface Standalone<object Subject> id "interface:standalone" revision "interface:standalone@1" {
|
||
|
|
module render id "member:standalone:render" contract "org.quixos.react.subject-only/1" subject object Subject;
|
||
|
|
}`);
|
||
|
|
const packageSource = `import interface Row; import interface Data; import interface Standalone;
|
||
|
|
package UI id "package:ui" revision "package:ui@1" {
|
||
|
|
module Row id "module:row" implements Row.render;
|
||
|
|
module Editor id "module:editor" implements Standalone<interface Data>.render;
|
||
|
|
}`;
|
||
|
|
function compile(
|
||
|
|
body = `conform Item as Data id "owner:data" {}
|
||
|
|
conform Item as Row id "owner:row" {bind render to UI.Row;}
|
||
|
|
conform Item as Standalone<interface Data> id "owner:standalone" {bind render to UI.Editor;}`,
|
||
|
|
) {
|
||
|
|
const pkg = compileCapabilityResourceSource(packageSource, {
|
||
|
|
source,
|
||
|
|
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
|
||
|
|
});
|
||
|
|
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
|
||
|
|
return compileCapabilitySource(
|
||
|
|
`workspace Demo id "workspace:demo" revision "workspace:demo@1" commit "${source.commit}" {
|
||
|
|
import interface Data; import interface Other; import interface Row; import interface Standalone; import package UI;
|
||
|
|
atom Item id "atom:item";
|
||
|
|
${body}
|
||
|
|
}`,
|
||
|
|
"workspace.qx",
|
||
|
|
{
|
||
|
|
interfaces,
|
||
|
|
interfaceClosure: [...interfaces.values()],
|
||
|
|
packages: new Map([["UI", pkg.resource.revision]]),
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
test("module members bind independent roles, including closed generic subjects, without operations or storage", () => {
|
||
|
|
const result = compile();
|
||
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||
|
|
const pkg = result.workspace.packageImports[0]!;
|
||
|
|
assert.equal(pkg.exports.length, 0);
|
||
|
|
assert.equal(pkg.modules?.length, 2);
|
||
|
|
const roles = result.workspace.conformances.filter((entry) => entry.moduleBindings?.length);
|
||
|
|
assert.equal(roles.length, 2);
|
||
|
|
for (const role of roles) {
|
||
|
|
assert.deepEqual(role.operationBindings, []);
|
||
|
|
assert.deepEqual(role.privateAttachments, []);
|
||
|
|
const iface: InterfaceRevision = result.workspace.interfaceImports.find(
|
||
|
|
(entry) => entry.revisionId === role.interfaceRevisionId,
|
||
|
|
)!;
|
||
|
|
assert.equal(iface.members[0]!.kind, "module");
|
||
|
|
assert.deepEqual(iface.members[0]!.operations, []);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("module bindings reject missing, duplicate and wrong nominal roles and unsatisfied subjects", () => {
|
||
|
|
for (const body of [
|
||
|
|
"conform Item as Data {} conform Item as Row {}",
|
||
|
|
"conform Item as Data {} conform Item as Row {bind render to UI.Editor;}",
|
||
|
|
"conform Item as Data {} conform Item as Row {bind render to UI.Row; bind render to UI.Row;}",
|
||
|
|
"conform Item as Other {} conform Item as Row {bind render to UI.Row;}",
|
||
|
|
"conform Item as Data {} conform Item as Row {bind render to UI.Missing;}",
|
||
|
|
]) {
|
||
|
|
const result = compile(body);
|
||
|
|
assert.equal(result.ok, false, body);
|
||
|
|
if (!result.ok) assert.match(JSON.stringify(result.diagnostics), /module/i);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("module declarations and bindings round-trip through the ordinary formatter", () => {
|
||
|
|
const formatted = formatQx(packageSource);
|
||
|
|
assert.equal(formatQx(formatted), formatted);
|
||
|
|
const result = compileCapabilityResourceSource(formatted, {
|
||
|
|
source,
|
||
|
|
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
|
||
|
|
});
|
||
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("the old React-specific package declaration is no longer a public language path", () => {
|
||
|
|
const result = compileCapabilityResourceSource(
|
||
|
|
'import interface Data; package UI id "package:ui" revision "package:ui@1" { component Card id "card" subject Data runtime react; }',
|
||
|
|
{ source, environment: { interfaces, interfaceClosure: [...interfaces.values()] } },
|
||
|
|
);
|
||
|
|
assert.equal(result.ok, false);
|
||
|
|
assert.ok(result.diagnostics.some((diagnostic) => diagnostic.phase === "syntax"));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("module closure retains the provider and subject but no synthetic state", () => {
|
||
|
|
const result = compile();
|
||
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||
|
|
const compiled = compileWorkspaceRevision(result.workspace);
|
||
|
|
assert.ok(compiled.ok);
|
||
|
|
const closure = computeCapabilityClosure(compiled.plan, [
|
||
|
|
{
|
||
|
|
atomId: capabilityId.atom("atom:item"),
|
||
|
|
interfaceRevisionId: capabilityId.interfaceRevision("interface:row@1"),
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
assert.deepEqual(closure.packageRevisionIds, ["package:ui@1"]);
|
||
|
|
assert.deepEqual(closure.attachmentIds, []);
|
||
|
|
assert.deepEqual(
|
||
|
|
new Set(closure.conformances.map((entry) => entry.interfaceRevisionId)),
|
||
|
|
new Set(["interface:row@1", "interface:data@1"]),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("unknown namespaced module contracts remain metadata and malformed contracts reject", () => {
|
||
|
|
const result = compile();
|
||
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||
|
|
const workspace = structuredClone(result.workspace);
|
||
|
|
const iface: InterfaceRevision = workspace.interfaceImports.find((entry) => entry.revisionId === "interface:row@1")!;
|
||
|
|
const member = iface.members[0]!;
|
||
|
|
assert.equal(member.kind, "module");
|
||
|
|
if (member.kind !== "module") throw Error("Expected module");
|
||
|
|
member.contract = "example.device.firmware/2";
|
||
|
|
assert.deepEqual(validateWorkspaceRevision(workspace), []);
|
||
|
|
member.contract = "react";
|
||
|
|
assert.ok(validateWorkspaceRevision(workspace).some((issue) => issue.code === "invalid-module-contract"));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("execution plans retain nominal module contracts without synthesizing callable operations", async () => {
|
||
|
|
const result = compile();
|
||
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||
|
|
const plan = compileExecutionPlan(result.workspace);
|
||
|
|
assert.deepEqual(plan.definition.operations, []);
|
||
|
|
const pkg = plan.definition.packages[0]!;
|
||
|
|
assert.equal(pkg.modules?.length, 2);
|
||
|
|
assert.deepEqual(pkg.modules?.find((module) => module.id === "module:editor")?.subject, {
|
||
|
|
kind: "interface",
|
||
|
|
interfaceRevisionId: "interface:data@1",
|
||
|
|
});
|
||
|
|
// A language declaration cannot silently pass installation without an artifact consumer.
|
||
|
|
await assert.rejects(
|
||
|
|
checkExecutionWorld(
|
||
|
|
plan,
|
||
|
|
[
|
||
|
|
{
|
||
|
|
schemaVersion: 1,
|
||
|
|
packageRevisionId: pkg.revision,
|
||
|
|
bindingDigest: pkg.bindingDigest,
|
||
|
|
sourceDigest: pkg.sourceDigest,
|
||
|
|
versions: plan.definition.versions,
|
||
|
|
implementations: [],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
async () => {
|
||
|
|
throw Error("Must reject before reading files");
|
||
|
|
},
|
||
|
|
),
|
||
|
|
/Missing component artifact/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("unrecognized module consumers fail before artifact execution, not during interface compilation", async () => {
|
||
|
|
const result = compile();
|
||
|
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||
|
|
const workspace = structuredClone(result.workspace);
|
||
|
|
const member = workspace.interfaceImports.find((iface) => iface.revisionId === "interface:row@1")!.members[0]!;
|
||
|
|
assert.equal(member.kind, "module");
|
||
|
|
if (member.kind !== "module") throw Error("Expected module");
|
||
|
|
member.contract = "example.device.firmware/2";
|
||
|
|
const plan = compileExecutionPlan(workspace);
|
||
|
|
const pkg = plan.definition.packages[0]!;
|
||
|
|
await assert.rejects(
|
||
|
|
checkExecutionWorld(
|
||
|
|
plan,
|
||
|
|
[
|
||
|
|
{
|
||
|
|
schemaVersion: 1,
|
||
|
|
packageRevisionId: pkg.revision,
|
||
|
|
bindingDigest: pkg.bindingDigest,
|
||
|
|
sourceDigest: pkg.sourceDigest,
|
||
|
|
versions: plan.definition.versions,
|
||
|
|
implementations: [],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
async () => {
|
||
|
|
throw Error("Must reject before reading files");
|
||
|
|
},
|
||
|
|
),
|
||
|
|
/No artifact consumer.*example.device.firmware/,
|
||
|
|
);
|
||
|
|
});
|