test(hooks): update multiple hook test suites
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -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<typeof import("./message-builder")> {
|
||||
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({
|
||||
|
||||
@@ -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<typeof import("./storage")> {
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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<typeof import("./session-last-agent")> {
|
||||
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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
)
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<typeof import("./auto-migrate")> {
|
||||
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)
|
||||
|
||||
@@ -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<typeof spyOn>
|
||||
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?: {
|
||||
|
||||
@@ -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: <TData>(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 () => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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<HookFactory> {
|
||||
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<void> {
|
||||
}
|
||||
|
||||
function runSessionCreatedEvent(
|
||||
hook: ReturnType<HookFactory>,
|
||||
hook: ReturnType<typeof createAutoUpdateCheckerHook>,
|
||||
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
|
||||
|
||||
@@ -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<typeof import("../auto-update-checker/checker/sync-package-json")> {
|
||||
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<string, string> }
|
||||
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<string, string> }
|
||||
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<string, string> }
|
||||
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<string, string> }
|
||||
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<string, string> }
|
||||
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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user