fix(tests): resolve 5 cross-file test isolation failures

- model-fallback hook: mock selectFallbackProvider and add _resetForTesting()
  to test-setup.ts to clear module-level state between files
- fallback-retry-handler: add afterAll(mock.restore) and use mockReturnValueOnce
  to prevent connected-providers mock leaking to subsequent test files
- opencode-config-dir: use win32.join for Windows APPDATA path construction
  so tests pass on macOS (path.join uses POSIX semantics regardless of
  process.platform override)
- system-loaded-version: use resolveSymlink from file-utils instead of
  realpathSync to handle macOS /var -> /private/var symlink consistently

All 4456 tests pass (0 failures) on full bun test suite.
This commit is contained in:
YeonGyu-Kim
2026-03-26 09:30:34 +09:00
parent 90919bf359
commit 7895361f42
8 changed files with 60 additions and 21 deletions

View File

@@ -1,9 +1,10 @@
import { afterEach, describe, expect, it } from "bun:test" import { afterEach, describe, expect, it } from "bun:test"
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs" import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
import { dirname, join } from "node:path" import { dirname, join } from "node:path"
import { PACKAGE_NAME } from "../constants" import { PACKAGE_NAME } from "../constants"
import { resolveSymlink } from "../../../shared/file-utils"
const systemLoadedVersionModulePath = "./system-loaded-version?system-loaded-version-test" const systemLoadedVersionModulePath = "./system-loaded-version?system-loaded-version-test"
@@ -125,7 +126,7 @@ describe("system loaded version", () => {
const loadedVersion = getLoadedPluginVersion() const loadedVersion = getLoadedPluginVersion()
//#then //#then
expect(loadedVersion.cacheDir).toBe(realpathSync(symlinkConfigDir)) expect(loadedVersion.cacheDir).toBe(resolveSymlink(symlinkConfigDir))
expect(loadedVersion.expectedVersion).toBe("4.5.6") expect(loadedVersion.expectedVersion).toBe("4.5.6")
expect(loadedVersion.loadedVersion).toBe("4.5.6") expect(loadedVersion.loadedVersion).toBe("4.5.6")
}) })

View File

