feat: release design system engine, studio and component contracts

This commit is contained in:
Clintchiz committed 2026-10-19 13:09:03 +05:30
1 parent ff1065fb27
commit 3943b946d6
693 files changed
+175546 -3894

No files matched your search

+250
View File
@@ -0,0 +1,250 @@
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);
});
@@ -0,0 +1,286 @@
import { test, expect } from "bun:test";
import { CORE_DESIGN_SYSTEMS, validateDesignSystem } from "../../design-system/src/index.ts";
import {
generateDesigns,
DEFAULT_DESIGN_BRIEF,
type GenerationRequest,
type GenerationProvider,
} from "../src/generation.ts";
import { designRevision, checkDecisionLocks, preserveDecisionLocks } from "../src/decisions.ts";
import { combineDesignProposals } from "../src/guidance.ts";
import { StudioSession } from "../src/session.ts";
const source = () => structuredClone(CORE_DESIGN_SYSTEMS.square);
async function request(
kind: "directions" | "edit" = "directions",
locks: string[] = [],
system = source(),
): Promise<GenerationRequest> {
return {
system,
locks,
brief: { ...DEFAULT_DESIGN_BRIEF, product: "Booking site", audience: "Mobile customers" },
baseRevision: await designRevision(system, locks),
kind,
instruction: kind === "edit" ? "make buttons rounded and use spacious spacing" : undefined,
};
}
const patched = (changes: unknown[]) =>
JSON.stringify({
alternatives: [{ name: "Edited", rationale: "Requested change", patch: changes }],
});
const set = (path: string, value: unknown) => ({ op: "set", path, value });
test("local guided generation produces three distinct validated directions without changing source", async () => {
const input = await request();
const original = JSON.stringify(input.system);
const result = await generateDesigns(input);
expect(result.diagnostics).toEqual([]);
expect(result.alternatives).toHaveLength(3);
expect(new Set(result.alternatives.map((p) => JSON.stringify(p.system))).size).toBe(3);
for (const proposal of result.alternatives) {
expect(validateDesignSystem(proposal.system)).toEqual([]);
expect(Object.keys(proposal.system.recipes!)).toHaveLength(104);
expect(proposal.system.id).toBe(input.system.id);
}
expect(JSON.stringify(input.system)).toBe(original);
expect(result.provenance.provider).toBe("local-rules");
expect(result.provenance.maxOutputTokens).toBe(0);
});
test("guided geometry, density, surface and brand choices affect the brief direction", async () => {
const input = await request();
input.brief = {
...input.brief,
geometry: "rounded",
density: "spacious",
surface: "soft",
brandColor: "#112233",
};
const result = await generateDesigns(input);
expect(result.diagnostics).toEqual([]);
const system = result.alternatives[0]!.system;
expect(system.tokens["shape.control"]!.value).toBe("16px");
expect(system.tokens["space.control"]!.value).toBe("1.25rem");
expect(system.tokens["elevation.panel"]!.value).toContain("24px");
expect(system.modes!.light!["color.primary"]!.value).toBe("#112233");
expect(result.brief).toEqual(input.brief);
});
test("direct locks, mode overrides and alias changes cannot alter a locked token", () => {
const base = source();
base.tokens["shape.alias"] = { type: "dimension", value: { ref: "shape.control" } };
let next = structuredClone(base);
next.tokens["shape.control"]!.value = "8px";
expect(checkDecisionLocks(base, next, ["/tokens/shape.alias"])).not.toEqual([]);
next = structuredClone(base);
next.modes!.light!["shape.control"] = { type: "dimension", value: "8px" };
expect(checkDecisionLocks(base, next, ["/tokens/shape.alias"])).not.toEqual([]);
next = structuredClone(base);
next.tokens["space.page"]!.value = "5rem";
expect(checkDecisionLocks(base, next, ["/tokens/shape.alias"])).toEqual([]);
});
test("component recipe locks protect referenced token values and absent decisions", () => {
const base = source(),
next = structuredClone(base);
next.tokens["shape.control"]!.value = "8px";
expect(checkDecisionLocks(base, next, ["/recipes/Button"])).not.toEqual([]);
const added = structuredClone(base);
added.recipes!.Button!.base!.control!.rotate = "2deg";
expect(checkDecisionLocks(base, added, ["/recipes/Button/base/control/rotate"])).not.toEqual([]);
});
test("directions preserve locks, including recipe token dependencies", async () => {
const input = await request("directions", ["/recipes/Button"]);
const result = await generateDesigns(input);
expect(result.diagnostics).toEqual([]);
for (const p of result.alternatives)
expect(checkDecisionLocks(input.system, p.system, input.locks)).toEqual([]);
const next = preserveDecisionLocks(
input.system,
structuredClone(CORE_DESIGN_SYSTEMS.soft),
input.locks,
);
expect(checkDecisionLocks(input.system, next, input.locks)).toEqual([]);
});
test("manual edits, undo, reload and unlock keep lock-violating drafts recoverable", () => {
const memory = new Map<string, string>();
const storage = {
getItem: (k: string) => memory.get(k) ?? null,
setItem: (k: string, v: string) => {
memory.set(k, v);
},
};
const session = new StudioSession(undefined, storage);
session.setLocks(["/tokens/shape.control"]);
session.mutate((s) => {
s.tokens["shape.control"]!.value = "9px";
}, "Locked edit");
expect(session.valid.tokens["shape.control"]!.value).toBe("0");
expect(session.diagnostics[0]!.code).toBe("WRN-STUDIO-LOCK");
expect(session.source).toContain("9px");
const restored = new StudioSession(undefined, storage);
expect(restored.restore()).toBe(true);
expect(restored.locks).toEqual(session.locks);
expect(restored.diagnostics[0]!.code).toBe("WRN-STUDIO-LOCK");
restored.setLocks([]);
expect(restored.valid.tokens["shape.control"]!.value).toBe("9px");
restored.undo();
expect(restored.valid.tokens["shape.control"]!.value).toBe("0");
});
test("word-based edits validate whole requests and reject unknown or locked changes", async () => {
const result = await generateDesigns(await request("edit"));
expect(result.diagnostics).toEqual([]);
expect(result.alternatives[0]!.system.tokens["shape.control"]!.value).toBe("16px");
const locked = await generateDesigns(await request("edit", ["/tokens/shape.control"]));
expect(locked.alternatives).toEqual([]);
expect(locked.diagnostics[0]!.message).toContain("LOCK");
const input = await request("edit");
input.instruction = "make buttons rounded and invent a new calendar";
const failed = await generateDesigns(input);
expect(failed.alternatives).toEqual([]);
expect(input.system.tokens["shape.control"]!.value).toBe("0");
input.instruction = "set primary color to #aabbcc and remove shadows and use serif typography";
expect((await generateDesigns(input)).diagnostics).toEqual([]);
});
test("stale generation base and stale combinations fail without applying", async () => {
const input = await request();
input.baseRevision = "a".repeat(64);
expect(generateDesigns(input)).rejects.toThrow("WRN-STUDIO-CONFLICT");
const good = await request();
const batch = await generateDesigns(good);
const next = structuredClone(good.system);
next.name = "Changed";
await expect(combineDesignProposals(next, batch, { colors: "direction-2" }, [])).rejects.toThrow(
"CONFLICT",
);
});
test("foundation decisions combine and revalidate before entering a draft", async () => {
const input = await request();
const batch = await generateDesigns(input);
const next = await combineDesignProposals(
input.system,
batch,
{ geometry: "direction-2", typography: "direction-3" },
[],
);
expect(validateDesignSystem(next)).toEqual([]);
expect(next.tokens["shape.control"]).toEqual(
batch.alternatives[1]!.system.tokens["shape.control"],
);
expect(next.tokens["type.body"]).toEqual(batch.alternatives[2]!.system.tokens["type.body"]);
});
test("provider adapter uses bounded patches, compact capabilities and one repair", async () => {
let calls = 0;
let context = "";
const provider: GenerationProvider = {
name: "fixture",
async generate(prompt, options) {
calls++;
context = prompt;
expect(options!.maxTokens).toBe(512);
expect(options!.signal).toBeDefined();
return {
provider: "fixture",
model: "test-model",
value: calls === 1 ? "not json" : patched([set("/tokens/shape.control/value", "11px")]),
};
},
};
const result = await generateDesigns(await request("edit"), { provider, maxOutputTokens: 512 });
expect(calls).toBe(2);
expect(result.diagnostics).toEqual([]);
expect(result.provenance.model).toBe("test-model");
expect(result.provenance.attempts).toBe(2);
expect(context).toContain("capabilities");
expect(context).toContain("Repair these diagnostics");
});
test("provider invalid parts and lock violations cannot survive bounded repair", async () => {
let calls = 0;
const provider: GenerationProvider = {
name: "fixture",
async generate() {
calls++;
return { provider: "fixture", value: patched([set("/tokens/shape.control/value", "12px")]) };
},
};
const result = await generateDesigns(await request("edit", ["/tokens/shape.control"]), {
provider,
});
expect(calls).toBe(2);
expect(result.alternatives).toEqual([]);
expect(result.diagnostics[0]!.code).toBe("WRN-STUDIO-PROPOSAL");
const invalid: GenerationProvider = {
name: "fixture",
async generate() {
return {
provider: "fixture",
value: patched([set("/recipes/Button/base/control/position", "fixed")]),
};
},
};
expect(
(await generateDesigns(await request("edit"), { provider: invalid })).alternatives,
).toEqual([]);
});
test("prototype paths, excessive operations and execution strings are rejected", async () => {
for (const patch of [
[set("/tokens/__proto__/value", "x")],
Array.from({ length: 49 }, () => set("/name", "x")),
[set("/tokens/shape.control/value", "0;}body{display:none")],
]) {
const provider: GenerationProvider = {
name: "fixture",
async generate() {
return { provider: "fixture", value: patched(patch) };
},
};
expect((await generateDesigns(await request("edit"), { provider })).alternatives).toEqual([]);
}
});
test("cancellation and deadline stop even a provider that ignores its signal", async () => {
const provider: GenerationProvider = { name: "hung", generate: () => new Promise(() => {}) };
const controller = new AbortController();
const input = await request("edit");
const pending = generateDesigns(input, { provider, signal: controller.signal });
controller.abort();
await expect(pending).rejects.toThrow();
await expect(generateDesigns(input, { provider, timeoutMs: 15 })).rejects.toThrow("cancelled");
expect(input.system.tokens["shape.control"]!.value).toBe("0");
});
test("transport failure is not retried or returned with secret-bearing messages", async () => {
let count = 0;
const provider: GenerationProvider = {
name: "fixture",
async generate() {
count++;
throw Error("secret-key-in-provider-error");
},
};
const result = await generateDesigns(await request("edit"), { provider });
expect(count).toBe(1);
expect(JSON.stringify(result)).not.toContain("secret-key-in-provider-error");
expect(result.diagnostics[0]!.code).toBe("WRN-STUDIO-PROVIDER");
});
test("untrusted brief fields and generation budgets are bounded", async () => {
const input = await request();
input.brief.product = "x".repeat(2001);
await expect(generateDesigns(input)).rejects.toThrow("2000");
await expect(generateDesigns(await request(), { maxOutputTokens: 9000 })).rejects.toThrow(
"BUDGET",
);
const altered = await request();
altered.brief.brandColor = "red;}body{}";
await expect(generateDesigns(altered)).rejects.toThrow("BRIEF");
});
+174
View File
@@ -0,0 +1,174 @@
import { test, expect, afterAll } from "bun:test";
import { mkdtempSync, readFileSync, existsSync, writeFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { createStudioHandler } from "../src/server.ts";
import { StudioApplication } from "../src/application.ts";
import { CORE_DESIGN_SYSTEMS } from "../../design-system/src/index.ts";
import { resolveThemeConfig } from "../../styles/src/theme.ts";
import { designRevision } from "../src/decisions.ts";
import { DEFAULT_DESIGN_BRIEF } from "../src/generation.ts";
const root = mkdtempSync(join(tmpdir(), "wrn-studio-test-"));
afterAll(() => rmSync(root, { recursive: true, force: true }));
const handler = await createStudioHandler({ applicationRoot: root });
const send = (path: string, body?: unknown, origin?: string) =>
handler(
new Request(
"http://127.0.0.1:4320" + path,
body === undefined
? undefined
: {
method: "POST",
headers: { "content-type": "application/json", ...(origin ? { origin } : {}) },
body: JSON.stringify(body),
},
),
);
test("hosted Studio keeps its path, same-origin checks and export-only boundary", async () => {
const hosted = await createStudioHandler({ hosted: { basePath: "/design-studio" } });
const origin = "https://components.example";
const html = await hosted(new Request(origin + "/design-studio/"));
expect(await html.text()).toContain('<base href="/design-studio/">');
expect((await hosted(new Request(origin + "/api/application"))).status).toBe(404);
const state = await hosted(new Request(origin + "/design-studio/api/application"));
expect((await state.json()).enabled).toBe(false);
const proxied = await hosted(
new Request("http://components.example/design-studio/api/application", {
headers: { origin, "x-forwarded-proto": "https" },
}),
);
expect(proxied.status).toBe(200);
const request = (path: string, body: unknown, from = origin) =>
hosted(
new Request(origin + "/design-studio" + path, {
method: "POST",
headers: { "content-type": "application/json", origin: from },
body: JSON.stringify(body),
}),
);
expect(
(await request("/api/apply", { system: CORE_DESIGN_SYSTEMS.soft, base: "none" })).status,
).toBe(409);
expect(
(await request("/api/preview", { system: CORE_DESIGN_SYSTEMS.soft }, "https://other.example"))
.status,
).toBe(403);
const preview = await request("/api/preview", { system: CORE_DESIGN_SYSTEMS.soft });
const { url } = await preview.json();
expect(url).toStartWith("/design-studio/preview/");
expect((await hosted(new Request(origin + url))).status).toBe(200);
await expect(
createStudioHandler({ applicationRoot: root, hosted: { basePath: "/design-studio" } }),
).rejects.toThrow("export-only");
});
test("generation endpoint exposes only configured metadata and stages validated proposals", async () => {
const meta = await (await send("/api/generation")).json();
expect(meta.provider).toBe("local-rules");
expect(JSON.stringify(meta)).not.toContain("apiKey");
const system = structuredClone(CORE_DESIGN_SYSTEMS.soft),
locks = ["/tokens/shape.control"];
const input = {
kind: "directions",
brief: { ...DEFAULT_DESIGN_BRIEF, product: "A website", audience: "Customers" },
system,
locks,
baseRevision: await designRevision(system, locks),
};
const response = await send("/api/generate", input);
expect(response.status).toBe(200);
const batch = await response.json();
expect(batch.alternatives).toHaveLength(3);
expect(batch.provenance.provider).toBe("local-rules");
expect((await send("/api/generate", { ...input, baseRevision: "a".repeat(64) })).status).toBe(
409,
);
expect((await send("/api/generate", input, "https://example.org")).status).toBe(403);
});
test("local application supports immutable bundles, conflicts, rollback and existing config preservation", () => {
const app = new StudioApplication(root);
writeFileSync(join(root, "wrn.config.ts"), "export default { existing: true };");
const first = app.apply(CORE_DESIGN_SYSTEMS.square, "none", { mode: "light" });
expect(existsSync(join(first.directory, "system.css"))).toBe(true);
const theme = JSON.parse(readFileSync(join(first.directory, "theme.json"), "utf8"));
expect(resolveThemeConfig(theme).recipeCss).toContain("data-wrn-owner");
expect(() => app.apply(CORE_DESIGN_SYSTEMS.soft, "none")).toThrow("WRN-STUDIO-CONFLICT");
const second = app.apply(CORE_DESIGN_SYSTEMS.soft, first.revision);
expect(app.rollback(second.revision).revision).toBe(first.revision);
expect(app.system()!.id).toBe("square");
expect(readFileSync(join(root, "wrn.config.ts"), "utf8")).toContain("existing: true");
const fresh = mkdtempSync(join(root, "first-"));
const initial = new StudioApplication(fresh);
const applied = initial.apply(CORE_DESIGN_SYSTEMS.soft, "none");
expect(initial.rollback(applied.revision).revision).toBe("none");
expect(initial.system()).toBeUndefined();
});
test("server serves a browser bundle, rejects cross-origin, invalid, oversized and unconfigured writes", async () => {
expect((await send("/")).status).toBe(200);
const bundle = await send("/studio.js");
expect(bundle.status).toBe(200);
expect(await bundle.text()).not.toContain("node:fs");
expect(
(
await send(
"/api/apply",
{ system: CORE_DESIGN_SYSTEMS.soft, base: "none" },
"https://other.example",
)
).status,
).toBe(403);
expect((await handler(new Request("http://evil.example/api/application"))).status).toBe(403);
expect((await send("/api/apply", { system: {}, base: "none" })).status).toBe(422);
expect((await send("/api/apply", { data: "x".repeat(2_000_001) })).status).toBe(413);
const readOnly = await createStudioHandler();
expect(
(
await readOnly(
new Request("http://127.0.0.1/api/apply", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ system: CORE_DESIGN_SYSTEMS.soft, base: "none" }),
}),
)
).status,
).toBe(409);
});
test("all presets generate isolated real component previews with light/dark and responsive/RTL controls", async () => {
for (const system of Object.values(CORE_DESIGN_SYSTEMS))
for (const mode of ["light", "dark"]) {
const result = await send("/api/preview", { system, mode, direction: "rtl" });
expect(result.status).toBe(200);
const { url } = await result.json();
const preview = await send(url);
const html = await preview.text();
expect(html).toContain(`data-theme="${mode}"`);
expect(html).toContain('dir="rtl"');
expect(html).toContain('data-wrn-component="Radio"');
expect(html).toContain('data-wrn-component="InputNumber"');
expect(html).toContain("data-scope");
expect(html).toContain("data-wrn-controls");
expect(html).not.toContain("failed to load component controllers");
expect(preview.headers.get("content-security-policy")).toContain("connect-src 'none'");
}
});
test("catalog previews cover all 104 component defaults and unknown routes do not expose files", async () => {
let count = 0;
for (let page = 0; page < 7; page++) {
const result = await send("/api/preview", {
system: CORE_DESIGN_SYSTEMS.soft,
content: "catalog",
page,
});
const { url } = await result.json();
const html = await (await send(url)).text();
count += (html.match(/data-wrn-component=/g) ?? []).length;
}
expect(count).toBe(104);
expect((await send("/../../package.json")).status).toBe(404);
});
+152
View File
@@ -0,0 +1,152 @@
import { test, expect } from "bun:test";
import { StudioSession, importDesign, exportDesign, diffDesign } from "../src/index.ts";
import { CORE_DESIGN_SYSTEMS, validateDesignSystem } from "../../design-system/src/index.ts";
test("invalid edits preserve a valid preview, raw input, undo/redo and reload", () => {
const data = new Map<string, string>();
const storage = {
getItem: (k: string) => data.get(k) ?? null,
setItem: (k: string, v: string) => {
data.set(k, v);
},
};
const session = new StudioSession(CORE_DESIGN_SYSTEMS.soft, storage);
session.mutate((s) => {
s.tokens["shape.control"]!.value = "13px";
}, "Changed radius");
session.edit('{"tokens":', "Incomplete JSON");
expect(session.diagnostics[0]!.code).toBe("WRN-STUDIO-IMPORT");
expect(session.valid.tokens["shape.control"]!.value).toBe("13px");
session.undo();
expect(session.diagnostics).toEqual([]);
session.redo();
expect(session.source).toBe('{"tokens":');
const restored = new StudioSession(undefined, storage);
expect(restored.restore()).toBe(true);
expect(restored.source).toBe(session.source);
expect(restored.valid.tokens["shape.control"]!.value).toBe("13px");
expect(() => restored.markApplied()).toThrow();
expect(() => restored.mutate(() => {}, "x")).toThrow();
});
test("history stays bounded, branches after undo, and realistic full-preset history reloads", () => {
const data = new Map<string, string>();
const storage = {
getItem: (k: string) => data.get(k) ?? null,
setItem: (k: string, v: string) => {
data.set(k, v);
},
};
const session = new StudioSession(undefined, storage);
for (let i = 0; i < 30; i++)
session.mutate((s) => {
s.name = "Revision " + i;
}, "Name");
expect(session.history).toHaveLength(25);
const restored = new StudioSession(undefined, storage);
expect(restored.restore()).toBe(true);
restored.undo();
restored.mutate((s) => {
s.name = "Branch";
}, "Branched");
restored.redo();
expect(restored.valid.name).toBe("Branch");
expect(restored.history.at(-1)!.label).toBe("Branched");
});
test("corrupted storage and quota failures do not destroy in-memory drafts", () => {
const broken = new StudioSession(undefined, {
getItem: () => "{",
setItem: () => {
throw Error("quota");
},
});
expect(broken.restore()).toBe(false);
expect(broken.storageError).toContain("could not be restored");
broken.mutate((s) => {
s.name = "Kept";
}, "Edit");
expect(broken.valid.name).toBe("Kept");
expect(broken.storageError).toContain("Autosave unavailable");
});
test("canonical import/export round trips all recipes, modes, aliases and metadata", () => {
for (const system of Object.values(CORE_DESIGN_SYSTEMS)) {
const artifact = exportDesign(system);
const report = importDesign(artifact.canonical);
expect(report.diagnostics).toEqual([]);
expect(report.system).toEqual(system);
expect(artifact.css).toContain("data-wrn-owner");
expect(JSON.parse(artifact.theme).designSystem.system).toEqual(system);
}
});
test("legacy import reports unsupported fields and preserves literal semantic bindings", () => {
const report = importDesign(
JSON.stringify({
default: "light",
palette: "violet",
themes: { light: { "color-primary": "#112233", unknown: "4px" } },
}),
"legacy",
);
expect(report.diagnostics).toEqual([]);
expect(report.unsupported).toContain("palette");
expect(report.unsupported).toContain("themes.light.unknown");
expect(report.system!.modes!.light!["color.primary"]).toEqual({
type: "color",
value: "#112233",
legacy: "color-primary",
});
});
test("exchange subset preserves aliases and reports ambiguous semantic mapping and unsupported composites", () => {
const report = importDesign(
JSON.stringify({
brand: {
$type: "color",
base: { $value: "#112233" },
action: { $value: "{brand.base}" },
complex: { $value: { colorSpace: "srgb", components: [0, 0, 0] } },
},
$extensions: { x: 1 },
}),
"tokens",
);
expect(report.diagnostics).toEqual([]);
expect(report.system!.tokens["brand.action"]!.value).toEqual({ ref: "brand.base" });
expect(report.unsupported).toContain("brand.complex");
expect(report.unsupported).toContain("$extensions");
expect(report.ambiguous).toHaveLength(2);
});
test("imports reject oversized, cyclic, executable, polluted and missing-reference documents", () => {
expect(importDesign(" ".repeat(2_000_001)).diagnostics).not.toEqual([]);
expect(
importDesign('{"__proto__":{"$type":"color","$value":"#000"}}', "tokens").diagnostics,
).not.toEqual([]);
for (const tokens of [
{ a: { type: "color", value: { ref: "missing" } } },
{ a: { type: "color", value: { ref: "b" } }, b: { type: "color", value: { ref: "a" } } },
{ a: { type: "color", value: "red;}body{display:none" } },
]) {
const system = { schemaVersion: 1, id: "bad", name: "Bad", version: "1", tokens };
expect(importDesign(JSON.stringify(system)).diagnostics).not.toEqual([]);
}
});
test("review diffs identify affected paths and applied snapshots are detached", () => {
const session = new StudioSession();
session.mutate((s) => {
s.tokens["shape.control"]!.value = "6px";
}, "Radius");
expect(session.changes()).toContain("$.tokens.shape.control.value");
session.markApplied();
expect(session.changes()).toEqual([]);
session.mutate((s) => {
s.name = "Other";
}, "Name");
expect(session.applied.name).not.toBe("Other");
expect(diffDesign({ a: 1 }, { a: 2, b: 3 })).toEqual(["$.a", "$.b"]);
expect(validateDesignSystem(session.valid)).toEqual([]);
});
@@ -0,0 +1,85 @@
import { test, expect } from "bun:test";
import { CORE_DESIGN_SYSTEMS } from "@wrnexus/design-system";
import { createDesignPackage, readDesignPackage } from "@wrnexus/design-system/package";
import { StudioSession } from "../src/session.ts";
import { planDesignUpgrade, applyDesignUpgrade } from "../src/upgrades.ts";
test("shared packages have deterministic pins independent of object key order and reject changed or future content", async () => {
const system = CORE_DESIGN_SYSTEMS.square;
const first = await createDesignPackage(system);
const reordered = {
...system,
tokens: Object.fromEntries(Object.entries(system.tokens).reverse()),
};
expect((await createDesignPackage(reordered)).integrity.digest).toBe(first.integrity.digest);
expect((await readDesignPackage(JSON.stringify(first))).package).toEqual(first);
const changed = structuredClone(first);
changed.system.tokens["color.primary"]!.value = "#abcdef";
await expect(readDesignPackage(JSON.stringify(changed))).rejects.toThrow("INTEGRITY");
await expect(readDesignPackage(JSON.stringify({ ...first, formatVersion: 2 }))).rejects.toThrow(
"VERSION",
);
await expect(createDesignPackage({ ...system, extends: ["company"] })).rejects.toThrow();
expect((await readDesignPackage(JSON.stringify(system))).migrations).toEqual([
"canonical-v1-to-package-v1",
]);
});
test("upgrade review and explicit adoption respect identity, pins, locks, stale revisions and recoverable history", async () => {
const session = new StudioSession();
const next = structuredClone(session.valid);
next.version = "1.1.0";
next.tokens["color.primary"]!.value = "#abcdef";
const source = JSON.stringify(await createDesignPackage(next));
const plan = await planDesignUpgrade(session, source);
expect(plan.canApply).toBe(true);
expect(session.valid.version).toBe("1.0.0");
session.setLocks(["/tokens/color.primary"]);
expect((await planDesignUpgrade(session, source)).canApply).toBe(false);
await expect(applyDesignUpgrade(session, source, plan.baseRevision)).rejects.toThrow("CONFLICT");
const locked = await planDesignUpgrade(session, source);
await expect(applyDesignUpgrade(session, source, locked.baseRevision)).rejects.toThrow("LOCK");
session.setLocks([]);
const fresh = await planDesignUpgrade(session, source);
await applyDesignUpgrade(session, source, fresh.baseRevision);
expect(session.valid.version).toBe("1.1.0");
session.undo();
expect(session.valid.version).toBe("1.0.0");
session.redo();
expect(session.valid.version).toBe("1.1.0");
await expect(
planDesignUpgrade(
session,
JSON.stringify(await createDesignPackage({ ...next, id: "different" })),
),
).rejects.toThrow("identity");
session.edit("{unfinished");
await expect(planDesignUpgrade(session, source)).rejects.toThrow("source errors");
});
test("legacy unversioned drafts migrate losslessly; future drafts remain untouched by autosave", () => {
let saved = "";
const storage = {
getItem: () => saved || null,
setItem: (_key: string, value: string) => {
saved = value;
},
};
const old = new StudioSession(undefined, storage);
old.edit("{unfinished");
const legacy = JSON.parse(saved);
delete legacy.storageVersion;
saved = JSON.stringify(legacy);
const restored = new StudioSession(undefined, storage);
expect(restored.restore()).toBe(true);
expect(restored.source).toBe("{unfinished");
restored.undo();
expect(JSON.parse(saved).storageVersion).toBe(1);
const future = JSON.stringify({ ...legacy, storageVersion: 99 });
saved = future;
const older = new StudioSession(undefined, storage);
expect(older.restore()).toBe(false);
expect(older.recoverySource).toBe(future);
older.edit("{another edit");
older.persist();
expect(saved).toBe(future);
expect(older.storageError).toContain("protected");
});