Format authored monorepo code with pinned language formatters
This commit is contained in:
@@ -9,32 +9,55 @@ import { convergeAuthoring } from "../src/capability-language/authoring-converge
|
||||
import { loadQuixosLock } from "../src/resource-lock/index.js";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("source convergence propagates nested edits and unchanged snapshots reach a fixed point", async context => {
|
||||
test("source convergence propagates nested edits and unchanged snapshots reach a fixed point", async (context) => {
|
||||
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-converge-test-"));
|
||||
context.after(() => fs.rm(temporary, { recursive: true, force: true }));
|
||||
const workbench = path.join(temporary, "workbench"), remotes = path.join(temporary, "remotes");
|
||||
const workbench = path.join(temporary, "workbench"),
|
||||
remotes = path.join(temporary, "remotes");
|
||||
await fs.mkdir(path.join(workbench, ".quixos"), { recursive: true });
|
||||
await fs.mkdir(remotes);
|
||||
const origin = "https://convergence.example.test/";
|
||||
const previous = Object.fromEntries(["GIT_CONFIG_COUNT", "GIT_CONFIG_KEY_0", "GIT_CONFIG_VALUE_0"].map(key => [key, process.env[key]]));
|
||||
const previous = Object.fromEntries(
|
||||
["GIT_CONFIG_COUNT", "GIT_CONFIG_KEY_0", "GIT_CONFIG_VALUE_0"].map((key) => [key, process.env[key]]),
|
||||
);
|
||||
process.env.GIT_CONFIG_COUNT = "1";
|
||||
process.env.GIT_CONFIG_KEY_0 = `url.file://${remotes}/.insteadOf`;
|
||||
process.env.GIT_CONFIG_VALUE_0 = origin;
|
||||
context.after(() => { for (const [key, value] of Object.entries(previous)) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } });
|
||||
context.after(() => {
|
||||
for (const [key, value] of Object.entries(previous)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
});
|
||||
const resources = [];
|
||||
let dependency = "";
|
||||
for (const [directory, kind, name] of [["resources/Base", "interface", "Base"], ["resources/Consumer", "package", "Consumer"], ["root", "workspace", "Root"]]) {
|
||||
for (const [directory, kind, name] of [
|
||||
["resources/Base", "interface", "Base"],
|
||||
["resources/Consumer", "package", "Consumer"],
|
||||
["root", "workspace", "Root"],
|
||||
]) {
|
||||
const root = path.join(workbench, directory);
|
||||
await fs.mkdir(root, { recursive: true });
|
||||
await execFile("jj", ["git", "init", "--colocate", root]);
|
||||
await execFile("git", ["init", "--bare", path.join(remotes, name)]);
|
||||
const repository = `${origin}${name}`;
|
||||
await execFile("git", ["-C", root, "remote", "add", "origin", repository]);
|
||||
await fs.writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } ${dependency} }`);
|
||||
await fs.writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
`quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } ${dependency} }`,
|
||||
);
|
||||
await fs.writeFile(path.join(root, `${kind}.qx`), "draft");
|
||||
await execFile("jj", ["status"], { cwd: root });
|
||||
const commit = (await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], { cwd: root })).stdout.trim();
|
||||
if (kind !== "workspace") resources.push({ directory, kind, resourceId: `${kind}:${name}`, source: { resolver: "git", repository, commit } });
|
||||
const commit = (
|
||||
await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], { cwd: root })
|
||||
).stdout.trim();
|
||||
if (kind !== "workspace")
|
||||
resources.push({
|
||||
directory,
|
||||
kind,
|
||||
resourceId: `${kind}:${name}`,
|
||||
source: { resolver: "git", repository, commit },
|
||||
});
|
||||
dependency = `${kind} ${name} source { repository "${repository}"; commit "${commit}"; }`;
|
||||
}
|
||||
await fs.writeFile(path.join(workbench, ".quixos/resource-graph.json"), JSON.stringify({ resources }));
|
||||
@@ -48,7 +71,10 @@ test("source convergence propagates nested edits and unchanged snapshots reach a
|
||||
assert.notEqual(second.candidate?.commit, first.candidate?.commit);
|
||||
const consumer = await loadQuixosLock(path.join(workbench, "resources/Consumer/quixos.lock"));
|
||||
assert.ok(consumer.ok);
|
||||
assert.equal(consumer.lock.resources[0].source.commit, second.retained.find(entry => entry.directory === "resources/Base")?.source.commit);
|
||||
assert.equal(
|
||||
consumer.lock.resources[0].source.commit,
|
||||
second.retained.find((entry) => entry.directory === "resources/Base")?.source.commit,
|
||||
);
|
||||
const third = await convergeAuthoring(workbench);
|
||||
assert.deepEqual(third, second);
|
||||
const base = path.join(workbench, "resources/Base");
|
||||
@@ -61,24 +87,30 @@ test("source convergence propagates nested edits and unchanged snapshots reach a
|
||||
const partial = await convergeAuthoring(workbench);
|
||||
assert.equal(partial.converged, false);
|
||||
const graph = JSON.parse(await fs.readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"));
|
||||
assert.equal(graph.resources[0].source.commit, partial.retained.find(entry => entry.directory === "resources/Base")?.source.commit);
|
||||
assert.equal(
|
||||
graph.resources[0].source.commit,
|
||||
partial.retained.find((entry) => entry.directory === "resources/Base")?.source.commit,
|
||||
);
|
||||
await fs.writeFile(path.join(consumerRoot, "quixos.lock"), goodLock);
|
||||
const resumed = await convergeAuthoring(workbench);
|
||||
assert.deepEqual(resumed.worklist, []);
|
||||
assert.deepEqual(await convergeAuthoring(workbench), resumed);
|
||||
await execFile("git", ["-C", base, "remote", "set-url", "origin", origin + "Wrong"]);
|
||||
const mismatch = await convergeAuthoring(workbench);
|
||||
assert.ok(mismatch.worklist.some(entry => entry.phase === "source" && /Origin differs/.test(entry.message)));
|
||||
assert.ok(mismatch.worklist.some((entry) => entry.phase === "source" && /Origin differs/.test(entry.message)));
|
||||
await execFile("git", ["-C", base, "remote", "set-url", "origin", origin + "Base"]);
|
||||
await fs.rename(path.join(remotes, "Base"), path.join(remotes, "Base-offline"));
|
||||
const offline = await convergeAuthoring(workbench);
|
||||
assert.equal(offline.candidate, null);
|
||||
assert.ok(offline.worklist.some(entry => entry.phase === "publication"));
|
||||
assert.ok(offline.worklist.some((entry) => entry.phase === "publication"));
|
||||
await fs.rename(path.join(remotes, "Base-offline"), path.join(remotes, "Base"));
|
||||
assert.equal((await convergeAuthoring(workbench)).converged, true);
|
||||
const consumerSource = resumed.retained.find(entry => entry.directory === "resources/Consumer")!.source;
|
||||
await fs.writeFile(path.join(base, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } package Consumer source { repository "${consumerSource.repository}"; commit "${consumerSource.commit}"; } }`);
|
||||
const consumerSource = resumed.retained.find((entry) => entry.directory === "resources/Consumer")!.source;
|
||||
await fs.writeFile(
|
||||
path.join(base, "quixos.lock"),
|
||||
`quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } package Consumer source { repository "${consumerSource.repository}"; commit "${consumerSource.commit}"; } }`,
|
||||
);
|
||||
const cycle = await convergeAuthoring(workbench);
|
||||
assert.equal(cycle.candidate, null);
|
||||
assert.ok(cycle.worklist.some(entry => /Source dependency cycle/.test(entry.message)));
|
||||
assert.ok(cycle.worklist.some((entry) => /Source dependency cycle/.test(entry.message)));
|
||||
});
|
||||
|
||||
@@ -8,12 +8,15 @@ import { promisify } from "node:util";
|
||||
import { inspectAuthoringRepository } from "../src/capability-language/authoring-inspect.js";
|
||||
|
||||
const execFile = promisify(callback);
|
||||
test("inspection keeps current files and labels historical recovery without granting verification", async context => {
|
||||
test("inspection keeps current files and labels historical recovery without granting verification", async (context) => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "qx-inspection-test-"));
|
||||
context.after(() => rm(root, { recursive: true, force: true }));
|
||||
const git = (...args: string[]) => execFile("git", ["-C", root, ...args]);
|
||||
await git("init");
|
||||
await writeFile(path.join(root, "interface.qx"), 'interface Example id "interface:example" revision "interface:example@1" {}');
|
||||
await writeFile(
|
||||
path.join(root, "interface.qx"),
|
||||
'interface Example id "interface:example" revision "interface:example@1" {}',
|
||||
);
|
||||
await git("add", ".");
|
||||
await git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "contract");
|
||||
const commit = (await git("rev-parse", "HEAD")).stdout.trim();
|
||||
@@ -30,7 +33,7 @@ test("inspection keeps current files and labels historical recovery without gran
|
||||
assert.match(inspected.files[1].declarations[0].source, /New/);
|
||||
assert.equal((await git("rev-parse", "HEAD")).stdout.trim(), commit);
|
||||
await symlink(path.join(root, "interface.qx"), path.join(root, "linked.qx"));
|
||||
const linked = (await inspectAuthoringRepository(root)).files.find(file => file.file === "linked.qx")!;
|
||||
const linked = (await inspectAuthoringRepository(root)).files.find((file) => file.file === "linked.qx")!;
|
||||
assert.equal(linked.status, "unavailable");
|
||||
assert.deepEqual(linked.declarations, []);
|
||||
assert.match(JSON.stringify(linked.currentErrors), /ordinary files/);
|
||||
|
||||
@@ -3,43 +3,81 @@ import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {execFile as callback} from "node:child_process";
|
||||
import {promisify} from "node:util";
|
||||
import {authoringWorklist} from "../src/capability-language/authoring-worklist.js";
|
||||
import {checkRecordName} from "../src/capability-language/authoring-check.js";
|
||||
import {checkerIdentity, snapshotCommit} from "../src/capability-language/checked-build.js";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { authoringWorklist } from "../src/capability-language/authoring-worklist.js";
|
||||
import { checkRecordName } from "../src/capability-language/authoring-check.js";
|
||||
import { checkerIdentity, snapshotCommit } from "../src/capability-language/checked-build.js";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("worklist grows and clears from current source, dependency and checker observations", async context => {
|
||||
test("worklist grows and clears from current source, dependency and checker observations", async (context) => {
|
||||
const workbench = await fs.mkdtemp(path.join(os.tmpdir(), "qx-worklist-test-"));
|
||||
context.after(() => fs.rm(workbench, {recursive: true, force: true}));
|
||||
const root = path.join(workbench, "root"), provider = path.join(workbench, "resources/Base");
|
||||
await fs.mkdir(root); await fs.mkdir(provider, {recursive: true});
|
||||
await fs.mkdir(path.join(workbench, ".quixos/checks"), {recursive: true});
|
||||
context.after(() => fs.rm(workbench, { recursive: true, force: true }));
|
||||
const root = path.join(workbench, "root"),
|
||||
provider = path.join(workbench, "resources/Base");
|
||||
await fs.mkdir(root);
|
||||
await fs.mkdir(provider, { recursive: true });
|
||||
await fs.mkdir(path.join(workbench, ".quixos/checks"), { recursive: true });
|
||||
const header = `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; }`;
|
||||
for (const directory of [root, provider]) await execFile("jj", ["git", "init", "--colocate", directory]);
|
||||
await fs.writeFile(path.join(provider, "interface.qx"), 'interface Base id "interface:base" revision "interface:base@1" {}');
|
||||
await fs.writeFile(
|
||||
path.join(provider, "interface.qx"),
|
||||
'interface Base id "interface:base" revision "interface:base@1" {}',
|
||||
);
|
||||
await fs.writeFile(path.join(provider, "quixos.lock"), `${header} }`);
|
||||
const baseCommit = await snapshotCommit(provider);
|
||||
await fs.writeFile(path.join(root, "workspace.qx"), `workspace Test id "workspace:test" revision "workspace:test@1" commit "${"b".repeat(40)}" { import interface Base; }`);
|
||||
await fs.writeFile(path.join(root, "quixos.lock"), `${header} interface Base source { repository "https://example.test/base"; commit "${baseCommit}"; } }`);
|
||||
await fs.writeFile(
|
||||
path.join(root, "workspace.qx"),
|
||||
`workspace Test id "workspace:test" revision "workspace:test@1" commit "${"b".repeat(40)}" { import interface Base; }`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
`${header} interface Base source { repository "https://example.test/base"; commit "${baseCommit}"; } }`,
|
||||
);
|
||||
const rootCommit = await snapshotCommit(root);
|
||||
await fs.writeFile(path.join(workbench, ".quixos/resource-graph.json"), JSON.stringify({resources: [
|
||||
{kind: "interface", directory: provider, source: {resolver: "git", repository: "https://example.test/base", commit: baseCommit}},
|
||||
]}));
|
||||
assert.equal((await authoringWorklist(workbench)).worklist.filter(entry => entry.phase === "unchecked").length, 2);
|
||||
const remember = (directory: string, commit: string, checker = checkerIdentity()) => fs.writeFile(
|
||||
path.join(workbench, ".quixos/checks", checkRecordName(directory)), JSON.stringify({commit, checker, phase: "checked", blockers: []}));
|
||||
await remember("root", rootCommit); await remember("resources/Base", baseCommit);
|
||||
await fs.writeFile(
|
||||
path.join(workbench, ".quixos/resource-graph.json"),
|
||||
JSON.stringify({
|
||||
resources: [
|
||||
{
|
||||
kind: "interface",
|
||||
directory: provider,
|
||||
source: { resolver: "git", repository: "https://example.test/base", commit: baseCommit },
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
assert.equal((await authoringWorklist(workbench)).worklist.filter((entry) => entry.phase === "unchecked").length, 2);
|
||||
const remember = (directory: string, commit: string, checker = checkerIdentity()) =>
|
||||
fs.writeFile(
|
||||
path.join(workbench, ".quixos/checks", checkRecordName(directory)),
|
||||
JSON.stringify({ commit, checker, phase: "checked", blockers: [] }),
|
||||
);
|
||||
await remember("root", rootCommit);
|
||||
await remember("resources/Base", baseCommit);
|
||||
assert.deepEqual((await authoringWorklist(workbench)).worklist, []);
|
||||
await fs.writeFile(path.join(provider, "interface.qx"), "interface broken {{{");
|
||||
const broken = await authoringWorklist(workbench);
|
||||
assert.ok(broken.worklist.some(entry => entry.directory === "resources/Base" && entry.phase === "syntax" && /historical/.test(entry.message)));
|
||||
assert.ok(broken.worklist.some(entry => entry.directory === "root" && entry.phase === "dependency"));
|
||||
await fs.writeFile(path.join(provider, "interface.qx"), 'interface Base id "interface:base" revision "interface:base@1" {}');
|
||||
assert.ok(
|
||||
broken.worklist.some(
|
||||
(entry) => entry.directory === "resources/Base" && entry.phase === "syntax" && /historical/.test(entry.message),
|
||||
),
|
||||
);
|
||||
assert.ok(broken.worklist.some((entry) => entry.directory === "root" && entry.phase === "dependency"));
|
||||
await fs.writeFile(
|
||||
path.join(provider, "interface.qx"),
|
||||
'interface Base id "interface:base" revision "interface:base@1" {}',
|
||||
);
|
||||
assert.deepEqual((await authoringWorklist(workbench)).worklist, []);
|
||||
await remember("resources/Base", baseCommit, "old-checker");
|
||||
assert.ok((await authoringWorklist(workbench)).worklist.some(entry => /checker changed/.test(entry.message)));
|
||||
await fs.writeFile(path.join(workbench, ".quixos/checks", checkRecordName("resources/Base")), JSON.stringify({phase: "publication", blockers: ["source retention unavailable"]}));
|
||||
assert.ok((await authoringWorklist(workbench)).worklist.some(entry => entry.phase === "publication" && /source retention/.test(entry.message)));
|
||||
assert.ok((await authoringWorklist(workbench)).worklist.some((entry) => /checker changed/.test(entry.message)));
|
||||
await fs.writeFile(
|
||||
path.join(workbench, ".quixos/checks", checkRecordName("resources/Base")),
|
||||
JSON.stringify({ phase: "publication", blockers: ["source retention unavailable"] }),
|
||||
);
|
||||
assert.ok(
|
||||
(await authoringWorklist(workbench)).worklist.some(
|
||||
(entry) => entry.phase === "publication" && /source retention/.test(entry.message),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
+19
-5
@@ -4,13 +4,18 @@ import { compileCapabilityResourceSource } from "../src/capability-language/pars
|
||||
import { generateTypeScriptBindings, type BindingSchema } from "../src/bindings/index.js";
|
||||
const source = { repository: "https://example.test/p.git", commit: "1".repeat(40) };
|
||||
const schemaFor = (body: string): BindingSchema => {
|
||||
const compiled = compileCapabilityResourceSource(`external atom Thing id "thing"; package Demo id "demo" revision "demo@1" { ${body} }`, { source });
|
||||
const compiled = compileCapabilityResourceSource(
|
||||
`external atom Thing id "thing"; package Demo id "demo" revision "demo@1" { ${body} }`,
|
||||
{ source },
|
||||
);
|
||||
assert.equal(compiled.ok, true, JSON.stringify(compiled.diagnostics));
|
||||
if (!compiled.ok || compiled.resource.kind !== "package") throw new Error("expected package");
|
||||
return { format: "quixos-bindings", version: 1, interfaces: [], packages: [compiled.resource.revision] };
|
||||
};
|
||||
test("generator uses exact IDs, restricted ports, nominal references, and lossless scalar types", () => {
|
||||
const schema = schemaFor('operation run id "run-id" : list<int64> -> optional<atom-ref<Thing>> mode call receiver atom Thing requires { state payload id "payload-id" : bytes [read]; };');
|
||||
const schema = schemaFor(
|
||||
'operation run id "run-id" : list<int64> -> optional<atom-ref<Thing>> mode call receiver atom Thing requires { state payload id "payload-id" : bytes [read]; };',
|
||||
);
|
||||
const generated = generateTypeScriptBindings(schema, "demo@1");
|
||||
assert.match(generated, /Array<bigint>/);
|
||||
assert.match(generated, /Promise<Uint8Array>/);
|
||||
@@ -22,9 +27,18 @@ test("generator uses exact IDs, restricted ports, nominal references, and lossle
|
||||
test("missing external types and constructor signatures fail generation", () => {
|
||||
const schema = schemaFor('function run id "run" : unit -> message "example.Payload";');
|
||||
assert.throws(() => generateTypeScriptBindings(schema, "demo@1"), /Missing TypeScript message binding/);
|
||||
assert.match(generateTypeScriptBindings(schema, "demo@1", { messages: { "example.Payload": { module: "./payload.js", export: "payload" } } }), /BindingValue<typeof message0>/);
|
||||
const constructor = schemaFor('function run id "run" : unit -> unit requires { constructor thing id "ctor" : Thing; };');
|
||||
assert.match(
|
||||
generateTypeScriptBindings(schema, "demo@1", {
|
||||
messages: { "example.Payload": { module: "./payload.js", export: "payload" } },
|
||||
}),
|
||||
/BindingValue<typeof message0>/,
|
||||
);
|
||||
const constructor = schemaFor(
|
||||
'function run id "run" : unit -> unit requires { constructor thing id "ctor" : Thing; };',
|
||||
);
|
||||
assert.throws(() => generateTypeScriptBindings(constructor, "demo@1"), /explicit input contract/);
|
||||
const typed = schemaFor('function run id "run" : unit -> unit requires { constructor thing id "ctor" : Thing input string; };');
|
||||
const typed = schemaFor(
|
||||
'function run id "run" : unit -> unit requires { constructor thing id "ctor" : Thing input string; };',
|
||||
);
|
||||
assert.match(generateTypeScriptBindings(typed, "demo@1"), /construct.*input: string/);
|
||||
});
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {bundlePolicyErrors} from "../src/bindings/bundle-policy.js";
|
||||
import {addImplementation} from "../src/capability-language/implementation-edit.js";
|
||||
import { bundlePolicyErrors } from "../src/bindings/bundle-policy.js";
|
||||
import { addImplementation } from "../src/capability-language/implementation-edit.js";
|
||||
|
||||
test("bundled source policy rejects location and dynamic-loading assumptions, not static assets or runtime I/O", () => {
|
||||
for (const code of ['new URL("../x", import.meta.url)', '__dirname', '__filename', 'import(name)', 'require(name)', 'eval(code)', 'new Function(code)'])
|
||||
for (const code of [
|
||||
'new URL("../x", import.meta.url)',
|
||||
"__dirname",
|
||||
"__filename",
|
||||
"import(name)",
|
||||
"require(name)",
|
||||
"eval(code)",
|
||||
"new Function(code)",
|
||||
])
|
||||
assert.ok(bundlePolicyErrors(code, "source.ts").length, code);
|
||||
assert.deepEqual(bundlePolicyErrors('import source from "./component.js?browser-source"; import fs from "node:fs"; fs.readFile(userSelectedPath);', "source.ts"), []);
|
||||
assert.deepEqual(
|
||||
bundlePolicyErrors(
|
||||
'import source from "./component.js?browser-source"; import fs from "node:fs"; fs.readFile(userSelectedPath);',
|
||||
"source.ts",
|
||||
),
|
||||
[],
|
||||
);
|
||||
assert.deepEqual(bundlePolicyErrors('// import.meta.url\nconst text = "__dirname";', "source.ts"), []);
|
||||
});
|
||||
|
||||
test("imperative handler insertion preserves arbitrary existing code and rejects ambiguous targets", () => {
|
||||
const original = 'const keep = "createRuntime({fake:1})";\nservePackageRuntime(createRuntime({ existing: customHandler }));\n';
|
||||
const original =
|
||||
'const keep = "createRuntime({fake:1})";\nservePackageRuntime(createRuntime({ existing: customHandler }));\n';
|
||||
const edited = addImplementation(original, "createRuntime", "newHandler", "./impl/new.js");
|
||||
assert.match(edited, /existing: customHandler/);
|
||||
assert.match(edited, /const keep =/);
|
||||
assert.match(edited, /"newHandler": qxImplementation/);
|
||||
assert.throws(() => addImplementation(original, "createRuntime", "existing", "./x.js"), /already exists/);
|
||||
assert.throws(() => addImplementation('createRuntime(one);', "createRuntime", "x", "./x.js"), /Cannot safely/);
|
||||
assert.throws(() => addImplementation("createRuntime(one);", "createRuntime", "x", "./x.js"), /Cannot safely/);
|
||||
});
|
||||
|
||||
@@ -35,16 +35,23 @@ test("candidate snapshots include dirty and new files without changing Git histo
|
||||
test("candidate check produces explicitly non-activation evidence and never overwrites a report", async (context) => {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "qx-check-test-"));
|
||||
context.after(() => fs.rm(directory, { recursive: true, force: true }));
|
||||
const root = path.join(directory, "source"), output = path.join(directory, "check");
|
||||
const root = path.join(directory, "source"),
|
||||
output = path.join(directory, "check");
|
||||
await fs.mkdir(root);
|
||||
await execFile("jj", ["git", "init", "--colocate", root]);
|
||||
await fs.writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos.git"; commit "${"a".repeat(40)}"; } }`);
|
||||
await fs.writeFile(path.join(root, "workspace.qx"), `workspace Test id "workspace:test" revision "workspace:test@1" commit "${"b".repeat(40)}" { atom Subject id "atom:subject"; }`);
|
||||
const result = await checkWorkspaceCandidate({root, output});
|
||||
await fs.writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
`quixos-lock version 1 { quixos source { repository "https://example.test/quixos.git"; commit "${"a".repeat(40)}"; } }`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(root, "workspace.qx"),
|
||||
`workspace Test id "workspace:test" revision "workspace:test@1" commit "${"b".repeat(40)}" { atom Subject id "atom:subject"; }`,
|
||||
);
|
||||
const result = await checkWorkspaceCandidate({ root, output });
|
||||
assert.equal(result.candidateOnly, true);
|
||||
assert.equal(result.activationEvidence, false);
|
||||
assert.deepEqual(result.blockers, []);
|
||||
const report = await fs.readFile(path.join(output, "report.json"));
|
||||
await assert.rejects(checkWorkspaceCandidate({root, output}), /EEXIST/);
|
||||
await assert.rejects(checkWorkspaceCandidate({ root, output }), /EEXIST/);
|
||||
assert.deepEqual(await fs.readFile(path.join(output, "report.json")), report);
|
||||
});
|
||||
|
||||
@@ -3,10 +3,7 @@ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import {
|
||||
compileCapabilityResourceRepository,
|
||||
compileWorkspaceRepository,
|
||||
} from "../src/capability-language/index.js";
|
||||
import { compileCapabilityResourceRepository, compileWorkspaceRepository } from "../src/capability-language/index.js";
|
||||
|
||||
const quixosCommit = "1".repeat(40);
|
||||
const namedCommit = "2".repeat(40);
|
||||
@@ -28,31 +25,46 @@ test("workspace assembly resolves resource-owned dependencies recursively", asyn
|
||||
const named = path.join(directory, "named");
|
||||
const runtime = path.join(directory, "runtime");
|
||||
await Promise.all([mkdir(root), mkdir(named), mkdir(runtime)]);
|
||||
await writeFile(path.join(root, "quixos.lock"), lock(`package Runtime source {
|
||||
await writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
lock(`package Runtime source {
|
||||
repository "https://example.test/package-runtime.git";
|
||||
commit "${packageCommit}";
|
||||
}`));
|
||||
await writeFile(path.join(root, "workspace.qx"), `workspace Test id "workspace:test" revision "workspace:test@1" commit "${"4".repeat(40)}" {
|
||||
}`),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "workspace.qx"),
|
||||
`workspace Test id "workspace:test" revision "workspace:test@1" commit "${"4".repeat(40)}" {
|
||||
atom Subject id "atom:subject";
|
||||
import package Runtime;
|
||||
}
|
||||
`);
|
||||
`,
|
||||
);
|
||||
await writeFile(path.join(named, "quixos.lock"), lock());
|
||||
await writeFile(path.join(named, "interface.qx"), `interface Named id "interface:named" revision "interface:named@1" {
|
||||
await writeFile(
|
||||
path.join(named, "interface.qx"),
|
||||
`interface Named id "interface:named" revision "interface:named@1" {
|
||||
value name id "member:named:name" : string {
|
||||
get id "operation:named:name:get";
|
||||
}
|
||||
}
|
||||
`);
|
||||
await writeFile(path.join(runtime, "quixos.lock"), lock(`interface Named source {
|
||||
`,
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runtime, "quixos.lock"),
|
||||
lock(`interface Named source {
|
||||
repository "https://example.test/interface-named.git";
|
||||
commit "${namedCommit}";
|
||||
}`));
|
||||
await writeFile(path.join(runtime, "package.qx"), `import interface Named;
|
||||
}`),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runtime, "package.qx"),
|
||||
`import interface Named;
|
||||
package Runtime id "package:runtime" revision "package:runtime@1" {
|
||||
function describe id "export:runtime:describe" : interface-ref<Named> -> string;
|
||||
}
|
||||
`);
|
||||
`,
|
||||
);
|
||||
|
||||
const directories = new Map([
|
||||
[`interface\0https://example.test/interface-named.git\0${namedCommit}`, named],
|
||||
@@ -100,15 +112,21 @@ package Runtime id "package:runtime" revision "package:runtime@1" {
|
||||
test("resource repositories cannot own workspace Quixos selection policy", async (context) => {
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), "quixos-resource-policy-"));
|
||||
context.after(() => rm(directory, { recursive: true, force: true }));
|
||||
await writeFile(path.join(directory, "quixos.lock"), `quixos-lock version 1 {
|
||||
await writeFile(
|
||||
path.join(directory, "quixos.lock"),
|
||||
`quixos-lock version 1 {
|
||||
quixos source {
|
||||
repository "https://example.test/quixos.git";
|
||||
policy track-development;
|
||||
ref "dev/alice/main";
|
||||
commit "${quixosCommit}";
|
||||
}
|
||||
}`);
|
||||
await writeFile(path.join(directory, "package.qx"), `package Runtime id "package:runtime" revision "package:runtime@1" { }\n`);
|
||||
}`,
|
||||
);
|
||||
await writeFile(
|
||||
path.join(directory, "package.qx"),
|
||||
`package Runtime id "package:runtime" revision "package:runtime@1" { }\n`,
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
compileCapabilityResourceRepository({
|
||||
@@ -131,19 +149,28 @@ test("resource manifests cannot hide lock dependencies", async (context) => {
|
||||
const root = path.join(directory, "root");
|
||||
const runtime = path.join(directory, "runtime");
|
||||
await Promise.all([mkdir(root), mkdir(runtime)]);
|
||||
await writeFile(path.join(root, "quixos.lock"), lock(`package Runtime source {
|
||||
await writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
lock(`package Runtime source {
|
||||
repository "https://example.test/package-runtime.git";
|
||||
commit "${packageCommit}";
|
||||
}`));
|
||||
await writeFile(path.join(root, "workspace.qx"), `workspace Test id "workspace:test" revision "workspace:test@1" commit "${"4".repeat(40)}" {
|
||||
}`),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "workspace.qx"),
|
||||
`workspace Test id "workspace:test" revision "workspace:test@1" commit "${"4".repeat(40)}" {
|
||||
atom Subject id "atom:subject";
|
||||
import package Runtime;
|
||||
}
|
||||
`);
|
||||
`,
|
||||
);
|
||||
await writeFile(path.join(runtime, "quixos.lock"), lock());
|
||||
await writeFile(path.join(runtime, "package.qx"), `import interface Hidden;
|
||||
await writeFile(
|
||||
path.join(runtime, "package.qx"),
|
||||
`import interface Hidden;
|
||||
package Runtime id "package:runtime" revision "package:runtime@1" { }
|
||||
`);
|
||||
`,
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
compileWorkspaceRepository({
|
||||
@@ -160,19 +187,28 @@ test("workspace assembly must satisfy nominal external interfaces", async (conte
|
||||
const root = path.join(directory, "root");
|
||||
const runtime = path.join(directory, "runtime");
|
||||
await Promise.all([mkdir(root), mkdir(runtime)]);
|
||||
await writeFile(path.join(root, "quixos.lock"), lock(`package Runtime source {
|
||||
await writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
lock(`package Runtime source {
|
||||
repository "https://example.test/package-runtime.git";
|
||||
commit "${packageCommit}";
|
||||
}`));
|
||||
await writeFile(path.join(root, "workspace.qx"), `workspace Test id "workspace:test" revision "workspace:test@1" commit "${"4".repeat(40)}" {
|
||||
}`),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(root, "workspace.qx"),
|
||||
`workspace Test id "workspace:test" revision "workspace:test@1" commit "${"4".repeat(40)}" {
|
||||
atom Subject id "atom:subject";
|
||||
import package Runtime;
|
||||
}
|
||||
`);
|
||||
`,
|
||||
);
|
||||
await writeFile(path.join(runtime, "quixos.lock"), lock());
|
||||
await writeFile(path.join(runtime, "package.qx"), `external interface Named revision "interface:named@1";
|
||||
await writeFile(
|
||||
path.join(runtime, "package.qx"),
|
||||
`external interface Named revision "interface:named@1";
|
||||
package Runtime id "package:runtime" revision "package:runtime@1" { }
|
||||
`);
|
||||
`,
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
compileWorkspaceRepository({
|
||||
|
||||
@@ -5,14 +5,12 @@ import { test } from "node:test";
|
||||
|
||||
const cli = resolve(process.cwd(), "dist/src/capability-language/cli.js");
|
||||
|
||||
const runResourceCheck = (source: string) => spawnSync(
|
||||
process.execPath,
|
||||
[cli, "--resource", "--check", "-"],
|
||||
{ input: source, encoding: "utf8" },
|
||||
);
|
||||
const runResourceCheck = (source: string) =>
|
||||
spawnSync(process.execPath, [cli, "--resource", "--check", "-"], { input: source, encoding: "utf8" });
|
||||
|
||||
test("capability CLI checks a standalone interface resource", () => {
|
||||
const result = runResourceCheck(`interface WeatherBase id "interface:weather-base" revision "interface:weather-base@1" {
|
||||
const result =
|
||||
runResourceCheck(`interface WeatherBase id "interface:weather-base" revision "interface:weather-base@1" {
|
||||
value temperature id "member:weather:temperature" : double {
|
||||
get id "operation:weather:temperature:get";
|
||||
}
|
||||
@@ -22,7 +20,8 @@ test("capability CLI checks a standalone interface resource", () => {
|
||||
});
|
||||
|
||||
test("capability CLI reports invalid standalone interface members", () => {
|
||||
const result = runResourceCheck(`interface WeatherBase id "interface:weather-base" revision "interface:weather-base@1" {
|
||||
const result =
|
||||
runResourceCheck(`interface WeatherBase id "interface:weather-base" revision "interface:weather-base@1" {
|
||||
value temperature : definitely-not-a-type;
|
||||
}\n`);
|
||||
assert.notEqual(result.status, 0);
|
||||
|
||||
@@ -2,10 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { test } from "node:test";
|
||||
import {
|
||||
compileCapabilityResourceSource,
|
||||
compileCapabilitySource,
|
||||
} from "../src/capability-language/index.js";
|
||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js";
|
||||
import {
|
||||
capabilityFixtureSource,
|
||||
capabilityResourceSources,
|
||||
@@ -14,91 +11,76 @@ import {
|
||||
} from "./fixtures/capability-model.js";
|
||||
|
||||
test("RPC record fields retain declared reference targets and reject duplicate fields", () => {
|
||||
const compile = (fields: string) => compileCapabilityResourceSource(`
|
||||
const compile = (fields: string) =>
|
||||
compileCapabilityResourceSource(
|
||||
`
|
||||
external atom Board id "atom:board";
|
||||
package Runtime id "package:runtime" revision "package:runtime@1" {
|
||||
function move id "export:move" : record { ${fields} } -> unit;
|
||||
}`, {source: {repository: "https://example.test/runtime.git", commit: "a".repeat(40)}});
|
||||
}`,
|
||||
{ source: { repository: "https://example.test/runtime.git", commit: "a".repeat(40) } },
|
||||
);
|
||||
const result = compile("board: atom-ref<Board>; position: record { x: double; y: double; }; note: optional<string>;");
|
||||
assert.equal(result.ok, true, JSON.stringify(result.diagnostics));
|
||||
assert.equal(compile("board: atom-ref<Board>; board: string;").ok, false);
|
||||
});
|
||||
|
||||
const compileWebStudioFixture = (packageTransform = (source: string) => source) => {
|
||||
const fixture = (name: string) => readFileSync(
|
||||
resolve(process.cwd(), "test/fixtures", name),
|
||||
"utf8",
|
||||
);
|
||||
const react = compileCapabilityResourceSource(
|
||||
fixture("react-component.interface.qx"),
|
||||
{
|
||||
source: {
|
||||
repository: "https://repos.quixos.org/org-quixos-web-studio/interface-react-component.git",
|
||||
commit: "2".repeat(40),
|
||||
},
|
||||
const fixture = (name: string) => readFileSync(resolve(process.cwd(), "test/fixtures", name), "utf8");
|
||||
const react = compileCapabilityResourceSource(fixture("react-component.interface.qx"), {
|
||||
source: {
|
||||
repository: "https://repos.quixos.org/org-quixos-web-studio/interface-react-component.git",
|
||||
commit: "2".repeat(40),
|
||||
},
|
||||
);
|
||||
});
|
||||
if (!react.ok || react.resource.kind !== "interface") {
|
||||
throw new Error("ReactComponent fixture did not compile");
|
||||
}
|
||||
const named = compileCapabilityResourceSource(
|
||||
fixture("named.interface.qx"),
|
||||
{
|
||||
source: {
|
||||
repository: "https://repos.quixos.org/quixos-test/interface-named.git",
|
||||
commit: "5".repeat(40),
|
||||
},
|
||||
const named = compileCapabilityResourceSource(fixture("named.interface.qx"), {
|
||||
source: {
|
||||
repository: "https://repos.quixos.org/quixos-test/interface-named.git",
|
||||
commit: "5".repeat(40),
|
||||
},
|
||||
);
|
||||
});
|
||||
if (!named.ok || named.resource.kind !== "interface") {
|
||||
throw new Error("Named fixture did not compile");
|
||||
}
|
||||
const has = compileCapabilityResourceSource(
|
||||
fixture("has-react-component.interface.qx"),
|
||||
{
|
||||
source: {
|
||||
repository: "https://repos.quixos.org/org-quixos-web-studio/interface-has-react-component.git",
|
||||
commit: "3".repeat(40),
|
||||
},
|
||||
environment: {
|
||||
interfaces: new Map([["ReactComponent", react.resource.revision]]),
|
||||
interfaceClosure: [react.resource.revision],
|
||||
},
|
||||
const has = compileCapabilityResourceSource(fixture("has-react-component.interface.qx"), {
|
||||
source: {
|
||||
repository: "https://repos.quixos.org/org-quixos-web-studio/interface-has-react-component.git",
|
||||
commit: "3".repeat(40),
|
||||
},
|
||||
);
|
||||
environment: {
|
||||
interfaces: new Map([["ReactComponent", react.resource.revision]]),
|
||||
interfaceClosure: [react.resource.revision],
|
||||
},
|
||||
});
|
||||
if (!has.ok || has.resource.kind !== "interface") {
|
||||
throw new Error("HasReactComponent fixture did not compile");
|
||||
}
|
||||
const component = compileCapabilityResourceSource(
|
||||
packageTransform(fixture("component-runtime.package.qx")),
|
||||
{
|
||||
source: {
|
||||
repository: "https://repos.quixos.org/quixos-test/package-component-runtime.git",
|
||||
commit: "4".repeat(40),
|
||||
},
|
||||
environment: {
|
||||
interfaces: new Map([["Named", named.resource.revision]]),
|
||||
interfaceClosure: [named.resource.revision],
|
||||
},
|
||||
const component = compileCapabilityResourceSource(packageTransform(fixture("component-runtime.package.qx")), {
|
||||
source: {
|
||||
repository: "https://repos.quixos.org/quixos-test/package-component-runtime.git",
|
||||
commit: "4".repeat(40),
|
||||
},
|
||||
);
|
||||
environment: {
|
||||
interfaces: new Map([["Named", named.resource.revision]]),
|
||||
interfaceClosure: [named.resource.revision],
|
||||
},
|
||||
});
|
||||
if (!component.ok || component.resource.kind !== "package") {
|
||||
throw new Error("ComponentRuntime fixture did not compile");
|
||||
}
|
||||
return compileCapabilitySource(
|
||||
fixture("web-studio.capabilities.qx"),
|
||||
"web-studio.capabilities.qx",
|
||||
{
|
||||
interfaces: new Map([
|
||||
["Named", named.resource.revision],
|
||||
["ReactComponent", react.resource.revision],
|
||||
["HasReactComponent", has.resource.revision],
|
||||
]),
|
||||
packages: new Map([["ComponentRuntime", component.resource.revision]]),
|
||||
interfaceClosure: [named.resource.revision, react.resource.revision, has.resource.revision],
|
||||
packageClosure: [component.resource.revision],
|
||||
},
|
||||
);
|
||||
return compileCapabilitySource(fixture("web-studio.capabilities.qx"), "web-studio.capabilities.qx", {
|
||||
interfaces: new Map([
|
||||
["Named", named.resource.revision],
|
||||
["ReactComponent", react.resource.revision],
|
||||
["HasReactComponent", has.resource.revision],
|
||||
]),
|
||||
packages: new Map([["ComponentRuntime", component.resource.revision]]),
|
||||
interfaceClosure: [named.resource.revision, react.resource.revision, has.resource.revision],
|
||||
packageClosure: [component.resource.revision],
|
||||
});
|
||||
};
|
||||
|
||||
test("ANTLR parses and validates a complete capability workspace", () => {
|
||||
@@ -128,7 +110,7 @@ test("the v1 language has no implicit relationship materialization rule", () =>
|
||||
const result = compileCapabilityFixture({
|
||||
workspace: capabilityFixtureSource.replace(
|
||||
/\n}\s*$/,
|
||||
'\n materialize ProjectOwner.owner if absent with Person;\n}\n',
|
||||
"\n materialize ProjectOwner.owner if absent with Person;\n}\n",
|
||||
),
|
||||
});
|
||||
assert.equal(result.ok, false);
|
||||
@@ -140,10 +122,7 @@ 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',
|
||||
);
|
||||
.replace(/\n}\s*$/, '\n atom Project id "atom:project";\n atom Person id "atom:person";\n}\n');
|
||||
assert.equal(compileCapabilityFixture({ workspace: source }).ok, true);
|
||||
});
|
||||
|
||||
@@ -153,18 +132,17 @@ test("interfaces can declare ordinary call operations", () => {
|
||||
"bind summarize.call to package TodoRuntime.summaryGet",
|
||||
);
|
||||
const summary = capabilityResourceSources.summary.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 }",
|
||||
' 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 }',
|
||||
);
|
||||
const todo = capabilityResourceSources.todo.replace(
|
||||
"operation summaryGet id \"export:todo-runtime:summary-get\" : unit -> string",
|
||||
"operation summaryGet id \"export:todo-runtime:summary-get\" : string -> string",
|
||||
'operation summaryGet id "export:todo-runtime:summary-get" : unit -> string',
|
||||
'operation summaryGet id "export:todo-runtime:summary-get" : string -> string',
|
||||
);
|
||||
const result = compileCapabilityFixture({ workspace, summary, todo });
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
const member = result.workspace.interfaceImports
|
||||
.find((entry) => entry.displayName === "Summary")?.members[0];
|
||||
const member = result.workspace.interfaceImports.find((entry) => entry.displayName === "Summary")?.members[0];
|
||||
assert.equal(member?.kind, "operation");
|
||||
});
|
||||
|
||||
@@ -178,9 +156,7 @@ test("state defaults accept recursive JSON values", () => {
|
||||
const result = compileCapabilityFixture({ workspace: source });
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
const metadata = result.workspace.sharedAttachments.find(
|
||||
(entry) => entry.id === "slot:project:metadata",
|
||||
);
|
||||
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, {
|
||||
@@ -197,25 +173,20 @@ test("syntax errors retain source locations", () => {
|
||||
});
|
||||
assert.equal(result.ok, false);
|
||||
if (result.ok) return;
|
||||
assert.ok(result.diagnostics.some((entry) =>
|
||||
entry.phase === "syntax" && entry.line > 0
|
||||
));
|
||||
assert.ok(result.diagnostics.some((entry) => entry.phase === "syntax" && entry.line > 0));
|
||||
});
|
||||
|
||||
test("unknown authoring names are lowering errors", () => {
|
||||
const result = compileCapabilityFixture({
|
||||
workspace: capabilityFixtureSource.replace(
|
||||
"to state ProjectTitle.read",
|
||||
"to state NotAState.read",
|
||||
),
|
||||
workspace: capabilityFixtureSource.replace("to state ProjectTitle.read", "to state NotAState.read"),
|
||||
});
|
||||
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")
|
||||
));
|
||||
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", () => {
|
||||
@@ -227,9 +198,7 @@ test("well-formed but invalid programs report semantic paths", () => {
|
||||
});
|
||||
assert.equal(result.ok, false);
|
||||
if (result.ok) return;
|
||||
const diagnostic = result.diagnostics.find(
|
||||
(entry) => entry.code === "invalid-state-binding",
|
||||
);
|
||||
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"));
|
||||
@@ -240,20 +209,21 @@ test("Web Studio sidecars declare lazy materialization and checked cross-object
|
||||
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"
|
||||
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",
|
||||
}]);
|
||||
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 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;
|
||||
@@ -268,35 +238,40 @@ test("Web Studio sidecars declare lazy materialization and checked cross-object
|
||||
});
|
||||
|
||||
test("relationship materializers require a constructor from the host atom", () => {
|
||||
const result = compileWebStudioFixture((source) => source.replace(
|
||||
"constructs ProjectComponent : atom-ref<Project>;",
|
||||
"constructs ProjectComponent : unit;",
|
||||
));
|
||||
const result = compileWebStudioFixture((source) =>
|
||||
source.replace("constructs ProjectComponent : atom-ref<Project>;", "constructs ProjectComponent : unit;"),
|
||||
);
|
||||
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")
|
||||
));
|
||||
assert.ok(
|
||||
result.diagnostics.some(
|
||||
(entry) => entry.code === "invalid-relationship-materialization" && entry.message.includes("must accept"),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("callable interface ports require a full source dependency, not a nominal reference", () => {
|
||||
const result = compileCapabilityResourceSource(`
|
||||
const result = compileCapabilityResourceSource(
|
||||
`
|
||||
external interface Named revision "interface:named@1";
|
||||
package Runtime id "package:runtime" revision "package:runtime@1" {
|
||||
function readName id "export:runtime:read-name" : unit -> string requires {
|
||||
interface named id "port:runtime:named" : Named;
|
||||
};
|
||||
}
|
||||
`, {
|
||||
source: {
|
||||
repository: "https://repos.quixos.org/quixos-test/package-runtime.git",
|
||||
commit: "6".repeat(40),
|
||||
`,
|
||||
{
|
||||
source: {
|
||||
repository: "https://repos.quixos.org/quixos-test/package-runtime.git",
|
||||
commit: "6".repeat(40),
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
assert.equal(result.ok, false);
|
||||
if (result.ok) return;
|
||||
assert.ok(result.diagnostics.some((entry) =>
|
||||
entry.code === "nominal-interface-port" && entry.message.includes("import interface Named")
|
||||
));
|
||||
assert.ok(
|
||||
result.diagnostics.some(
|
||||
(entry) => entry.code === "nominal-interface-port" && entry.message.includes("import interface Named"),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,36 +9,36 @@ import {
|
||||
type CapabilityValidationIssueCode,
|
||||
type WorkspaceRevision,
|
||||
} from "../src/capability-model/index.js";
|
||||
import {
|
||||
fixtureId,
|
||||
makeValidCapabilityWorkspace,
|
||||
} from "./fixtures/capability-model.js";
|
||||
import { fixtureId, makeValidCapabilityWorkspace } from "./fixtures/capability-model.js";
|
||||
|
||||
test("ordinary state rejects managed references even under list/optional wrappers", () => {
|
||||
const workspace = makeValidCapabilityWorkspace();
|
||||
const state = [...workspace.sharedAttachments, ...workspace.conformances.flatMap((entry) => entry.privateAttachments)].find((entry) => entry.kind === "state")!;
|
||||
const state = [
|
||||
...workspace.sharedAttachments,
|
||||
...workspace.conformances.flatMap((entry) => entry.privateAttachments),
|
||||
].find((entry) => entry.kind === "state")!;
|
||||
if (state.kind !== "state") throw new Error("fixture missing state");
|
||||
state.valueType = {kind: "list", value: {kind: "optional", value: {kind: "object-ref", expectation: {kind: "atom", atomId: state.attachedTo}}}};
|
||||
state.valueType = {
|
||||
kind: "list",
|
||||
value: { kind: "optional", value: { kind: "object-ref", expectation: { kind: "atom", atomId: state.attachedTo } } },
|
||||
};
|
||||
assert.ok(validateWorkspaceRevision(workspace).some((entry) => entry.message.includes("graph relationships")));
|
||||
});
|
||||
|
||||
const expectIssue = (
|
||||
workspace: WorkspaceRevision,
|
||||
code: CapabilityValidationIssueCode,
|
||||
) => {
|
||||
const expectIssue = (workspace: WorkspaceRevision, code: CapabilityValidationIssueCode) => {
|
||||
const issues = validateWorkspaceRevision(workspace);
|
||||
assert.ok(
|
||||
issues.some((entry) => entry.code === code),
|
||||
`Expected ${code}, got:\n${issues
|
||||
.map((entry) => `${entry.code} ${entry.path}: ${entry.message}`)
|
||||
.join("\n")}`,
|
||||
`Expected ${code}, got:\n${issues.map((entry) => `${entry.code} ${entry.path}: ${entry.message}`).join("\n")}`,
|
||||
);
|
||||
assert.equal(compileWorkspaceRevision(workspace).ok, false);
|
||||
};
|
||||
|
||||
test("constructor dependency input contracts match the selected constructor", () => {
|
||||
const workspace = makeValidCapabilityWorkspace();
|
||||
const port = workspace.packageImports.flatMap((pkg) => pkg.exports).flatMap((entry) => entry.dependencyPorts)
|
||||
const port = workspace.packageImports
|
||||
.flatMap((pkg) => pkg.exports)
|
||||
.flatMap((entry) => entry.dependencyPorts)
|
||||
.find((port) => port.requirement.kind === "constructor")!;
|
||||
assert.equal(port.requirement.kind, "constructor");
|
||||
if (port.requirement.kind !== "constructor") return;
|
||||
@@ -52,14 +52,10 @@ const conformance = (
|
||||
workspace: WorkspaceRevision,
|
||||
identity: Pick<(typeof workspace.conformances)[number], "atomId" | "interfaceRevisionId">,
|
||||
) => {
|
||||
const result = workspace.conformances.find((entry) =>
|
||||
entry.atomId === identity.atomId &&
|
||||
entry.interfaceRevisionId === identity.interfaceRevisionId
|
||||
);
|
||||
assert.ok(
|
||||
result,
|
||||
`Missing fixture conformance ${identity.atomId} as ${identity.interfaceRevisionId}`,
|
||||
const result = workspace.conformances.find(
|
||||
(entry) => entry.atomId === identity.atomId && entry.interfaceRevisionId === identity.interfaceRevisionId,
|
||||
);
|
||||
assert.ok(result, `Missing fixture conformance ${identity.atomId} as ${identity.interfaceRevisionId}`);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -70,12 +66,7 @@ test("the representative v1 workspace compiles to native and package plans", ()
|
||||
assert.equal(compiled.ok, true);
|
||||
if (!compiled.ok) return;
|
||||
|
||||
const title = resolveOperationPlan(
|
||||
compiled.plan,
|
||||
fixtureId.project,
|
||||
fixtureId.namedV1,
|
||||
fixtureId.namedGet,
|
||||
);
|
||||
const title = resolveOperationPlan(compiled.plan, fixtureId.project, fixtureId.namedV1, fixtureId.namedGet);
|
||||
assert.equal(title?.kind, "state");
|
||||
if (title?.kind === "state") {
|
||||
assert.equal(title.binding.slotId, fixtureId.projectTitle);
|
||||
@@ -83,12 +74,7 @@ test("the representative v1 workspace compiles to native and package plans", ()
|
||||
assert.deepEqual(title.attachment.owner, { kind: "workspace" });
|
||||
}
|
||||
|
||||
const owner = resolveOperationPlan(
|
||||
compiled.plan,
|
||||
fixtureId.project,
|
||||
fixtureId.ownedV1,
|
||||
fixtureId.ownerResolve,
|
||||
);
|
||||
const owner = resolveOperationPlan(compiled.plan, fixtureId.project, fixtureId.ownedV1, fixtureId.ownerResolve);
|
||||
assert.equal(owner?.kind, "edge");
|
||||
if (owner?.kind === "edge") {
|
||||
assert.equal(owner.binding.edgeTypeId, fixtureId.projectOwner);
|
||||
@@ -98,12 +84,7 @@ test("the representative v1 workspace compiles to native and package plans", ()
|
||||
});
|
||||
}
|
||||
|
||||
const summary = resolveOperationPlan(
|
||||
compiled.plan,
|
||||
fixtureId.project,
|
||||
fixtureId.summaryV1,
|
||||
fixtureId.summaryGet,
|
||||
);
|
||||
const summary = resolveOperationPlan(compiled.plan, fixtureId.project, fixtureId.summaryV1, fixtureId.summaryGet);
|
||||
assert.equal(summary?.kind, "package");
|
||||
if (summary?.kind === "package") {
|
||||
assert.equal(summary.packageRevision.revisionId, fixtureId.todoRuntimeV1);
|
||||
@@ -126,10 +107,7 @@ test("the closure is an exact tree-shaking boundary", () => {
|
||||
const closure = computeCapabilityClosure(result.plan, [
|
||||
{ atomId: fixtureId.project, interfaceRevisionId: fixtureId.summaryV1 },
|
||||
]);
|
||||
assert.deepEqual(closure.conformances, [
|
||||
fixtureId.projectNamedConformance,
|
||||
fixtureId.projectSummaryConformance,
|
||||
]);
|
||||
assert.deepEqual(closure.conformances, [fixtureId.projectNamedConformance, fixtureId.projectSummaryConformance]);
|
||||
assert.deepEqual(closure.packageRevisionIds, [fixtureId.todoRuntimeV1]);
|
||||
assert.deepEqual(closure.attachmentIds, [fixtureId.projectTitle]);
|
||||
assert.deepEqual(closure.constructorAtomIds, [fixtureId.person]);
|
||||
@@ -140,18 +118,12 @@ test("compiled plans are snapshots, not mutable authoring state", () => {
|
||||
const result = compileWorkspaceRevision(workspace);
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
conformance(workspace, fixtureId.projectNamedConformance)
|
||||
.operationBindings[0]!.binding = {
|
||||
kind: "state",
|
||||
slotId: fixtureId.personName,
|
||||
primitive: "read",
|
||||
};
|
||||
const resolved = resolveOperationPlan(
|
||||
result.plan,
|
||||
fixtureId.project,
|
||||
fixtureId.namedV1,
|
||||
fixtureId.namedGet,
|
||||
);
|
||||
conformance(workspace, fixtureId.projectNamedConformance).operationBindings[0]!.binding = {
|
||||
kind: "state",
|
||||
slotId: fixtureId.personName,
|
||||
primitive: "read",
|
||||
};
|
||||
const resolved = resolveOperationPlan(result.plan, fixtureId.project, fixtureId.namedV1, fixtureId.namedGet);
|
||||
assert.equal(resolved?.kind, "state");
|
||||
if (resolved?.kind === "state") {
|
||||
assert.equal(resolved.binding.slotId, fixtureId.projectTitle);
|
||||
@@ -164,20 +136,14 @@ test("a conformance binds every operation exactly once", () => {
|
||||
expectIssue(missing, "missing-operation-binding");
|
||||
|
||||
const duplicate = makeValidCapabilityWorkspace();
|
||||
const bindings = conformance(
|
||||
duplicate,
|
||||
fixtureId.projectNamedConformance,
|
||||
).operationBindings;
|
||||
const bindings = conformance(duplicate, fixtureId.projectNamedConformance).operationBindings;
|
||||
bindings.push(structuredClone(bindings[0]!));
|
||||
expectIssue(duplicate, "duplicate-operation-binding");
|
||||
});
|
||||
|
||||
test("private attachments are visible only to their owning conformance", () => {
|
||||
const workspace = makeValidCapabilityWorkspace();
|
||||
const binding = conformance(
|
||||
workspace,
|
||||
fixtureId.projectNamedConformance,
|
||||
).operationBindings[0]!.binding;
|
||||
const binding = conformance(workspace, fixtureId.projectNamedConformance).operationBindings[0]!.binding;
|
||||
assert.equal(binding.kind, "state");
|
||||
if (binding.kind !== "state") return;
|
||||
binding.slotId = fixtureId.personName;
|
||||
@@ -186,15 +152,10 @@ test("private attachments are visible only to their owning conformance", () => {
|
||||
|
||||
test("related-object dependency views cannot traverse another conformance's private edge", () => {
|
||||
const workspace = makeValidCapabilityWorkspace();
|
||||
const packageBinding = conformance(
|
||||
workspace,
|
||||
fixtureId.projectSummaryConformance,
|
||||
).operationBindings[0]!.binding;
|
||||
const packageBinding = conformance(workspace, fixtureId.projectSummaryConformance).operationBindings[0]!.binding;
|
||||
assert.equal(packageBinding.kind, "package");
|
||||
if (packageBinding.kind !== "package") return;
|
||||
const named = packageBinding.dependencies.find(
|
||||
(dependency) => dependency.portId === fixtureId.namedPort,
|
||||
);
|
||||
const named = packageBinding.dependencies.find((dependency) => dependency.portId === fixtureId.namedPort);
|
||||
assert.ok(named && named.binding.kind === "interface");
|
||||
named.binding.via = {
|
||||
edgeTypeId: fixtureId.projectOwner,
|
||||
@@ -205,20 +166,31 @@ test("related-object dependency views cannot traverse another conformance's priv
|
||||
const edge = owner.privateAttachments.find((entry) => entry.kind === "edge" && entry.id === fixtureId.projectOwner);
|
||||
assert.ok(edge?.kind === "edge");
|
||||
edge.endpoints.find((endpoint) => endpoint.projectionId === fixtureId.projectOwnerProjection)!.publicTraversal = true;
|
||||
assert.equal(validateWorkspaceRevision(workspace).some((entry) => entry.code === "private-attachment-access"), false, "Only an explicitly exported read-only traversal crosses ownership");
|
||||
assert.equal(
|
||||
validateWorkspaceRevision(workspace).some((entry) => entry.code === "private-attachment-access"),
|
||||
false,
|
||||
"Only an explicitly exported read-only traversal crosses ownership",
|
||||
);
|
||||
});
|
||||
|
||||
test("public traversal permits a native inverse read without exporting mutation authority", () => {
|
||||
const workspace = makeValidCapabilityWorkspace();
|
||||
const owned = conformance(workspace, fixtureId.projectOwnedConformance);
|
||||
const index = owned.privateAttachments.findIndex(entry => entry.kind === "edge" && entry.id === fixtureId.projectOwner);
|
||||
const index = owned.privateAttachments.findIndex(
|
||||
(entry) => entry.kind === "edge" && entry.id === fixtureId.projectOwner,
|
||||
);
|
||||
const edge = owned.privateAttachments.splice(index, 1)[0];
|
||||
assert.ok(edge?.kind === "edge");
|
||||
conformance(workspace, fixtureId.projectNamedConformance).privateAttachments.push(edge);
|
||||
expectIssue(workspace, "private-attachment-access");
|
||||
edge.endpoints.find(endpoint => endpoint.projectionId === fixtureId.projectOwnerProjection)!.publicTraversal = true;
|
||||
edge.endpoints.find((endpoint) => endpoint.projectionId === fixtureId.projectOwnerProjection)!.publicTraversal = true;
|
||||
const readPath = `conformances[${workspace.conformances.indexOf(owned)}].operationBindings[0]`;
|
||||
assert.equal(validateWorkspaceRevision(workspace).some(issue => issue.code === "private-attachment-access" && issue.path.startsWith(readPath)), false);
|
||||
assert.equal(
|
||||
validateWorkspaceRevision(workspace).some(
|
||||
(issue) => issue.code === "private-attachment-access" && issue.path.startsWith(readPath),
|
||||
),
|
||||
false,
|
||||
);
|
||||
const binding = owned.operationBindings[0].binding;
|
||||
assert.ok(binding.kind === "edge");
|
||||
binding.primitive = "connect";
|
||||
@@ -227,19 +199,13 @@ test("public traversal permits a native inverse read without exporting mutation
|
||||
|
||||
test("native state and edge providers must match operation shape", () => {
|
||||
const stateWorkspace = makeValidCapabilityWorkspace();
|
||||
const state = conformance(
|
||||
stateWorkspace,
|
||||
fixtureId.projectNamedConformance,
|
||||
).operationBindings[0]!.binding;
|
||||
const state = conformance(stateWorkspace, fixtureId.projectNamedConformance).operationBindings[0]!.binding;
|
||||
assert.equal(state.kind, "state");
|
||||
if (state.kind === "state") state.primitive = "write";
|
||||
expectIssue(stateWorkspace, "invalid-state-binding");
|
||||
|
||||
const edgeWorkspace = makeValidCapabilityWorkspace();
|
||||
const edge = conformance(
|
||||
edgeWorkspace,
|
||||
fixtureId.projectOwnedConformance,
|
||||
).operationBindings[0]!.binding;
|
||||
const edge = conformance(edgeWorkspace, fixtureId.projectOwnedConformance).operationBindings[0]!.binding;
|
||||
assert.equal(edge.kind, "edge");
|
||||
if (edge.kind === "edge") edge.primitive = "connect";
|
||||
expectIssue(edgeWorkspace, "invalid-edge-binding");
|
||||
@@ -247,18 +213,13 @@ test("native state and edge providers must match operation shape", () => {
|
||||
|
||||
test("package dependencies are complete, exact, and explicitly injected", () => {
|
||||
const missing = makeValidCapabilityWorkspace();
|
||||
const packageBinding = conformance(
|
||||
missing,
|
||||
fixtureId.projectSummaryConformance,
|
||||
).operationBindings[0]!.binding;
|
||||
const packageBinding = conformance(missing, fixtureId.projectSummaryConformance).operationBindings[0]!.binding;
|
||||
assert.equal(packageBinding.kind, "package");
|
||||
if (packageBinding.kind === "package") packageBinding.dependencies.pop();
|
||||
expectIssue(missing, "invalid-dependency-binding");
|
||||
|
||||
const wrongType = makeValidCapabilityWorkspace();
|
||||
const summaryExport = wrongType.packageImports[0]!.exports.find(
|
||||
(entry) => entry.id === fixtureId.summaryGetExport,
|
||||
);
|
||||
const summaryExport = wrongType.packageImports[0]!.exports.find((entry) => entry.id === fixtureId.summaryGetExport);
|
||||
assert.ok(summaryExport);
|
||||
summaryExport.dependencyPorts[0]!.requirement = {
|
||||
kind: "state",
|
||||
@@ -270,19 +231,14 @@ test("package dependencies are complete, exact, and explicitly injected", () =>
|
||||
|
||||
test("package receiver requirements cannot depend on themselves", () => {
|
||||
const workspace = makeValidCapabilityWorkspace();
|
||||
const summaryExport = workspace.packageImports[0]!.exports.find(
|
||||
(entry) => entry.id === fixtureId.summaryGetExport,
|
||||
);
|
||||
const summaryExport = workspace.packageImports[0]!.exports.find((entry) => entry.id === fixtureId.summaryGetExport);
|
||||
assert.ok(summaryExport && summaryExport.kind === "operation");
|
||||
summaryExport.receiverRequirement = {
|
||||
kind: "all-interfaces",
|
||||
interfaceRevisionIds: [fixtureId.summaryV1],
|
||||
};
|
||||
summaryExport.dependencyPorts = [];
|
||||
const packageBinding = conformance(
|
||||
workspace,
|
||||
fixtureId.projectSummaryConformance,
|
||||
).operationBindings[0]!.binding;
|
||||
const packageBinding = conformance(workspace, fixtureId.projectSummaryConformance).operationBindings[0]!.binding;
|
||||
assert.equal(packageBinding.kind, "package");
|
||||
if (packageBinding.kind === "package") packageBinding.dependencies = [];
|
||||
expectIssue(workspace, "cyclic-conformance-requirement");
|
||||
|
||||
+56
-16
@@ -1,13 +1,30 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { capabilityId as id, valueType, validateWorkspaceRevision } from "../src/capability-model/index.js";
|
||||
import { contentDigest, planEvolution, runtimeContracts, storageContracts, storageChangeRequiresMigration } from "../src/capability-model/evolution.js";
|
||||
import { capabilityFixtureSource, capabilityResourceSources, compileCapabilityFixture, makeValidCapabilityWorkspace } from "./fixtures/capability-model.js";
|
||||
import {
|
||||
contentDigest,
|
||||
planEvolution,
|
||||
runtimeContracts,
|
||||
storageContracts,
|
||||
storageChangeRequiresMigration,
|
||||
} from "../src/capability-model/evolution.js";
|
||||
import {
|
||||
capabilityFixtureSource,
|
||||
capabilityResourceSources,
|
||||
compileCapabilityFixture,
|
||||
makeValidCapabilityWorkspace,
|
||||
} from "./fixtures/capability-model.js";
|
||||
|
||||
test("QX carries stable conformance IDs and implementation semantic majors", () => {
|
||||
const result = compileCapabilityFixture({
|
||||
workspace: capabilityFixtureSource.replace("conform Project as Named {", 'conform Project as Named id "conformance:project:named" semantic-major 3 {'),
|
||||
todo: capabilityResourceSources.todo.replace('revision "package:todo-runtime@1" {', 'revision "package:todo-runtime@1" semantic-major 2 {'),
|
||||
workspace: capabilityFixtureSource.replace(
|
||||
"conform Project as Named {",
|
||||
'conform Project as Named id "conformance:project:named" semantic-major 3 {',
|
||||
),
|
||||
todo: capabilityResourceSources.todo.replace(
|
||||
'revision "package:todo-runtime@1" {',
|
||||
'revision "package:todo-runtime@1" semantic-major 2 {',
|
||||
),
|
||||
});
|
||||
assert.ok(result.ok, JSON.stringify(result));
|
||||
assert.equal(result.workspace.conformances[0]!.id, "conformance:project:named");
|
||||
@@ -43,16 +60,32 @@ test("a workspace root change and display names do not restart package runtimes"
|
||||
after.conformances.reverse();
|
||||
after.interfaceImports.reverse();
|
||||
const report = planEvolution(before, after, { allowLegacy: true });
|
||||
assert.deepEqual(report.runtimeActions.map((entry) => entry.action), ["keep"]);
|
||||
assert.deepEqual(
|
||||
report.runtimeActions.map((entry) => entry.action),
|
||||
["keep"],
|
||||
);
|
||||
assert.deepEqual(report.storageChanges, []);
|
||||
assert.deepEqual(report.packageChecks, []);
|
||||
});
|
||||
|
||||
test("consumer changes preserve unrelated resource owners", () => {
|
||||
const before = makeValidCapabilityWorkspace();
|
||||
before.packageImports.push({ packageId: id.package("package:resource-owner"), revisionId: id.packageRevision("package:resource-owner@1"),
|
||||
displayName: "ResourceOwner", source: { repository: "https://example.org/owner.git", commit: "a".repeat(40) },
|
||||
exports: [{ kind: "function", id: id.packageExport("export:owner:ping"), displayName: "ping", inputType: valueType.unit, outputType: valueType.unit, dependencyPorts: [] }] });
|
||||
before.packageImports.push({
|
||||
packageId: id.package("package:resource-owner"),
|
||||
revisionId: id.packageRevision("package:resource-owner@1"),
|
||||
displayName: "ResourceOwner",
|
||||
source: { repository: "https://example.org/owner.git", commit: "a".repeat(40) },
|
||||
exports: [
|
||||
{
|
||||
kind: "function",
|
||||
id: id.packageExport("export:owner:ping"),
|
||||
displayName: "ping",
|
||||
inputType: valueType.unit,
|
||||
outputType: valueType.unit,
|
||||
dependencyPorts: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const after = structuredClone(before);
|
||||
after.packageImports[0]!.source.commit = "e".repeat(40);
|
||||
const actions = planEvolution(before, after, { allowLegacy: true }).runtimeActions;
|
||||
@@ -82,8 +115,12 @@ test("semantic review receipts cover exact consumer/provider contracts", () => {
|
||||
const report = planEvolution(before, after, { allowLegacy: true });
|
||||
assert.equal(report.reviews.length, 1);
|
||||
assert.equal(report.reviews[0]!.accepted, false);
|
||||
const receipt = { requirementDigest: report.reviews[0]!.requirementDigest, decision: "accepted-unchanged" as const,
|
||||
rationale: "Reviewed the semantic change against summary behavior", agentId: "workspace-agent" };
|
||||
const receipt = {
|
||||
requirementDigest: report.reviews[0]!.requirementDigest,
|
||||
decision: "accepted-unchanged" as const,
|
||||
rationale: "Reviewed the semantic change against summary behavior",
|
||||
agentId: "workspace-agent",
|
||||
};
|
||||
assert.equal(planEvolution(before, after, { allowLegacy: true, reviews: [receipt] }).reviews[0]!.accepted, true);
|
||||
after.packageImports[0]!.source.commit = "f".repeat(40);
|
||||
assert.equal(planEvolution(before, after, { allowLegacy: true, reviews: [receipt] }).reviews[0]!.accepted, false);
|
||||
@@ -95,15 +132,18 @@ test("evolution enrollment is explicit and never erases legacy ownership", () =>
|
||||
assert.ok(report.blockers.some((entry) => entry.includes("workspace-shared")));
|
||||
assert.ok(report.blockers.some((entry) => entry.includes("authored ID")));
|
||||
assert.equal(runtimeContracts(workspace).length, 1);
|
||||
assert.throws(() => planEvolution(workspace, { ...workspace, workspaceId: id.workspace("other") }), /different workspace/);
|
||||
assert.throws(
|
||||
() => planEvolution(workspace, { ...workspace, workspaceId: id.workspace("other") }),
|
||||
/different workspace/,
|
||||
);
|
||||
});
|
||||
|
||||
test("automatic preservation distinguishes additions and defaults from incompatible storage", () => {
|
||||
const workspace = makeValidCapabilityWorkspace();
|
||||
const before = storageContracts(workspace).find(entry => entry.kind === "state")!;
|
||||
const before = storageContracts(workspace).find((entry) => entry.kind === "state")!;
|
||||
const next = structuredClone(before);
|
||||
const value = next.definition as Record<string, unknown>;
|
||||
value.defaultValue = {displayName: "important user data"};
|
||||
value.defaultValue = { displayName: "important user data" };
|
||||
assert.equal(storageChangeRequiresMigration(before, next, new Set()), false);
|
||||
next.ownerId = "another-owner";
|
||||
assert.equal(storageChangeRequiresMigration(before, next, new Set()), true);
|
||||
@@ -111,10 +151,10 @@ test("automatic preservation distinguishes additions and defaults from incompati
|
||||
delete value.defaultValue;
|
||||
assert.equal(storageChangeRequiresMigration(undefined, next, new Set([value.attachedTo as string])), true);
|
||||
assert.equal(storageChangeRequiresMigration(undefined, next, new Set()), false);
|
||||
const slot = workspace.sharedAttachments.find(entry => entry.kind === "state")!;
|
||||
const slot = workspace.sharedAttachments.find((entry) => entry.kind === "state")!;
|
||||
if (slot.kind !== "state") throw new Error("fixture slot");
|
||||
slot.defaultValue = {displayName: "one"};
|
||||
slot.defaultValue = { displayName: "one" };
|
||||
const first = storageContracts(workspace);
|
||||
slot.defaultValue = {displayName: "two"};
|
||||
slot.defaultValue = { displayName: "two" };
|
||||
assert.notDeepEqual(storageContracts(workspace), first, "user data must not be stripped as schema metadata");
|
||||
});
|
||||
|
||||
+21
-11
@@ -1,34 +1,44 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {mkdtemp, rm} from "node:fs/promises";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import {spawn} from "node:child_process";
|
||||
import {once} from "node:events";
|
||||
import {withFileLock} from "../src/capability-language/file-lock.js";
|
||||
import { spawn } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { withFileLock } from "../src/capability-language/file-lock.js";
|
||||
|
||||
test("authoring lock excludes concurrent mutations and survives owner death", async context => {
|
||||
test("authoring lock excludes concurrent mutations and survives owner death", async (context) => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "qx-lock-test-"));
|
||||
context.after(() => rm(root, {recursive: true, force: true}));
|
||||
context.after(() => rm(root, { recursive: true, force: true }));
|
||||
const filename = path.join(root, "lock");
|
||||
const events: string[] = [];
|
||||
let queued: Promise<void>;
|
||||
await withFileLock(filename, async () => {
|
||||
queued = withFileLock(filename, async () => {events.push("second");});
|
||||
queued = withFileLock(filename, async () => {
|
||||
events.push("second");
|
||||
});
|
||||
events.push("first");
|
||||
});
|
||||
await queued!;
|
||||
assert.deepEqual(events, ["first", "second"]);
|
||||
const module = new URL("../src/capability-language/file-lock.js", import.meta.url).href;
|
||||
const owner = spawn(process.execPath, ["--input-type=module", "-e",
|
||||
`import {withFileLock} from ${JSON.stringify(module)}; await withFileLock(${JSON.stringify(filename)}, async () => {process.stdout.write('ready'); await new Promise(() => {});});`],
|
||||
{stdio: ["ignore", "pipe", "pipe"]});
|
||||
const owner = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
`import {withFileLock} from ${JSON.stringify(module)}; await withFileLock(${JSON.stringify(filename)}, async () => {process.stdout.write('ready'); await new Promise(() => {});});`,
|
||||
],
|
||||
{ stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
context.after(() => owner.kill("SIGKILL"));
|
||||
await once(owner.stdout, "data");
|
||||
const exited = once(owner, "exit");
|
||||
owner.kill("SIGKILL");
|
||||
await exited;
|
||||
let acquired = false;
|
||||
await withFileLock(filename, async () => {acquired = true;});
|
||||
await withFileLock(filename, async () => {
|
||||
acquired = true;
|
||||
});
|
||||
assert.equal(acquired, true);
|
||||
});
|
||||
|
||||
Vendored
+48
-105
@@ -1,18 +1,12 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
capabilityId,
|
||||
type WorkspaceRevision,
|
||||
} from "../../src/capability-model/index.js";
|
||||
import { capabilityId, type WorkspaceRevision } from "../../src/capability-model/index.js";
|
||||
import {
|
||||
compileCapabilityResourceSource,
|
||||
compileCapabilitySource,
|
||||
type CapabilityImportEnvironment,
|
||||
} from "../../src/capability-language/index.js";
|
||||
import type {
|
||||
InterfaceRevision,
|
||||
PackageRevision,
|
||||
} from "../../src/capability-model/index.js";
|
||||
import type { InterfaceRevision, PackageRevision } from "../../src/capability-model/index.js";
|
||||
|
||||
export const fixtureId = {
|
||||
workspace: capabilityId.workspace("workspace:todo"),
|
||||
@@ -29,9 +23,7 @@ export const fixtureId = {
|
||||
projectTitle: capabilityId.slot("slot:project:title"),
|
||||
personName: capabilityId.slot("slot:person:name"),
|
||||
projectOwner: capabilityId.edgeType("edge:project:owner"),
|
||||
projectOwnerProjection: capabilityId.edgeProjection(
|
||||
"projection:project-owner:owner",
|
||||
),
|
||||
projectOwnerProjection: capabilityId.edgeProjection("projection:project-owner:owner"),
|
||||
projectNamedConformance: {
|
||||
atomId: capabilityId.atom("atom:project"),
|
||||
interfaceRevisionId: capabilityId.interfaceRevision("interface:named@1"),
|
||||
@@ -49,31 +41,18 @@ export const fixtureId = {
|
||||
interfaceRevisionId: capabilityId.interfaceRevision("interface:summary@1"),
|
||||
},
|
||||
todoRuntimeV1: capabilityId.packageRevision("package:todo-runtime@1"),
|
||||
summaryGetExport: capabilityId.packageExport(
|
||||
"export:todo-runtime:summary-get",
|
||||
),
|
||||
createPersonExport: capabilityId.packageExport(
|
||||
"export:todo-runtime:create-person",
|
||||
),
|
||||
summaryGetExport: capabilityId.packageExport("export:todo-runtime:summary-get"),
|
||||
createPersonExport: capabilityId.packageExport("export:todo-runtime:create-person"),
|
||||
titlePort: capabilityId.dependencyPort("port:summary:title"),
|
||||
namedPort: capabilityId.dependencyPort("port:summary:named"),
|
||||
personPort: capabilityId.dependencyPort("port:summary:person"),
|
||||
} as const;
|
||||
|
||||
export const capabilityFixturePath = resolve(
|
||||
process.cwd(),
|
||||
"test/fixtures/todo.capabilities.qx",
|
||||
);
|
||||
export const capabilityFixturePath = resolve(process.cwd(), "test/fixtures/todo.capabilities.qx");
|
||||
|
||||
export const capabilityFixtureSource = readFileSync(
|
||||
capabilityFixturePath,
|
||||
"utf8",
|
||||
);
|
||||
export const capabilityFixtureSource = readFileSync(capabilityFixturePath, "utf8");
|
||||
|
||||
const resourceSource = (name: string) => readFileSync(
|
||||
resolve(process.cwd(), "test/fixtures", name),
|
||||
"utf8",
|
||||
);
|
||||
const resourceSource = (name: string) => readFileSync(resolve(process.cwd(), "test/fixtures", name), "utf8");
|
||||
|
||||
export const capabilityResourceSources = {
|
||||
named: resourceSource("named.interface.qx"),
|
||||
@@ -85,110 +64,74 @@ export const capabilityResourceSources = {
|
||||
const source = (repository: string, commit: string) => ({ repository, commit });
|
||||
|
||||
const interfaceRevision = (
|
||||
resource: { kind: "interface"; revision: InterfaceRevision } |
|
||||
{ kind: "package"; revision: PackageRevision },
|
||||
resource: { kind: "interface"; revision: InterfaceRevision } | { kind: "package"; revision: PackageRevision },
|
||||
) => {
|
||||
if (resource.kind !== "interface") throw new Error("Expected interface fixture");
|
||||
return resource.revision;
|
||||
};
|
||||
|
||||
const packageRevision = (
|
||||
resource: { kind: "interface"; revision: InterfaceRevision } |
|
||||
{ kind: "package"; revision: PackageRevision },
|
||||
resource: { kind: "interface"; revision: InterfaceRevision } | { kind: "package"; revision: PackageRevision },
|
||||
) => {
|
||||
if (resource.kind !== "package") throw new Error("Expected package fixture");
|
||||
return resource.revision;
|
||||
};
|
||||
|
||||
export const compileCapabilityFixture = (overrides: Partial<{
|
||||
workspace: string;
|
||||
named: string;
|
||||
owned: string;
|
||||
summary: string;
|
||||
todo: string;
|
||||
}> = {}) => {
|
||||
const named = compileCapabilityResourceSource(
|
||||
overrides.named ?? capabilityResourceSources.named,
|
||||
{
|
||||
source: source(
|
||||
"https://repos.quixos.org/quixos-todo/interface-named.git",
|
||||
"2".repeat(40),
|
||||
),
|
||||
fileName: "named.interface.qx",
|
||||
},
|
||||
);
|
||||
export const compileCapabilityFixture = (
|
||||
overrides: Partial<{
|
||||
workspace: string;
|
||||
named: string;
|
||||
owned: string;
|
||||
summary: string;
|
||||
todo: string;
|
||||
}> = {},
|
||||
) => {
|
||||
const named = compileCapabilityResourceSource(overrides.named ?? capabilityResourceSources.named, {
|
||||
source: source("https://repos.quixos.org/quixos-todo/interface-named.git", "2".repeat(40)),
|
||||
fileName: "named.interface.qx",
|
||||
});
|
||||
if (!named.ok) return named;
|
||||
const namedRevision = interfaceRevision(named.resource);
|
||||
const namedEnvironment: CapabilityImportEnvironment = {
|
||||
interfaces: new Map([["Named", namedRevision]]),
|
||||
interfaceClosure: [namedRevision],
|
||||
};
|
||||
const owned = compileCapabilityResourceSource(
|
||||
overrides.owned ?? capabilityResourceSources.owned,
|
||||
{
|
||||
source: source(
|
||||
"https://repos.quixos.org/quixos-todo/interface-owned.git",
|
||||
"3".repeat(40),
|
||||
),
|
||||
fileName: "owned.interface.qx",
|
||||
environment: namedEnvironment,
|
||||
},
|
||||
);
|
||||
const owned = compileCapabilityResourceSource(overrides.owned ?? capabilityResourceSources.owned, {
|
||||
source: source("https://repos.quixos.org/quixos-todo/interface-owned.git", "3".repeat(40)),
|
||||
fileName: "owned.interface.qx",
|
||||
environment: namedEnvironment,
|
||||
});
|
||||
if (!owned.ok) return owned;
|
||||
const ownedRevision = interfaceRevision(owned.resource);
|
||||
const summary = compileCapabilityResourceSource(
|
||||
overrides.summary ?? capabilityResourceSources.summary,
|
||||
{
|
||||
source: source(
|
||||
"https://repos.quixos.org/quixos-todo/interface-summary.git",
|
||||
"4".repeat(40),
|
||||
),
|
||||
fileName: "summary.interface.qx",
|
||||
},
|
||||
);
|
||||
const summary = compileCapabilityResourceSource(overrides.summary ?? capabilityResourceSources.summary, {
|
||||
source: source("https://repos.quixos.org/quixos-todo/interface-summary.git", "4".repeat(40)),
|
||||
fileName: "summary.interface.qx",
|
||||
});
|
||||
if (!summary.ok) return summary;
|
||||
const summaryRevision = interfaceRevision(summary.resource);
|
||||
const todo = compileCapabilityResourceSource(
|
||||
overrides.todo ?? capabilityResourceSources.todo,
|
||||
{
|
||||
source: source(
|
||||
"https://repos.quixos.org/quixos-todo/package-todo-runtime.git",
|
||||
"5".repeat(40),
|
||||
),
|
||||
fileName: "todo.package.qx",
|
||||
environment: namedEnvironment,
|
||||
},
|
||||
);
|
||||
const todo = compileCapabilityResourceSource(overrides.todo ?? capabilityResourceSources.todo, {
|
||||
source: source("https://repos.quixos.org/quixos-todo/package-todo-runtime.git", "5".repeat(40)),
|
||||
fileName: "todo.package.qx",
|
||||
environment: namedEnvironment,
|
||||
});
|
||||
if (!todo.ok) return todo;
|
||||
const todoRevision = packageRevision(todo.resource);
|
||||
return compileCapabilitySource(
|
||||
overrides.workspace ?? capabilityFixtureSource,
|
||||
capabilityFixturePath,
|
||||
{
|
||||
interfaces: new Map([
|
||||
["Named", namedRevision],
|
||||
["Owned", ownedRevision],
|
||||
["Summary", summaryRevision],
|
||||
]),
|
||||
packages: new Map([["TodoRuntime", todoRevision]]),
|
||||
interfaceClosure: [
|
||||
namedRevision,
|
||||
ownedRevision,
|
||||
summaryRevision,
|
||||
],
|
||||
packageClosure: [todoRevision],
|
||||
},
|
||||
);
|
||||
return compileCapabilitySource(overrides.workspace ?? capabilityFixtureSource, capabilityFixturePath, {
|
||||
interfaces: new Map([
|
||||
["Named", namedRevision],
|
||||
["Owned", ownedRevision],
|
||||
["Summary", summaryRevision],
|
||||
]),
|
||||
packages: new Map([["TodoRuntime", todoRevision]]),
|
||||
interfaceClosure: [namedRevision, ownedRevision, summaryRevision],
|
||||
packageClosure: [todoRevision],
|
||||
});
|
||||
};
|
||||
|
||||
export const makeValidCapabilityWorkspace = (): WorkspaceRevision => {
|
||||
const result = compileCapabilityFixture();
|
||||
if (!result.ok) {
|
||||
throw new Error(
|
||||
result.diagnostics
|
||||
.map((entry) => `${entry.phase}/${entry.code}: ${entry.message}`)
|
||||
.join("\n"),
|
||||
);
|
||||
throw new Error(result.diagnostics.map((entry) => `${entry.phase}/${entry.code}: ${entry.message}`).join("\n"));
|
||||
}
|
||||
return structuredClone(result.workspace);
|
||||
};
|
||||
|
||||
Vendored
+2
-2
@@ -8,7 +8,7 @@ workspace Todo id "workspace:todo" revision "workspace:todo@1" commit "111111111
|
||||
import package TodoRuntime;
|
||||
|
||||
shared state ProjectTitle id "slot:project:title" on Project : string
|
||||
policy optimistic-register default "Untitled project";
|
||||
policy optimistic-register default "Untitled project";
|
||||
|
||||
conform Project as Named {
|
||||
bind name.get to state ProjectTitle.read;
|
||||
@@ -19,7 +19,7 @@ workspace Todo id "workspace:todo" revision "workspace:todo@1" commit "111111111
|
||||
|
||||
conform Person as Named {
|
||||
private state PersonName id "slot:person:name" on Person : string
|
||||
policy optimistic-register default "Anonymous";
|
||||
policy optimistic-register default "Anonymous";
|
||||
bind name.get to state PersonName.read;
|
||||
bind name.set to state PersonName.write;
|
||||
bind name.watch-start to state PersonName.watch-start;
|
||||
|
||||
Vendored
+5
-5
@@ -3,10 +3,10 @@ external atom Person id "atom:person";
|
||||
|
||||
package TodoRuntime id "package:todo-runtime" revision "package:todo-runtime@1" {
|
||||
operation summaryGet id "export:todo-runtime:summary-get" : unit -> string
|
||||
mode call receiver interfaces [Named] requires {
|
||||
state title id "port:summary:title" : string [read];
|
||||
interface named id "port:summary:named" : Named;
|
||||
constructor person id "port:summary:person" : Person;
|
||||
};
|
||||
mode call receiver interfaces [Named] requires {
|
||||
state title id "port:summary:title" : string [read];
|
||||
interface named id "port:summary:named" : Named;
|
||||
constructor person id "port:summary:person" : Person;
|
||||
};
|
||||
constructor createPerson id "export:todo-runtime:create-person" constructs Person : unit;
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ workspace WebStudioFixture id "workspace:web-studio-fixture" revision "workspace
|
||||
import package ComponentRuntime;
|
||||
|
||||
shared state ProjectName id "slot:project:name" on Project : string
|
||||
policy optimistic-register default "Untitled project";
|
||||
policy optimistic-register default "Untitled project";
|
||||
shared edge ProjectComponentEdge id "edge:project:component" {
|
||||
atom ProjectComponent projection subject id "projection:component:subject" exactly-one;
|
||||
atom Project projection component id "projection:project:component" optional-one;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { promisify } from "node:util";
|
||||
import { createGitCapabilityResolver } from "../src/capability-language/git-resolver.js";
|
||||
|
||||
const execFile = promisify(callback);
|
||||
test("dependency resolution is reusable across processes, concurrent and rejects modified checkouts", async context => {
|
||||
test("dependency resolution is reusable across processes, concurrent and rejects modified checkouts", async (context) => {
|
||||
const temporary = await mkdtemp(path.join(os.tmpdir(), "qx-resolver-test-"));
|
||||
context.after(() => rm(temporary, { recursive: true, force: true }));
|
||||
const origin = path.join(temporary, "origin");
|
||||
|
||||
+54
-15
@@ -1,28 +1,67 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { contentDigest, validateMigrationCatalog, selectMigrationPath, type MigrationCatalog } from "../src/capability-model/index.js";
|
||||
const before = { fields: ["name"] }, after = { fields: ["first", "last"] };
|
||||
const from = contentDigest(before), to = contentDigest(after);
|
||||
const catalog = (): MigrationCatalog => ({ schemaVersion: 1, contracts: { [from]: before, [to]: after }, migrations: [{
|
||||
id: "split-name", scopeId: "person-name", from, to,
|
||||
implementation: { exportId: "migrate-name", file: "src/migrate-name.ts", digest: contentDigest("implementation") }, predecessors: [],
|
||||
ports: [{ name: "source", view: "old", access: ["read"], contractDigest: from }, { name: "target", view: "new", access: ["write"], contractDigest: to }],
|
||||
}] });
|
||||
import {
|
||||
contentDigest,
|
||||
validateMigrationCatalog,
|
||||
selectMigrationPath,
|
||||
type MigrationCatalog,
|
||||
} from "../src/capability-model/index.js";
|
||||
const before = { fields: ["name"] },
|
||||
after = { fields: ["first", "last"] };
|
||||
const from = contentDigest(before),
|
||||
to = contentDigest(after);
|
||||
const catalog = (): MigrationCatalog => ({
|
||||
schemaVersion: 1,
|
||||
contracts: { [from]: before, [to]: after },
|
||||
migrations: [
|
||||
{
|
||||
id: "split-name",
|
||||
scopeId: "person-name",
|
||||
from,
|
||||
to,
|
||||
implementation: {
|
||||
exportId: "migrate-name",
|
||||
file: "src/migrate-name.ts",
|
||||
digest: contentDigest("implementation"),
|
||||
},
|
||||
predecessors: [],
|
||||
ports: [
|
||||
{ name: "source", view: "old", access: ["read"], contractDigest: from },
|
||||
{ name: "target", view: "new", access: ["write"], contractDigest: to },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
test("published migrations retain exact old contracts and explicit local bindings", () => {
|
||||
const value = validateMigrationCatalog(catalog(), new Set(["migrate-name"]));
|
||||
const selection = { scopeId: "person-name", from, to, path: ["split-name"], bindings: { source: "old-slot", target: "new-slot" } };
|
||||
const selection = {
|
||||
scopeId: "person-name",
|
||||
from,
|
||||
to,
|
||||
path: ["split-name"],
|
||||
bindings: { source: "old-slot", target: "new-slot" },
|
||||
};
|
||||
const result = selectMigrationPath(value, selection);
|
||||
assert.equal(result[0].alreadyApplied, false);
|
||||
assert.equal(selectMigrationPath(value, selection, new Map([["split-name", result[0].digest]]))[0].alreadyApplied, true);
|
||||
const changed = catalog(); changed.migrations[0].implementation.digest = contentDigest("new code");
|
||||
assert.throws(() => selectMigrationPath(changed, selection, new Map([["split-name", result[0].digest]])), /different code/);
|
||||
assert.equal(
|
||||
selectMigrationPath(value, selection, new Map([["split-name", result[0].digest]]))[0].alreadyApplied,
|
||||
true,
|
||||
);
|
||||
const changed = catalog();
|
||||
changed.migrations[0].implementation.digest = contentDigest("new code");
|
||||
assert.throws(
|
||||
() => selectMigrationPath(changed, selection, new Map([["split-name", result[0].digest]])),
|
||||
/different code/,
|
||||
);
|
||||
assert.throws(() => selectMigrationPath(value, { ...selection, path: [] }), /does not cover/);
|
||||
assert.throws(() => selectMigrationPath(value, { ...selection, bindings: {} }), /Missing local/);
|
||||
});
|
||||
test("old migration views cannot be writable and contract hashes are verified", () => {
|
||||
const value = catalog(); value.migrations[0].ports[0].access = ["write"];
|
||||
const value = catalog();
|
||||
value.migrations[0].ports[0].access = ["write"];
|
||||
assert.throws(() => validateMigrationCatalog(value), /read-only/);
|
||||
const corrupt = catalog(); corrupt.contracts[from] = {};
|
||||
const corrupt = catalog();
|
||||
corrupt.contracts[from] = {};
|
||||
assert.throws(() => validateMigrationCatalog(corrupt), /digest mismatch/);
|
||||
});
|
||||
|
||||
@@ -31,6 +70,6 @@ test("migration history rejects cycles and non-boolean compatibility promises",
|
||||
cyclic.migrations[0].predecessors = ["split-name"];
|
||||
assert.throws(() => validateMigrationCatalog(cyclic), /Cyclic/);
|
||||
const misleading = catalog();
|
||||
(misleading.migrations[0] as unknown as {preservesOldReaders: string}).preservesOldReaders = "false";
|
||||
(misleading.migrations[0] as unknown as { preservesOldReaders: string }).preservesOldReaders = "false";
|
||||
assert.throws(() => validateMigrationCatalog(misleading), /must be booleans/);
|
||||
});
|
||||
|
||||
+66
-25
@@ -7,28 +7,69 @@ import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("Nix checks a retained immutable source without a local overlay and reuses the result", {skip: !process.env.QX_CHECK_GENERATOR}, async context => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-nix-candidate-test-"));
|
||||
context.after(() => fs.rm(root, {recursive: true, force: true}));
|
||||
const git = (...args: string[]) => execFile("git", ["-C", root, ...args]);
|
||||
await git("init");
|
||||
await fs.writeFile(path.join(root, "interface.qx"), 'interface Example id "interface:example" revision "interface:example@1" {}');
|
||||
await fs.writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } }`);
|
||||
await git("add", ".");
|
||||
await git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "contract");
|
||||
const commit = (await git("rev-parse", "HEAD")).stdout.trim();
|
||||
await git("tag", `quixos-reachability/${commit}`);
|
||||
const generator = process.env.QX_CHECK_GENERATOR!;
|
||||
const repository = "https://immutable-candidate.example.test/contract.git";
|
||||
const env = {...process.env, GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: `url.file://${root}.insteadOf`, GIT_CONFIG_VALUE_0: repository};
|
||||
const build = async () => (await execFile("nix", ["build", "--impure", "--file", path.join(generator, "share/checked-candidate.nix"),
|
||||
"--argstr", "repository", repository, "--argstr", "commit", commit,
|
||||
"--argstr", "kind", "interface", "--argstr", "generator", generator,
|
||||
"--option", "substitute", "false", "--no-link", "--print-out-paths"], {env, maxBuffer: 4 * 1024 * 1024})).stdout.trim();
|
||||
const output = await build();
|
||||
const candidate = JSON.parse(await fs.readFile(path.join(output, "candidate.json"), "utf8"));
|
||||
assert.equal(candidate.revision.source.commit, commit);
|
||||
await fs.writeFile(path.join(root, "interface.qx"), "broken draft");
|
||||
assert.equal(await build(), output);
|
||||
assert.deepEqual(JSON.parse(await fs.readFile(path.join(output, "checks.json"), "utf8")), []);
|
||||
});
|
||||
test(
|
||||
"Nix checks a retained immutable source without a local overlay and reuses the result",
|
||||
{ skip: !process.env.QX_CHECK_GENERATOR },
|
||||
async (context) => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-nix-candidate-test-"));
|
||||
context.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
const git = (...args: string[]) => execFile("git", ["-C", root, ...args]);
|
||||
await git("init");
|
||||
await fs.writeFile(
|
||||
path.join(root, "interface.qx"),
|
||||
'interface Example id "interface:example" revision "interface:example@1" {}',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
`quixos-lock version 1 { quixos source { repository "https://example.test/quixos"; commit "${"a".repeat(40)}"; } }`,
|
||||
);
|
||||
await git("add", ".");
|
||||
await git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "contract");
|
||||
const commit = (await git("rev-parse", "HEAD")).stdout.trim();
|
||||
await git("tag", `quixos-reachability/${commit}`);
|
||||
const generator = process.env.QX_CHECK_GENERATOR!;
|
||||
const repository = "https://immutable-candidate.example.test/contract.git";
|
||||
const env = {
|
||||
...process.env,
|
||||
GIT_CONFIG_COUNT: "1",
|
||||
GIT_CONFIG_KEY_0: `url.file://${root}.insteadOf`,
|
||||
GIT_CONFIG_VALUE_0: repository,
|
||||
};
|
||||
const build = async () =>
|
||||
(
|
||||
await execFile(
|
||||
"nix",
|
||||
[
|
||||
"build",
|
||||
"--impure",
|
||||
"--file",
|
||||
path.join(generator, "share/checked-candidate.nix"),
|
||||
"--argstr",
|
||||
"repository",
|
||||
repository,
|
||||
"--argstr",
|
||||
"commit",
|
||||
commit,
|
||||
"--argstr",
|
||||
"kind",
|
||||
"interface",
|
||||
"--argstr",
|
||||
"generator",
|
||||
generator,
|
||||
"--option",
|
||||
"substitute",
|
||||
"false",
|
||||
"--no-link",
|
||||
"--print-out-paths",
|
||||
],
|
||||
{ env, maxBuffer: 4 * 1024 * 1024 },
|
||||
)
|
||||
).stdout.trim();
|
||||
const output = await build();
|
||||
const candidate = JSON.parse(await fs.readFile(path.join(output, "candidate.json"), "utf8"));
|
||||
assert.equal(candidate.revision.source.commit, commit);
|
||||
await fs.writeFile(path.join(root, "interface.qx"), "broken draft");
|
||||
assert.equal(await build(), output);
|
||||
assert.deepEqual(JSON.parse(await fs.readFile(path.join(output, "checks.json"), "utf8")), []);
|
||||
},
|
||||
);
|
||||
|
||||
+143
-64
@@ -3,100 +3,179 @@ import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {promisify} from "node:util";
|
||||
import {execFile as callback} from "node:child_process";
|
||||
import {planPinUpgrades, applyPinUpgrades, type UpgradeEffects} from "../src/capability-language/pin-upgrades.js";
|
||||
import { promisify } from "node:util";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { planPinUpgrades, applyPinUpgrades, type UpgradeEffects } from "../src/capability-language/pin-upgrades.js";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("real jj snapshots and immutable Git publication propagate a changed interface into the root", {skip: !process.env.QX_CHECK_GENERATOR}, async (context) => {
|
||||
const workbench = await fs.mkdtemp(path.join(os.tmpdir(), "qx-real-upgrade-"));
|
||||
context.after(() => fs.rm(workbench, {recursive: true, force: true}));
|
||||
// Exercise the actual effects with local bare remotes, without external writes.
|
||||
const environment = {
|
||||
GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: `url.file://${workbench}/remotes/.insteadOf`,
|
||||
GIT_CONFIG_VALUE_0: "https://upgrade.test/", QUIXOS_JJ_NO_CHECKPOINT: "1",
|
||||
QUIXOS_CHECK_GENERATOR: process.env.QX_CHECK_GENERATOR!,
|
||||
};
|
||||
const previous = Object.fromEntries(Object.keys(environment).map(key => [key, process.env[key]]));
|
||||
Object.assign(process.env, environment);
|
||||
context.after(() => {for (const [key, value] of Object.entries(previous)) if (value === undefined) delete process.env[key]; else process.env[key] = value;});
|
||||
const run = async (cwd: string, command: string, args: string[]) => (await execFile(command, args, {cwd})).stdout.trim();
|
||||
await fs.mkdir(path.join(workbench, "remotes"));
|
||||
const nodes = [];
|
||||
for (const [kind, directory, remote] of [["interface", "resources/Named", "named.git"], ["workspace", "root", "workspace.git"]] as const) {
|
||||
const root = path.join(workbench, directory);
|
||||
await fs.mkdir(root, {recursive: true});
|
||||
await run(workbench, "git", ["init", "--bare", path.join(workbench, "remotes", remote)]);
|
||||
await run(root, "jj", ["git", "init", "--colocate"]);
|
||||
await run(root, "git", ["remote", "add", "origin", `https://upgrade.test/${remote}`]);
|
||||
await fs.writeFile(path.join(root, ".gitignore"), ".quixos/\n");
|
||||
const child = nodes[0];
|
||||
await fs.writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://upgrade.test/quixos.git"; commit "${"a".repeat(40)}"; } ${child ? `interface Named source { repository "${child.source.repository}"; commit "${child.source.commit}"; }` : ""} }`);
|
||||
await fs.writeFile(path.join(root, `${kind}.qx`), kind === "interface" ? 'interface Named id "interface:named" revision "interface:named@1" {}' : `workspace W id "workspace:w" revision "workspace:w@1" commit "${"a".repeat(40)}" { import interface Named; atom A id "atom:a"; }`);
|
||||
await run(root, "jj", ["describe", "-m", "Initial source"]);
|
||||
const commit = await run(root, "jj", ["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
|
||||
await run(root, "git", ["push", "origin", `${commit}:refs/tags/quixos-reachability/${commit}`]);
|
||||
nodes.push({kind, directory, source: {repository: `https://upgrade.test/${remote}`, commit}});
|
||||
}
|
||||
await fs.mkdir(path.join(workbench, ".quixos"));
|
||||
await fs.writeFile(path.join(workbench, ".quixos/resource-graph.json"), JSON.stringify({resources: [nodes[0]]}));
|
||||
await fs.appendFile(path.join(workbench, nodes[0].directory, "interface.qx"), "\n// incremental author edit\n");
|
||||
const plan = await planPinUpgrades(workbench, {nodes, bootstrap: true});
|
||||
const result = await applyPinUpgrades(plan);
|
||||
assert.equal(result.activated, false);
|
||||
const graph = JSON.parse(await fs.readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"));
|
||||
const commit = graph.resources[0].source.commit;
|
||||
assert.notEqual(commit, nodes[0].source.commit);
|
||||
assert.match(await fs.readFile(path.join(workbench, "root/quixos.lock"), "utf8"), new RegExp(commit));
|
||||
assert.match(await run(workbench, "git", ["--git-dir", path.join(workbench, "remotes/named.git"), "show-ref"]), new RegExp(commit));
|
||||
});
|
||||
test(
|
||||
"real jj snapshots and immutable Git publication propagate a changed interface into the root",
|
||||
{ skip: !process.env.QX_CHECK_GENERATOR },
|
||||
async (context) => {
|
||||
const workbench = await fs.mkdtemp(path.join(os.tmpdir(), "qx-real-upgrade-"));
|
||||
context.after(() => fs.rm(workbench, { recursive: true, force: true }));
|
||||
// Exercise the actual effects with local bare remotes, without external writes.
|
||||
const environment = {
|
||||
GIT_CONFIG_COUNT: "1",
|
||||
GIT_CONFIG_KEY_0: `url.file://${workbench}/remotes/.insteadOf`,
|
||||
GIT_CONFIG_VALUE_0: "https://upgrade.test/",
|
||||
QUIXOS_JJ_NO_CHECKPOINT: "1",
|
||||
QUIXOS_CHECK_GENERATOR: process.env.QX_CHECK_GENERATOR!,
|
||||
};
|
||||
const previous = Object.fromEntries(Object.keys(environment).map((key) => [key, process.env[key]]));
|
||||
Object.assign(process.env, environment);
|
||||
context.after(() => {
|
||||
for (const [key, value] of Object.entries(previous))
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
});
|
||||
const run = async (cwd: string, command: string, args: string[]) =>
|
||||
(await execFile(command, args, { cwd })).stdout.trim();
|
||||
await fs.mkdir(path.join(workbench, "remotes"));
|
||||
const nodes = [];
|
||||
for (const [kind, directory, remote] of [
|
||||
["interface", "resources/Named", "named.git"],
|
||||
["workspace", "root", "workspace.git"],
|
||||
] as const) {
|
||||
const root = path.join(workbench, directory);
|
||||
await fs.mkdir(root, { recursive: true });
|
||||
await run(workbench, "git", ["init", "--bare", path.join(workbench, "remotes", remote)]);
|
||||
await run(root, "jj", ["git", "init", "--colocate"]);
|
||||
await run(root, "git", ["remote", "add", "origin", `https://upgrade.test/${remote}`]);
|
||||
await fs.writeFile(path.join(root, ".gitignore"), ".quixos/\n");
|
||||
const child = nodes[0];
|
||||
await fs.writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
`quixos-lock version 1 { quixos source { repository "https://upgrade.test/quixos.git"; commit "${"a".repeat(40)}"; } ${child ? `interface Named source { repository "${child.source.repository}"; commit "${child.source.commit}"; }` : ""} }`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(root, `${kind}.qx`),
|
||||
kind === "interface"
|
||||
? 'interface Named id "interface:named" revision "interface:named@1" {}'
|
||||
: `workspace W id "workspace:w" revision "workspace:w@1" commit "${"a".repeat(40)}" { import interface Named; atom A id "atom:a"; }`,
|
||||
);
|
||||
await run(root, "jj", ["describe", "-m", "Initial source"]);
|
||||
const commit = await run(root, "jj", ["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
|
||||
await run(root, "git", ["push", "origin", `${commit}:refs/tags/quixos-reachability/${commit}`]);
|
||||
nodes.push({ kind, directory, source: { repository: `https://upgrade.test/${remote}`, commit } });
|
||||
}
|
||||
await fs.mkdir(path.join(workbench, ".quixos"));
|
||||
await fs.writeFile(path.join(workbench, ".quixos/resource-graph.json"), JSON.stringify({ resources: [nodes[0]] }));
|
||||
await fs.appendFile(path.join(workbench, nodes[0].directory, "interface.qx"), "\n// incremental author edit\n");
|
||||
const plan = await planPinUpgrades(workbench, { nodes, bootstrap: true });
|
||||
const result = await applyPinUpgrades(plan);
|
||||
assert.equal(result.activated, false);
|
||||
const graph = JSON.parse(await fs.readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"));
|
||||
const commit = graph.resources[0].source.commit;
|
||||
assert.notEqual(commit, nodes[0].source.commit);
|
||||
assert.match(await fs.readFile(path.join(workbench, "root/quixos.lock"), "utf8"), new RegExp(commit));
|
||||
assert.match(
|
||||
await run(workbench, "git", ["--git-dir", path.join(workbench, "remotes/named.git"), "show-ref"]),
|
||||
new RegExp(commit),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("pin upgrades publish children before parent locks and resume without republishing completed nodes", async (context) => {
|
||||
const workbench = await fs.mkdtemp(path.join(os.tmpdir(), "qx-upgrade-test-"));
|
||||
context.after(() => fs.rm(workbench, {recursive: true, force: true}));
|
||||
const from = "a".repeat(40), to = "b".repeat(40), framework = "c".repeat(40);
|
||||
const sources = [{kind: "workspace" as const, directory: "root", source: {repository: "https://example.test/workspace.git", commit: from}},
|
||||
{kind: "interface" as const, directory: "resources/Named", source: {repository: "https://example.test/named.git", commit: from}}];
|
||||
context.after(() => fs.rm(workbench, { recursive: true, force: true }));
|
||||
const from = "a".repeat(40),
|
||||
to = "b".repeat(40),
|
||||
framework = "c".repeat(40);
|
||||
const sources = [
|
||||
{
|
||||
kind: "workspace" as const,
|
||||
directory: "root",
|
||||
source: { repository: "https://example.test/workspace.git", commit: from },
|
||||
},
|
||||
{
|
||||
kind: "interface" as const,
|
||||
directory: "resources/Named",
|
||||
source: { repository: "https://example.test/named.git", commit: from },
|
||||
},
|
||||
];
|
||||
const lock = `quixos-lock version 1 { quixos source { repository "https://example.test/quixos.git"; commit "${framework}"; }`;
|
||||
for (const node of sources) {
|
||||
const directory = path.join(workbench, node.directory);
|
||||
await fs.mkdir(directory, {recursive: true});
|
||||
await fs.mkdir(directory, { recursive: true });
|
||||
await execFile("git", ["-C", directory, "init"]);
|
||||
await execFile("git", ["-C", directory, "remote", "add", "origin", node.source.repository]);
|
||||
await fs.writeFile(path.join(directory, ".gitignore"), ".quixos/\n");
|
||||
await fs.writeFile(path.join(directory, "quixos.lock"), lock + (node.kind === "workspace" ? ` interface Named source { repository "${sources[1].source.repository}"; commit "${from}"; }` : "") + " }");
|
||||
await fs.writeFile(
|
||||
path.join(directory, "quixos.lock"),
|
||||
lock +
|
||||
(node.kind === "workspace"
|
||||
? ` interface Named source { repository "${sources[1].source.repository}"; commit "${from}"; }`
|
||||
: "") +
|
||||
" }",
|
||||
);
|
||||
}
|
||||
await fs.writeFile(path.join(workbench, "resources/Named/interface.qx"), 'interface Named id "interface:named" revision "interface:named@1" { value name id "member:name" : string { get id "op:get"; } }');
|
||||
await fs.writeFile(path.join(workbench, "root/workspace.qx"), `workspace W id "workspace:w" revision "workspace:w@1" commit "${from}" { import interface Named; atom A id "atom:a"; }`);
|
||||
await fs.writeFile(
|
||||
path.join(workbench, "resources/Named/interface.qx"),
|
||||
'interface Named id "interface:named" revision "interface:named@1" { value name id "member:name" : string { get id "op:get"; } }',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(workbench, "root/workspace.qx"),
|
||||
`workspace W id "workspace:w" revision "workspace:w@1" commit "${from}" { import interface Named; atom A id "atom:a"; }`,
|
||||
);
|
||||
const snapshotMap = path.join(workbench, "snapshots.json");
|
||||
await fs.writeFile(snapshotMap, JSON.stringify({resources: [{kind: "interface", repository: sources[1].source.repository, commit: to, directory: path.join(workbench, "resources/Named")}]}));
|
||||
await fs.writeFile(
|
||||
snapshotMap,
|
||||
JSON.stringify({
|
||||
resources: [
|
||||
{
|
||||
kind: "interface",
|
||||
repository: sources[1].source.repository,
|
||||
commit: to,
|
||||
directory: path.join(workbench, "resources/Named"),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const oldMap = process.env.QUIXOS_SNAPSHOT_MAP;
|
||||
process.env.QUIXOS_SNAPSHOT_MAP = snapshotMap;
|
||||
context.after(() => {if (oldMap === undefined) delete process.env.QUIXOS_SNAPSHOT_MAP; else process.env.QUIXOS_SNAPSHOT_MAP = oldMap;});
|
||||
const plan = await planPinUpgrades(workbench, {nodes: sources});
|
||||
await fs.mkdir(path.join(workbench, ".quixos"), {recursive: true});
|
||||
await fs.writeFile(path.join(workbench, ".quixos/resource-graph.json"), JSON.stringify({resources: [{...sources[1], source: {resolver: "git", ...sources[1].source}}]}));
|
||||
assert.deepEqual(plan.nodes.map((node) => node.directory), ["resources/Named", "root"]);
|
||||
context.after(() => {
|
||||
if (oldMap === undefined) delete process.env.QUIXOS_SNAPSHOT_MAP;
|
||||
else process.env.QUIXOS_SNAPSHOT_MAP = oldMap;
|
||||
});
|
||||
const plan = await planPinUpgrades(workbench, { nodes: sources });
|
||||
await fs.mkdir(path.join(workbench, ".quixos"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(workbench, ".quixos/resource-graph.json"),
|
||||
JSON.stringify({ resources: [{ ...sources[1], source: { resolver: "git", ...sources[1].source } }] }),
|
||||
);
|
||||
assert.deepEqual(
|
||||
plan.nodes.map((node) => node.directory),
|
||||
["resources/Named", "root"],
|
||||
);
|
||||
let fail = true;
|
||||
const published: string[] = [];
|
||||
const effects: UpgradeEffects = {
|
||||
check: async (node) => {if (node.kind === "workspace" && fail) throw new Error("refactor required");},
|
||||
check: async (node) => {
|
||||
if (node.kind === "workspace" && fail) throw new Error("refactor required");
|
||||
},
|
||||
snapshot: async () => to,
|
||||
publish: async (root) => {published.push(path.relative(workbench, root));},
|
||||
publish: async (root) => {
|
||||
published.push(path.relative(workbench, root));
|
||||
},
|
||||
};
|
||||
await assert.rejects(() => applyPinUpgrades(plan, undefined, effects), /refactor required/);
|
||||
assert.deepEqual(published, ["resources/Named"]);
|
||||
assert.match(await fs.readFile(path.join(workbench, "root/quixos.lock"), "utf8"), new RegExp(to));
|
||||
const id = (await fs.readdir(path.join(workbench, ".quixos/upgrades"))).find((name) => name.endsWith(".json"))!.slice(0, -5);
|
||||
const id = (await fs.readdir(path.join(workbench, ".quixos/upgrades")))
|
||||
.find((name) => name.endsWith(".json"))!
|
||||
.slice(0, -5);
|
||||
fail = false;
|
||||
await fs.appendFile(path.join(workbench, "root/workspace.qx"), "\n// explicit refactor\n");
|
||||
await assert.rejects(() => applyPinUpgrades(plan, id, effects), /accept-edits/);
|
||||
const result = await applyPinUpgrades(plan, id, effects, {acceptEdits: true});
|
||||
const result = await applyPinUpgrades(plan, id, effects, { acceptEdits: true });
|
||||
assert.equal(result.activated, false);
|
||||
assert.deepEqual(published, ["resources/Named", "root"]);
|
||||
const graph = JSON.parse(await fs.readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"));
|
||||
assert.equal(graph.resources.length, 1);
|
||||
assert.equal(graph.resources[0].source.commit, to);
|
||||
const next = await planPinUpgrades(workbench, {nodes: [sources[0], {...sources[1], source: graph.resources[0].source}]});
|
||||
const next = await planPinUpgrades(workbench, {
|
||||
nodes: [sources[0], { ...sources[1], source: graph.resources[0].source }],
|
||||
});
|
||||
assert.deepEqual(next.nodes.find((node) => node.kind === "workspace")!.dependencies, ["resources/Named"]);
|
||||
});
|
||||
|
||||
+51
-14
@@ -3,15 +3,30 @@ import test from "node:test";
|
||||
import { mkdtemp, writeFile, readFile, rm, symlink } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { parseQx, formatQx, lintQx, applySourceEdits, addWorkspaceImport, walkSyntax,
|
||||
resolveQxSources, readQxSource, compileCapabilitySource, scaffoldAtom, compileWorkspaceRepository } from "../src/capability-language/index.js";
|
||||
import {
|
||||
parseQx,
|
||||
formatQx,
|
||||
lintQx,
|
||||
applySourceEdits,
|
||||
addWorkspaceImport,
|
||||
walkSyntax,
|
||||
resolveQxSources,
|
||||
readQxSource,
|
||||
compileCapabilitySource,
|
||||
scaffoldAtom,
|
||||
compileWorkspaceRepository,
|
||||
} from "../src/capability-language/index.js";
|
||||
|
||||
const workspace = `workspace Test id "w" revision "w@1" commit "${"1".repeat(40)}" {\n// keep 🐈 comment\n}\n`;
|
||||
test("lint reports invalid and redundant imports without resolving repositories", () => {
|
||||
const source = workspace.replace("}\n", 'import "a.qx"; import "a.qx"; import "../bad.qx";\n}\n');
|
||||
assert.deepEqual(lintQx(source).map((entry) => [entry.code, entry.severity]), [
|
||||
["duplicate-source-import", "warning"], ["invalid-source-import", "error"],
|
||||
]);
|
||||
assert.deepEqual(
|
||||
lintQx(source).map((entry) => [entry.code, entry.severity]),
|
||||
[
|
||||
["duplicate-source-import", "warning"],
|
||||
["invalid-source-import", "error"],
|
||||
],
|
||||
);
|
||||
});
|
||||
test("lossless tokens, UTF-16 ranges, and safe source edits", () => {
|
||||
const text = workspace.replace("}\n", 'atom Cat id "🐈";\n}\n');
|
||||
@@ -20,8 +35,18 @@ test("lossless tokens, UTF-16 ranges, and safe source edits", () => {
|
||||
assert.equal(parsed.tokens.map((token) => token.text).join(""), text);
|
||||
const atom = [...walkSyntax(parsed.root)].find((node) => node.kind === "atomDecl")!;
|
||||
assert.equal(text.slice(atom.start, atom.end), 'atom Cat id "🐈";');
|
||||
assert.equal(applySourceEdits(text, [{ ...atom, text: 'atom Dog id "dog";' }]), text.replace('atom Cat id "🐈";', 'atom Dog id "dog";'));
|
||||
assert.throws(() => applySourceEdits(text, [{ start: 0, end: 4, text: "" }, { start: 3, end: 7, text: "" }]), /overlapping/);
|
||||
assert.equal(
|
||||
applySourceEdits(text, [{ ...atom, text: 'atom Dog id "dog";' }]),
|
||||
text.replace('atom Cat id "🐈";', 'atom Dog id "dog";'),
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
applySourceEdits(text, [
|
||||
{ start: 0, end: 4, text: "" },
|
||||
{ start: 3, end: 7, text: "" },
|
||||
]),
|
||||
/overlapping/,
|
||||
);
|
||||
assert.throws(() => applySourceEdits(text, [{ start: -1, end: 0, text: "" }]), /Invalid/);
|
||||
});
|
||||
test("formatting preserves comments and strings, is idempotent, and rejects invalid source", () => {
|
||||
@@ -36,10 +61,12 @@ test("imports preserve root text, deduplicate diamonds, reject cycles, and compi
|
||||
const root = addWorkspaceImport(addWorkspaceImport(workspace, "a.qx"), "b.qx");
|
||||
assert.equal(addWorkspaceImport(root, "a.qx"), root);
|
||||
assert.match(root, /keep 🐈 comment/);
|
||||
const files: Record<string, string> = { "workspace.qx": root,
|
||||
const files: Record<string, string> = {
|
||||
"workspace.qx": root,
|
||||
"a.qx": 'fragment { import "shared.qx"; atom A id "a"; }',
|
||||
"b.qx": 'fragment { import "shared.qx"; atom B id "b"; }',
|
||||
"shared.qx": 'fragment { atom Shared id "shared"; }' };
|
||||
"shared.qx": 'fragment { atom Shared id "shared"; }',
|
||||
};
|
||||
const result = await resolveQxSources(async (name) => files[name]!);
|
||||
assert.equal(result.sourceFiles.length, 4);
|
||||
const compiled = compileCapabilitySource(result.source);
|
||||
@@ -49,15 +76,23 @@ test("imports preserve root text, deduplicate diamonds, reject cycles, and compi
|
||||
assert.equal(unresolved.ok, false);
|
||||
assert.match(JSON.stringify(unresolved.diagnostics), /unresolved-source-import/);
|
||||
files["shared.qx"] = 'fragment { import "a.qx"; }';
|
||||
await assert.rejects(resolveQxSources(async (name) => files[name]!), /cycle/);
|
||||
await assert.rejects(
|
||||
resolveQxSources(async (name) => files[name]!),
|
||||
/cycle/,
|
||||
);
|
||||
assert.throws(() => addWorkspaceImport(workspace, "../x.qx"), /Invalid/);
|
||||
});
|
||||
test("repository scaffolding validates before writing and refuses duplicates and symlinks", async (t) => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "qx-scaffold-"));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
await writeFile(path.join(root, "workspace.qx"), workspace);
|
||||
await writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/q.git"; commit "${"1".repeat(40)}"; } }`);
|
||||
const resolveResource = async (): Promise<never> => { throw new Error("unexpected resource"); };
|
||||
await writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
`quixos-lock version 1 { quixos source { repository "https://example.test/q.git"; commit "${"1".repeat(40)}"; } }`,
|
||||
);
|
||||
const resolveResource = async (): Promise<never> => {
|
||||
throw new Error("unexpected resource");
|
||||
};
|
||||
const options = { root, name: "Cat", id: "cat", write: false, resolveResource };
|
||||
await scaffoldAtom(options);
|
||||
assert.equal(await readFile(path.join(root, "workspace.qx"), "utf8"), workspace);
|
||||
@@ -72,8 +107,10 @@ test("repository scaffolding validates before writing and refuses duplicates and
|
||||
await assert.rejects(readQxSource(root, "link.qx"), /ordinary files/);
|
||||
});
|
||||
test("lowering diagnostics map to the imported file", async () => {
|
||||
const files: Record<string, string> = { "workspace.qx": addWorkspaceImport(workspace, "bad.qx"),
|
||||
"bad.qx": 'fragment {\n conform Missing as Nope {}\n}' };
|
||||
const files: Record<string, string> = {
|
||||
"workspace.qx": addWorkspaceImport(workspace, "bad.qx"),
|
||||
"bad.qx": "fragment {\n conform Missing as Nope {}\n}",
|
||||
};
|
||||
const sources = await resolveQxSources(async (name) => files[name]!);
|
||||
const compiled = compileCapabilitySource(sources.source);
|
||||
assert.equal(compiled.ok, false);
|
||||
|
||||
+58
-33
@@ -141,25 +141,34 @@ test("resolves root-relative lock fragments into one deterministic resource clos
|
||||
}
|
||||
}`;
|
||||
const sources = new Map([
|
||||
["locks/web-studio.lock", `quixos-lock fragment version 1 {
|
||||
[
|
||||
"locks/web-studio.lock",
|
||||
`quixos-lock fragment version 1 {
|
||||
import "locks/shared.lock";
|
||||
interface Placeable source {
|
||||
repository "https://repos.example/alice/interface-placeable.git";
|
||||
commit "${namedCommit}";
|
||||
}
|
||||
}`],
|
||||
["locks/shared.lock", `quixos-lock fragment version 1 {
|
||||
}`,
|
||||
],
|
||||
[
|
||||
"locks/shared.lock",
|
||||
`quixos-lock fragment version 1 {
|
||||
interface Named source {
|
||||
repository "https://repos.example/alice/interface-named.git";
|
||||
commit "${namedCommit}";
|
||||
}
|
||||
}`],
|
||||
["locks/domain.lock", `quixos-lock fragment version 1 {
|
||||
}`,
|
||||
],
|
||||
[
|
||||
"locks/domain.lock",
|
||||
`quixos-lock fragment version 1 {
|
||||
package TodoRuntime source {
|
||||
repository "https://repos.example/alice/package-todo.git";
|
||||
commit "${packageCommit}";
|
||||
}
|
||||
}`],
|
||||
}`,
|
||||
],
|
||||
]);
|
||||
const result = await resolveQuixosLock(root, async (relativePath) => {
|
||||
const source = sources.get(relativePath);
|
||||
@@ -174,12 +183,15 @@ test("resolves root-relative lock fragments into one deterministic resource clos
|
||||
"locks/shared.lock",
|
||||
"locks/domain.lock",
|
||||
]);
|
||||
assert.deepEqual(result.lock.resources.map(({ kind, binding }) => ({ kind, binding })), [
|
||||
{ kind: "package", binding: "RootRuntime" },
|
||||
{ kind: "interface", binding: "Placeable" },
|
||||
{ kind: "interface", binding: "Named" },
|
||||
{ kind: "package", binding: "TodoRuntime" },
|
||||
]);
|
||||
assert.deepEqual(
|
||||
result.lock.resources.map(({ kind, binding }) => ({ kind, binding })),
|
||||
[
|
||||
{ kind: "package", binding: "RootRuntime" },
|
||||
{ kind: "interface", binding: "Placeable" },
|
||||
{ kind: "interface", binding: "Named" },
|
||||
{ kind: "package", binding: "TodoRuntime" },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("lock fragments format canonically and cannot redeclare the Quixos source", () => {
|
||||
@@ -187,35 +199,43 @@ test("lock fragments format canonically and cannot redeclare the Quixos source",
|
||||
kind: "fragment" as const,
|
||||
formatVersion: 1 as const,
|
||||
imports: ["locks/shared.lock"],
|
||||
resources: [{
|
||||
kind: "interface" as const,
|
||||
binding: "Named",
|
||||
source: {
|
||||
resolver: "git" as const,
|
||||
repository: "https://repos.example/alice/interface-named.git",
|
||||
commit: namedCommit,
|
||||
resources: [
|
||||
{
|
||||
kind: "interface" as const,
|
||||
binding: "Named",
|
||||
source: {
|
||||
resolver: "git" as const,
|
||||
repository: "https://repos.example/alice/interface-named.git",
|
||||
commit: namedCommit,
|
||||
},
|
||||
},
|
||||
}],
|
||||
],
|
||||
};
|
||||
assert.deepEqual(parseQuixosLockDocument(formatQuixosLockDocument(document)), {
|
||||
ok: true,
|
||||
document,
|
||||
diagnostics: [],
|
||||
});
|
||||
const invalid = parseQuixosLockDocument(`quixos-lock fragment version 1 {
|
||||
const invalid = parseQuixosLockDocument(
|
||||
`quixos-lock fragment version 1 {
|
||||
quixos source {
|
||||
repository "https://gitea.example/quixos/quixos.git";
|
||||
commit "${quixosCommit}";
|
||||
}
|
||||
}`, "bad.lock");
|
||||
}`,
|
||||
"bad.lock",
|
||||
);
|
||||
assert.equal(invalid.ok, false);
|
||||
if (!invalid.ok) assert.equal(invalid.diagnostics[0]?.code, "fragment-has-quixos-source");
|
||||
});
|
||||
|
||||
test("lock imports reject traversal, cycles, root documents, and cross-file binding collisions", async () => {
|
||||
const invalidPath = parseQuixosLockDocument(`quixos-lock fragment version 1 {
|
||||
const invalidPath = parseQuixosLockDocument(
|
||||
`quixos-lock fragment version 1 {
|
||||
import "../outside.lock";
|
||||
}`, "bad-path.lock");
|
||||
}`,
|
||||
"bad-path.lock",
|
||||
);
|
||||
assert.equal(invalidPath.ok, false);
|
||||
if (!invalidPath.ok) assert.equal(invalidPath.diagnostics[0]?.code, "invalid-import-path");
|
||||
|
||||
@@ -232,24 +252,26 @@ test("lock imports reject traversal, cycles, root documents, and cross-file bind
|
||||
}
|
||||
}`;
|
||||
const sources = new Map([
|
||||
["a.lock", `quixos-lock fragment version 1 {
|
||||
[
|
||||
"a.lock",
|
||||
`quixos-lock fragment version 1 {
|
||||
import "b.lock";
|
||||
interface Named source {
|
||||
repository "https://repos.example/alice/interface-named-copy.git";
|
||||
commit "${namedCommit}";
|
||||
}
|
||||
}`],
|
||||
}`,
|
||||
],
|
||||
["b.lock", `quixos-lock fragment version 1 { import "a.lock"; }`],
|
||||
["root-again.lock", root],
|
||||
]);
|
||||
const result = await resolveQuixosLock(root, async (relativePath) => sources.get(relativePath) ?? "");
|
||||
assert.equal(result.ok, false);
|
||||
if (!result.ok) {
|
||||
assert.deepEqual(new Set(result.diagnostics.map(({ code }) => code)), new Set([
|
||||
"duplicate-resource-binding",
|
||||
"import-cycle",
|
||||
"imported-root-lock",
|
||||
]));
|
||||
assert.deepEqual(
|
||||
new Set(result.diagnostics.map(({ code }) => code)),
|
||||
new Set(["duplicate-resource-binding", "import-cycle", "imported-root-lock"]),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -260,13 +282,16 @@ test("file loading rejects a symlink in any import path component", async (conte
|
||||
await mkdir(outside);
|
||||
await writeFile(path.join(outside, "fragment.lock"), "quixos-lock fragment version 1 {}\n");
|
||||
await symlink(outside, path.join(directory, "linked"), "dir");
|
||||
await writeFile(path.join(directory, "quixos.lock"), `quixos-lock version 1 {
|
||||
await writeFile(
|
||||
path.join(directory, "quixos.lock"),
|
||||
`quixos-lock version 1 {
|
||||
quixos source {
|
||||
repository "https://gitea.example/quixos/quixos.git";
|
||||
commit "${quixosCommit}";
|
||||
}
|
||||
import "linked/fragment.lock";
|
||||
}`);
|
||||
}`,
|
||||
);
|
||||
|
||||
const result = await loadQuixosLock(path.join(directory, "quixos.lock"));
|
||||
assert.equal(result.ok, false);
|
||||
|
||||
@@ -3,46 +3,85 @@ import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {execFile as callback} from "node:child_process";
|
||||
import {promisify} from "node:util";
|
||||
import {scaffoldRecipe} from "../src/capability-language/scaffold-recipes.js";
|
||||
import {planStructure, applyStructure} from "../src/capability-language/structural-plan.js";
|
||||
import {contentDigest} from "../src/capability-model/evolution.js";
|
||||
import {sealMigrations} from "../src/capability-language/migration-seal.js";
|
||||
import {reactPlatformTypes} from "../src/bindings/react-platform.js";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { scaffoldRecipe } from "../src/capability-language/scaffold-recipes.js";
|
||||
import { planStructure, applyStructure } from "../src/capability-language/structural-plan.js";
|
||||
import { contentDigest } from "../src/capability-model/evolution.js";
|
||||
import { sealMigrations } from "../src/capability-language/migration-seal.js";
|
||||
import { reactPlatformTypes } from "../src/bindings/react-platform.js";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("React preset applies its browser build script and shared-platform imports", async (context) => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-react-recipe-"));
|
||||
context.after(() => fs.rm(root, {recursive: true, force: true}));
|
||||
context.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
await execFile("git", ["-C", root, "init"]);
|
||||
const source = {repository: "https://example.test/react.git", commit: "a".repeat(40)};
|
||||
const request = await scaffoldRecipe(root, "package", {source, name: "React", id: "package:react", revision: "package:react@1", template: "typescript-react", tools: {quixos: source, protocol: source, helpers: source, sdk: source}});
|
||||
const source = { repository: "https://example.test/react.git", commit: "a".repeat(40) };
|
||||
const request = await scaffoldRecipe(root, "package", {
|
||||
source,
|
||||
name: "React",
|
||||
id: "package:react",
|
||||
revision: "package:react@1",
|
||||
template: "typescript-react",
|
||||
tools: { quixos: source, protocol: source, helpers: source, sdk: source },
|
||||
});
|
||||
await applyStructure(await planStructure(root, request));
|
||||
assert.match(await fs.readFile(path.join(root, "src/impl/sourceGet.ts"), "utf8"), /component.js\?browser-source/);
|
||||
assert.match(await fs.readFile(path.join(root, "flake.nix"), "utf8"), /browserSources = true/);
|
||||
assert.equal(await fs.readFile(path.join(root, "src/gen/web-studio-react-runtime.d.ts"), "utf8"), reactPlatformTypes);
|
||||
assert.match(await fs.readFile(path.join(root, "src/browser-assets.d.ts"), "utf8"), /declare module "\*\.css"/);
|
||||
assert.equal(JSON.parse(await fs.readFile(path.join(root, "quixos.check.json"), "utf8")).options.messages["org.quixos.web-studio.ReactProps"].export, "opaqueReactPropsBinding");
|
||||
assert.equal(
|
||||
JSON.parse(await fs.readFile(path.join(root, "quixos.check.json"), "utf8")).options.messages[
|
||||
"org.quixos.web-studio.ReactProps"
|
||||
].export,
|
||||
"opaqueReactPropsBinding",
|
||||
);
|
||||
await assert.rejects(fs.access(path.join(root, "quixos.scaffold.json")));
|
||||
assert.equal(JSON.parse(await fs.readFile(path.join(root, "tsconfig.json"), "utf8")).compilerOptions.jsx, "react-jsx");
|
||||
assert.equal(
|
||||
JSON.parse(await fs.readFile(path.join(root, "tsconfig.json"), "utf8")).compilerOptions.jsx,
|
||||
"react-jsx",
|
||||
);
|
||||
});
|
||||
|
||||
test("imperative scaffolds preserve authored wiring; migration sealing is explicit and separate", async (context) => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-recipes-"));
|
||||
context.after(() => fs.rm(root, {recursive: true, force: true}));
|
||||
context.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
await execFile("git", ["-C", root, "init"]);
|
||||
await fs.writeFile(path.join(root, ".gitignore"), ".quixos/\n");
|
||||
const source = {repository: "https://example.test/chess.git", commit: "a".repeat(40)};
|
||||
const base = {source, directory: "packages/Chess"};
|
||||
const apply = async (command: Parameters<typeof scaffoldRecipe>[1], input: Parameters<typeof scaffoldRecipe>[2]) => applyStructure(await planStructure(root, await scaffoldRecipe(root, command, input)));
|
||||
await apply("package", {...base, name: "Chess", id: "package:chess", revision: "package:chess@1", tools: {quixos: source, protocol: source, helpers: source, sdk: source}});
|
||||
await apply("function", {...base, name: "play", id: "export:play"});
|
||||
const source = { repository: "https://example.test/chess.git", commit: "a".repeat(40) };
|
||||
const base = { source, directory: "packages/Chess" };
|
||||
const apply = async (command: Parameters<typeof scaffoldRecipe>[1], input: Parameters<typeof scaffoldRecipe>[2]) =>
|
||||
applyStructure(await planStructure(root, await scaffoldRecipe(root, command, input)));
|
||||
await apply("package", {
|
||||
...base,
|
||||
name: "Chess",
|
||||
id: "package:chess",
|
||||
revision: "package:chess@1",
|
||||
tools: { quixos: source, protocol: source, helpers: source, sdk: source },
|
||||
});
|
||||
await apply("function", { ...base, name: "play", id: "export:play" });
|
||||
const filename = path.join(root, base.directory, "src/impl/play.ts");
|
||||
const edited = 'import type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation["play"] = async () => null;\n';
|
||||
const edited =
|
||||
'import type {Implementation} from "../gen/qx.js";\nexport const handler: Implementation["play"] = async () => null;\n';
|
||||
await fs.writeFile(filename, edited);
|
||||
const old = {version: 1}, next = {version: 2}, from = contentDigest(old), to = contentDigest(next);
|
||||
await apply("migration", {...base, name: "upgrade", id: "export:upgrade", migration: {id: "upgrade-v2", scopeId: "board", from, to, predecessors: [], ports: [], contracts: {[from]: old, [to]: next}}});
|
||||
const old = { version: 1 },
|
||||
next = { version: 2 },
|
||||
from = contentDigest(old),
|
||||
to = contentDigest(next);
|
||||
await apply("migration", {
|
||||
...base,
|
||||
name: "upgrade",
|
||||
id: "export:upgrade",
|
||||
migration: {
|
||||
id: "upgrade-v2",
|
||||
scopeId: "board",
|
||||
from,
|
||||
to,
|
||||
predecessors: [],
|
||||
ports: [],
|
||||
contracts: { [from]: old, [to]: next },
|
||||
},
|
||||
});
|
||||
const migrationFile = path.join(root, base.directory, "src/migrations/upgrade.ts");
|
||||
await fs.appendFile(migrationFile, "\n// authored migration change\n");
|
||||
await sealMigrations(path.join(root, base.directory));
|
||||
@@ -50,18 +89,38 @@ test("imperative scaffolds preserve authored wiring; migration sealing is explic
|
||||
const catalog = JSON.parse(await fs.readFile(path.join(root, base.directory, "quixos.migrations.json"), "utf8"));
|
||||
assert.equal(catalog.migrations[0].implementation.digest, contentDigest(await fs.readFile(migrationFile, "utf8")));
|
||||
assert.match(await fs.readFile(path.join(root, base.directory, "src/migrate.ts"), "utf8"), /export:upgrade/);
|
||||
await assert.rejects(() => apply("function", {...base, name: "play", id: "export:play"}), /unique/);
|
||||
await assert.rejects(() => apply("function", { ...base, name: "play", id: "export:play" }), /unique/);
|
||||
const declarations = path.join(root, base.directory, "package.qx");
|
||||
await fs.writeFile(declarations, (await fs.readFile(declarations, "utf8")).replace(/}\s*$/, ' function authored id "export:authored" : unit -> unit;\n}\n'));
|
||||
await fs.writeFile(
|
||||
declarations,
|
||||
(await fs.readFile(declarations, "utf8")).replace(
|
||||
/}\s*$/,
|
||||
' function authored id "export:authored" : unit -> unit;\n}\n',
|
||||
),
|
||||
);
|
||||
const server = path.join(root, base.directory, "src/server.ts");
|
||||
await fs.appendFile(server, "\n// authored comment must survive\n");
|
||||
await apply("function", {...base, name: "another", id: "export:another"});
|
||||
await apply("function", { ...base, name: "another", id: "export:another" });
|
||||
assert.match(await fs.readFile(server, "utf8"), /authored comment must survive/);
|
||||
await assert.rejects(() => apply("refresh", base), /removed/);
|
||||
await assert.rejects(fs.access(path.join(root, base.directory, "src/impl/authored.ts")));
|
||||
assert.equal(await fs.readFile(filename, "utf8"), edited);
|
||||
await planStructure(root, {kind: "package", source, resourceRoot: base.directory, validation: "syntax", files: [{
|
||||
file: `${base.directory}/package.qx`, edits: [{operation: "replace", target: {kind: "packageResourceDecl", id: "package:chess"},
|
||||
source: 'package Other id "package:other" revision "package:other@1" {}'}],
|
||||
}]});
|
||||
await planStructure(root, {
|
||||
kind: "package",
|
||||
source,
|
||||
resourceRoot: base.directory,
|
||||
validation: "syntax",
|
||||
files: [
|
||||
{
|
||||
file: `${base.directory}/package.qx`,
|
||||
edits: [
|
||||
{
|
||||
operation: "replace",
|
||||
target: { kind: "packageResourceDecl", id: "package:chess" },
|
||||
source: 'package Other id "package:other" revision "package:other@1" {}',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { editStructure, scaffoldResourceSource, instantiateWorkspaceIdentity } from "../src/capability-language/structural-edits.js";
|
||||
import {
|
||||
editStructure,
|
||||
scaffoldResourceSource,
|
||||
instantiateWorkspaceIdentity,
|
||||
} from "../src/capability-language/structural-edits.js";
|
||||
|
||||
test("template identity binding changes only the workspace header and is idempotent", () => {
|
||||
const source = '// workspace fake id "do-not-touch"\nworkspace Todo id "workspace:todo" revision "workspace:todo@1" commit "' + "a".repeat(40) + '" { atom Task id "atom:task"; }';
|
||||
const source =
|
||||
'// workspace fake id "do-not-touch"\nworkspace Todo id "workspace:todo" revision "workspace:todo@1" commit "' +
|
||||
"a".repeat(40) +
|
||||
'" { atom Task id "atom:task"; }';
|
||||
const id = "00000000-0000-0000-0000-000000000123";
|
||||
const bound = instantiateWorkspaceIdentity(source, id);
|
||||
assert.match(bound, /atom Task id "atom:task"/);
|
||||
@@ -16,41 +23,81 @@ test("template identity binding changes only the workspace header and is idempot
|
||||
test("package scaffolding and function edits preserve surrounding source and use exact selectors", () => {
|
||||
const source = `// 🧭 resource comment\n${scaffoldResourceSource("package", "Chess", "package:chess", "package:chess@1")}`;
|
||||
const functionSource = 'function evaluate id "export:evaluate" : string -> string;';
|
||||
const appended = editStructure(source, {operation: "append", parent: {kind: "packageResourceDecl", id: "package:chess"}, source: functionSource});
|
||||
const appended = editStructure(source, {
|
||||
operation: "append",
|
||||
parent: { kind: "packageResourceDecl", id: "package:chess" },
|
||||
source: functionSource,
|
||||
});
|
||||
assert.ok(appended.startsWith("// 🧭 resource comment\n"));
|
||||
assert.ok(appended.includes(functionSource));
|
||||
const replaced = editStructure(appended, {operation: "replace", target: {kind: "packageFunctionExport", id: "export:evaluate"}, source: 'function evaluate id "export:evaluate" : unit -> string;'});
|
||||
const replaced = editStructure(appended, {
|
||||
operation: "replace",
|
||||
target: { kind: "packageFunctionExport", id: "export:evaluate" },
|
||||
source: 'function evaluate id "export:evaluate" : unit -> string;',
|
||||
});
|
||||
assert.ok(replaced.includes(": unit -> string;"));
|
||||
assert.ok(!editStructure(replaced, {operation: "remove", target: {kind: "packageFunctionExport", id: "export:evaluate"}}).includes("evaluate"));
|
||||
assert.throws(() => editStructure(source, {operation: "remove", target: {kind: "packageResourceDecl", id: "package:chess@1"}}), /exactly once/);
|
||||
assert.throws(() => editStructure(source, {operation: "append", parent: {kind: "packageResourceDecl"}, source: "not valid QX"}), /Invalid structural/);
|
||||
assert.ok(
|
||||
!editStructure(replaced, {
|
||||
operation: "remove",
|
||||
target: { kind: "packageFunctionExport", id: "export:evaluate" },
|
||||
}).includes("evaluate"),
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
editStructure(source, { operation: "remove", target: { kind: "packageResourceDecl", id: "package:chess@1" } }),
|
||||
/exactly once/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
editStructure(source, { operation: "append", parent: { kind: "packageResourceDecl" }, source: "not valid QX" }),
|
||||
/Invalid structural/,
|
||||
);
|
||||
const prefixed = `import interface Board;\nexternal atom Game id "atom:game";\n${source}`;
|
||||
const replacedPackage = editStructure(prefixed, {operation: "replace", target: {kind: "packageResourceDecl", id: "package:chess"}, source: scaffoldResourceSource("package", "Chess", "package:chess", "package:chess@2")});
|
||||
const replacedPackage = editStructure(prefixed, {
|
||||
operation: "replace",
|
||||
target: { kind: "packageResourceDecl", id: "package:chess" },
|
||||
source: scaffoldResourceSource("package", "Chess", "package:chess", "package:chess@2"),
|
||||
});
|
||||
assert.ok(replacedPackage.startsWith('import interface Board;\nexternal atom Game id "atom:game";'));
|
||||
});
|
||||
|
||||
test("dependency scaffolding validates exact sources and preserves unrelated lock comments", () => {
|
||||
const source = `// 🧭 lock\nquixos-lock version 1 {\n quixos source { repository "https://example.test/quixos.git"; commit "${"a".repeat(40)}"; }\n // retained comment\n}\n`;
|
||||
const dependency = {operation: "dependency" as const, kind: "package" as const, name: "Chess", source: {repository: "https://example.test/chess.git", commit: "b".repeat(40)}};
|
||||
const dependency = {
|
||||
operation: "dependency" as const,
|
||||
kind: "package" as const,
|
||||
name: "Chess",
|
||||
source: { repository: "https://example.test/chess.git", commit: "b".repeat(40) },
|
||||
};
|
||||
const appended = editStructure(source, dependency);
|
||||
assert.ok(appended.includes("// retained comment"));
|
||||
assert.ok(appended.startsWith("// 🧭 lock\n"));
|
||||
assert.throws(() => editStructure(source, {...dependency, source: {...dependency.source, commit: "main"}}), /Invalid dependency/);
|
||||
const changed = editStructure(appended, {...dependency, source: {...dependency.source, commit: "c".repeat(40)}});
|
||||
assert.throws(
|
||||
() => editStructure(source, { ...dependency, source: { ...dependency.source, commit: "main" } }),
|
||||
/Invalid dependency/,
|
||||
);
|
||||
const changed = editStructure(appended, { ...dependency, source: { ...dependency.source, commit: "c".repeat(40) } });
|
||||
assert.ok(!changed.includes("b".repeat(40)));
|
||||
assert.ok(!editStructure(changed, {...dependency, source: null}).includes("package Chess"));
|
||||
assert.ok(!editStructure(changed, { ...dependency, source: null }).includes("package Chess"));
|
||||
});
|
||||
|
||||
test("conformance enrollment, major edits, imports, and private attachment removal preserve valid structure", () => {
|
||||
const source = `fragment { conform Game as Playable { private state Board id "slot:board" on Game : string policy optimistic-register; } }`;
|
||||
const selector = {kind: "conformanceDecl", names: ["Game", "Playable"]};
|
||||
const enrolled = editStructure(source, {operation: "conformance-id", target: selector, id: "conformance:playable"});
|
||||
const selector = { kind: "conformanceDecl", names: ["Game", "Playable"] };
|
||||
const enrolled = editStructure(source, { operation: "conformance-id", target: selector, id: "conformance:playable" });
|
||||
assert.ok(enrolled.includes('as Playable id "conformance:playable"'));
|
||||
const major = editStructure(enrolled, {operation: "semantic-major", target: {kind: "conformanceDecl", id: "conformance:playable"}, major: 2});
|
||||
const major = editStructure(enrolled, {
|
||||
operation: "semantic-major",
|
||||
target: { kind: "conformanceDecl", id: "conformance:playable" },
|
||||
major: 2,
|
||||
});
|
||||
assert.ok(major.includes("semantic-major 2"));
|
||||
assert.throws(() => editStructure(major, {operation: "conformance-id", target: selector, id: "different"}), /Cannot change/);
|
||||
const removed = editStructure(major, {operation: "remove", target: {kind: "stateDecl", id: "slot:board"}});
|
||||
assert.throws(
|
||||
() => editStructure(major, { operation: "conformance-id", target: selector, id: "different" }),
|
||||
/Cannot change/,
|
||||
);
|
||||
const removed = editStructure(major, { operation: "remove", target: { kind: "stateDecl", id: "slot:board" } });
|
||||
assert.ok(!removed.includes("private"));
|
||||
const imported = editStructure(removed, {operation: "import", kind: "interface", name: "Playable"});
|
||||
assert.equal(editStructure(imported, {operation: "import", kind: "interface", name: "Playable"}), imported);
|
||||
const imported = editStructure(removed, { operation: "import", kind: "interface", name: "Playable" });
|
||||
assert.equal(editStructure(imported, { operation: "import", kind: "interface", name: "Playable" }), imported);
|
||||
});
|
||||
|
||||
@@ -5,18 +5,34 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { planStructure, applyStructure, resumeStructure, type StructuralRequest } from "../src/capability-language/structural-plan.js";
|
||||
import {
|
||||
planStructure,
|
||||
applyStructure,
|
||||
resumeStructure,
|
||||
type StructuralRequest,
|
||||
} from "../src/capability-language/structural-plan.js";
|
||||
const execFile = promisify(callback);
|
||||
|
||||
test("structural plans validate the graph, journal originals, and reject stale edits", async (context) => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-structural-test-"));
|
||||
context.after(() => fs.rm(root, {recursive: true, force: true}));
|
||||
context.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
await execFile("git", ["-C", root, "init"]);
|
||||
await fs.writeFile(path.join(root, ".gitignore"), ".quixos/\n");
|
||||
await fs.writeFile(path.join(root, "quixos.lock"), `quixos-lock version 1 { quixos source { repository "https://example.test/quixos.git"; commit "${"a".repeat(40)}"; } }`);
|
||||
await fs.writeFile(
|
||||
path.join(root, "quixos.lock"),
|
||||
`quixos-lock version 1 { quixos source { repository "https://example.test/quixos.git"; commit "${"a".repeat(40)}"; } }`,
|
||||
);
|
||||
const before = `workspace Test id "workspace:test" revision "workspace:test@1" commit "${"b".repeat(40)}" { atom Subject id "atom:subject"; }`;
|
||||
await fs.writeFile(path.join(root, "workspace.qx"), before);
|
||||
const request: StructuralRequest = {kind: "workspace", files: [{file: "workspace.qx", edits: [{operation: "append", parent: {kind: "workspaceDecl"}, source: 'atom Game id "atom:game";'}]}]};
|
||||
const request: StructuralRequest = {
|
||||
kind: "workspace",
|
||||
files: [
|
||||
{
|
||||
file: "workspace.qx",
|
||||
edits: [{ operation: "append", parent: { kind: "workspaceDecl" }, source: 'atom Game id "atom:game";' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const plan = await planStructure(root, request);
|
||||
assert.equal(await fs.readFile(path.join(root, "workspace.qx"), "utf8"), before);
|
||||
await fs.writeFile(path.join(root, "workspace.qx"), `${before}\n// newer edit`);
|
||||
@@ -35,12 +51,14 @@ test("structural plans validate the graph, journal originals, and reject stale e
|
||||
assert.equal((await resumeStructure(root, applied.id)).phase, "complete");
|
||||
await assert.rejects(() => planStructure(root, request), /Duplicate|duplicate/);
|
||||
// Cross-repository edits may temporarily refer to an unfinished provider.
|
||||
const provisional: StructuralRequest = {kind: "workspace", validation: "syntax", files: [{file: "workspace.qx", edits: [
|
||||
{operation: "import", kind: "interface", name: "NotImplementedYet"},
|
||||
]}]};
|
||||
const provisional: StructuralRequest = {
|
||||
kind: "workspace",
|
||||
validation: "syntax",
|
||||
files: [{ file: "workspace.qx", edits: [{ operation: "import", kind: "interface", name: "NotImplementedYet" }] }],
|
||||
};
|
||||
const draft = await planStructure(root, provisional);
|
||||
assert.equal(draft.validation, "syntax");
|
||||
await applyStructure(draft);
|
||||
assert.match(await fs.readFile(path.join(root, "workspace.qx"), "utf8"), /import interface NotImplementedYet/);
|
||||
await assert.rejects(() => planStructure(root, {...provisional, validation: "resource-graph"}));
|
||||
await assert.rejects(() => planStructure(root, { ...provisional, validation: "resource-graph" }));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user