104 lines
5.0 KiB
TypeScript
104 lines
5.0 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
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 id = "00000000-0000-0000-0000-000000000123";
|
|
const bound = instantiateWorkspaceIdentity(source, id);
|
|
assert.match(bound, /atom Task id "atom:task"/);
|
|
assert.ok(bound.startsWith('// workspace fake id "do-not-touch"'));
|
|
assert.ok(bound.includes(`workspace Todo id "${id}"`));
|
|
assert.equal(instantiateWorkspaceIdentity(bound, id), bound);
|
|
assert.throws(() => instantiateWorkspaceIdentity(source, "not-a-workspace"), /UUID/);
|
|
});
|
|
|
|
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,
|
|
});
|
|
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;',
|
|
});
|
|
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/,
|
|
);
|
|
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"),
|
|
});
|
|
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 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.ok(!changed.includes("b".repeat(40)));
|
|
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" });
|
|
assert.ok(enrolled.includes('as Playable id "conformance:playable"'));
|
|
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.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);
|
|
});
|