import test from "node:test"; 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 { 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})); 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)}"; } }`); 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 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`); await assert.rejects(() => applyStructure(plan), /Stale scaffold/); await fs.writeFile(path.join(root, "workspace.qx"), before); const applied = await applyStructure(plan); assert.equal(applied.phase, "complete"); assert.ok((await fs.readFile(path.join(root, "workspace.qx"), "utf8")).includes("atom Game")); const journal = JSON.parse(await fs.readFile(applied.journalPath, "utf8")); assert.equal(journal.changes[0].before, before); assert.equal((await resumeStructure(root, applied.id)).phase, "complete"); // Simulate a process interrupted after one file was written, before recording // completion. Replay accepts the already-written exact after image. journal.phase = "prepared"; await fs.writeFile(applied.journalPath, JSON.stringify(journal)); 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 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"})); });