From 5f3b698543212698dfdc42910432717591304852 Mon Sep 17 00:00:00 2001 From: Oscar Hong Date: Tue, 10 Mar 2026 23:46:57 -0700 Subject: [PATCH 1/2] fix: prettifyModel uses normalized (trimmed) input consistently The function computed `normalized = model.trim()` but then used raw `model` for most regex tests and .includes() calls. Whitespace-padded model names failed anchor-based matches (^o3, ^o4, ^gpt-) and returned untrimmed strings for unknown models. All references now use `normalized`. Exported the function so tests import from source instead of copy-pasting. Added 12 unit tests covering whitespace, all provider patterns, and legacy fallbacks. Created 6 GitHub issues for larger items found during TD review: - #31 Race condition in usage/submit multi-device flow - #32 Deduplicate prettifyModel across 3 files - #33 Add rate limiting to DELETE endpoints - #34 Add missing test coverage for 17 untested API routes - #35 PR #22 multi-provider support review feedback - #36 Validate leaderboard cursor input Nightshift-Task: td-review Nightshift-Ref: https://github.com/marcus/nightshift Co-Authored-By: Claude Opus 4.6 --- .../web/__tests__/unit/prettify-model.test.ts | 75 +++++++++++++++++++ apps/web/components/app/feed/ActivityCard.tsx | 20 ++--- docs/CHANGELOG.md | 1 + 3 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 apps/web/__tests__/unit/prettify-model.test.ts diff --git a/apps/web/__tests__/unit/prettify-model.test.ts b/apps/web/__tests__/unit/prettify-model.test.ts new file mode 100644 index 00000000..de32aaf2 --- /dev/null +++ b/apps/web/__tests__/unit/prettify-model.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from "vitest"; +import { prettifyModel } from "@/components/app/feed/ActivityCard"; + +describe("prettifyModel", () => { + describe("Claude models", () => { + it("prettifies claude-opus-4 variants", () => { + expect(prettifyModel("claude-opus-4-20260301")).toBe("Claude Opus"); + expect(prettifyModel("claude-opus-4")).toBe("Claude Opus"); + }); + + it("prettifies claude-sonnet-4 variants", () => { + expect(prettifyModel("claude-sonnet-4-20260301")).toBe("Claude Sonnet"); + expect(prettifyModel("claude-sonnet-4")).toBe("Claude Sonnet"); + }); + + it("prettifies claude-haiku-4 variants", () => { + expect(prettifyModel("claude-haiku-4-20260301")).toBe("Claude Haiku"); + expect(prettifyModel("claude-haiku-4")).toBe("Claude Haiku"); + }); + + it("handles legacy Claude model names via .includes()", () => { + expect(prettifyModel("anthropic/claude-3-opus")).toBe("Claude Opus"); + expect(prettifyModel("anthropic/claude-3-sonnet")).toBe("Claude Sonnet"); + expect(prettifyModel("anthropic/claude-3-haiku")).toBe("Claude Haiku"); + }); + }); + + describe("OpenAI models", () => { + it("prettifies GPT models preserving full name", () => { + expect(prettifyModel("gpt-5.3-codex")).toBe("GPT-5.3-Codex"); + expect(prettifyModel("gpt-4o")).toBe("GPT-4o"); + expect(prettifyModel("gpt-5")).toBe("GPT-5"); + }); + + it("prettifies o3/o4 models", () => { + expect(prettifyModel("o3-mini")).toBe("o3"); + expect(prettifyModel("o4-mini")).toBe("o4"); + expect(prettifyModel("o3")).toBe("o3"); + expect(prettifyModel("o4")).toBe("o4"); + }); + }); + + describe("whitespace handling (the bug fix)", () => { + it("trims leading/trailing whitespace before matching", () => { + expect(prettifyModel(" claude-opus-4 ")).toBe("Claude Opus"); + expect(prettifyModel("\tclaude-sonnet-4\n")).toBe("Claude Sonnet"); + }); + + it("trims whitespace for anchor-dependent patterns (o3/o4)", () => { + expect(prettifyModel(" o3-mini")).toBe("o3"); + expect(prettifyModel(" o4-mini")).toBe("o4"); + }); + + it("trims whitespace for GPT patterns", () => { + expect(prettifyModel(" gpt-5.3-codex ")).toBe("GPT-5.3-Codex"); + }); + + it("returns trimmed string for unknown models", () => { + expect(prettifyModel(" some-unknown-model ")).toBe("some-unknown-model"); + }); + + it("trims whitespace for legacy .includes() fallbacks", () => { + expect(prettifyModel(" some-opus-variant ")).toBe("Claude Opus"); + expect(prettifyModel(" some-sonnet-variant ")).toBe("Claude Sonnet"); + }); + }); + + describe("unknown models", () => { + it("returns the model name as-is for unrecognized models", () => { + expect(prettifyModel("gemini-2.0-flash")).toBe("gemini-2.0-flash"); + expect(prettifyModel("qwen-2.5-coder")).toBe("qwen-2.5-coder"); + expect(prettifyModel("mistral-large")).toBe("mistral-large"); + }); + }); +}); diff --git a/apps/web/components/app/feed/ActivityCard.tsx b/apps/web/components/app/feed/ActivityCard.tsx index 666ce9d0..c1d3a01f 100644 --- a/apps/web/components/app/feed/ActivityCard.tsx +++ b/apps/web/components/app/feed/ActivityCard.tsx @@ -35,24 +35,24 @@ function timeAgo(dateStr: string, usageDate?: string | null) { return new Date(dateStr).toLocaleDateString("en-US", { month: "short", day: "numeric" }); } -function prettifyModel(model: string): string { +export function prettifyModel(model: string): string { const normalized = model.trim(); - if (/claude-opus-4/i.test(model)) return "Claude Opus"; - if (/claude-sonnet-4/i.test(model)) return "Claude Sonnet"; - if (/claude-haiku-4/i.test(model)) return "Claude Haiku"; + if (/claude-opus-4/i.test(normalized)) return "Claude Opus"; + if (/claude-sonnet-4/i.test(normalized)) return "Claude Sonnet"; + if (/claude-haiku-4/i.test(normalized)) return "Claude Haiku"; // Preserve full OpenAI model names (e.g. gpt-5.3-codex -> GPT-5.3-Codex) if (/^gpt-/i.test(normalized)) { return normalized .replace(/^gpt/i, "GPT") .replace(/-codex$/i, "-Codex"); } - if (/^o4/i.test(model)) return "o4"; - if (/^o3/i.test(model)) return "o3"; + if (/^o4/i.test(normalized)) return "o4"; + if (/^o3/i.test(normalized)) return "o3"; // Legacy: broader Claude matching - if (model.includes("opus")) return "Claude Opus"; - if (model.includes("sonnet")) return "Claude Sonnet"; - if (model.includes("haiku")) return "Claude Haiku"; - return model; + if (normalized.includes("opus")) return "Claude Opus"; + if (normalized.includes("sonnet")) return "Claude Sonnet"; + if (normalized.includes("haiku")) return "Claude Haiku"; + return normalized; } function formatModels( diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 79916128..2d4efb67 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed +- **prettifyModel inconsistent variable references.** The `prettifyModel` function in `ActivityCard.tsx` used the raw `model` parameter instead of the trimmed `normalized` variable for regex tests and `.includes()` fallbacks. Whitespace-padded model names (e.g., `" o3-mini"`) would fail anchor-based regex matches (`^o3`, `^o4`) and return the untrimmed string for unknown models. All references now use `normalized`. Added 12 unit tests covering whitespace handling, all provider patterns, and legacy fallbacks. (#32 tracks deduplicating the 3 independent copies of this function.) - **CLI now works for Codex-only users.** If `ccusage` fails because no local Claude Code data directories exist, `straude` now treats that as a non-fatal absence and continues syncing Codex usage instead of exiting with an error. - **Logged-out leaderboard now shows region views.** Guests can access the same regional leaderboard filters as logged-in users instead of being limited to the global view. - **CLI broken on Windows.** `execFileSync`/`execFile` can't resolve `.cmd` shims (`ccusage.cmd`, `npx.cmd`, `bunx.cmd`) on Windows without `shell: true`. Added `shell: process.platform === "win32"` to all child process calls in `ccusage.ts` and `codex.ts`. Also fixed `isOnPath()` to check `.cmd`/`.exe` extensions on Windows, and replaced hardcoded `~/.straude/config.json` in login output with the actual resolved path. From b044db18acc8d403bb8cf98b6779a64399848aa0 Mon Sep 17 00:00:00 2001 From: Oscar Hong Date: Tue, 10 Mar 2026 23:51:35 -0700 Subject: [PATCH 2/2] fix: update CHANGELOG issue ref from duplicate #32 to canonical #25 Closed duplicate issues #31-36 (duplicates of #24-29 from prior iteration). Nightshift-Task: td-review Nightshift-Ref: https://github.com/marcus/nightshift Co-Authored-By: Claude Opus 4.6 --- docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2d4efb67..ad075abe 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -9,7 +9,7 @@ ### Fixed -- **prettifyModel inconsistent variable references.** The `prettifyModel` function in `ActivityCard.tsx` used the raw `model` parameter instead of the trimmed `normalized` variable for regex tests and `.includes()` fallbacks. Whitespace-padded model names (e.g., `" o3-mini"`) would fail anchor-based regex matches (`^o3`, `^o4`) and return the untrimmed string for unknown models. All references now use `normalized`. Added 12 unit tests covering whitespace handling, all provider patterns, and legacy fallbacks. (#32 tracks deduplicating the 3 independent copies of this function.) +- **prettifyModel inconsistent variable references.** The `prettifyModel` function in `ActivityCard.tsx` used the raw `model` parameter instead of the trimmed `normalized` variable for regex tests and `.includes()` fallbacks. Whitespace-padded model names (e.g., `" o3-mini"`) would fail anchor-based regex matches (`^o3`, `^o4`) and return the untrimmed string for unknown models. All references now use `normalized`. Added 12 unit tests covering whitespace handling, all provider patterns, and legacy fallbacks. (#25 tracks deduplicating the 3 independent copies of this function.) - **CLI now works for Codex-only users.** If `ccusage` fails because no local Claude Code data directories exist, `straude` now treats that as a non-fatal absence and continues syncing Codex usage instead of exiting with an error. - **Logged-out leaderboard now shows region views.** Guests can access the same regional leaderboard filters as logged-in users instead of being limited to the global view. - **CLI broken on Windows.** `execFileSync`/`execFile` can't resolve `.cmd` shims (`ccusage.cmd`, `npx.cmd`, `bunx.cmd`) on Windows without `shell: true`. Added `shell: process.platform === "win32"` to all child process calls in `ccusage.ts` and `codex.ts`. Also fixed `isOnPath()` to check `.cmd`/`.exe` extensions on Windows, and replaced hardcoded `~/.straude/config.json` in login output with the actual resolved path.