diff --git a/changelog.d/fixes/6718-reasoningcontrols-case-collision.md b/changelog.d/fixes/6718-reasoningcontrols-case-collision.md new file mode 100644 index 0000000000..4c4449755f --- /dev/null +++ b/changelog.d/fixes/6718-reasoningcontrols-case-collision.md @@ -0,0 +1 @@ +- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) diff --git a/src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx b/src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx index 85777f71c1..d77beeb16c 100644 --- a/src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx +++ b/src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx @@ -7,7 +7,7 @@ // (fallback to the canonical vocabulary), resolved by `resolveReasoningControls`. import type { PlaygroundParams } from "./ParamSliders"; -import type { ReasoningControlSpec } from "./reasoningControls"; +import type { ReasoningControlSpec } from "./reasoningControlUtils"; interface ReasoningControlsProps { spec: ReasoningControlSpec; diff --git a/src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx b/src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx index 0efa69f6e0..ecf79741d2 100644 --- a/src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx +++ b/src/app/(dashboard)/dashboard/playground/components/StudioConfigPane.tsx @@ -20,7 +20,7 @@ import ReasoningControls from "./ReasoningControls"; import { resolveReasoningControls, type ReasoningControlSpec, -} from "./reasoningControls"; +} from "./reasoningControlUtils"; export interface ConfigState { endpoint: PlaygroundEndpoint; diff --git a/src/app/(dashboard)/dashboard/playground/components/reasoningControls.ts b/src/app/(dashboard)/dashboard/playground/components/reasoningControlUtils.ts similarity index 100% rename from src/app/(dashboard)/dashboard/playground/components/reasoningControls.ts rename to src/app/(dashboard)/dashboard/playground/components/reasoningControlUtils.ts diff --git a/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx b/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx index 8a0c3512f5..7e31060ee4 100644 --- a/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx +++ b/src/app/(dashboard)/dashboard/playground/components/tabs/ChatTab.tsx @@ -9,7 +9,7 @@ import { useStreamMetrics } from "../../hooks/useStreamMetrics"; import { getModelPricing } from "@/lib/playground/types"; import type { ConfigState } from "../StudioConfigPane"; import type { StreamMetrics } from "@/shared/schemas/playground"; -import { buildReasoningRequestFields } from "../reasoningControls"; +import { buildReasoningRequestFields } from "../reasoningControlUtils"; interface Message { role: "system" | "user" | "assistant"; diff --git a/src/app/(dashboard)/dashboard/translator/hooks/useAvailableModels.tsx b/src/app/(dashboard)/dashboard/translator/hooks/useAvailableModels.tsx index 339c716466..0a74df0386 100644 --- a/src/app/(dashboard)/dashboard/translator/hooks/useAvailableModels.tsx +++ b/src/app/(dashboard)/dashboard/translator/hooks/useAvailableModels.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { compareTr } from "@/shared/utils/turkishText"; -import type { ModelReasoningCapabilities } from "@/app/(dashboard)/dashboard/playground/components/reasoningControls"; +import type { ModelReasoningCapabilities } from "@/app/(dashboard)/dashboard/playground/components/reasoningControlUtils"; /** * Prefix-based format→model matching, used to pick a smart default diff --git a/tests/unit/case-collision-6584.test.ts b/tests/unit/case-collision-6584.test.ts new file mode 100644 index 0000000000..b3c3931785 --- /dev/null +++ b/tests/unit/case-collision-6584.test.ts @@ -0,0 +1,78 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Regression guard for issue #6584: webpack (OMNIROUTE_USE_TURBOPACK=0) build breaks on +// case-insensitive filesystems (macOS APFS default, Windows) because two source +// files in the same directory differ only by casing: +// src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx +// src/app/(dashboard)/dashboard/playground/components/reasoningControls.ts +// On a case-sensitive FS (Linux, this sandbox) both files resolve fine, so this +// test does not literally reproduce the webpack warning/build error. Instead it +// encodes the same-directory case-collision root cause as a filesystem-level +// invariant check that fails deterministically on ANY OS: two distinct files +// whose relative path is identical after lower-casing (stem, ignoring extension, +// since extensionless imports are what collide) must not coexist in the same +// directory tree. + +const ROOT = path.resolve(__dirname, "..", ".."); +const SCAN_DIRS = ["src", "open-sse"]; +const EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]); +const IGNORE_DIR_NAMES = new Set(["node_modules", ".next", "dist", "build", ".git"]); + +function walk(dir: string, out: string[]): void { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + if (IGNORE_DIR_NAMES.has(entry.name)) continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(full, out); + } else if (EXTENSIONS.has(path.extname(entry.name))) { + out.push(full); + } + } +} + +test("#6584: no two source files in the same directory differ only by casing", () => { + const files: string[] = []; + for (const d of SCAN_DIRS) { + const abs = path.join(ROOT, d); + if (fs.existsSync(abs)) walk(abs, files); + } + + // Group by (directory, lower-cased basename WITHOUT extension). Extensionless + // imports (e.g. `from "./reasoningControls"`) are resolved by webpack/Node's + // module resolver trying several extensions; on a case-insensitive filesystem + // that resolution collapses "ReasoningControls.tsx" and "reasoningControls.ts" + // into the same candidate, so the stem — not the full filename — is what must + // stay unique per directory regardless of extension. + const byDirLowerStem = new Map(); + for (const f of files) { + const dir = path.dirname(f); + const ext = path.extname(f); + const stem = path.basename(f, ext); + const lowerStem = stem.toLowerCase(); + const key = `${dir}::${lowerStem}`; + const arr = byDirLowerStem.get(key) ?? []; + arr.push(f); + byDirLowerStem.set(key, arr); + } + + const collisions: string[][] = []; + for (const arr of byDirLowerStem.values()) { + const distinctStems = new Set(arr.map((f) => path.basename(f, path.extname(f)))); + if (distinctStems.size > 1) { + collisions.push(arr); + } + } + + assert.deepEqual( + collisions, + [], + `Found case-only filename collisions (breaks webpack builds on case-insensitive filesystems): ${JSON.stringify(collisions, null, 2)}` + ); +}); diff --git a/tests/unit/playground-reasoning-controls-6241.test.ts b/tests/unit/playground-reasoning-controls-6241.test.ts index 318eb1e2b2..3dda9effce 100644 --- a/tests/unit/playground-reasoning-controls-6241.test.ts +++ b/tests/unit/playground-reasoning-controls-6241.test.ts @@ -4,7 +4,7 @@ import { resolveReasoningControls, buildReasoningRequestFields, type ReasoningControlSpec, -} from "../../src/app/(dashboard)/dashboard/playground/components/reasoningControls.ts"; +} from "../../src/app/(dashboard)/dashboard/playground/components/reasoningControlUtils.ts"; import { CANONICAL_EFFORT_VALUES } from "../../src/shared/reasoning/effortStandardization.ts"; // #6241: the Playground effort selector + thinking toggle read a model's `supportsThinking` /