Merge pull request #2841 from code-yeongyu/fix/model-fallback-test-isolation

fix(tests): resolve 5 cross-file test isolation failures
This commit is contained in:
YeonGyu-Kim
2026-03-26 09:31:09 +09:00
committed by GitHub
8 changed files with 60 additions and 21 deletions

View File

@@ -1,9 +1,10 @@
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 { dirname, join } from "node:path"
import { PACKAGE_NAME } from "../constants"
import { resolveSymlink } from "../../../shared/file-utils"
const systemLoadedVersionModulePath = "./system-loaded-version?system-loaded-version-test"
@@ -125,7 +126,7 @@ describe("system loaded version", () => {
const loadedVersion = getLoadedPluginVersion()
//#then
expect(loadedVersion.cacheDir).toBe(realpathSync(symlinkConfigDir))
expect(loadedVersion.cacheDir).toBe(resolveSymlink(symlinkConfigDir))
expect(loadedVersion.expectedVersion).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 { join } from "node:path"
import { resolveSymlink } from "../../../shared/file-utils"
import { getLatestVersion } from "../../../hooks/auto-update-checker/checker"
import { extractChannel } from "../../../hooks/auto-update-checker"
import { PACKAGE_NAME } from "../constants"
@@ -38,12 +38,7 @@ function resolveOpenCodeCacheDir(): string {
function resolveExistingDir(dirPath: string): string {
if (!existsSync(dirPath)) return dirPath
try {
return realpathSync(dirPath)
} catch {
return dirPath
}
return resolveSymlink(dirPath)
}
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", () => ({
log: mock(() => {}),
@@ -82,6 +82,10 @@ function createDefaultArgs(taskOverrides: Partial<BackgroundTask> = {}) {
}
describe("tryFallbackRetry", () => {
afterAll(() => {
mock.restore()
})
beforeEach(() => {
;(shouldRetryError as any).mockImplementation(() => true)
;(selectFallbackProvider as any).mockImplementation((providers: string[]) => providers[0])
@@ -274,8 +278,8 @@ describe("tryFallbackRetry", () => {
describe("#given disconnected fallback providers with connected preferred provider", () => {
test("keeps fallback entry and selects connected preferred provider", () => {
;(readProviderModelsCache as any).mockReturnValue({ connected: ["provider-a"] })
;(selectFallbackProvider as any).mockImplementation(
;(readProviderModelsCache as any).mockReturnValueOnce({ connected: ["provider-a"] })
;(selectFallbackProvider as any).mockImplementationOnce(
(_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 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) => {
if (provider === "github-copilot") {
return model
@@ -31,6 +49,10 @@ mock.module("../../shared/provider-model-id-transform", () => ({
transformModelForProvider: transformModelForProviderMock,
}))
mock.module("../../shared/model-error-classifier", () => ({
selectFallbackProvider: selectFallbackProviderMock,
}))
import {
clearPendingModelFallback,
createModelFallbackHook,
@@ -44,6 +66,7 @@ describe("model fallback hook", () => {
readProviderModelsCacheMock.mockReturnValue(null)
readConnectedProvidersCacheMock.mockClear()
readProviderModelsCacheMock.mockClear()
selectFallbackProviderMock.mockClear()
clearPendingModelFallback("ses_model_fallback_main")
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 { homedir } from "node:os"
import { join, resolve } from "node:path"
import { join, resolve, win32 } from "node:path"
import {
getOpenCodeConfigDir,
getOpenCodeConfigPaths,
@@ -241,9 +241,10 @@ describe("opencode-config-dir", () => {
// when getOpenCodeConfigDir is called with binary="opencode-desktop"
const result = getOpenCodeConfigDir({ binary: "opencode-desktop", version: "1.0.200", checkExisting: false })
// then returns %APPDATA%/ai.opencode.desktop
expect(result).toBe(join("C:\\Users\\TestUser\\AppData\\Roaming", TAURI_APP_IDENTIFIER))
// then returns %APPDATA%/ai.opencode.desktop using Windows path semantics
expect(result).toBe(win32.join("C:\\Users\\TestUser\\AppData\\Roaming", TAURI_APP_IDENTIFIER))
})
})
describe("dev build detection", () => {

View File

@@ -1,6 +1,6 @@
import { existsSync, realpathSync } from "node:fs"
import { homedir } from "node:os"
import { join, resolve } from "node:path"
import { join, resolve, win32 } from "node:path"
import type {
OpenCodeBinaryType,
@@ -31,7 +31,7 @@ function getTauriConfigDir(identifier: string): string {
case "win32": {
const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming")
return join(appData, identifier)
return win32.join(appData, identifier)
}
case "linux":
@@ -71,7 +71,10 @@ export function getOpenCodeConfigDir(options: OpenCodeConfigDirOptions): string
}
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) {
const legacyDir = getCliConfigDir()

View File

@@ -1,6 +1,8 @@
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(() => {
_resetForTesting()
resetClaudeSessionState()
resetModelFallbackState()
})