diff --git a/src/cli/config-manager/bun-install.test.ts b/src/cli/config-manager/bun-install.test.ts index a6a1ba240..5564b3ff6 100644 --- a/src/cli/config-manager/bun-install.test.ts +++ b/src/cli/config-manager/bun-install.test.ts @@ -2,13 +2,14 @@ import * as fs from "node:fs" -import { afterEach, beforeEach, describe, expect, it, jest, spyOn } from "bun:test" +import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test" import * as dataPath from "../../shared/data-path" import * as logger from "../../shared/logger" import * as spawnHelpers from "../../shared/spawn-with-windows-hide" import type { BunInstallResult } from "./bun-install" -import { runBunInstallWithDetails } from "./bun-install" + +type BunInstallModule = typeof import("./bun-install") type CreateProcOptions = { exitCode?: number | null @@ -37,12 +38,16 @@ describe("runBunInstallWithDetails", () => { let logSpy: ReturnType let spawnWithWindowsHideSpy: ReturnType let existsSyncSpy: ReturnType + let runBunInstallWithDetails: BunInstallModule["runBunInstallWithDetails"] - beforeEach(() => { + beforeEach(async () => { getOpenCodeCacheDirSpy = spyOn(dataPath, "getOpenCodeCacheDir").mockReturnValue("/tmp/opencode-cache") logSpy = spyOn(logger, "log").mockImplementation(() => {}) spawnWithWindowsHideSpy = spyOn(spawnHelpers, "spawnWithWindowsHide").mockReturnValue(createProc()) existsSyncSpy = spyOn(fs, "existsSync").mockReturnValue(true) + + const bunInstallModule = await import(`./bun-install?test=${Date.now()}-${Math.random()}`) + runBunInstallWithDetails = bunInstallModule.runBunInstallWithDetails }) afterEach(() => { @@ -136,9 +141,30 @@ describe("runBunInstallWithDetails", () => { describe("#when the install times out and proc.exited never resolves", () => { it("#then returns timedOut true without hanging", async () => { // given - jest.useFakeTimers() - let killCallCount = 0 + const originalSetTimeout = globalThis.setTimeout + const originalClearTimeout = globalThis.clearTimeout + + Object.defineProperty(globalThis, "setTimeout", { + configurable: true, + value: Object.assign( + (callback: TimerHandler) => { + if (typeof callback === "function") { + callback() + } + + return 0 + }, + { + __promisify__: originalSetTimeout.__promisify__, + } + ), + }) + Object.defineProperty(globalThis, "clearTimeout", { + configurable: true, + value: () => undefined, + }) + spawnWithWindowsHideSpy.mockReturnValue( createProc({ exitCode: null, @@ -148,38 +174,28 @@ describe("runBunInstallWithDetails", () => { }, }) ) + const timeoutAwareModule = await import(`./bun-install?timeout-test=${Date.now()}-${Math.random()}`) try { // when - const resultPromise = runBunInstallWithDetails({ outputMode: "pipe" }) - jest.advanceTimersByTime(60_000) - jest.runOnlyPendingTimers() - await Promise.resolve() - - const outcome = await Promise.race([ - resultPromise.then((result) => ({ - status: "resolved" as const, - result, - })), - new Promise<{ status: "pending" }>((resolve) => { - queueMicrotask(() => resolve({ status: "pending" })) - }), - ]) + const outcome = await timeoutAwareModule.runBunInstallWithDetails({ outputMode: "pipe" }) // then - if (outcome.status === "pending") { - throw new Error("runBunInstallWithDetails did not resolve after timing out") - } - - expect(outcome.result).toEqual({ + expect(outcome).toEqual({ success: false, timedOut: true, error: 'bun install timed out after 60 seconds. Try running manually: cd "/tmp/opencode-cache/packages" && bun i', } satisfies BunInstallResult) expect(killCallCount).toBe(1) } finally { - jest.clearAllTimers() - jest.useRealTimers() + Object.defineProperty(globalThis, "setTimeout", { + configurable: true, + value: originalSetTimeout, + }) + Object.defineProperty(globalThis, "clearTimeout", { + configurable: true, + value: originalClearTimeout, + }) } }) }) diff --git a/src/cli/doctor/checks/system.test.ts b/src/cli/doctor/checks/system.test.ts index 96d8bde00..536638324 100644 --- a/src/cli/doctor/checks/system.test.ts +++ b/src/cli/doctor/checks/system.test.ts @@ -1,12 +1,15 @@ /// -import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test" +import { beforeEach, describe, expect, it, mock } from "bun:test" import { PLUGIN_NAME } from "../../../shared" import type { PluginInfo } from "./system-plugin" +import type { OpenCodeBinaryInfo } from "./system-binary" +import { checkSystem } from "./system" -type SystemModule = typeof import("./system") - -const mockFindOpenCodeBinary = mock(async () => ({ path: "/usr/local/bin/opencode" })) +const mockFindOpenCodeBinary = mock<() => Promise>(async () => ({ + binary: "opencode", + path: "/usr/local/bin/opencode", +})) const mockGetOpenCodeVersion = mock(async () => "1.0.200") const mockCompareVersions = mock((_leftVersion?: string, _rightVersion?: string) => true) const mockGetPluginInfo = mock((): PluginInfo => ({ @@ -27,35 +30,17 @@ const mockGetLoadedPluginVersion = mock(() => ({ const mockGetLatestPluginVersion = mock(async (_currentVersion: string | null) => null as string | null) const mockGetSuggestedInstallTag = mock(() => "latest") -const realSystemBinary = require("./system-binary") -const realSystemPlugin = require("./system-plugin") -const realSystemLoadedVersion = require("./system-loaded-version") -afterAll(() => { - mock.module("./system-binary", () => realSystemBinary) - mock.module("./system-plugin", () => realSystemPlugin) - mock.module("./system-loaded-version", () => realSystemLoadedVersion) - mock.restore() -}) - -async function importFreshSystemModule(): Promise { - mock.module("./system-binary", () => ({ +function createSystemDeps() { + return { findOpenCodeBinary: mockFindOpenCodeBinary, getOpenCodeVersion: mockGetOpenCodeVersion, compareVersions: mockCompareVersions, - })) - - mock.module("./system-plugin", () => ({ getPluginInfo: mockGetPluginInfo, - })) - - mock.module("./system-loaded-version", () => ({ getLoadedPluginVersion: mockGetLoadedPluginVersion, getLatestPluginVersion: mockGetLatestPluginVersion, getSuggestedInstallTag: mockGetSuggestedInstallTag, - })) - - return import(`./system?test=${Date.now()}-${Math.random()}`) + } } describe("system check", () => { @@ -68,7 +53,10 @@ describe("system check", () => { mockGetLatestPluginVersion.mockReset() mockGetSuggestedInstallTag.mockReset() - mockFindOpenCodeBinary.mockResolvedValue({ path: "/usr/local/bin/opencode" }) + mockFindOpenCodeBinary.mockResolvedValue({ + binary: "opencode", + path: "/usr/local/bin/opencode", + }) mockGetOpenCodeVersion.mockResolvedValue("1.0.200") mockCompareVersions.mockReturnValue(true) mockGetPluginInfo.mockReturnValue({ @@ -93,10 +81,8 @@ describe("system check", () => { describe("#given cache directory contains spaces", () => { it("uses a quoted cache directory in mismatch fix command", async () => { //#given - const { checkSystem } = await importFreshSystemModule() - //#when - const result = await checkSystem() + const result = await checkSystem(createSystemDeps()) //#then const mismatchIssue = result.issues.find((issue) => issue.title === "Loaded plugin version mismatch") @@ -114,13 +100,12 @@ describe("system check", () => { }) mockGetLatestPluginVersion.mockResolvedValue("3.0.0-canary.2") mockGetSuggestedInstallTag.mockReturnValue("canary") - mockCompareVersions.mockImplementation((leftVersion?: string, rightVersion?: string) => { - return !(leftVersion === "3.0.0-canary.1" && rightVersion === "3.0.0-canary.2") - }) - const { checkSystem } = await importFreshSystemModule() + mockCompareVersions + .mockImplementationOnce(() => true) + .mockImplementationOnce(() => false) //#when - const result = await checkSystem() + const result = await checkSystem(createSystemDeps()) //#then const outdatedIssue = result.issues.find((issue) => issue.title === "Loaded plugin is outdated") @@ -141,10 +126,9 @@ describe("system check", () => { configPath: null, isLocalDev: false, }) - const { checkSystem } = await importFreshSystemModule() //#when - const result = await checkSystem() + const result = await checkSystem(createSystemDeps()) //#then const legacyEntryIssue = result.issues.find((issue) => issue.title === "Using legacy package name") @@ -164,10 +148,9 @@ describe("system check", () => { configPath: null, isLocalDev: false, }) - const { checkSystem } = await importFreshSystemModule() //#when - const result = await checkSystem() + const result = await checkSystem(createSystemDeps()) //#then const legacyEntryIssue = result.issues.find((issue) => issue.title === "Using legacy package name") @@ -187,10 +170,9 @@ describe("system check", () => { configPath: null, isLocalDev: false, }) - const { checkSystem } = await importFreshSystemModule() //#when - const result = await checkSystem() + const result = await checkSystem(createSystemDeps()) //#then expect(result.issues.some((issue) => issue.title === "Using legacy package name")).toBe(false) @@ -206,10 +188,9 @@ describe("system check", () => { configPath: null, isLocalDev: true, }) - const { checkSystem } = await importFreshSystemModule() //#when - const result = await checkSystem() + const result = await checkSystem(createSystemDeps()) //#then expect(result.issues.some((issue) => issue.title === "Using legacy package name")).toBe(false) diff --git a/src/cli/run/continuation-state.json-backend.test.ts b/src/cli/run/continuation-state.json-backend.test.ts index c2652538b..f53cdd547 100644 --- a/src/cli/run/continuation-state.json-backend.test.ts +++ b/src/cli/run/continuation-state.json-backend.test.ts @@ -8,6 +8,7 @@ const testDirs: string[] = [] const TEST_STORAGE_ROOT = join(tmpdir(), `omo-run-json-storage-${Date.now()}`) const TEST_MESSAGE_STORAGE = join(TEST_STORAGE_ROOT, "message") +const sessionLastAgentBySessionID = new Map() mock.module("../../shared/opencode-storage-detection", () => ({ isSqliteBackend: () => false, @@ -20,9 +21,21 @@ mock.module("../../shared/opencode-message-dir", () => ({ }, })) +mock.module("../../hooks/atlas/session-last-agent", () => ({ + getLastAgentFromSession: async (sessionID: string) => { + return sessionLastAgentBySessionID.get(sessionID) ?? null + }, +})) +mock.module("../../hooks/atlas/session-last-agent.ts", () => ({ + getLastAgentFromSession: async (sessionID: string) => { + return sessionLastAgentBySessionID.get(sessionID) ?? null + }, +})) + afterAll(() => { mock.restore() }) afterEach(() => { + sessionLastAgentBySessionID.clear() while (testDirs.length > 0) { const dir = testDirs.pop() if (dir) { @@ -73,6 +86,7 @@ describe("getContinuationState JSON backend descendant coverage", () => { }), "utf-8") writeJsonMessage("ses_child_session", "msg_001.json", "atlas") writeJsonMessage("ses_child_session", "msg_002.json", "compaction") + sessionLastAgentBySessionID.set("ses_child_session", "atlas") const { getContinuationState } = await import("./continuation-state") @@ -150,6 +164,7 @@ describe("getContinuationState JSON backend descendant coverage", () => { model: { providerID: "openai", modelID: "gpt-5.4" }, time: { created: 100 }, }), "utf-8") + sessionLastAgentBySessionID.set(sessionID, "sisyphus-junior") const { getContinuationState } = await import("./continuation-state")