From bfbbe5761009f3dc73d8a698314c0a3077e0f018 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 26 Apr 2026 23:45:51 -0700 Subject: [PATCH] =?UTF-8?q?test(canvas):=20cover=20utils.cn=20+=20runtime-?= =?UTF-8?q?names.runtimeDisplayName=20(0%=20=E2=86=92=20100%)=20(#1815)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Molecule-Platform-Evolvement-Manager] Closes two of the 0%-coverage files surfaced by the baseline run in PR #2147 (vitest coverage instrumentation). Both files are tiny utility helpers with high-touch read paths. ## utils.cn (8 cases) Wraps `twMerge(clsx(inputs))` — every conditionally-styled component flows through here. The load-bearing case is the **last-wins Tailwind dedup**: `cn("p-2", "p-4")` → "p-4". A regression that lost twMerge would silently double-apply utilities (cosmetically broken, breaks `:where()` rules + theme overrides). Cases: - single class unchanged - multiple positional classes joined - array input flattening (clsx) - object syntax with truthy/falsy keys - last-wins dedup on conflicting Tailwind utilities (the regression-locked guarantee) - non-conflicting utilities both survive (p-2 + m-4) - mixed input shapes (string + array + object + string) - nullish / empty inputs don't throw ## runtime-names.runtimeDisplayName (4 it.each cases + 3 it()) Friendly-name lookup that surfaces the workspace runtime in the chat indicator, details tab, and a few component labels. Cases: - known runtimes map to display strings (claude-code → Claude Code, langgraph → LangGraph, etc.) - unknown runtime falls back to input string verbatim (a NEW runtime not yet in the lookup still renders something operator-debuggable rather than a generic placeholder) - empty string falls back to "agent" (final default) - case-sensitivity pinned: "Claude-Code" / "LANGGRAPH" miss the lookup. The upstream slug is already normalized lowercase, so a future refactor that lowercases input "for safety" would silently change behavior — pinning the contract here. ## Test plan - [x] All 17 cases pass locally (~129ms) - [x] No SUT changes — pure additive coverage - [ ] CI green ## #1815 progress - [x] Step 1+2: coverage instrumentation + script (#2147) - [x] 0%-file gaps utils.ts + runtime-names.ts (this PR) - [ ] More 0%/low-coverage files: lib/canvas-actions.ts (25%), store/classNames.ts (17%) — separate PRs - [ ] Step 3b: thresholds + CI gate once baseline catches up 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/__tests__/runtime-names.test.ts | 48 ++++++++++++ canvas/src/lib/__tests__/utils.test.ts | 74 +++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 canvas/src/lib/__tests__/runtime-names.test.ts create mode 100644 canvas/src/lib/__tests__/utils.test.ts diff --git a/canvas/src/lib/__tests__/runtime-names.test.ts b/canvas/src/lib/__tests__/runtime-names.test.ts new file mode 100644 index 00000000..ed9fbe37 --- /dev/null +++ b/canvas/src/lib/__tests__/runtime-names.test.ts @@ -0,0 +1,48 @@ +/** + * Tests for `runtimeDisplayName` — the friendly-name lookup that + * surfaces the workspace runtime in the chat indicator, details + * tab, and a few component labels. Tiny but high-touch: every + * surface that shows "this workspace runs on X" goes through here. + * + * Issue: #1815 follow-up — `src/lib/runtime-names.ts` was at 0% + * coverage despite being read by 3+ rendering paths. + */ +import { describe, it, expect } from "vitest"; +import { runtimeDisplayName } from "../runtime-names"; + +describe("runtimeDisplayName", () => { + it.each([ + ["claude-code", "Claude Code"], + ["langgraph", "LangGraph"], + ["deepagents", "DeepAgents"], + ["openclaw", "OpenClaw"], + ["crewai", "CrewAI"], + ["autogen", "AutoGen"], + ])("known runtime %q maps to %q", (input, expected) => { + expect(runtimeDisplayName(input)).toBe(expected); + }); + + it("unknown runtime falls back to the input string verbatim", () => { + // A future runtime not yet in the lookup map should render with + // its own id — better than a generic placeholder for ops debugging. + expect(runtimeDisplayName("hermes")).toBe("hermes"); + expect(runtimeDisplayName("custom-runtime-9000")).toBe( + "custom-runtime-9000", + ); + }); + + it("empty string falls back to 'agent' (final default)", () => { + // Any code path that loses the runtime field still renders SOMETHING; + // the chat indicator never shows a blank label. + expect(runtimeDisplayName("")).toBe("agent"); + }); + + it("is case-sensitive — uppercase variants miss the lookup", () => { + // The lookup keys are lowercase by convention. Pin the case + // sensitivity explicitly so a future refactor that lowercases + // the input "for safety" doesn't silently change behavior — the + // upstream slug is already normalized lowercase. + expect(runtimeDisplayName("Claude-Code")).toBe("Claude-Code"); + expect(runtimeDisplayName("LANGGRAPH")).toBe("LANGGRAPH"); + }); +}); diff --git a/canvas/src/lib/__tests__/utils.test.ts b/canvas/src/lib/__tests__/utils.test.ts new file mode 100644 index 00000000..30402c2c --- /dev/null +++ b/canvas/src/lib/__tests__/utils.test.ts @@ -0,0 +1,74 @@ +/** + * Tests for `cn` — the canvas's className-merging helper. Wraps + * `twMerge(clsx(inputs))` so callers can: + * 1. Combine class strings + arrays + objects (clsx), + * 2. Resolve Tailwind conflicts so the LAST value wins on the same + * utility (twMerge — e.g. `cn("p-2", "p-4")` → "p-4"). + * + * Tiny surface but load-bearing — every component that conditionally + * styles uses this. A regression that loses Tailwind-merge dedup would + * show as silent class duplication in the rendered DOM (cosmetic, but + * accumulates and breaks `:where()` rules + theme overrides). + * + * Issue: #1815 follow-up — `src/lib/utils.ts` was at 0% coverage. + */ +import { describe, it, expect } from "vitest"; +import { cn } from "../utils"; + +describe("cn", () => { + it("returns a single class unchanged", () => { + expect(cn("text-red-500")).toBe("text-red-500"); + }); + + it("joins multiple positional classes", () => { + expect(cn("text-red-500", "bg-zinc-900")).toBe("text-red-500 bg-zinc-900"); + }); + + it("flattens array inputs (clsx-style)", () => { + expect(cn(["text-red-500", "bg-zinc-900"])).toBe( + "text-red-500 bg-zinc-900", + ); + }); + + it("respects truthy / falsy conditional object syntax", () => { + expect( + cn({ "text-red-500": true, "text-blue-500": false, "bg-zinc-900": true }), + ).toBe("text-red-500 bg-zinc-900"); + }); + + it("dedups conflicting Tailwind utilities — last wins", () => { + // The single load-bearing reason for twMerge over plain clsx — + // a regression here would silently double-apply padding tokens + // and confuse the visible style. + expect(cn("p-2", "p-4")).toBe("p-4"); + expect(cn("text-red-500", "text-blue-500")).toBe("text-blue-500"); + }); + + it("keeps non-conflicting Tailwind utilities", () => { + // Make sure the dedup is keyed on utility group, not blanket + // merge. p-2 and m-2 don't conflict; both must survive. + expect(cn("p-2", "m-4")).toBe("p-2 m-4"); + }); + + it("handles a mix of all input shapes", () => { + expect( + cn( + "base-class", + ["array-class-1", "array-class-2"], + { "object-true": true, "object-false": false }, + "trailing-class", + ), + ).toBe( + "base-class array-class-1 array-class-2 object-true trailing-class", + ); + }); + + it("handles empty / nullish inputs without throwing", () => { + expect(cn()).toBe(""); + expect(cn("")).toBe(""); + expect(cn(null, undefined, false)).toBe(""); + expect(cn("active", null, "highlighted", undefined)).toBe( + "active highlighted", + ); + }); +});