Files
quixos-protocol/test/modules.test.ts
T

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