@@ -1,7 +1,7 @@
import { existsSync, readFileSync, realpathSync } from "node:fs" import { existsSync, readFileSync } from "node:fs"
import { homedir } from "node:os" import { homedir } from "node:os"
import { join } from "node:path" import { join } from "node:path"
import { resolveSymlink } from "../../../shared/file-utils"
import { getLatestVersion } from "../../../hooks/auto-update-checker/checker" import { getLatestVersion } from "../../../hooks/auto-update-checker/checker"
import { extractChannel } from "../../../hooks/auto-update-checker" import { extractChannel } from "../../../hooks/auto-update-checker"
import { PACKAGE_NAME } from "../constants" import { PACKAGE_NAME } from "../constants"
@@ -38,12 +38,7 @@ function resolveOpenCodeCacheDir(): string {
function resolveExistingDir(dirPath: string): string { function resolveExistingDir(dirPath: string): string {
if (!existsSync(dirPath)) return dirPath if (!existsSync(dirPath)) return dirPath
return resolveSymlink(dirPath)
try {
return realpathSync(dirPath)
} catch {
return dirPath
}
} }
function readPackageJson(filePath: string): PackageJsonShape | null { function readPackageJson(filePath: string): PackageJsonShape | null {

View File

@@ -1,4 +1,4 @@
import { describe, test, expect, mock, beforeEach } from "bun:test" import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test"
mock.module("../../shared", () => ({ mock.module("../../shared", () => ({
log: mock(() => {}), log: mock(() => {}),
@@ -82,6 +82,10 @@ function createDefaultArgs(taskOverrides: Partial<BackgroundTask> = {}) {
} }
describe("tryFallbackRetry", () => { describe("tryFallbackRetry", () => {
afterAll(() => {
mock.restore()
})
beforeEach(() => { beforeEach(() => {
;(shouldRetryError as any).mockImplementation(() => true) ;(shouldRetryError as any).mockImplementation(() => true)
;(selectFallbackProvider as any).mockImplementation((providers: string[]) => providers[0]) ;(selectFallbackProvider as any).mockImplementation((providers: string[]) => providers[0])
@@ -274,8 +278,8 @@ describe("tryFallbackRetry", () => {
describe("#given disconnected fallback providers with connected preferred provider", () => { describe("#given disconnected fallback providers with connected preferred provider", () => {
test("keeps fallback entry and selects connected preferred provider", () => { test("keeps fallback entry and selects connected preferred provider", () => {
;(readProviderModelsCache as any).mockReturnValue({ connected: ["provider-a"] }) ;(readProviderModelsCache as any).mockReturnValueOnce({ connected: ["provider-a"] })
;(selectFallbackProvider as any).mockImplementation( ;(selectFallbackProvider as any).mockImplementationOnce(
(_providers: string[], preferredProviderID?: string) => preferredProviderID ?? "provider-b", (_providers: string[], preferredProviderID?: string) => preferredProviderID ?? "provider-b",
) )

View File

@@ -3,6 +3,24 @@ const { beforeEach, describe, expect, mock, test } = require("bun:test")
const readConnectedProvidersCacheMock = mock(() => null) const readConnectedProvidersCacheMock = mock(() => null)
const readProviderModelsCacheMock = mock(() => null) const readProviderModelsCacheMock = mock(() => null)
const selectFallbackProviderMock = mock((providers: string[], preferredProviderID?: string) => {
const connectedProviders = readConnectedProvidersCacheMock()
if (connectedProviders) {
const connectedSet = new Set(connectedProviders.map((provider: string) => provider.toLowerCase()))
for (const provider of providers) {
if (connectedSet.has(provider.toLowerCase())) {
return provider
}
}
if (preferredProviderID && connectedSet.has(preferredProviderID.toLowerCase())) {
return preferredProviderID
}
}
return providers[0] || preferredProviderID || "opencode"
})
const transformModelForProviderMock = mock((provider: string, model: string) => { const transformModelForProviderMock = mock((provider: string, model: string) => {
if (provider === "github-copilot") { if (provider === "github-copilot") {
return model return model
@@ -31,6 +49,10 @@ mock.module("../../shared/provider-model-id-transform", () => ({
transformModelForProvider: transformModelForProviderMock, transformModelForProvider: transformModelForProviderMock,
})) }))
mock.module("../../shared/model-error-classifier", () => ({
selectFallbackProvider: selectFallbackProviderMock,
}))
import { import {
clearPendingModelFallback, clearPendingModelFallback,
createModelFallbackHook, createModelFallbackHook,
@@ -44,6 +66,7 @@ describe("model fallback hook", () => {
readProviderModelsCacheMock.mockReturnValue(null) readProviderModelsCacheMock.mockReturnValue(null)
readConnectedProvidersCacheMock.mockClear() readConnectedProvidersCacheMock.mockClear()
readProviderModelsCacheMock.mockClear() readProviderModelsCacheMock.mockClear()
selectFallbackProviderMock.mockClear()
clearPendingModelFallback("ses_model_fallback_main") clearPendingModelFallback("ses_model_fallback_main")
clearPendingModelFallback("ses_model_fallback_ghcp") clearPendingModelFallback("ses_model_fallback_ghcp")

View File

@@ -274,3 +274,13 @@ export function createModelFallbackHook(args?: { toast?: FallbackToast; onApplie
}, },
} }
} }
/**
* Resets all module-global state for testing.
* Clears pending fallbacks, toast keys, and session chains.
*/
export function _resetForTesting(): void {
pendingModelFallbacks.clear()
lastToastKey.clear()
sessionFallbackChains.clear()
}

View File

@@ -1,6 +1,6 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test" import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import { homedir } from "node:os" import { homedir } from "node:os"
import { join, resolve } from "node:path" import { join, resolve, win32 } from "node:path"
import { import {
getOpenCodeConfigDir, getOpenCodeConfigDir,
getOpenCodeConfigPaths, getOpenCodeConfigPaths,
@@ -241,9 +241,10 @@ describe("opencode-config-dir", () => {
// when getOpenCodeConfigDir is called with binary="opencode-desktop" // when getOpenCodeConfigDir is called with binary="opencode-desktop"
const result = getOpenCodeConfigDir({ binary: "opencode-desktop", version: "1.0.200", checkExisting: false }) const result = getOpenCodeConfigDir({ binary: "opencode-desktop", version: "1.0.200", checkExisting: false })
// then returns %APPDATA%/ai.opencode.desktop // then returns %APPDATA%/ai.opencode.desktop using Windows path semantics
expect(result).toBe(join("C:\\Users\\TestUser\\AppData\\Roaming", TAURI_APP_IDENTIFIER)) expect(result).toBe(win32.join("C:\\Users\\TestUser\\AppData\\Roaming", TAURI_APP_IDENTIFIER))
}) })
}) })
describe("dev build detection", () => { describe("dev build detection", () => {

View File

@@ -1,6 +1,6 @@
import { existsSync, realpathSync } from "node:fs" import { existsSync, realpathSync } from "node:fs"
import { homedir } from "node:os" import { homedir } from "node:os"
import { join, resolve } from "node:path" import { join, resolve, win32 } from "node:path"
import type { import type {
OpenCodeBinaryType, OpenCodeBinaryType,
@@ -31,7 +31,7 @@ function getTauriConfigDir(identifier: string): string {
case "win32": { case "win32": {
const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming") const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming")
return join(appData, identifier) return win32.join(appData, identifier)
} }
case "linux": case "linux":
@@ -71,7 +71,10 @@ export function getOpenCodeConfigDir(options: OpenCodeConfigDirOptions): string
} }
const identifier = isDevBuild(version) ? TAURI_APP_IDENTIFIER_DEV : TAURI_APP_IDENTIFIER const identifier = isDevBuild(version) ? TAURI_APP_IDENTIFIER_DEV : TAURI_APP_IDENTIFIER
const tauriDir = resolveConfigPath(getTauriConfigDir(identifier)) const tauriDirBase = getTauriConfigDir(identifier)
const tauriDir = process.platform === "win32"
? (win32.isAbsolute(tauriDirBase) ? win32.normalize(tauriDirBase) : win32.resolve(tauriDirBase))
: resolveConfigPath(tauriDirBase)
if (checkExisting) { if (checkExisting) {
const legacyDir = getCliConfigDir() const legacyDir = getCliConfigDir()

View File

@@ -1,6 +1,8 @@
import { beforeEach } from "bun:test" import { beforeEach } from "bun:test"
import { _resetForTesting } from "./src/features/claude-code-session-state/state" import { _resetForTesting as resetClaudeSessionState } from "./src/features/claude-code-session-state/state"
import { _resetForTesting as resetModelFallbackState } from "./src/hooks/model-fallback/hook"
beforeEach(() => { beforeEach(() => {
_resetForTesting() resetClaudeSessionState()
resetModelFallbackState()
}) })