Files

251 lines
12 KiB
TypeScript

import { test, expect, afterEach } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync, existsSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { createFrameworkMcpServer } from "../../mcp/src/index.ts";
import { DesignAgentService } from "../src/agent-server.ts";
import { agentGenerationContext } from "../src/agent-context.ts";
import { checkDesignConformance } from "../src/conformance.ts";
import { agentPageSource, checkAgentPage, type AgentPage } from "../src/agent-pages.ts";
import { CORE_DESIGN_SYSTEMS } from "../../design-system/src/index.ts";
import { compileWrnFile } from "../../compiler/src/index.ts";
const roots: string[] = [];
function root() {
const path = mkdtempSync(join(tmpdir(), "wrn-phase6-"));
roots.push(path);
writeFileSync(join(path, "package.json"), "{}");
return path;
}
afterEach(() => {
for (const path of roots.splice(0)) rmSync(path, { recursive: true, force: true });
});
const page = (id: string): AgentPage => ({
id,
title: id === "account" ? "Account settings" : "Contact support",
components: [
{ component: "PageHeader", props: { title: "Your workspace" } },
{ component: "Input", props: { id: id + "-name", label: "Name" } },
{ component: "Button", props: { label: "Continue", variant: "default" } },
],
});
test("MCP defaults stay unchanged; explicit design opt-in exposes read tools and separate write opt-in", async () => {
const path = root();
expect(createFrameworkMcpServer(path).tools()).toHaveLength(8);
const readonly = createFrameworkMcpServer(path, { design: true });
expect(readonly.tools().some((t) => t.name === "wrnexus_design_context")).toBe(true);
expect(readonly.tools().some((t) => t.name === "wrnexus_design_apply")).toBe(false);
expect(
((await readonly.call("wrnexus_design_context")) as any).components.Button.parts,
).toBeDefined();
expect(existsSync(join(path, ".wrnexus"))).toBe(false);
const response = await readonly.handle({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: "wrnexus_design_apply", arguments: { baseRevision: "none" } },
});
expect((response as any).result.isError).toBe(true);
});
test("recover invalid edits, stale conflicts, locks, undo and checkpoints across service instances", async () => {
const path = root();
let service = new DesignAgentService(path, true);
let draft: any = await service.call("create", { baseRevision: "none" });
const first = draft.revision;
draft = await service.call("locks", { baseRevision: first, locks: ["/tokens/color.primary"] });
await expect(service.call("create", { baseRevision: first })).rejects.toThrow("CONFLICT");
draft = await service.call("propose", { baseRevision: draft.revision, source: "{unfinished" });
expect(draft.diagnostics.length).toBeGreaterThan(0);
service = new DesignAgentService(path, true);
let state: any = await service.call("inspect");
expect(state.source).toBe("{unfinished");
expect(state.valid.id).toBe("square");
await expect(
service.call("apply", { baseRevision: state.revision, baseApplicationRevision: "none" }),
).rejects.toThrow("INVALID");
draft = await service.call("revert", { baseRevision: state.revision });
state = await service.call("inspect");
expect(state.diagnostics).toEqual([]);
const locked = structuredClone(state.valid);
locked.tokens["color.primary"].value = "#abcdef";
draft = await service.call("propose", {
baseRevision: draft.revision,
source: JSON.stringify(locked),
});
expect(draft.diagnostics.length).toBeGreaterThan(0);
await service.call("revert", { baseRevision: draft.revision, target: "checkpoint" });
state = await service.call("inspect");
expect(state.diagnostics).toEqual([]);
expect(state.locks).toEqual(["/tokens/color.primary"]);
});
test("agents create two compilable consistent pages across all three designs and both modes", async () => {
for (const system of Object.values(CORE_DESIGN_SYSTEMS)) {
const service = new DesignAgentService(root(), true);
let draft: any = await service.call("create", {
baseRevision: "none",
source: JSON.stringify(system),
});
for (const id of ["account", "support"]) {
draft = await service.call("page", { baseRevision: draft.revision, page: page(id) });
expect(draft.result.ok).toBe(true);
const source = agentPageSource(page(id));
expect(checkDesignConformance(source).ok).toBe(true);
expect(compileWrnFile(source)).toBeTruthy();
}
for (const mode of ["light", "dark"])
for (const id of ["account", "support"]) {
const preview: any = await service.call("preview", { pageId: id, mode });
expect(preview.html).toContain("Content-Security-Policy");
expect(preview.html).toContain('data-wrn-component="Button"');
expect(preview.html).toContain('data-theme="' + mode + '"');
expect(preview.html).toContain("--wrn-radius");
}
const exported: any = await service.call("export");
expect(Object.keys(exported.pages)).toEqual(["account.wrn", "support.wrn"]);
expect(((await service.call("validate")) as any).ok).toBe(true);
}
});
test("conformance rejects invented axes and props; only exact warnings can be waived", () => {
const source =
'page Test { view { <div data-component="Button" variant="invented" nonsense="x"></div> } style { main { color: #fff; padding: 17px; } } }';
const report = checkDesignConformance(source);
expect(report.ok).toBe(false);
expect(report.diagnostics.some((d) => d.code === "WRN-CONFORMANCE-AXIS")).toBe(true);
expect(report.diagnostics.some((d) => d.code === "WRN-CONFORMANCE-PROP")).toBe(true);
const warning = "page Test { view { <main>Test</main> } style { main { color: #fff; } } }";
const d = checkDesignConformance(warning).diagnostics[0]!;
expect(
checkDesignConformance(warning, [
{ code: d.code, path: d.path, reason: "Reviewed brand illustration" },
]).ok,
).toBe(true);
expect(
checkDesignConformance(source, [
{ code: "WRN-CONFORMANCE-AXIS", path: "$.view[0].variant", reason: "No" },
]).ok,
).toBe(false);
expect(checkDesignConformance("broken").ok).toBe(false);
expect(
checkAgentPage({
...page("account"),
components: [{ component: "Button", props: { variant: "invented" } }],
}).ok,
).toBe(false);
expect(
checkAgentPage({
...page("account"),
components: [{ component: "Button", props: { label: "javascript:alert(1)" } }],
}).ok,
).toBe(false);
});
test("application revision checks, exports and reversal preserve existing app configuration", async () => {
const path = root();
writeFileSync(join(path, "wrn.config.ts"), "existing config");
const service = new DesignAgentService(path, true);
let draft: any = await service.call("create", { baseRevision: "none" });
await expect(
service.call("apply", { baseRevision: draft.revision, baseApplicationRevision: "stale" }),
).rejects.toThrow("CONFLICT");
draft = await service.call("apply", {
baseRevision: draft.revision,
baseApplicationRevision: "none",
});
expect(draft.result.revision).not.toBe("none");
expect(readFileSync(join(path, "wrn.config.ts"), "utf8")).toBe("existing config");
const reverted: any = await service.call("revert", {
baseRevision: draft.revision,
target: "application",
baseApplicationRevision: draft.result.revision,
});
expect(reverted.result.revision).toBe("none");
mkdirSync(join(path, ".wrnexus", "design-studio", "agent", "LOCK"));
await expect(service.call("create", { baseRevision: reverted.revision })).rejects.toThrow("BUSY");
});
test("agent context carries resolved tokens and real contract boundaries, with bounded selection", () => {
for (const system of Object.values(CORE_DESIGN_SYSTEMS)) {
const context = agentGenerationContext(system, { components: ["Button", "Input"] });
expect(Object.keys(context.components)).toEqual(["Button", "Input"]);
expect(context.tokens["color.primary"]).toBeDefined();
expect((context.components.Button.axes as any).variant.values).toContain("default");
}
expect(() =>
agentGenerationContext(CORE_DESIGN_SYSTEMS.square, { components: ["Invented"] }),
).toThrow("CONTEXT");
});
test("stdio design protocol advertises opt-in tools and returns structured context without model credentials", async () => {
const child = Bun.spawn(["bun", "packages/mcp/src/stdio.ts", "--root=" + root(), "--design"], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
child.stdin.write(
JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }) +
"\n" +
JSON.stringify({
jsonrpc: "2.0",
id: 2,
method: "tools/call",
params: { name: "wrnexus_design_context", arguments: { components: ["Button"] } },
}) +
"\n",
);
child.stdin.end();
const lines = (await new Response(child.stdout).text())
.trim()
.split("\n")
.map((line) => JSON.parse(line));
expect(await child.exited).toBe(0);
expect(lines[0].result.tools.some((t: any) => t.name === "wrnexus_design_context")).toBe(true);
expect(lines[1].result.structuredContent.value.components.Button).toBeDefined();
});
test("exported pages preserve literal string, boolean and structured props in compiled mount markup", async () => {
const source = agentPageSource({
id: "literal-props",
title: "Literal props",
components: [
{ component: "Button", props: { label: "Continue", disabled: false } },
{ component: "Select", props: { options: [{ label: "One", value: "one" }] } },
],
});
const code = new Bun.Transpiler({ loader: "ts" }).transformSync(
compileWrnFile(source).replace(
/from (["'])@wrnexus\/core\1/g,
`from ${JSON.stringify(new URL("../../core/src/index.ts", import.meta.url).href)}`,
),
);
const mod = await import("data:text/javascript;base64," + Buffer.from(code).toString("base64"));
const html = await mod.default({});
expect(html).toContain('label="Continue"');
expect(html).toContain('disabled="false"');
expect(html).toContain(
'options="[{&quot;label&quot;:&quot;One&quot;,&quot;value&quot;:&quot;one&quot;}]"',
);
});
import { createDesignPackage } from "@wrnexus/design-system/package";
test("MCP package upgrades preserve history and reject future agent storage without overwriting it", async () => {
const path = root();
const server = createFrameworkMcpServer(path, { design: true, designWrite: true });
let draft: any = await server.call("wrnexus_design_create", { baseRevision: "none" });
const next = structuredClone(CORE_DESIGN_SYSTEMS.square);
next.version = "1.2.0";
const source = JSON.stringify(await createDesignPackage(next));
expect(((await server.call("wrnexus_design_upgrade_plan", { source })) as any).canApply).toBe(
true,
);
draft = await server.call("wrnexus_design_upgrade", { baseRevision: draft.revision, source });
expect(((await server.call("wrnexus_design_inspect")) as any).valid.version).toBe("1.2.0");
await expect(
server.call("wrnexus_design_upgrade", { baseRevision: "none", source }),
).rejects.toThrow("CONFLICT");
await server.call("wrnexus_design_revert", { baseRevision: draft.revision });
expect(((await server.call("wrnexus_design_inspect")) as any).valid.version).toBe("1.0.0");
const file = join(path, ".wrnexus", "design-studio", "agent", "STATE.json");
const state = JSON.parse(readFileSync(file, "utf8"));
const future = JSON.stringify({ ...state, storageVersion: 2 });
writeFileSync(file, future);
await expect(
server.call("wrnexus_design_create", { baseRevision: state.revision }),
).rejects.toThrow("RECOVERY");
expect(readFileSync(file, "utf8")).toBe(future);
});