251 lines
12 KiB
TypeScript
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="[{"label":"One","value":"one"}]"',
|
|
);
|
|
});
|
|
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);
|
|
});
|