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 { execFile as callback } from "node:child_process"; import { promisify } from "node:util"; import { componentEntries } from "../src/capability-language/component-nix.js"; import { scaffoldRecipe } from "../src/capability-language/scaffold-recipes.js"; import { planStructure, applyStructure } from "../src/capability-language/structural-plan.js"; const execFile = promisify(callback); test("checked component scaffolding composes with execution handlers without replacing authored files", async (context) => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-component-scaffold-")); context.after(() => fs.rm(root, { recursive: true, force: true })); await execFile("git", ["-C", root, "init"]); const source = { repository: "https://example.test/component.git", commit: "a".repeat(40) }; const apply = async ( command: Parameters[1], spec: Omit[2], "source">, ) => applyStructure(await planStructure(root, await scaffoldRecipe(root, command, { ...spec, source }))); await apply("package", { name: "Widgets", id: "package:widgets", revision: "package:widgets@1", template: "typescript-component", tools: { quixos: source, protocol: source, helpers: source, sdk: source }, }); const packageFile = path.join(root, "package.qx"); await fs.writeFile(packageFile, "import interface View;\n" + (await fs.readFile(packageFile, "utf8"))); await apply("component", { name: "Card", id: "component:card", declaration: 'module Card id "component:card" implements View.render;', }); assert.deepEqual( { ...(await componentEntries(await fs.readFile(path.join(root, "components.nix"), "utf8"))) }, { "component:card": { entry: "src/components/Card.tsx", export: "Card" }, }, ); const card = path.join(root, "src/components/Card.tsx"); await fs.appendFile(card, "\n// authored extension\n"); const retained = await fs.readFile(card, "utf8"); const flake = path.join(root, "flake.nix"); await fs.appendFile(flake, "\n# retained authored toolchain configuration\n"); const retainedFlake = await fs.readFile(flake, "utf8"); const entriesFile = path.join(root, "components.nix"); await fs.writeFile( entriesFile, (await fs.readFile(entriesFile, "utf8")).replace("{\n", "{\n # retained entry comment\n"), ); await apply("function", { name: "echo", id: "echo", declaration: 'function echo id "echo" : string -> string execution snapshot targets [browser,server];', }); await apply("component", { name: "Details", id: "component:details", declaration: 'module Details id "component:details" implements View.render;', }); assert.equal(await fs.readFile(card, "utf8"), retained); assert.equal(await fs.readFile(flake, "utf8"), retainedFlake); assert.match(await fs.readFile(entriesFile, "utf8"), /retained entry comment/); assert.match(await fs.readFile(path.join(root, "src/server.ts"), "utf8"), /echo/); assert.match(await fs.readFile(path.join(root, "flake.nix"), "utf8"), /mkCaminoReactPackage/); await assert.rejects( apply("component", { name: "Card", id: "component:other", declaration: 'module Card id "component:other" implements View.render;', }), /unique/, ); await assert.rejects( apply("component", { name: "Injected", id: "component:inject", declaration: 'module Injected id "component:inject" implements View.render; function sneak id "sneak" : unit -> unit;', }), /exactly one/, ); const beforeUnsupported = await fs.readFile(packageFile, "utf8"); await fs.writeFile(entriesFile, '{\n custom = builtins.abort "must not evaluate";\n}\n'); await assert.rejects( apply("component", { name: "Unsupported", id: "component:unsupported", declaration: 'module Unsupported id "component:unsupported" implements View.render;', }), /literal-entry shape/, ); assert.equal(await fs.readFile(packageFile, "utf8"), beforeUnsupported); }); test("controller scaffolding preserves existing registrations and rejects duplicate identities", async (context) => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-controller-scaffold-")); context.after(() => fs.rm(root, { recursive: true, force: true })); await execFile("git", ["-C", root, "init"]); const source = { repository: "https://example.test/adapter.git", commit: "a".repeat(40) }; const apply = async ( command: Parameters[1], spec: Omit[2], "source">, ) => applyStructure(await planStructure(root, await scaffoldRecipe(root, command, { ...spec, source }))); await apply("package", { name: "Adapter", id: "adapter", revision: "adapter@1", tools: { quixos: source, protocol: source, helpers: source }, }); const controller = { subject: "display@1", snapshot: "display:requested", remote: "display:apply", input: "value" as const, }; await apply("controller", { id: "display:controller", controller }); await apply("controller", { id: "display:monitor", controller: { ...controller, snapshot: "display:status" } }); const entries = JSON.parse(await fs.readFile(path.join(root, "quixos.controllers.json"), "utf8")).controllers; assert.equal(entries.length, 2); assert.deepEqual(entries[0], { id: "display:controller", ...controller }); await assert.rejects(apply("controller", { id: "display:controller", controller }), /duplicate/); });