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("package structural validation does not require source-generated bindings", async (context) => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-structural-package-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 = 'package Test id "package:test" revision "package:test@1" {}'; await fs.writeFile(path.join(root, "package.qx"), before); const request: StructuralRequest = { kind: "package", source: { repository: "https://example.test/package.git", commit: "b".repeat(40) }, files: [{ file: "package.qx", expected: before, replace: `${before}\n// updated` }], }; const plan = await planStructure(root, request); assert.deepEqual( plan.changes.map((change) => change.file), ["package.qx"], ); assert.equal(plan.validation, "resource-graph"); await fs.writeFile(path.join(root, "quixos.check.json"), "not valid json"); await assert.rejects(() => planStructure(root, request), SyntaxError); await fs.writeFile(path.join(root, "quixos.check.json"), '{"backend":"execution"}'); assert.deepEqual( (await planStructure(root, request)).changes.map((change) => change.file), ["package.qx"], ); }); 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" })); const plugin = ".yarn/plugins/yarn-plugin-nixify.cjs"; await applyStructure( await planStructure(root, { kind: "workspace", validation: "syntax", files: [ { file: plugin, create: "module.exports = {};\n" }, { file: "yarn.lock", create: "__metadata:\n version: 8\n" }, { file: "flake.lock", create: '{"version":7}\n' }, ], }), ); assert.equal(await fs.readFile(path.join(root, plugin), "utf8"), "module.exports = {};\n"); for (const file of [".yarn/../escape.cjs", ".yarn/cache/entry.cjs", ".git/hooks/hook.cjs"]) { await assert.rejects( () => planStructure(root, { kind: "workspace", validation: "syntax", files: [{ file, create: "" }], }), /Unsafe scaffold path/, ); } });