From 4c6cfef912498e8526331e7b918c486756edb11a Mon Sep 17 00:00:00 2001 From: Molecule AI Core-FE Date: Sun, 10 May 2026 03:59:32 +0000 Subject: [PATCH] test(canvas): add pure-function tests for runtimeProfiles, getIcon, createMessage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runtimeProfiles.test.ts: getRuntimeProfile and provisionTimeoutForRuntime covering undefined/unknown runtime, overrides precedence, convenience equivalence. - getIcon.test.ts: 23 cases — dirs, all FILE_ICONS extensions (.md/.yaml/.py/.ts/.tsx/.js/.json/.html/.css/.sh), fallback, case insensitivity, nested paths. - createMessage.test.ts: role, content, id, timestamp, attachment handling, Object.isFrozen, key shape. Co-Authored-By: Claude Opus 4.7 --- .../__tests__/createMessage.test.ts | 75 +++++++++++++ .../src/components/__tests__/getIcon.test.ts | 104 ++++++++++++++++++ .../src/lib/__tests__/runtimeProfiles.test.ts | 89 +++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 canvas/src/components/__tests__/createMessage.test.ts create mode 100644 canvas/src/components/__tests__/getIcon.test.ts create mode 100644 canvas/src/lib/__tests__/runtimeProfiles.test.ts diff --git a/canvas/src/components/__tests__/createMessage.test.ts b/canvas/src/components/__tests__/createMessage.test.ts new file mode 100644 index 00000000..6ce40c06 --- /dev/null +++ b/canvas/src/components/__tests__/createMessage.test.ts @@ -0,0 +1,75 @@ +// @vitest-environment jsdom +/** + * Tests for createMessage — the ChatMessage factory from types.ts. + */ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { createMessage } from "../tabs/chat/types"; + +describe("createMessage", () => { + beforeEach(() => { + // Freeze time so timestamp is deterministic. + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-05-10T12:00:00.000Z")); + // Stub crypto.randomUUID so message IDs are deterministic. + vi.stubGlobal("crypto", { randomUUID: vi.fn(() => "fixed-uuid-1234") }); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it("creates a message with the correct role", () => { + const userMsg = createMessage("user", "hello"); + expect(userMsg.role).toBe("user"); + + const agentMsg = createMessage("agent", "hi there"); + expect(agentMsg.role).toBe("agent"); + + const systemMsg = createMessage("system", "prompt loaded"); + expect(systemMsg.role).toBe("system"); + }); + + it("creates a message with the correct content", () => { + const msg = createMessage("user", "Deploy the agent now"); + expect(msg.content).toBe("Deploy the agent now"); + }); + + it("sets a deterministic id via crypto.randomUUID", () => { + const msg = createMessage("agent", "response"); + expect(msg.id).toBe("fixed-uuid-1234"); + }); + + it("sets a deterministic ISO timestamp", () => { + const msg = createMessage("user", "hello"); + expect(msg.timestamp).toBe("2026-05-10T12:00:00.000Z"); + }); + + it("omits attachments field when none provided", () => { + const msg = createMessage("user", "hello"); + expect(msg.attachments).toBeUndefined(); + }); + + it("omits attachments field when empty array is provided", () => { + const msg = createMessage("agent", "result", []); + expect(msg.attachments).toBeUndefined(); + }); + + it("includes attachments field when non-empty array is provided", () => { + const atts = [{ name: "report.pdf", uri: "workspace:/docs/report.pdf" }]; + const msg = createMessage("agent", "see attached", atts); + expect(msg.attachments).toEqual(atts); + }); + + it("returns a frozen object (prevents accidental mutation)", () => { + const msg = createMessage("user", "hello"); + expect(Object.isFrozen(msg)).toBe(true); + }); + + it("returns a plain object with expected keys", () => { + const msg = createMessage("user", "hello"); + expect(Object.keys(msg).sort()).toEqual( + ["id", "role", "content", "timestamp"].sort() + ); + }); +}); diff --git a/canvas/src/components/__tests__/getIcon.test.ts b/canvas/src/components/__tests__/getIcon.test.ts new file mode 100644 index 00000000..c681e334 --- /dev/null +++ b/canvas/src/components/__tests__/getIcon.test.ts @@ -0,0 +1,104 @@ +// @vitest-environment jsdom +/** + * Tests for getIcon — the pure icon-selector from FilesTab/tree.ts. + */ +import { describe, it, expect } from "vitest"; +import { getIcon } from "../tabs/FilesTab/tree"; + +describe("getIcon", () => { + // ─── Directories ────────────────────────────────────────────────────────── + + it("returns 📁 for directories regardless of extension", () => { + expect(getIcon("src", true)).toBe("📁"); + expect(getIcon("node_modules", true)).toBe("📁"); + expect(getIcon(".claude", true)).toBe("📁"); + expect(getIcon("foo/bar/baz", true)).toBe("📁"); + }); + + it("returns 📁 even for paths that look like files", () => { + expect(getIcon("foo.txt", true)).toBe("📁"); + expect(getIcon("script.sh", true)).toBe("📁"); + }); + + // ─── Files by extension ──────────────────────────────────────────────────── + + it("returns 📄 for .md files", () => { + expect(getIcon("README.md", false)).toBe("📄"); + expect(getIcon("CHANGELOG.md", false)).toBe("📄"); + expect(getIcon("docs/guide.md", false)).toBe("📄"); + }); + + it("returns ⚙ for .yaml and .yml files", () => { + expect(getIcon("config.yaml", false)).toBe("⚙"); + expect(getIcon("values.yml", false)).toBe("⚙"); + expect(getIcon("deploy.yaml", false)).toBe("⚙"); + }); + + it("returns 🐍 for .py files", () => { + expect(getIcon("main.py", false)).toBe("🐍"); + expect(getIcon("utils/helpers.py", false)).toBe("🐍"); + }); + + it("returns 💠 for .ts and .tsx files", () => { + expect(getIcon("index.ts", false)).toBe("💠"); + expect(getIcon("Component.tsx", false)).toBe("💠"); + expect(getIcon("types.d.ts", false)).toBe("💠"); + }); + + it("returns 📜 for .js files", () => { + expect(getIcon("bundle.js", false)).toBe("📜"); + expect(getIcon("src/index.js", false)).toBe("📜"); + }); + + it("returns {} for .json files", () => { + expect(getIcon("package.json", false)).toBe("{}"); + expect(getIcon("config.json", false)).toBe("{}"); + }); + + it("returns 🌐 for .html files", () => { + expect(getIcon("index.html", false)).toBe("🌐"); + expect(getIcon("templates/page.html", false)).toBe("🌐"); + }); + + it("returns 🎨 for .css files", () => { + expect(getIcon("style.css", false)).toBe("🎨"); + expect(getIcon("src/app.css", false)).toBe("🎨"); + }); + + it("returns ▸ for .sh files", () => { + expect(getIcon("deploy.sh", false)).toBe("▸"); + expect(getIcon("scripts/setup.sh", false)).toBe("▸"); + }); + + // ─── Fallback ───────────────────────────────────────────────────────────── + + it("returns 📄 for unknown extensions", () => { + expect(getIcon("README", false)).toBe("📄"); + expect(getIcon("Dockerfile", false)).toBe("📄"); + expect(getIcon("Makefile", false)).toBe("📄"); + expect(getIcon("notes.txt", false)).toBe("📄"); + expect(getIcon("archive.tar.gz", false)).toBe("📄"); + }); + + it("returns 📄 for paths with no extension", () => { + expect(getIcon("Makefile", false)).toBe("📄"); + expect(getIcon("README", false)).toBe("📄"); + expect(getIcon("Dockerfile", false)).toBe("📄"); + }); + + // ─── Case sensitivity ────────────────────────────────────────────────────── + + it("is case-insensitive for extension lookup", () => { + expect(getIcon("image.PNG", false)).toBe("📄"); + expect(getIcon("data.JSON", false)).toBe("{}"); + expect(getIcon("script.SH", false)).toBe("▸"); + }); + + // ─── Nested paths ───────────────────────────────────────────────────────── + + it("uses the leaf extension for nested paths", () => { + expect(getIcon("src/utils/helpers.ts", false)).toBe("💠"); + expect(getIcon("docs/api.yaml", false)).toBe("⚙"); + expect(getIcon(".github/workflows/ci.yml", false)).toBe("⚙"); + }); +}); diff --git a/canvas/src/lib/__tests__/runtimeProfiles.test.ts b/canvas/src/lib/__tests__/runtimeProfiles.test.ts new file mode 100644 index 00000000..c0ce3746 --- /dev/null +++ b/canvas/src/lib/__tests__/runtimeProfiles.test.ts @@ -0,0 +1,89 @@ +// @vitest-environment jsdom +/** + * Tests for runtimeProfiles.ts — getRuntimeProfile and provisionTimeoutForRuntime. + */ +import { describe, expect, it } from "vitest"; +import { + getRuntimeProfile, + provisionTimeoutForRuntime, + DEFAULT_RUNTIME_PROFILE, + RUNTIME_PROFILES, +} from "../runtimeProfiles"; + +describe("getRuntimeProfile", () => { + it("returns DEFAULT_RUNTIME_PROFILE when runtime is undefined and no overrides", () => { + const result = getRuntimeProfile(undefined); + expect(result.provisionTimeoutMs).toBe(DEFAULT_RUNTIME_PROFILE.provisionTimeoutMs); + }); + + it("returns DEFAULT_RUNTIME_PROFILE when runtime is empty string", () => { + const result = getRuntimeProfile(""); + expect(result.provisionTimeoutMs).toBe(DEFAULT_RUNTIME_PROFILE.provisionTimeoutMs); + }); + + it("falls back to DEFAULT_RUNTIME_PROFILE for an unknown runtime", () => { + const result = getRuntimeProfile("unknown-lang"); + expect(result.provisionTimeoutMs).toBe(DEFAULT_RUNTIME_PROFILE.provisionTimeoutMs); + }); + + it("returns DEFAULT_RUNTIME_PROFILE when RUNTIME_PROFILES is empty (current state)", () => { + // RUNTIME_PROFILES is currently {} — verify the empty-map path works + expect(RUNTIME_PROFILES).toEqual({}); + const result = getRuntimeProfile("claude-code"); + expect(result.provisionTimeoutMs).toBe(120_000); + }); + + it("uses overrides.provisionTimeoutMs when provided (highest priority)", () => { + const result = getRuntimeProfile("claude-code", { provisionTimeoutMs: 300_000 }); + expect(result.provisionTimeoutMs).toBe(300_000); + }); + + it("overrides wins over RUNTIME_PROFILES entry", () => { + // Even if RUNTIME_PROFILES had an entry, overrides take priority + const result = getRuntimeProfile("claude-code", { provisionTimeoutMs: 999_000 }); + expect(result.provisionTimeoutMs).toBe(999_000); + }); + + it("uses overrides even when runtime is undefined", () => { + const result = getRuntimeProfile(undefined, { provisionTimeoutMs: 60_000 }); + expect(result.provisionTimeoutMs).toBe(60_000); + }); + + it("returns Required — always has provisionTimeoutMs", () => { + // The return type is guaranteed non-nullable + const result = getRuntimeProfile(undefined); + expect(typeof result.provisionTimeoutMs).toBe("number"); + expect(result.provisionTimeoutMs).toBeGreaterThan(0); + }); +}); + +describe("provisionTimeoutForRuntime", () => { + it("returns DEFAULT_RUNTIME_PROFILE value when no runtime or overrides", () => { + expect(provisionTimeoutForRuntime(undefined)).toBe(120_000); + expect(provisionTimeoutForRuntime("")).toBe(120_000); + }); + + it("returns overrides value when overrides provided", () => { + expect(provisionTimeoutForRuntime("claude-code", { provisionTimeoutMs: 90_000 })).toBe(90_000); + }); + + it("returns 120_000 for any unknown runtime", () => { + expect(provisionTimeoutForRuntime("langgraph")).toBe(120_000); + expect(provisionTimeoutForRuntime("crewai")).toBe(120_000); + expect(provisionTimeoutForRuntime("some-new-runtime")).toBe(120_000); + }); + + it("convenience: same as getRuntimeProfile().provisionTimeoutMs", () => { + const cases: Array<[string | undefined, { provisionTimeoutMs?: number } | undefined]> = [ + [undefined, undefined], + ["claude-code", undefined], + ["langgraph", { provisionTimeoutMs: 500_000 }], + [undefined, { provisionTimeoutMs: 45_000 }], + ]; + for (const [runtime, overrides] of cases) { + const profile = getRuntimeProfile(runtime, overrides); + const direct = provisionTimeoutForRuntime(runtime, overrides); + expect(direct).toBe(profile.provisionTimeoutMs); + } + }); +});