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:
@@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user