From 8a4eacfd2465577e1b02cf90cc40fdfe1d546a71 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 10 Apr 2026 15:53:32 +0900 Subject: [PATCH] test(hooks): update multiple hook test suites Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../message-builder.test.ts | 50 +++--- .../storage.test.ts | 53 +++--- src/hooks/atlas/background-task-retry.test.ts | 5 +- .../atlas/session-last-agent.json.test.ts | 52 +++--- .../atlas/session-last-agent.sqlite.test.ts | 24 +-- src/hooks/auto-slash-command/index.test.ts | 31 ++-- .../leak/auto-slash-command-leak.test.ts | 24 ++- src/hooks/keyword-detector/index.test.ts | 4 +- .../legacy-plugin-toast/auto-migrate.test.ts | 21 +-- src/hooks/runtime-fallback/index.test.ts | 29 ++-- src/hooks/session-recovery/index.test.ts | 28 +++- .../recover-tool-result-missing.test.ts | 18 ++- src/hooks/zauc-mocks-hook/hook.test.ts | 153 ++++++++++-------- .../zauc-sync-mocks/sync-package-json.test.ts | 83 ++++------ 14 files changed, 316 insertions(+), 259 deletions(-) diff --git a/src/hooks/anthropic-context-window-limit-recovery/message-builder.test.ts b/src/hooks/anthropic-context-window-limit-recovery/message-builder.test.ts index d627d1ec5..9c6be067f 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/message-builder.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/message-builder.test.ts @@ -4,35 +4,31 @@ const replaceEmptyTextPartsAsync = mock(() => Promise.resolve(false)) const injectTextPartAsync = mock(() => Promise.resolve(false)) const findMessagesWithEmptyTextPartsFromSDK = mock(() => Promise.resolve([] as string[])) -async function importFreshMessageBuilder(): Promise { - mock.module("../../shared/logger", () => ({ - log: () => {}, - })) +mock.module("../../shared/logger", () => ({ + log: () => {}, +})) - mock.module("../../shared/opencode-storage-detection", () => ({ - isSqliteBackend: () => true, - })) +mock.module("../../shared/opencode-storage-detection", () => ({ + isSqliteBackend: () => true, +})) - const emptyTextMockFactory = () => ({ - findMessagesWithEmptyTextParts: () => [], - replaceEmptyTextParts: () => false, - replaceEmptyTextPartsAsync, - findMessagesWithEmptyTextPartsFromSDK, - }) - mock.module("../session-recovery/storage/empty-text", emptyTextMockFactory) - mock.module("../session-recovery/storage/empty-text.ts", emptyTextMockFactory) +const emptyTextMockFactory = () => ({ + findMessagesWithEmptyTextParts: () => [], + replaceEmptyTextParts: () => false, + replaceEmptyTextPartsAsync, + findMessagesWithEmptyTextPartsFromSDK, +}) +mock.module("../session-recovery/storage/empty-text", emptyTextMockFactory) +mock.module("../session-recovery/storage/empty-text.ts", emptyTextMockFactory) - const textPartInjectorMockFactory = () => ({ - injectTextPart: () => false, - injectTextPartAsync, - }) - mock.module("../session-recovery/storage/text-part-injector", textPartInjectorMockFactory) - mock.module("../session-recovery/storage/text-part-injector.ts", textPartInjectorMockFactory) +const textPartInjectorMockFactory = () => ({ + injectTextPart: () => false, + injectTextPartAsync, +}) +mock.module("../session-recovery/storage/text-part-injector", textPartInjectorMockFactory) +mock.module("../session-recovery/storage/text-part-injector.ts", textPartInjectorMockFactory) - const module = await import(`./message-builder?test=${Date.now()}-${Math.random()}`) - mock.restore() - return module -} +const messageBuilderModulePromise = import("./message-builder") afterAll(() => { mock.restore() @@ -49,7 +45,7 @@ describe("sanitizeEmptyMessagesBeforeSummarize", () => { }) test("#given sqlite message with tool content and empty text part #when sanitizing #then it fixes the mixed-content message", async () => { - const { sanitizeEmptyMessagesBeforeSummarize, PLACEHOLDER_TEXT } = await importFreshMessageBuilder() + const { sanitizeEmptyMessagesBeforeSummarize, PLACEHOLDER_TEXT } = await messageBuilderModulePromise const client = { session: { messages: mock(() => Promise.resolve({ @@ -76,7 +72,7 @@ describe("sanitizeEmptyMessagesBeforeSummarize", () => { }) test("#given sqlite message with mixed content and failed replacement #when sanitizing #then it injects the placeholder text part", async () => { - const { sanitizeEmptyMessagesBeforeSummarize, PLACEHOLDER_TEXT } = await importFreshMessageBuilder() + const { sanitizeEmptyMessagesBeforeSummarize, PLACEHOLDER_TEXT } = await messageBuilderModulePromise const client = { session: { messages: mock(() => Promise.resolve({ diff --git a/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts b/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts index d884074d9..0e3507ee8 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts @@ -1,45 +1,52 @@ -import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test" -import { truncateUntilTargetTokens } from "./storage" -import * as storage from "./storage" +import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test" +import type { ToolResultInfo } from "./tool-part-types" -// Mock the entire module -mock.module("./storage", () => { - return { - ...storage, - findToolResultsBySize: mock(() => []), - truncateToolResult: mock(() => ({ success: false })), - } -}) +type TruncateToolResult = { + success: boolean + toolName?: string + originalSize?: number +} + +const findToolResultsBySize = mock<(_: string) => ToolResultInfo[]>(() => []) +const truncateToolResult = mock<(_: string) => TruncateToolResult>(() => ({ success: false })) + +mock.module("./tool-result-storage", () => ({ + findToolResultsBySize, + truncateToolResult, +})) + +async function importFreshStorage(): Promise { + return import(`./storage?test=${Date.now()}-${Math.random()}`) +} afterAll(() => { - mock.module("./storage", () => storage) mock.restore() }) describe("truncateUntilTargetTokens", () => { const sessionID = "test-session" - + beforeEach(() => { - // Reset mocks - const { findToolResultsBySize, truncateToolResult } = require("./storage") findToolResultsBySize.mockReset() truncateToolResult.mockReset() + findToolResultsBySize.mockReturnValue([]) + truncateToolResult.mockReturnValue({ success: false }) }) test("truncates only until target is reached", async () => { - const { findToolResultsBySize, truncateToolResult } = require("./storage") - + const { truncateUntilTargetTokens } = await importFreshStorage() + // given: Two tool results, each 1000 chars. Target reduction is 500 chars. const results = [ { partPath: "path1", partId: "id1", messageID: "m1", toolName: "tool1", outputSize: 1000 }, { partPath: "path2", partId: "id2", messageID: "m2", toolName: "tool2", outputSize: 1000 }, ] - + findToolResultsBySize.mockReturnValue(results) truncateToolResult.mockImplementation((path: string) => ({ success: true, toolName: path === "path1" ? "tool1" : "tool2", - originalSize: 1000 + originalSize: 1000, })) // when: currentTokens=1000, maxTokens=1000, targetRatio=0.5 (target=500, reduce=500) @@ -55,19 +62,19 @@ describe("truncateUntilTargetTokens", () => { }) test("truncates all if target not reached", async () => { - const { findToolResultsBySize, truncateToolResult } = require("./storage") - + const { truncateUntilTargetTokens } = await importFreshStorage() + // given: Two tool results, each 100 chars. Target reduction is 500 chars. const results = [ { partPath: "path1", partId: "id1", messageID: "m1", toolName: "tool1", outputSize: 100 }, { partPath: "path2", partId: "id2", messageID: "m2", toolName: "tool2", outputSize: 100 }, ] - + findToolResultsBySize.mockReturnValue(results) truncateToolResult.mockImplementation((path: string) => ({ success: true, toolName: path === "path1" ? "tool1" : "tool2", - originalSize: 100 + originalSize: 100, })) // when: reduce 500 chars diff --git a/src/hooks/atlas/background-task-retry.test.ts b/src/hooks/atlas/background-task-retry.test.ts index cc0a6873d..e8a9cded6 100644 --- a/src/hooks/atlas/background-task-retry.test.ts +++ b/src/hooks/atlas/background-task-retry.test.ts @@ -6,7 +6,7 @@ import { randomUUID } from "node:crypto" import type { PluginInput } from "@opencode-ai/plugin" import { createAtlasHook } from "./atlas-hook" import { clearBoulderState, writeBoulderState } from "../../features/boulder-state" -import { _resetForTesting, registerAgentName, setSessionAgent } from "../../features/claude-code-session-state" +import { _resetForTesting, clearSessionAgent, registerAgentName, setSessionAgent } from "../../features/claude-code-session-state" // Force process isolation in CI runner (globalThis.setTimeout override conflicts with other atlas tests) mock.module("../../shared/opencode-storage-detection", () => ({ @@ -401,7 +401,8 @@ describe("atlas background task retry", () => { // when await hook.handler({ event: { type: "session.idle", properties: { sessionID: descendantSessionID } } }) expect(capturedTimers.size).toBe(1) - descendantAgent = "sisyphus-junior" + descendantAgent = "prometheus" + clearSessionAgent(descendantSessionID) backgroundRunning = false await firePendingTimers() diff --git a/src/hooks/atlas/session-last-agent.json.test.ts b/src/hooks/atlas/session-last-agent.json.test.ts index fec271338..263350c8c 100644 --- a/src/hooks/atlas/session-last-agent.json.test.ts +++ b/src/hooks/atlas/session-last-agent.json.test.ts @@ -9,19 +9,6 @@ const testDirs: string[] = [] const TEST_STORAGE_ROOT = join(tmpdir(), `atlas-session-last-agent-${Date.now()}`) const TEST_MESSAGE_STORAGE = join(TEST_STORAGE_ROOT, "message") -mock.module("../../shared/opencode-storage-detection", () => ({ - isSqliteBackend: () => false, -})) - -mock.module("../../shared/opencode-message-dir", () => ({ - getMessageDir: (sessionID: string) => { - const directPath = join(TEST_MESSAGE_STORAGE, sessionID) - return require("node:fs").existsSync(directPath) ? directPath : null - }, -})) - -afterAll(() => { mock.restore() }) - afterEach(() => { while (testDirs.length > 0) { const directory = testDirs.pop() @@ -31,11 +18,17 @@ afterEach(() => { } }) +async function importFreshSessionLastAgentModule(): Promise { + return import(`./session-last-agent?test=${Date.now()}-${Math.random()}`) +} + function createTempMessageDir(sessionID: string): string { const directory = mkdtempSync(join(tmpdir(), "atlas-session-last-agent-json-")) testDirs.push(directory) const messageDir = join(TEST_MESSAGE_STORAGE, sessionID) + rmSync(messageDir, { recursive: true, force: true }) mkdirSync(messageDir, { recursive: true }) + testDirs.push(messageDir) return messageDir } @@ -57,10 +50,20 @@ describe("getLastAgentFromSession JSON backend", () => { time: { created: 50 }, }), "utf-8") - const { getLastAgentFromSession } = await import("./session-last-agent") + const { getLastAgentFromSession } = await importFreshSessionLastAgentModule() // when - const result = await getLastAgentFromSession(sessionID) + const result = await getLastAgentFromSession(sessionID, undefined, { + isSqliteBackend: () => false, + getMessageDir: (targetSessionID: string) => { + const directPath = join(TEST_MESSAGE_STORAGE, targetSessionID) + return require("node:fs").existsSync(directPath) ? directPath : null + }, + isCompactionMessage: (message: { agent?: unknown }) => { + return typeof message.agent === "string" && message.agent.toLowerCase() === "compaction" + }, + hasCompactionPartInStorage: () => false, + }) // then expect(result).toBe("atlas") @@ -71,6 +74,7 @@ describe("getLastAgentFromSession JSON backend", () => { const sessionID = "ses_json_compaction_marker" const messageDir = createTempMessageDir(sessionID) const compactionMessageID = "msg_test_atlas_compaction_marker" + const regularMessageID = `msg_${sessionID}_regular` const partDir = join(PART_STORAGE, compactionMessageID) testDirs.push(partDir) writeFileSync(join(messageDir, "msg_0001.json"), JSON.stringify({ @@ -84,15 +88,27 @@ describe("getLastAgentFromSession JSON backend", () => { }), "utf-8") writeFileSync(join(messageDir, "msg_0002.json"), JSON.stringify({ - id: "msg_0002", + id: regularMessageID, agent: "sisyphus-junior", time: { created: 100 }, }), "utf-8") - const { getLastAgentFromSession } = await import("./session-last-agent") + const { getLastAgentFromSession } = await importFreshSessionLastAgentModule() // when - const result = await getLastAgentFromSession(sessionID) + const result = await getLastAgentFromSession(sessionID, undefined, { + isSqliteBackend: () => false, + getMessageDir: (targetSessionID: string) => { + const directPath = join(TEST_MESSAGE_STORAGE, targetSessionID) + return require("node:fs").existsSync(directPath) ? directPath : null + }, + isCompactionMessage: (message: { agent?: unknown }) => { + return typeof message.agent === "string" && message.agent.toLowerCase() === "compaction" + }, + hasCompactionPartInStorage: (messageID: string | undefined) => { + return messageID === compactionMessageID + }, + }) // then expect(result).toBe("sisyphus-junior") diff --git a/src/hooks/atlas/session-last-agent.sqlite.test.ts b/src/hooks/atlas/session-last-agent.sqlite.test.ts index 5ae770298..5c54ce6e7 100644 --- a/src/hooks/atlas/session-last-agent.sqlite.test.ts +++ b/src/hooks/atlas/session-last-agent.sqlite.test.ts @@ -1,11 +1,5 @@ export {} -const { describe, expect, mock, test, afterAll } = require("bun:test") - -mock.module("../../shared/opencode-storage-detection", () => ({ - isSqliteBackend: () => true, -})) - -afterAll(() => { mock.restore() }) +const { describe, expect, test } = require("bun:test") const { getLastAgentFromSession } = await import("./session-last-agent") @@ -25,7 +19,9 @@ describe("getLastAgentFromSession SQLite backend ordering", () => { } // when - const result = await getLastAgentFromSession("ses_sqlite_last_agent", client as never) + const result = await getLastAgentFromSession("ses_sqlite_last_agent", client as never, { + isSqliteBackend: () => true, + }) // then expect(result).toBe("sisyphus-junior") @@ -46,7 +42,9 @@ describe("getLastAgentFromSession SQLite backend ordering", () => { } // when - const result = await getLastAgentFromSession("ses_sqlite_last_agent_equal_time", client as never) + const result = await getLastAgentFromSession("ses_sqlite_last_agent_equal_time", client as never, { + isSqliteBackend: () => true, + }) // then expect(result).toBe("sisyphus-junior") @@ -70,7 +68,9 @@ describe("getLastAgentFromSession SQLite backend ordering", () => { } // when - const result = await getLastAgentFromSession("ses_sqlite_compaction_marker", client as never) + const result = await getLastAgentFromSession("ses_sqlite_compaction_marker", client as never, { + isSqliteBackend: () => true, + }) // then expect(result).toBe("sisyphus") @@ -87,7 +87,9 @@ describe("getLastAgentFromSession SQLite backend ordering", () => { } // when - const result = await getLastAgentFromSession("ses_sqlite_error", client as never) + const result = await getLastAgentFromSession("ses_sqlite_error", client as never, { + isSqliteBackend: () => true, + }) // then expect(result).toBeNull() diff --git a/src/hooks/auto-slash-command/index.test.ts b/src/hooks/auto-slash-command/index.test.ts index 75984d8d7..543341b0b 100644 --- a/src/hooks/auto-slash-command/index.test.ts +++ b/src/hooks/auto-slash-command/index.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeEach, afterEach, spyOn } from "bun:test" +import { describe, expect, it, beforeEach, afterEach, spyOn, mock } from "bun:test" import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" @@ -13,12 +13,7 @@ import type { // Import real shared module to avoid mock leaking to other test files import * as shared from "../../shared" -// Spy on log instead of mocking the entire module -const logMock = spyOn(shared, "log").mockImplementation(() => {}) - - - -const { createAutoSlashCommandHook } = await import("./index") +type AutoSlashCommandModule = typeof import("./hook") function createMockInput(sessionID: string, messageID?: string): AutoSlashCommandHookInput { return { @@ -44,16 +39,26 @@ function createMockOutput(text: string): AutoSlashCommandHookOutput { describe("createAutoSlashCommandHook", () => { let tempDir = "" let originalWorkingDirectory = "" + let logCalls: Array<[string, unknown?]> + let createAutoSlashCommandHook: AutoSlashCommandModule["createAutoSlashCommandHook"] - beforeEach(() => { - logMock.mockClear() + beforeEach(async () => { + mock.restore() + logCalls = [] + spyOn(shared, "log").mockImplementation((message: string, data?: unknown) => { + logCalls.push([message, data]) + }) tempDir = mkdtempSync(join(tmpdir(), "omo-auto-slash-hook-test-")) originalWorkingDirectory = process.cwd() + + const autoSlashCommandModule = await import(`./hook?test=${Date.now()}-${Math.random()}`) + createAutoSlashCommandHook = autoSlashCommandModule.createAutoSlashCommandHook }) afterEach(() => { process.chdir(originalWorkingDirectory) rmSync(tempDir, { recursive: true, force: true }) + mock.restore() }) describe("slash command replacement", () => { @@ -237,7 +242,7 @@ describe("createAutoSlashCommandHook", () => { // when hook is called // then should not throw - await expect(hook["chat.message"](input, output)).resolves.toBeUndefined() + await hook["chat.message"](input, output) }) it("should handle just slash", async () => { @@ -374,13 +379,13 @@ describe("createAutoSlashCommandHook", () => { await hook["command.execute.before"](input, output) //#then - expect(logMock).toHaveBeenCalledWith( + expect(logCalls).toContainEqual([ "[auto-slash-command] command.execute.before received", expect.objectContaining({ command: "some-command", arguments: "arg1 arg2 arg3", - }) - ) + }), + ]) }) }) diff --git a/src/hooks/auto-slash-command/leak/auto-slash-command-leak.test.ts b/src/hooks/auto-slash-command/leak/auto-slash-command-leak.test.ts index 15198f174..a32d7b235 100644 --- a/src/hooks/auto-slash-command/leak/auto-slash-command-leak.test.ts +++ b/src/hooks/auto-slash-command/leak/auto-slash-command-leak.test.ts @@ -7,6 +7,9 @@ import type { CommandExecuteBeforeOutput, } from "../types" import * as shared from "../../../shared" +import * as executorModule from "../executor" + +type AutoSlashCommandModule = typeof import("../hook") const executeSlashCommandMock = mock( async (parsed: { command: string; args: string; raw: string }) => ({ @@ -15,21 +18,11 @@ const executeSlashCommandMock = mock( }) ) -mock.module("../executor", () => ({ - executeSlashCommand: executeSlashCommandMock, -})) - afterAll(async () => { mock.restore() - // Restore the real executor module so subsequent test files in the same batch - // (e.g. executor-resolution.test.ts) don't get the mocked version - const realExecutor = await import("../executor") - mock.module("../executor", () => realExecutor) }) -const logMock = spyOn(shared, "log").mockImplementation(() => {}) - -const { createAutoSlashCommandHook } = await import("../hook") +let createAutoSlashCommandHook: AutoSlashCommandModule["createAutoSlashCommandHook"] function createChatInput(sessionID: string, messageID: string): AutoSlashCommandHookInput { return { @@ -60,9 +53,14 @@ function createCommandOutput(text: string): CommandExecuteBeforeOutput { } describe("createAutoSlashCommandHook leak prevention", () => { - beforeEach(() => { + beforeEach(async () => { + mock.restore() executeSlashCommandMock.mockClear() - logMock.mockClear() + spyOn(executorModule, "executeSlashCommand").mockImplementation(executeSlashCommandMock) + spyOn(shared, "log").mockImplementation(() => {}) + + const autoSlashCommandModule = await import(`../hook?test=${Date.now()}-${Math.random()}`) + createAutoSlashCommandHook = autoSlashCommandModule.createAutoSlashCommandHook }) describe("#given hook with sessionProcessedCommandExecutions", () => { diff --git a/src/hooks/keyword-detector/index.test.ts b/src/hooks/keyword-detector/index.test.ts index 083c6b4f3..c54ff1c6e 100644 --- a/src/hooks/keyword-detector/index.test.ts +++ b/src/hooks/keyword-detector/index.test.ts @@ -146,8 +146,8 @@ describe("keyword-detector session filtering", () => { ) // then - search keyword should be filtered out based on mainSessionID comparison - const skipLog = logCalls.find(c => c.msg.includes("Skipping non-ultrawork keywords in non-main session")) - expect(skipLog).toBeDefined() + expect(output.message.variant).toBeUndefined() + expect(output.parts[0]?.text).toBe("search mode 찾아줘") }) test("should allow ultrawork keywords in non-main session", async () => { diff --git a/src/hooks/legacy-plugin-toast/auto-migrate.test.ts b/src/hooks/legacy-plugin-toast/auto-migrate.test.ts index b38686019..1ef64e108 100644 --- a/src/hooks/legacy-plugin-toast/auto-migrate.test.ts +++ b/src/hooks/legacy-plugin-toast/auto-migrate.test.ts @@ -8,10 +8,11 @@ const mockMigrateLegacyPluginEntry = mock(() => true) mock.module("./plugin-entry-migrator", () => ({ migrateLegacyPluginEntry: mockMigrateLegacyPluginEntry, })) +mock.module("./plugin-entry-migrator.ts", () => ({ + migrateLegacyPluginEntry: mockMigrateLegacyPluginEntry, +})) -async function importFreshAutoMigrateModule(): Promise { - return import(`./auto-migrate?test=${Date.now()}-${Math.random()}`) -} +const autoMigrateModulePromise = import("./auto-migrate") describe("autoMigrateLegacyPluginEntry", () => { let testConfigDir = "" @@ -35,7 +36,7 @@ describe("autoMigrateLegacyPluginEntry", () => { JSON.stringify({ plugin: ["oh-my-opencode"] }, null, 2) + "\n", ) - const { autoMigrateLegacyPluginEntry } = await importFreshAutoMigrateModule() + const { autoMigrateLegacyPluginEntry } = await autoMigrateModulePromise // when const result = autoMigrateLegacyPluginEntry(testConfigDir) @@ -56,7 +57,7 @@ describe("autoMigrateLegacyPluginEntry", () => { JSON.stringify({ plugin: ["oh-my-opencode@3.10.0"] }, null, 2) + "\n", ) - const { autoMigrateLegacyPluginEntry } = await importFreshAutoMigrateModule() + const { autoMigrateLegacyPluginEntry } = await autoMigrateModulePromise // when const result = autoMigrateLegacyPluginEntry(testConfigDir) @@ -77,7 +78,7 @@ describe("autoMigrateLegacyPluginEntry", () => { JSON.stringify({ plugin: ["oh-my-openagent", "oh-my-opencode"] }, null, 2) + "\n", ) - const { autoMigrateLegacyPluginEntry } = await importFreshAutoMigrateModule() + const { autoMigrateLegacyPluginEntry } = await autoMigrateModulePromise // when const result = autoMigrateLegacyPluginEntry(testConfigDir) @@ -92,7 +93,7 @@ describe("autoMigrateLegacyPluginEntry", () => { describe("#given no config file exists", () => { it("#then returns migrated false", async () => { // given - empty dir - const { autoMigrateLegacyPluginEntry } = await importFreshAutoMigrateModule() + const { autoMigrateLegacyPluginEntry } = await autoMigrateModulePromise // when const result = autoMigrateLegacyPluginEntry(testConfigDir) @@ -112,7 +113,7 @@ describe("autoMigrateLegacyPluginEntry", () => { '{\n // my config\n "plugin": ["oh-my-opencode"]\n}\n', ) - const { autoMigrateLegacyPluginEntry } = await importFreshAutoMigrateModule() + const { autoMigrateLegacyPluginEntry } = await autoMigrateModulePromise // when const result = autoMigrateLegacyPluginEntry(testConfigDir) @@ -138,7 +139,7 @@ describe("autoMigrateLegacyPluginEntry", () => { `, ) - const { autoMigrateLegacyPluginEntry } = await importFreshAutoMigrateModule() + const { autoMigrateLegacyPluginEntry } = await autoMigrateModulePromise // when const result = autoMigrateLegacyPluginEntry(testConfigDir) @@ -157,7 +158,7 @@ describe("autoMigrateLegacyPluginEntry", () => { const original = JSON.stringify({ plugin: ["oh-my-openagent"] }, null, 2) + "\n" writeFileSync(join(testConfigDir, "opencode.json"), original) - const { autoMigrateLegacyPluginEntry } = await importFreshAutoMigrateModule() + const { autoMigrateLegacyPluginEntry } = await autoMigrateModulePromise // when const result = autoMigrateLegacyPluginEntry(testConfigDir) diff --git a/src/hooks/runtime-fallback/index.test.ts b/src/hooks/runtime-fallback/index.test.ts index df4f7cd3c..8c9ad6aa4 100644 --- a/src/hooks/runtime-fallback/index.test.ts +++ b/src/hooks/runtime-fallback/index.test.ts @@ -1,26 +1,37 @@ -import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test" -import { createRuntimeFallbackHook } from "./index" +import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test" import type { RuntimeFallbackConfig, OhMyOpenCodeConfig } from "../../config" -import * as sharedModule from "../../shared" +import * as loggerModule from "../../shared/logger" import { SessionCategoryRegistry } from "../../shared/session-category-registry" +type RuntimeFallbackModule = typeof import("./hook") + describe("runtime-fallback", () => { let logCalls: Array<{ msg: string; data?: unknown }> - let logSpy: ReturnType let toastCalls: Array<{ title: string; message: string; variant: string }> + let createRuntimeFallbackHook: RuntimeFallbackModule["createRuntimeFallbackHook"] - beforeEach(() => { + beforeEach(async () => { + mock.restore() logCalls = [] toastCalls = [] SessionCategoryRegistry.clear() - logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => { - logCalls.push({ msg, data }) - }) + + const cacheBuster = `${Date.now()}-${Math.random()}` + + mock.module("../../shared/logger", () => ({ + ...loggerModule, + log: (msg: string, data?: unknown) => { + logCalls.push({ msg, data }) + }, + })) + + const runtimeFallbackModule: RuntimeFallbackModule = await import(`./hook?test=${cacheBuster}`) + createRuntimeFallbackHook = runtimeFallbackModule.createRuntimeFallbackHook }) afterEach(() => { SessionCategoryRegistry.clear() - logSpy?.mockRestore() + mock.restore() }) function createMockPluginInput(overrides?: { diff --git a/src/hooks/session-recovery/index.test.ts b/src/hooks/session-recovery/index.test.ts index 1cb3768d9..c5316fbed 100644 --- a/src/hooks/session-recovery/index.test.ts +++ b/src/hooks/session-recovery/index.test.ts @@ -1,10 +1,24 @@ import { existsSync, readFileSync, rmSync } from "node:fs" +import { randomUUID } from "node:crypto" +import { tmpdir } from "node:os" import { join } from "node:path" +import { describe, expect, it, mock } from "bun:test" import { detectErrorType } from "./index" -import { prependThinkingPart, prependThinkingPartAsync } from "./storage/thinking-prepend" -import { PART_STORAGE } from "../../shared/opencode-storage-paths" -const { describe, expect, it, mock } = require("bun:test") +const TEST_STORAGE_ROOT = join(tmpdir(), `session-recovery-thinking-prepend-${randomUUID()}`) +const TEST_PART_STORAGE = join(TEST_STORAGE_ROOT, "part") + +mock.module("../../shared", () => ({ + OPENCODE_STORAGE: TEST_STORAGE_ROOT, + MESSAGE_STORAGE: join(TEST_STORAGE_ROOT, "message"), + PART_STORAGE: TEST_PART_STORAGE, + log: () => {}, + isSqliteBackend: () => false, + patchPart: async () => true, + normalizeSDKResponse: (response: { data?: TData }, fallback: TData) => response.data ?? fallback, +})) + +const { prependThinkingPart, prependThinkingPartAsync } = await import("./storage/thinking-prepend") describe("detectErrorType", () => { describe("thinking_block_order errors", () => { @@ -295,7 +309,7 @@ type StoredPartRecord = { } function cleanupParts(messageID: string): void { - rmSync(join(PART_STORAGE, messageID), { recursive: true, force: true }) + rmSync(join(TEST_PART_STORAGE, messageID), { recursive: true, force: true }) } describe("thinking-prepend", () => { @@ -322,7 +336,7 @@ describe("thinking-prepend", () => { }) expect(result).toBe(true) - const writtenPath = join(PART_STORAGE, targetMessageID, `${originalPart.id}.json`) + const writtenPath = join(TEST_PART_STORAGE, targetMessageID, `${originalPart.id}.json`) expect(existsSync(writtenPath)).toBe(true) expect(JSON.parse(readFileSync(writtenPath, "utf-8"))).toEqual(originalPart) @@ -344,7 +358,7 @@ describe("thinking-prepend", () => { }) expect(result).toBe(false) - expect(existsSync(join(PART_STORAGE, targetMessageID))).toBe(false) + expect(existsSync(join(TEST_PART_STORAGE, targetMessageID))).toBe(false) cleanupParts(targetMessageID) }) @@ -386,7 +400,7 @@ describe("thinking-prepend", () => { }) expect(result).toBe(false) - expect(existsSync(join(PART_STORAGE, targetMessageID))).toBe(false) + expect(existsSync(join(TEST_PART_STORAGE, targetMessageID))).toBe(false) }) it("patches the original signed thinking part verbatim for sdk-backed recovery", async () => { diff --git a/src/hooks/session-recovery/recover-tool-result-missing.test.ts b/src/hooks/session-recovery/recover-tool-result-missing.test.ts index 09d557f84..a720ef079 100644 --- a/src/hooks/session-recovery/recover-tool-result-missing.test.ts +++ b/src/hooks/session-recovery/recover-tool-result-missing.test.ts @@ -1,13 +1,20 @@ -const { describe, it, expect, mock, beforeEach, afterEach, spyOn } = require("bun:test") +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" import type { MessageData } from "./types" -import * as storageDetection from "../../shared/opencode-storage-detection" -import * as storage from "./storage" -import { recoverToolResultMissing } from "./recover-tool-result-missing" let sqliteBackend = false let storedParts: Array<{ type: string; id?: string; callID?: string; [key: string]: unknown }> = [] +mock.module("../../shared/opencode-storage-detection", () => ({ + isSqliteBackend: () => sqliteBackend, +})) + +mock.module("./storage", () => ({ + readParts: () => storedParts, +})) + +const { recoverToolResultMissing } = await import("./recover-tool-result-missing") + const failedAssistantMsg: MessageData = { info: { id: "msg_failed", role: "assistant" }, parts: [], @@ -31,9 +38,6 @@ describe("recoverToolResultMissing", () => { beforeEach(() => { sqliteBackend = false storedParts = [] - - spyOn(storageDetection, "isSqliteBackend").mockImplementation(() => sqliteBackend) - spyOn(storage, "readParts").mockImplementation(() => storedParts) }) afterEach(() => { diff --git a/src/hooks/zauc-mocks-hook/hook.test.ts b/src/hooks/zauc-mocks-hook/hook.test.ts index c6fab5c11..2c291b1f0 100644 --- a/src/hooks/zauc-mocks-hook/hook.test.ts +++ b/src/hooks/zauc-mocks-hook/hook.test.ts @@ -1,4 +1,5 @@ -import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { createAutoUpdateCheckerHook } from "../auto-update-checker/hook" const mockShowConfigErrorsIfAny = mock(async () => {}) const mockShowModelCacheWarningIfNeeded = mock(async () => {}) @@ -10,60 +11,6 @@ const mockRunBackgroundUpdateCheck = mock(async () => {}) const mockGetCachedVersion = mock(() => "3.6.0") const mockGetLocalDevVersion = mock<(directory: string) => string | null>(() => null) -const _realConfigErrorsToast = require("../auto-update-checker/hook/config-errors-toast") -const _realModelCacheWarning = require("../auto-update-checker/hook/model-cache-warning") -const _realConnectedProvidersStatus = require("../auto-update-checker/hook/connected-providers-status") -const _realModelCapabilitiesStatus = require("../auto-update-checker/hook/model-capabilities-status") -const _realStartupToasts = require("../auto-update-checker/hook/startup-toasts") -const _realBackgroundUpdateCheck = require("../auto-update-checker/hook/background-update-check") -const _realChecker = require("../auto-update-checker/checker") -const _realLogger = require("../../shared/logger") - -afterAll(() => { - mock.module("../auto-update-checker/hook/config-errors-toast", () => _realConfigErrorsToast) - mock.module("../auto-update-checker/hook/model-cache-warning", () => _realModelCacheWarning) - mock.module("../auto-update-checker/hook/connected-providers-status", () => _realConnectedProvidersStatus) - mock.module("../auto-update-checker/hook/model-capabilities-status", () => _realModelCapabilitiesStatus) - mock.module("../auto-update-checker/hook/startup-toasts", () => _realStartupToasts) - mock.module("../auto-update-checker/hook/background-update-check", () => _realBackgroundUpdateCheck) - mock.module("../auto-update-checker/checker", () => _realChecker) - mock.module("../../shared/logger", () => _realLogger) - mock.restore() -}) - -type HookFactory = typeof import("../auto-update-checker/hook").createAutoUpdateCheckerHook - -async function importFreshHookFactory(): Promise { - mock.module("../auto-update-checker/hook/config-errors-toast", () => ({ - showConfigErrorsIfAny: mockShowConfigErrorsIfAny, - })) - mock.module("../auto-update-checker/hook/model-cache-warning", () => ({ - showModelCacheWarningIfNeeded: mockShowModelCacheWarningIfNeeded, - })) - mock.module("../auto-update-checker/hook/connected-providers-status", () => ({ - updateAndShowConnectedProvidersCacheStatus: mockUpdateAndShowConnectedProvidersCacheStatus, - })) - mock.module("../auto-update-checker/hook/model-capabilities-status", () => ({ - refreshModelCapabilitiesOnStartup: mockRefreshModelCapabilitiesOnStartup, - })) - mock.module("../auto-update-checker/hook/startup-toasts", () => ({ - showLocalDevToast: mockShowLocalDevToast, - showVersionToast: mockShowVersionToast, - })) - mock.module("../auto-update-checker/hook/background-update-check", () => ({ - runBackgroundUpdateCheck: mockRunBackgroundUpdateCheck, - })) - mock.module("../auto-update-checker/checker", () => ({ - getCachedVersion: mockGetCachedVersion, - getLocalDevVersion: mockGetLocalDevVersion, - })) - mock.module("../../shared/logger", () => ({ - log: () => {}, - })) - const hookModule = await import(`../auto-update-checker/hook?test-${Date.now()}-${Math.random()}`) - return hookModule.createAutoUpdateCheckerHook -} - function createPluginInput() { return { directory: "/test", @@ -80,7 +27,7 @@ async function flushScheduledWork(): Promise { } function runSessionCreatedEvent( - hook: ReturnType, + hook: ReturnType, properties?: { info?: { parentID?: string } } ): void { hook.event({ @@ -114,12 +61,22 @@ describe("createAutoUpdateCheckerHook", () => { it("skips startup toasts and checks in CLI run mode", async () => { //#given - CLI run mode enabled process.env.OPENCODE_CLI_RUN_MODE = "true" - const createAutoUpdateCheckerHook = await importFreshHookFactory() const hook = createAutoUpdateCheckerHook(createPluginInput(), { showStartupToast: true, isSisyphusEnabled: true, autoUpdate: true, + }, { + getCachedVersion: mockGetCachedVersion, + getLocalDevVersion: mockGetLocalDevVersion, + showConfigErrorsIfAny: mockShowConfigErrorsIfAny, + updateAndShowConnectedProvidersCacheStatus: mockUpdateAndShowConnectedProvidersCacheStatus, + refreshModelCapabilitiesOnStartup: mockRefreshModelCapabilitiesOnStartup, + showModelCacheWarningIfNeeded: mockShowModelCacheWarningIfNeeded, + showLocalDevToast: mockShowLocalDevToast, + showVersionToast: mockShowVersionToast, + runBackgroundUpdateCheck: mockRunBackgroundUpdateCheck, + log: () => {}, }) //#when - session.created event arrives @@ -138,8 +95,18 @@ describe("createAutoUpdateCheckerHook", () => { it("runs all startup checks on normal session.created", async () => { //#given - normal mode and no local dev version - const createAutoUpdateCheckerHook = await importFreshHookFactory() - const hook = createAutoUpdateCheckerHook(createPluginInput()) + const hook = createAutoUpdateCheckerHook(createPluginInput(), {}, { + getCachedVersion: mockGetCachedVersion, + getLocalDevVersion: mockGetLocalDevVersion, + showConfigErrorsIfAny: mockShowConfigErrorsIfAny, + updateAndShowConnectedProvidersCacheStatus: mockUpdateAndShowConnectedProvidersCacheStatus, + refreshModelCapabilitiesOnStartup: mockRefreshModelCapabilitiesOnStartup, + showModelCacheWarningIfNeeded: mockShowModelCacheWarningIfNeeded, + showLocalDevToast: mockShowLocalDevToast, + showVersionToast: mockShowVersionToast, + runBackgroundUpdateCheck: mockRunBackgroundUpdateCheck, + log: () => {}, + }) //#when - session.created event arrives on primary session runSessionCreatedEvent(hook) @@ -156,8 +123,18 @@ describe("createAutoUpdateCheckerHook", () => { it("ignores subagent sessions (parentID present)", async () => { //#given - a subagent session with parentID - const createAutoUpdateCheckerHook = await importFreshHookFactory() - const hook = createAutoUpdateCheckerHook(createPluginInput()) + const hook = createAutoUpdateCheckerHook(createPluginInput(), {}, { + getCachedVersion: mockGetCachedVersion, + getLocalDevVersion: mockGetLocalDevVersion, + showConfigErrorsIfAny: mockShowConfigErrorsIfAny, + updateAndShowConnectedProvidersCacheStatus: mockUpdateAndShowConnectedProvidersCacheStatus, + refreshModelCapabilitiesOnStartup: mockRefreshModelCapabilitiesOnStartup, + showModelCacheWarningIfNeeded: mockShowModelCacheWarningIfNeeded, + showLocalDevToast: mockShowLocalDevToast, + showVersionToast: mockShowVersionToast, + runBackgroundUpdateCheck: mockRunBackgroundUpdateCheck, + log: () => {}, + }) //#when - session.created event contains parentID runSessionCreatedEvent(hook, { info: { parentID: "parent-123" } }) @@ -175,8 +152,18 @@ describe("createAutoUpdateCheckerHook", () => { it("runs only once (hasChecked guard)", async () => { //#given - one hook instance in normal mode - const createAutoUpdateCheckerHook = await importFreshHookFactory() - const hook = createAutoUpdateCheckerHook(createPluginInput()) + const hook = createAutoUpdateCheckerHook(createPluginInput(), {}, { + getCachedVersion: mockGetCachedVersion, + getLocalDevVersion: mockGetLocalDevVersion, + showConfigErrorsIfAny: mockShowConfigErrorsIfAny, + updateAndShowConnectedProvidersCacheStatus: mockUpdateAndShowConnectedProvidersCacheStatus, + refreshModelCapabilitiesOnStartup: mockRefreshModelCapabilitiesOnStartup, + showModelCacheWarningIfNeeded: mockShowModelCacheWarningIfNeeded, + showLocalDevToast: mockShowLocalDevToast, + showVersionToast: mockShowVersionToast, + runBackgroundUpdateCheck: mockRunBackgroundUpdateCheck, + log: () => {}, + }) //#when - session.created event is fired twice runSessionCreatedEvent(hook) @@ -195,8 +182,18 @@ describe("createAutoUpdateCheckerHook", () => { it("shows localDevToast when local dev version exists", async () => { //#given - local dev version is present mockGetLocalDevVersion.mockReturnValue("3.6.0-dev") - const createAutoUpdateCheckerHook = await importFreshHookFactory() - const hook = createAutoUpdateCheckerHook(createPluginInput()) + const hook = createAutoUpdateCheckerHook(createPluginInput(), {}, { + getCachedVersion: mockGetCachedVersion, + getLocalDevVersion: mockGetLocalDevVersion, + showConfigErrorsIfAny: mockShowConfigErrorsIfAny, + updateAndShowConnectedProvidersCacheStatus: mockUpdateAndShowConnectedProvidersCacheStatus, + refreshModelCapabilitiesOnStartup: mockRefreshModelCapabilitiesOnStartup, + showModelCacheWarningIfNeeded: mockShowModelCacheWarningIfNeeded, + showLocalDevToast: mockShowLocalDevToast, + showVersionToast: mockShowVersionToast, + runBackgroundUpdateCheck: mockRunBackgroundUpdateCheck, + log: () => {}, + }) //#when - session.created event arrives runSessionCreatedEvent(hook) @@ -214,8 +211,18 @@ describe("createAutoUpdateCheckerHook", () => { it("ignores non-session.created events", async () => { //#given - a hook instance in normal mode - const createAutoUpdateCheckerHook = await importFreshHookFactory() - const hook = createAutoUpdateCheckerHook(createPluginInput()) + const hook = createAutoUpdateCheckerHook(createPluginInput(), {}, { + getCachedVersion: mockGetCachedVersion, + getLocalDevVersion: mockGetLocalDevVersion, + showConfigErrorsIfAny: mockShowConfigErrorsIfAny, + updateAndShowConnectedProvidersCacheStatus: mockUpdateAndShowConnectedProvidersCacheStatus, + refreshModelCapabilitiesOnStartup: mockRefreshModelCapabilitiesOnStartup, + showModelCacheWarningIfNeeded: mockShowModelCacheWarningIfNeeded, + showLocalDevToast: mockShowLocalDevToast, + showVersionToast: mockShowVersionToast, + runBackgroundUpdateCheck: mockRunBackgroundUpdateCheck, + log: () => {}, + }) //#when - a non-session.created event arrives hook.event({ @@ -237,9 +244,19 @@ describe("createAutoUpdateCheckerHook", () => { it("passes correct toast message with sisyphus enabled", async () => { //#given - sisyphus mode enabled - const createAutoUpdateCheckerHook = await importFreshHookFactory() const hook = createAutoUpdateCheckerHook(createPluginInput(), { isSisyphusEnabled: true, + }, { + getCachedVersion: mockGetCachedVersion, + getLocalDevVersion: mockGetLocalDevVersion, + showConfigErrorsIfAny: mockShowConfigErrorsIfAny, + updateAndShowConnectedProvidersCacheStatus: mockUpdateAndShowConnectedProvidersCacheStatus, + refreshModelCapabilitiesOnStartup: mockRefreshModelCapabilitiesOnStartup, + showModelCacheWarningIfNeeded: mockShowModelCacheWarningIfNeeded, + showLocalDevToast: mockShowLocalDevToast, + showVersionToast: mockShowVersionToast, + runBackgroundUpdateCheck: mockRunBackgroundUpdateCheck, + log: () => {}, }) //#when - session.created event arrives diff --git a/src/hooks/zauc-sync-mocks/sync-package-json.test.ts b/src/hooks/zauc-sync-mocks/sync-package-json.test.ts index d94ce936f..acb2abefb 100644 --- a/src/hooks/zauc-sync-mocks/sync-package-json.test.ts +++ b/src/hooks/zauc-sync-mocks/sync-package-json.test.ts @@ -2,59 +2,40 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs" import { join } from "node:path" import type { PluginEntryInfo } from "../auto-update-checker/checker/plugin-entry" +import { CACHE_DIR } from "../auto-update-checker/constants" -const TEST_CACHE_DIR = join(import.meta.dir, "__test-sync-cache__") +const CACHE_PACKAGES_DIR = CACHE_DIR +const CACHE_PACKAGE_JSON_PATH = join(CACHE_PACKAGES_DIR, "package.json") +const ORIGINAL_CACHE_PACKAGE_JSON = existsSync(CACHE_PACKAGE_JSON_PATH) + ? readFileSync(CACHE_PACKAGE_JSON_PATH, "utf-8") + : null let importCounter = 0 -// Capture real modules BEFORE mocking -const _realConstants = require("../auto-update-checker/constants") -const _realLogger = require("../../shared/logger") -const _realNodeFs = require("node:fs") - async function importFreshSyncPackageJsonModule(): Promise { - mock.module("../auto-update-checker/constants", () => ({ - CACHE_DIR: TEST_CACHE_DIR, - PACKAGE_NAME: "oh-my-opencode", - NPM_REGISTRY_URL: "https://registry.npmjs.org/-/package/oh-my-opencode/dist-tags", - NPM_FETCH_TIMEOUT: 5000, - VERSION_FILE: join(TEST_CACHE_DIR, "version"), - INSTALLED_PACKAGE_JSON: join(TEST_CACHE_DIR, "node_modules", "oh-my-opencode", "package.json"), - getUserConfigDir: () => "/tmp/opencode-config", - getUserOpencodeConfig: () => "/tmp/opencode-config/opencode.json", - getUserOpencodeConfigJsonc: () => "/tmp/opencode-config/opencode.jsonc", - getWindowsAppdataDir: () => null, - })) - mock.module("../../shared/logger", () => ({ log: () => {}, })) - const syncPackageJsonModule = await import(`../auto-update-checker/checker/sync-package-json?test=${importCounter++}`) - mock.restore() - return syncPackageJsonModule + return import(`../auto-update-checker/checker/sync-package-json?test=${importCounter++}`) } function resetTestCache(currentVersion = "3.10.0"): void { - if (existsSync(TEST_CACHE_DIR)) { - rmSync(TEST_CACHE_DIR, { recursive: true, force: true }) - } - - mkdirSync(TEST_CACHE_DIR, { recursive: true }) + mkdirSync(CACHE_PACKAGES_DIR, { recursive: true }) writeFileSync( - join(TEST_CACHE_DIR, "package.json"), + CACHE_PACKAGE_JSON_PATH, JSON.stringify({ dependencies: { "oh-my-opencode": currentVersion, other: "1.0.0" } }, null, 2) ) } function cleanupTestCache(): void { - if (existsSync(TEST_CACHE_DIR)) { - rmSync(TEST_CACHE_DIR, { recursive: true, force: true }) + if (existsSync(CACHE_PACKAGE_JSON_PATH)) { + rmSync(CACHE_PACKAGE_JSON_PATH, { force: true }) } } function readCachePackageJsonVersion(): string | undefined { - const content = readFileSync(join(TEST_CACHE_DIR, "package.json"), "utf-8") + const content = readFileSync(CACHE_PACKAGE_JSON_PATH, "utf-8") const pkg = JSON.parse(content) as { dependencies?: Record } return pkg.dependencies?.["oh-my-opencode"] } @@ -65,6 +46,7 @@ describe("syncCachePackageJsonToIntent", () => { }) afterEach(() => { + mock.restore() cleanupTestCache() }) @@ -170,9 +152,9 @@ describe("syncCachePackageJsonToIntent", () => { describe("#given plugin not in cache package.json dependencies", () => { it("#then adds the plugin dependency and preserves existing dependencies", async () => { cleanupTestCache() - mkdirSync(TEST_CACHE_DIR, { recursive: true }) + mkdirSync(CACHE_PACKAGES_DIR, { recursive: true }) writeFileSync( - join(TEST_CACHE_DIR, "package.json"), + join(CACHE_PACKAGES_DIR, "package.json"), JSON.stringify({ dependencies: { other: "1.0.0" } }, null, 2) ) @@ -190,10 +172,10 @@ describe("syncCachePackageJsonToIntent", () => { expect(result.synced).toBe(true) expect(result.error).toBeNull() - const content = readFileSync(join(TEST_CACHE_DIR, "package.json"), "utf-8") - const pkg = JSON.parse(content) as { dependencies?: Record } - expect(pkg.dependencies?.["oh-my-opencode"]).toBe("latest") - expect(pkg.dependencies?.other).toBe("1.0.0") + const content = readFileSync(join(CACHE_PACKAGES_DIR, "package.json"), "utf-8") + const pkg = JSON.parse(content) as { dependencies?: Record } + expect(pkg.dependencies?.["oh-my-opencode"]).toBe("latest") + expect(pkg.dependencies?.other).toBe("1.0.0") }) }) @@ -233,17 +215,17 @@ describe("syncCachePackageJsonToIntent", () => { expect(result.synced).toBe(true) expect(result.error).toBeNull() - const content = readFileSync(join(TEST_CACHE_DIR, "package.json"), "utf-8") - const pkg = JSON.parse(content) as { dependencies?: Record } - expect(pkg.dependencies?.["other"]).toBe("1.0.0") + const content = readFileSync(join(CACHE_PACKAGES_DIR, "package.json"), "utf-8") + const pkg = JSON.parse(content) as { dependencies?: Record } + expect(pkg.dependencies?.["other"]).toBe("1.0.0") }) }) describe("#given malformed JSON in cache package.json", () => { it("#then returns parse_error", async () => { cleanupTestCache() - mkdirSync(TEST_CACHE_DIR, { recursive: true }) - writeFileSync(join(TEST_CACHE_DIR, "package.json"), "{ invalid json }") + mkdirSync(CACHE_PACKAGES_DIR, { recursive: true }) + writeFileSync(join(CACHE_PACKAGES_DIR, "package.json"), "{ invalid json }") const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() @@ -264,9 +246,9 @@ describe("syncCachePackageJsonToIntent", () => { describe("#given write permission denied", () => { it("#then returns write_error", async () => { cleanupTestCache() - mkdirSync(TEST_CACHE_DIR, { recursive: true }) + mkdirSync(CACHE_PACKAGES_DIR, { recursive: true }) writeFileSync( - join(TEST_CACHE_DIR, "package.json"), + join(CACHE_PACKAGES_DIR, "package.json"), JSON.stringify({ dependencies: { "oh-my-opencode": "3.10.0" } }, null, 2) ) @@ -309,9 +291,9 @@ describe("syncCachePackageJsonToIntent", () => { describe("#given rename fails after successful write", () => { it("#then returns write_error and cleans up temp file", async () => { cleanupTestCache() - mkdirSync(TEST_CACHE_DIR, { recursive: true }) + mkdirSync(CACHE_PACKAGES_DIR, { recursive: true }) writeFileSync( - join(TEST_CACHE_DIR, "package.json"), + join(CACHE_PACKAGES_DIR, "package.json"), JSON.stringify({ dependencies: { "oh-my-opencode": "3.10.0" } }, null, 2) ) @@ -360,8 +342,11 @@ describe("syncCachePackageJsonToIntent", () => { }) afterAll(() => { - mock.module("../auto-update-checker/constants", () => _realConstants) - mock.module("../../shared/logger", () => _realLogger) - mock.module("node:fs", () => _realNodeFs) + if (ORIGINAL_CACHE_PACKAGE_JSON === null) { + cleanupTestCache() + } else { + mkdirSync(CACHE_PACKAGES_DIR, { recursive: true }) + writeFileSync(CACHE_PACKAGE_JSON_PATH, ORIGINAL_CACHE_PACKAGE_JSON) + } mock.restore() })