test(canvas): cover utils.cn + runtime-names.runtimeDisplayName (0% → 100%) (#1815)
[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) <noreply@anthropic.com>
This commit is contained in:
parent
34b92c33b7
commit
bfbbe57610
48
canvas/src/lib/__tests__/runtime-names.test.ts
Normal file
48
canvas/src/lib/__tests__/runtime-names.test.ts
Normal file
@ -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");
|
||||
});
|
||||
});
|
||||
74
canvas/src/lib/__tests__/utils.test.ts
Normal file
74
canvas/src/lib/__tests__/utils.test.ts
Normal file
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user