Files

125 lines
5.6 KiB
TypeScript
Raw Permalink Normal View History

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<typeof scaffoldRecipe>[1],
spec: Omit<Parameters<typeof scaffoldRecipe>[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 [JavaScript,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<typeof scaffoldRecipe>[1],
spec: Omit<Parameters<typeof scaffoldRecipe>[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/);
});