test(cli): update all CLI command tests

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

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