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"; import { STUDIO_PRESETS } from "../src/presets.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(''); 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(STUDIO_PRESETS)) 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); });