Implement typed module contracts and conformance bindings
This commit is contained in:
@@ -25,11 +25,11 @@ test("checked component scaffolding composes with execution handlers without rep
|
||||
tools: { quixos: source, protocol: source, helpers: source, sdk: source },
|
||||
});
|
||||
const packageFile = path.join(root, "package.qx");
|
||||
await fs.writeFile(packageFile, "import interface Data;\n" + (await fs.readFile(packageFile, "utf8")));
|
||||
await fs.writeFile(packageFile, "import interface View;\n" + (await fs.readFile(packageFile, "utf8")));
|
||||
await apply("component", {
|
||||
name: "Card",
|
||||
id: "component:card",
|
||||
declaration: 'component Card id "component:card" subject Data runtime react;',
|
||||
declaration: 'module Card id "component:card" implements View.render;',
|
||||
});
|
||||
assert.deepEqual(JSON.parse(await fs.readFile(path.join(root, "quixos.components.json"), "utf8")), {
|
||||
"component:card": { entry: "src/components/Card.tsx", export: "Card" },
|
||||
@@ -45,7 +45,7 @@ test("checked component scaffolding composes with execution handlers without rep
|
||||
await apply("component", {
|
||||
name: "Details",
|
||||
id: "component:details",
|
||||
declaration: 'component Details id "component:details" subject Data runtime react;',
|
||||
declaration: 'module Details id "component:details" implements View.render;',
|
||||
});
|
||||
assert.equal(await fs.readFile(card, "utf8"), retained);
|
||||
assert.match(await fs.readFile(path.join(root, "src/server.ts"), "utf8"), /echo/);
|
||||
@@ -54,7 +54,7 @@ test("checked component scaffolding composes with execution handlers without rep
|
||||
apply("component", {
|
||||
name: "Card",
|
||||
id: "component:other",
|
||||
declaration: 'component Card id "component:other" subject Data runtime react;',
|
||||
declaration: 'module Card id "component:other" implements View.render;',
|
||||
}),
|
||||
/unique/,
|
||||
);
|
||||
@@ -63,7 +63,7 @@ test("checked component scaffolding composes with execution handlers without rep
|
||||
name: "Injected",
|
||||
id: "component:inject",
|
||||
declaration:
|
||||
'component Injected id "component:inject" subject Data runtime react; function sneak id "sneak" : unit -> unit;',
|
||||
'module Injected id "component:inject" implements View.render; function sneak id "sneak" : unit -> unit;',
|
||||
}),
|
||||
/exactly one/,
|
||||
);
|
||||
|
||||
@@ -46,6 +46,10 @@ pkg.controllers = parseControllerCatalog(
|
||||
[iface, controllerInterface],
|
||||
pkg.checkedQueries,
|
||||
);
|
||||
const viewRole = compileCapabilityResourceSource(await read("DeviceView.qx"), { source, environment });
|
||||
assert.ok(viewRole.ok && viewRole.resource.kind === "interface", JSON.stringify(viewRole.diagnostics));
|
||||
environment.interfaces.set("DeviceView", viewRole.resource.revision);
|
||||
environment.interfaceClosure.push(viewRole.resource.revision);
|
||||
const viewer = compileCapabilityResourceSource(await read("Viewer.qx"), {
|
||||
source: { ...source, repository: "https://example.test/device-viewer.git" },
|
||||
environment,
|
||||
|
||||
+34
-7
@@ -45,10 +45,25 @@ assert.ok(data.ok && data.resource.kind === "interface", JSON.stringify(data.dia
|
||||
assert.ok(constructors.ok && constructors.resource.kind === "interface", JSON.stringify(constructors.diagnostics));
|
||||
const dataInterface = data.resource.revision,
|
||||
factoriesInterface = constructors.resource.revision;
|
||||
const ui = compileCapabilityResourceSource(
|
||||
'import interface Data; package UI id "package:ui" revision "package:ui@1" { component Card id "component:card" subject Data runtime react; }',
|
||||
const view = compileCapabilityResourceSource(
|
||||
'import interface Data; interface View id "interface:device-view" revision "interface:device-view@1" { module render id "member:device-view:render" contract "org.quixos.react.subject-only/1" subject interface Data; }',
|
||||
{ source, environment: { interfaces: new Map([["Data", dataInterface]]), interfaceClosure: [dataInterface] } },
|
||||
);
|
||||
assert.ok(view.ok && view.resource.kind === "interface", JSON.stringify(view.diagnostics));
|
||||
const viewInterface = view.resource.revision;
|
||||
const ui = compileCapabilityResourceSource(
|
||||
'import interface Data; import interface View; package UI id "package:ui" revision "package:ui@1" { module Card id "component:card" implements View.render; }',
|
||||
{
|
||||
source,
|
||||
environment: {
|
||||
interfaces: new Map([
|
||||
["Data", dataInterface],
|
||||
["View", viewInterface],
|
||||
]),
|
||||
interfaceClosure: [dataInterface, viewInterface],
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.ok(ui.ok && ui.resource.kind === "package", JSON.stringify(ui.diagnostics));
|
||||
for (const variant of ["base", "added", "gateway", "gateway-next", "session"]) {
|
||||
const session = variant === "session",
|
||||
@@ -56,13 +71,13 @@ for (const variant of ["base", "added", "gateway", "gateway-next", "session"]) {
|
||||
commit = { base: "a", added: "b", gateway: "c", "gateway-next": "d", session: "e" }[variant]!.repeat(40);
|
||||
const pkg = compileCapabilityResourceSource(
|
||||
`${gateway ? 'external atom Entry id "atom:entry";' : ""}
|
||||
${session ? 'external atom Independent id "atom:independent"; import interface Data;' : ""}
|
||||
${session ? 'external atom Independent id "atom:independent"; import interface Data; import interface View;' : ""}
|
||||
package Owner id "package:owner" revision "package:owner@1" {
|
||||
function identity id "identity" : int32 -> record { pid: int32; child: int32; } execution remote;
|
||||
${gateway ? 'function create id "create" : unit -> atom-ref<Entry> execution transaction targets [server] requires { creation entry id "entry:birth" : Entry input unit; };' : ""}
|
||||
${
|
||||
session
|
||||
? `component Editor id "component:owner-editor" subject Data runtime react;
|
||||
? `module Editor id "component:owner-editor" implements View.render;
|
||||
function createOne id "create-one" : string -> atom-ref<Entry> execution transaction targets [browser, server] requires { creation entry id "one:birth" : Entry input unit; };
|
||||
function createTwo id "create-two" : unit -> atom-ref<Independent> execution transaction targets [browser, server] requires { creation entry id "two:birth" : Independent input unit; };
|
||||
operation revise id "revise" : record {body:change<string>; notes:change<string>; reject:bool;} -> unit mode call execution transaction targets [browser, server] receiver any requires {
|
||||
@@ -78,7 +93,15 @@ package Owner id "package:owner" revision "package:owner@1" {
|
||||
{
|
||||
source,
|
||||
...(session
|
||||
? { environment: { interfaces: new Map([["Data", dataInterface]]), interfaceClosure: [dataInterface] } }
|
||||
? {
|
||||
environment: {
|
||||
interfaces: new Map([
|
||||
["Data", dataInterface],
|
||||
["View", viewInterface],
|
||||
]),
|
||||
interfaceClosure: [dataInterface, viewInterface],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
@@ -107,7 +130,7 @@ package Owner id "package:owner" revision "package:owner@1" {
|
||||
${session ? "import package UI;" : ""}
|
||||
${
|
||||
session
|
||||
? `import interface Data; import interface Factories;
|
||||
? `import interface Data; import interface View; import interface Factories;
|
||||
conform Entry as Factories id "owner:factories" { bind createOne.call to package Owner.createOne with { entry to creation Entry with {}; }; bind createTwo.call to package Owner.createTwo with { entry to creation Independent with {}; }; }
|
||||
${["Entry", "Independent"].map((atom) => `conform ${atom} as Data id "owner:${atom}:data" { private state ${atom}Label id "label:${atom}" on ${atom} : string policy register default "${atom}"; bind label.get to state ${atom}Label.read; bind label.set to state ${atom}Label.write; private state ${atom}Body id "body:${atom}" on ${atom} : string policy crdt(string) default "hello 🌍"; private state ${atom}Notes id "notes:${atom}" on ${atom} : string policy crdt(string) default "notes"; bind body.get to state ${atom}Body.read; bind body.edit to state ${atom}Body.edit; bind notes.get to state ${atom}Notes.read; bind notes.edit to state ${atom}Notes.edit; bind revise.call to package Owner.revise with {body to state ${atom}Body; notes to state ${atom}Notes;}; }`).join("\n")}
|
||||
`
|
||||
@@ -137,11 +160,15 @@ package Owner id "package:owner" revision "package:owner@1" {
|
||||
...(session
|
||||
? ([
|
||||
["Data", dataInterface],
|
||||
["View", viewInterface],
|
||||
["Factories", factoriesInterface],
|
||||
] as const)
|
||||
: []),
|
||||
]),
|
||||
interfaceClosure: [factory.resource.revision, ...(session ? [dataInterface, factoriesInterface] : [])],
|
||||
interfaceClosure: [
|
||||
factory.resource.revision,
|
||||
...(session ? [dataInterface, viewInterface, factoriesInterface] : []),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import interface Device;
|
||||
external atom Display id "device:display";
|
||||
interface DeviceView id "device-view" revision "device-view@1" {
|
||||
module render id "device-view:render" contract "org.quixos.react.subject-only/1" subject interface Device;
|
||||
}
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import interface DeviceView;
|
||||
import interface Device;
|
||||
external atom Display id "device:display";
|
||||
package DeviceViewer id "device-viewer" revision "device-viewer@1" {
|
||||
component Status id "component:device:status" subject Device runtime react;
|
||||
module Status id "component:device:status" implements DeviceView.render;
|
||||
query Inputs id "device-viewer:inputs" root Device document "viewer.graphql" operation "Status" {replica;}
|
||||
query View id "device-viewer:view" root Device document "viewer.graphql" operation "Status" {local from Inputs;watch;}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
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/,
|
||||
);
|
||||
});
|
||||
+31
-11
@@ -39,8 +39,17 @@ const interfaces = new Map([
|
||||
["Data", data],
|
||||
["Other", other],
|
||||
]);
|
||||
const componentSource = (body = 'component Card id "component:card" subject Data runtime react;') =>
|
||||
`import interface Data; package UI id "package:ui" revision "package:ui@1" { ${body} }`;
|
||||
const role = compileCapabilityResourceSource(
|
||||
`import interface Data;
|
||||
interface View id "interface:view" revision "interface:view@1" {
|
||||
module render id "member:view:render" contract "org.quixos.react.subject-only/1" subject interface Data;
|
||||
}`,
|
||||
{ source, environment: { interfaces, interfaceClosure: [...interfaces.values()] } },
|
||||
);
|
||||
assert.ok(role.ok && role.resource.kind === "interface", JSON.stringify(role.diagnostics));
|
||||
interfaces.set("View", role.resource.revision);
|
||||
const componentSource = (body = 'module Card id "component:card" implements View.render;') =>
|
||||
`import interface Data; import interface View; package UI id "package:ui" revision "package:ui@1" { ${body} }`;
|
||||
const compilePackage = (body?: string) =>
|
||||
compileCapabilityResourceSource(componentSource(body), {
|
||||
source,
|
||||
@@ -51,7 +60,7 @@ const workspace = (presentation = 'presentation Card id "presentation:card" for
|
||||
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
|
||||
return compileCapabilitySource(
|
||||
`workspace Example id "workspace:example" revision "workspace:example@1" commit "${source.commit}" {
|
||||
import interface Data; import interface Other; import package UI;
|
||||
import interface Data; import interface Other; import interface View; import package UI;
|
||||
atom One id "atom:one"; atom Two id "atom:two";
|
||||
conform One as Data id "conformance:one:data" { }
|
||||
conform Two as Data id "conformance:two:data" { }
|
||||
@@ -88,21 +97,21 @@ test("component declarations survive formatting and syntax inspection", () => {
|
||||
const text = formatQx(componentSource());
|
||||
const parsed = parseQx(text);
|
||||
assert.deepEqual(parsed.diagnostics, []);
|
||||
assert.ok([...walkSyntax(parsed.root)].some((node) => node.kind === "componentDecl"));
|
||||
assert.ok([...walkSyntax(parsed.root)].some((node) => node.kind === "moduleExport"));
|
||||
assert.equal(formatQx(text), text);
|
||||
});
|
||||
|
||||
test("component and presentation contracts reject wrong subjects, duplicates and unavailable runtimes", () => {
|
||||
for (const [input, message] of [
|
||||
['component Card id "component:card" subject Missing runtime react;', /Missing/],
|
||||
['component Card id "component:card" subject Data runtime arduino;', /Unsupported component runtime/],
|
||||
['module Card id "component:card" implements Missing.render;', /Missing/],
|
||||
['module Card id "component:card" implements Data.missing;', /module member/],
|
||||
[
|
||||
'component Card id "component:card" subject Data runtime react; component Other id "component:card" subject Data runtime react;',
|
||||
/Duplicate component/,
|
||||
'module Card id "component:card" implements View.render; module Other id "component:card" implements View.render;',
|
||||
/Duplicate or empty module/,
|
||||
],
|
||||
[
|
||||
'component Card id "component:card" subject Data runtime react; component Card id "component:other" subject Data runtime react;',
|
||||
/Duplicate component/,
|
||||
'module Card id "component:card" implements View.render; module Card id "component:other" implements View.render;',
|
||||
/Duplicate or empty module/,
|
||||
],
|
||||
] as const) {
|
||||
const result = compilePackage(input);
|
||||
@@ -131,6 +140,17 @@ test("model validation rechecks presentation metadata instead of trusting the so
|
||||
assert.throws(() => compileExecutionPlan(result.workspace), /no installed package component/);
|
||||
});
|
||||
|
||||
test("standalone presentation rejects a general component contract that may require extra props", () => {
|
||||
const result = workspace();
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
const candidate = structuredClone(result.workspace);
|
||||
const member = candidate.interfaceImports.find((iface) => iface.revisionId === "interface:view@1")!.members[0]!;
|
||||
assert.equal(member.kind, "module");
|
||||
if (member.kind !== "module") throw Error("Expected module");
|
||||
member.contract = "org.quixos.react.component/1";
|
||||
assert.ok(validateWorkspaceRevision(candidate).some((issue) => /subject-only/.test(issue.message)));
|
||||
});
|
||||
|
||||
test("checked component-only artifacts install without a service and reject missing or substituted entries", async () => {
|
||||
const result = workspace();
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
@@ -150,7 +170,7 @@ test("checked component-only artifacts install without a service and reject miss
|
||||
const body: Omit<ComponentArtifact, "contentDigest"> = {
|
||||
id: "component:card",
|
||||
subject: "interface:Data@1",
|
||||
compatibility: "quixos-react-component-v1",
|
||||
compatibility: "org.quixos.react.esm/1",
|
||||
path: paths[0]!,
|
||||
exportName: "Card",
|
||||
files,
|
||||
|
||||
Reference in New Issue
Block a user