Compare commits

...

4 Commits

Author SHA1 Message Date
YeonGyu-Kim
271929a9e4 ci: restore mock.module() overrides in afterAll to prevent cross-file pollution
Add afterAll hooks that restore original module implementations after
mock.module() overrides. This prevents mock state from leaking across
test files when bun runs them in the same process. Pattern: capture
original module with await import() before mocking, restore in afterAll.
2026-02-14 16:19:55 +09:00
YeonGyu-Kim
945329e261 fix: prevent node:fs mock pollution in directory injector tests
Move mock.module() calls from top-level to beforeEach and restore in
afterEach to prevent readFileSync mock from leaking into other test
files. Use dynamic import with cache-busting query to get fresh modules.
2026-02-14 16:19:40 +09:00
YeonGyu-Kim
f27733eae2 fix: correct test type casts, timeouts, and mock structures
- Fix PluginInput type casts to use 'as unknown as PluginInput'
- Add explicit TodoSnapshot type annotations
- Add timeouts to slow todo-continuation-enforcer tests
- Remove unnecessary test storage mocks in atlas and prometheus-md-only
- Restructure sync-executor mocks to use beforeEach/afterEach pattern
2026-02-14 16:19:29 +09:00
YeonGyu-Kim
e9c9cb696d fix: resolve symlinks in skill config source discovery and test paths
Use fs.realpath() in config-source-discovery to resolve symlinks before
loading skills, preventing duplicate/mismatched paths on systems where
tmpdir() returns a symlink (e.g., macOS /var → /private/var). Also adds
agents-config-dir utility for ~/.agents path resolution.
2026-02-14 16:19:18 +09:00
30 changed files with 347 additions and 107 deletions

View File

@@ -1,6 +1,10 @@
import { afterEach, describe, expect, it, mock } from "bun:test" import { afterEach, afterAll, describe, expect, it, mock } from "bun:test"
import type { DoctorResult } from "./types" import type { DoctorResult } from "./types"
const realFormatDefault = await import("./format-default")
const realFormatStatus = await import("./format-status")
const realFormatVerbose = await import("./format-verbose")
function createDoctorResult(): DoctorResult { function createDoctorResult(): DoctorResult {
return { return {
results: [ results: [
@@ -44,6 +48,12 @@ describe("formatter", () => {
mock.restore() mock.restore()
}) })
afterAll(() => {
mock.module("./format-default", () => ({ ...realFormatDefault }))
mock.module("./format-status", () => ({ ...realFormatStatus }))
mock.module("./format-verbose", () => ({ ...realFormatVerbose }))
})
describe("formatDoctorOutput", () => { describe("formatDoctorOutput", () => {
it("dispatches to default formatter for default mode", async () => { it("dispatches to default formatter for default mode", async () => {
//#given //#given

View File

@@ -1,6 +1,9 @@
import { afterEach, describe, expect, it, mock } from "bun:test" import { afterEach, afterAll, describe, expect, it, mock } from "bun:test"
import type { CheckDefinition, CheckResult, DoctorResult, SystemInfo, ToolsSummary } from "./types" import type { CheckDefinition, CheckResult, DoctorResult, SystemInfo, ToolsSummary } from "./types"
const realChecks = await import("./checks")
const realFormatter = await import("./formatter")
function createSystemInfo(): SystemInfo { function createSystemInfo(): SystemInfo {
return { return {
opencodeVersion: "1.0.200", opencodeVersion: "1.0.200",
@@ -47,6 +50,11 @@ describe("runner", () => {
mock.restore() mock.restore()
}) })
afterAll(() => {
mock.module("./checks", () => ({ ...realChecks }))
mock.module("./formatter", () => ({ ...realFormatter }))
})
describe("runCheck", () => { describe("runCheck", () => {
it("returns fail result with issue when check throws", async () => { it("returns fail result with issue when check throws", async () => {
//#given //#given

View File

@@ -1,4 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test" import { describe, it, expect, beforeEach, afterEach, afterAll, mock } from "bun:test"
const realMcpOauthProvider = await import("../../features/mcp-oauth/provider")
const mockLogin = mock(() => Promise.resolve({ accessToken: "test-token", expiresAt: 1710000000 })) const mockLogin = mock(() => Promise.resolve({ accessToken: "test-token", expiresAt: 1710000000 }))
@@ -11,6 +13,10 @@ mock.module("../../features/mcp-oauth/provider", () => ({
}, },
})) }))
afterAll(() => {
mock.module("../../features/mcp-oauth/provider", () => ({ ...realMcpOauthProvider }))
})
const { login } = await import("./login") const { login } = await import("./login")
describe("login command", () => { describe("login command", () => {

View File

@@ -1,10 +1,13 @@
import { describe, it, expect, mock, spyOn, beforeEach, afterEach } from "bun:test" import { describe, it, expect, mock, spyOn, beforeEach, afterEach, afterAll } from "bun:test"
import type { RunResult } from "./types" import type { RunResult } from "./types"
import { createJsonOutputManager } from "./json-output" import { createJsonOutputManager } from "./json-output"
import { resolveSession } from "./session-resolver" import { resolveSession } from "./session-resolver"
import { executeOnCompleteHook } from "./on-complete-hook" import { executeOnCompleteHook } from "./on-complete-hook"
import type { OpencodeClient } from "./types" import type { OpencodeClient } from "./types"
const realSdk = await import("@opencode-ai/sdk")
const realPortUtils = await import("../../shared/port-utils")
const mockServerClose = mock(() => {}) const mockServerClose = mock(() => {})
const mockCreateOpencode = mock(() => const mockCreateOpencode = mock(() =>
Promise.resolve({ Promise.resolve({
@@ -17,16 +20,23 @@ const mockIsPortAvailable = mock(() => Promise.resolve(true))
const mockGetAvailableServerPort = mock(() => Promise.resolve({ port: 9999, wasAutoSelected: false })) const mockGetAvailableServerPort = mock(() => Promise.resolve({ port: 9999, wasAutoSelected: false }))
mock.module("@opencode-ai/sdk", () => ({ mock.module("@opencode-ai/sdk", () => ({
...realSdk,
createOpencode: mockCreateOpencode, createOpencode: mockCreateOpencode,
createOpencodeClient: mockCreateOpencodeClient, createOpencodeClient: mockCreateOpencodeClient,
})) }))
mock.module("../../shared/port-utils", () => ({ mock.module("../../shared/port-utils", () => ({
...realPortUtils,
isPortAvailable: mockIsPortAvailable, isPortAvailable: mockIsPortAvailable,
getAvailableServerPort: mockGetAvailableServerPort, getAvailableServerPort: mockGetAvailableServerPort,
DEFAULT_SERVER_PORT: 4096, DEFAULT_SERVER_PORT: 4096,
})) }))
afterAll(() => {
mock.module("@opencode-ai/sdk", () => ({ ...realSdk }))
mock.module("../../shared/port-utils", () => ({ ...realPortUtils }))
})
const { createServerConnection } = await import("./server-connection") const { createServerConnection } = await import("./server-connection")
interface MockWriteStream { interface MockWriteStream {

View File

@@ -1,4 +1,7 @@
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test" import { describe, it, expect, mock, beforeEach, afterEach, afterAll } from "bun:test"
const realSdk = await import("@opencode-ai/sdk")
const realPortUtils = await import("../../shared/port-utils")
const originalConsole = globalThis.console const originalConsole = globalThis.console
@@ -15,16 +18,23 @@ const mockGetAvailableServerPort = mock(() => Promise.resolve({ port: 4096, wasA
const mockConsoleLog = mock(() => {}) const mockConsoleLog = mock(() => {})
mock.module("@opencode-ai/sdk", () => ({ mock.module("@opencode-ai/sdk", () => ({
...realSdk,
createOpencode: mockCreateOpencode, createOpencode: mockCreateOpencode,
createOpencodeClient: mockCreateOpencodeClient, createOpencodeClient: mockCreateOpencodeClient,
})) }))
mock.module("../../shared/port-utils", () => ({ mock.module("../../shared/port-utils", () => ({
...realPortUtils,
isPortAvailable: mockIsPortAvailable, isPortAvailable: mockIsPortAvailable,
getAvailableServerPort: mockGetAvailableServerPort, getAvailableServerPort: mockGetAvailableServerPort,
DEFAULT_SERVER_PORT: 4096, DEFAULT_SERVER_PORT: 4096,
})) }))
afterAll(() => {
mock.module("@opencode-ai/sdk", () => ({ ...realSdk }))
mock.module("../../shared/port-utils", () => ({ ...realPortUtils }))
})
const { createServerConnection } = await import("./server-connection") const { createServerConnection } = await import("./server-connection")
describe("createServerConnection", () => { describe("createServerConnection", () => {

View File

@@ -6,15 +6,20 @@ import { tmpdir } from "os"
const TEST_DIR = join(tmpdir(), "mcp-loader-test-" + Date.now()) const TEST_DIR = join(tmpdir(), "mcp-loader-test-" + Date.now())
const TEST_HOME = join(TEST_DIR, "home") const TEST_HOME = join(TEST_DIR, "home")
const realOs = await import("os")
const realShared = await import("../../shared")
describe("getSystemMcpServerNames", () => { describe("getSystemMcpServerNames", () => {
beforeEach(() => { beforeEach(() => {
mkdirSync(TEST_DIR, { recursive: true }) mkdirSync(TEST_DIR, { recursive: true })
mkdirSync(TEST_HOME, { recursive: true }) mkdirSync(TEST_HOME, { recursive: true })
mock.module("os", () => ({ mock.module("os", () => ({
...realOs,
homedir: () => TEST_HOME, homedir: () => TEST_HOME,
tmpdir, tmpdir,
})) }))
mock.module("../../shared", () => ({ mock.module("../../shared", () => ({
...realShared,
getClaudeConfigDir: () => join(TEST_HOME, ".claude"), getClaudeConfigDir: () => join(TEST_HOME, ".claude"),
})) }))
}) })
@@ -22,6 +27,9 @@ describe("getSystemMcpServerNames", () => {
afterEach(() => { afterEach(() => {
mock.restore() mock.restore()
rmSync(TEST_DIR, { recursive: true, force: true }) rmSync(TEST_DIR, { recursive: true, force: true })
mock.module("os", () => ({ ...realOs }))
mock.module("../../shared", () => ({ ...realShared }))
}) })
it("returns empty set when no .mcp.json files exist", async () => { it("returns empty set when no .mcp.json files exist", async () => {

View File

@@ -53,26 +53,28 @@ async function loadSourcePath(options: {
const stat = await fs.stat(absolutePath).catch(() => null) const stat = await fs.stat(absolutePath).catch(() => null)
if (!stat) return [] if (!stat) return []
const realBasePath = await fs.realpath(absolutePath).catch(() => absolutePath)
if (stat.isFile()) { if (stat.isFile()) {
if (!isMarkdownPath(absolutePath)) return [] if (!isMarkdownPath(realBasePath)) return []
const loaded = await loadSkillFromPath({ const loaded = await loadSkillFromPath({
skillPath: absolutePath, skillPath: realBasePath,
resolvedPath: dirname(absolutePath), resolvedPath: dirname(realBasePath),
defaultName: inferSkillNameFromFileName(absolutePath), defaultName: inferSkillNameFromFileName(realBasePath),
scope: "config", scope: "config",
}) })
if (!loaded) return [] if (!loaded) return []
return filterByGlob([loaded], dirname(absolutePath), options.globPattern) return filterByGlob([loaded], dirname(realBasePath), options.globPattern)
} }
if (!stat.isDirectory()) return [] if (!stat.isDirectory()) return []
const directorySkills = await loadSkillsFromDir({ const directorySkills = await loadSkillsFromDir({
skillsDir: absolutePath, skillsDir: realBasePath,
scope: "config", scope: "config",
maxDepth: options.recursive ? MAX_RECURSIVE_DEPTH : 0, maxDepth: options.recursive ? MAX_RECURSIVE_DEPTH : 0,
}) })
return filterByGlob(directorySkills, absolutePath, options.globPattern) return filterByGlob(directorySkills, realBasePath, options.globPattern)
} }
export async function discoverConfigSourceSkills(options: { export async function discoverConfigSourceSkills(options: {

View File

@@ -1,8 +1,13 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test" import { describe, it, expect, beforeEach, afterEach, mock, spyOn, afterAll } from "bun:test"
import { SkillMcpManager } from "./manager" import { SkillMcpManager } from "./manager"
import type { SkillMcpClientInfo, SkillMcpServerContext } from "./types" import type { SkillMcpClientInfo, SkillMcpServerContext } from "./types"
import type { ClaudeCodeMcpServer } from "../claude-code-mcp-loader/types" import type { ClaudeCodeMcpServer } from "../claude-code-mcp-loader/types"
const realStreamableHttp = await import(
"@modelcontextprotocol/sdk/client/streamableHttp.js"
)
const realMcpOauthProvider = await import("../mcp-oauth/provider")
// Mock the MCP SDK transports to avoid network calls // Mock the MCP SDK transports to avoid network calls
const mockHttpConnect = mock(() => Promise.reject(new Error("Mocked HTTP connection failure"))) const mockHttpConnect = mock(() => Promise.reject(new Error("Mocked HTTP connection failure")))
const mockHttpClose = mock(() => Promise.resolve()) const mockHttpClose = mock(() => Promise.resolve())
@@ -37,6 +42,13 @@ mock.module("../mcp-oauth/provider", () => ({
}, },
})) }))
afterAll(() => {
mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
...realStreamableHttp,
}))
mock.module("../mcp-oauth/provider", () => ({ ...realMcpOauthProvider }))
})

View File

@@ -1,9 +1,13 @@
import { describe, test, expect, mock, beforeEach } from 'bun:test' import { describe, test, expect, mock, beforeEach, afterAll } from 'bun:test'
import type { TmuxConfig } from '../../config/schema' import type { TmuxConfig } from '../../config/schema'
import type { WindowState, PaneAction } from './types' import type { WindowState, PaneAction } from './types'
import type { ActionResult, ExecuteContext } from './action-executor' import type { ActionResult, ExecuteContext } from './action-executor'
import type { TmuxUtilDeps } from './manager' import type { TmuxUtilDeps } from './manager'
const realPaneStateQuerier = await import('./pane-state-querier')
const realActionExecutor = await import('./action-executor')
const realSharedTmux = await import('../../shared/tmux')
type ExecuteActionsResult = { type ExecuteActionsResult = {
success: boolean success: boolean
spawnedPaneId?: string spawnedPaneId?: string
@@ -71,6 +75,12 @@ mock.module('../../shared/tmux', () => {
} }
}) })
afterAll(() => {
mock.module('./pane-state-querier', () => ({ ...realPaneStateQuerier }))
mock.module('./action-executor', () => ({ ...realActionExecutor }))
mock.module('../../shared/tmux', () => ({ ...realSharedTmux }))
})
const trackedSessions = new Set<string>() const trackedSessions = new Set<string>()
function createMockContext(overrides?: { function createMockContext(overrides?: {

View File

@@ -1,13 +1,21 @@
import { describe, test, expect, mock, beforeEach } from "bun:test" import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import type { ExperimentalConfig } from "../../config" import type { ExperimentalConfig } from "../../config"
const attemptDeduplicationRecoveryMock = mock(async () => {}) const realDeduplicationRecovery = await import("./deduplication-recovery")
const attemptDeduplicationRecoveryMock = mock<(sessionID: string) => Promise<void>>(
async () => {}
)
mock.module("./deduplication-recovery", () => ({ mock.module("./deduplication-recovery", () => ({
attemptDeduplicationRecovery: attemptDeduplicationRecoveryMock, attemptDeduplicationRecovery: attemptDeduplicationRecoveryMock,
})) }))
afterAll(() => {
mock.module("./deduplication-recovery", () => ({ ...realDeduplicationRecovery }))
})
function createImmediateTimeouts(): () => void { function createImmediateTimeouts(): () => void {
const originalSetTimeout = globalThis.setTimeout const originalSetTimeout = globalThis.setTimeout
const originalClearTimeout = globalThis.clearTimeout const originalClearTimeout = globalThis.clearTimeout
@@ -37,13 +45,15 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
const experimental = { const experimental = {
dynamic_context_pruning: { dynamic_context_pruning: {
enabled: true, enabled: true,
notification: "off",
protected_tools: [],
strategies: { strategies: {
deduplication: { enabled: true }, deduplication: { enabled: true },
}, },
}, },
} satisfies ExperimentalConfig } satisfies ExperimentalConfig
let resolveSummarize: (() => void) | null = null let resolveSummarize: ((value?: void) => void) | null = null
const summarizePromise = new Promise<void>((resolve) => { const summarizePromise = new Promise<void>((resolve) => {
resolveSummarize = resolve resolveSummarize = resolve
}) })
@@ -62,7 +72,7 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
try { try {
const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook") const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook")
const ctx = { client: mockClient, directory: "/tmp" } as PluginInput const ctx = { client: mockClient, directory: "/tmp" } as unknown as PluginInput
const hook = createAnthropicContextWindowLimitRecoveryHook(ctx, { experimental }) const hook = createAnthropicContextWindowLimitRecoveryHook(ctx, { experimental })
// first error triggers compaction (setTimeout runs immediately due to mock) // first error triggers compaction (setTimeout runs immediately due to mock)
@@ -105,7 +115,7 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
} }
const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook") const { createAnthropicContextWindowLimitRecoveryHook } = await import("./recovery-hook")
const ctx = { client: mockClient, directory: "/tmp" } as PluginInput const ctx = { client: mockClient, directory: "/tmp" } as unknown as PluginInput
const hook = createAnthropicContextWindowLimitRecoveryHook(ctx) const hook = createAnthropicContextWindowLimitRecoveryHook(ctx)
//#when - single error (no compaction in progress) //#when - single error (no compaction in progress)

View File

@@ -1,4 +1,4 @@
import { describe, test, expect, mock, beforeEach } from "bun:test" import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test"
import { truncateUntilTargetTokens } from "./storage" import { truncateUntilTargetTokens } from "./storage"
import * as storage from "./storage" import * as storage from "./storage"
@@ -11,6 +11,10 @@ mock.module("./storage", () => {
} }
}) })
afterAll(() => {
mock.module("./storage", () => ({ ...storage }))
})
describe("truncateUntilTargetTokens", () => { describe("truncateUntilTargetTokens", () => {
const sessionID = "test-session" const sessionID = "test-session"

View File

@@ -1,4 +1,4 @@
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test" import { describe, expect, test, beforeEach, afterEach, afterAll, mock } from "bun:test"
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
@@ -9,20 +9,19 @@ import {
readBoulderState, readBoulderState,
} from "../../features/boulder-state" } from "../../features/boulder-state"
import type { BoulderState } from "../../features/boulder-state" import type { BoulderState } from "../../features/boulder-state"
const realClaudeCodeSessionState = await import(
const TEST_STORAGE_ROOT = join(tmpdir(), `atlas-message-storage-${randomUUID()}`) "../../features/claude-code-session-state"
const TEST_MESSAGE_STORAGE = join(TEST_STORAGE_ROOT, "message") )
const TEST_PART_STORAGE = join(TEST_STORAGE_ROOT, "part")
mock.module("../../features/hook-message-injector/constants", () => ({
OPENCODE_STORAGE: TEST_STORAGE_ROOT,
MESSAGE_STORAGE: TEST_MESSAGE_STORAGE,
PART_STORAGE: TEST_PART_STORAGE,
}))
const { createAtlasHook } = await import("./index") const { createAtlasHook } = await import("./index")
const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector") const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector")
afterAll(() => {
mock.module("../../features/claude-code-session-state", () => ({
...realClaudeCodeSessionState,
}))
})
describe("atlas hook", () => { describe("atlas hook", () => {
let TEST_DIR: string let TEST_DIR: string
let SISYPHUS_DIR: string let SISYPHUS_DIR: string
@@ -77,7 +76,6 @@ describe("atlas hook", () => {
if (existsSync(TEST_DIR)) { if (existsSync(TEST_DIR)) {
rmSync(TEST_DIR, { recursive: true, force: true }) rmSync(TEST_DIR, { recursive: true, force: true })
} }
rmSync(TEST_STORAGE_ROOT, { recursive: true, force: true })
}) })
describe("tool.execute.after handler", () => { describe("tool.execute.after handler", () => {

View File

@@ -1,4 +1,11 @@
import { describe, it, expect, mock, beforeEach } from "bun:test" import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test"
const realChecker = await import("../checker")
const realVersionChannel = await import("../version-channel")
const realCache = await import("../cache")
const realConfigManager = await import("../../../cli/config-manager")
const realUpdateToasts = await import("./update-toasts")
const realLogger = await import("../../../shared/logger")
// Mock modules before importing // Mock modules before importing
const mockFindPluginEntry = mock(() => null as any) const mockFindPluginEntry = mock(() => null as any)
@@ -39,6 +46,15 @@ mock.module("../../../shared/logger", () => ({
log: () => {}, log: () => {},
})) }))
afterAll(() => {
mock.module("../checker", () => ({ ...realChecker }))
mock.module("../version-channel", () => ({ ...realVersionChannel }))
mock.module("../cache", () => ({ ...realCache }))
mock.module("../../../cli/config-manager", () => ({ ...realConfigManager }))
mock.module("./update-toasts", () => ({ ...realUpdateToasts }))
mock.module("../../../shared/logger", () => ({ ...realLogger }))
})
const { runBackgroundUpdateCheck } = await import("./background-update-check") const { runBackgroundUpdateCheck } = await import("./background-update-check")
describe("runBackgroundUpdateCheck", () => { describe("runBackgroundUpdateCheck", () => {

View File

@@ -1,7 +1,10 @@
import { describe, it, expect, mock, beforeEach } from "bun:test" import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test"
import type { ClaudeHooksConfig } from "./types" import type { ClaudeHooksConfig } from "./types"
import type { StopContext } from "./stop" import type { StopContext } from "./stop"
const realCommandExecutor = await import("../../shared/command-executor")
const realLogger = await import("../../shared/logger")
const mockExecuteHookCommand = mock(() => const mockExecuteHookCommand = mock(() =>
Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }) Promise.resolve({ exitCode: 0, stdout: "", stderr: "" })
) )
@@ -17,6 +20,11 @@ mock.module("../../shared/logger", () => ({
getLogFilePath: () => "/tmp/test.log", getLogFilePath: () => "/tmp/test.log",
})) }))
afterAll(() => {
mock.module("../../shared/command-executor", () => ({ ...realCommandExecutor }))
mock.module("../../shared/logger", () => ({ ...realLogger }))
})
const { executeStopHooks } = await import("./stop") const { executeStopHooks } = await import("./stop")
function createStopContext(overrides?: Partial<StopContext>): StopContext { function createStopContext(overrides?: Partial<StopContext>): StopContext {

View File

@@ -1,10 +1,19 @@
import { describe, test, expect, mock } from "bun:test" import { describe, test, expect, mock, afterAll } from "bun:test"
import { chmodSync, mkdtempSync, writeFileSync } from "node:fs" import { chmodSync, mkdtempSync, writeFileSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
import type { PendingCall } from "./types" import type { PendingCall } from "./types"
const realCli = await import("./cli")
const cliTsHref = new URL("./cli.ts", import.meta.url).href
afterAll(() => {
mock.module("./cli", () => ({ ...realCli }))
mock.module("./cli.ts", () => ({ ...realCli }))
mock.module(cliTsHref, () => ({ ...realCli }))
})
function createMockInput() { function createMockInput() {
return { return {
session_id: "test", session_id: "test",

View File

@@ -1,4 +1,6 @@
import { describe, it, expect, mock, beforeEach } from "bun:test" import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test"
const realCliRunner = await import("./cli-runner")
const processApplyPatchEditsWithCli = mock(async () => {}) const processApplyPatchEditsWithCli = mock(async () => {})
@@ -10,6 +12,10 @@ mock.module("./cli-runner", () => ({
processApplyPatchEditsWithCli, processApplyPatchEditsWithCli,
})) }))
afterAll(() => {
mock.module("./cli-runner", () => ({ ...realCliRunner }))
})
const { createCommentCheckerHooks } = await import("./hook") const { createCommentCheckerHooks } = await import("./hook")
describe("comment-checker apply_patch integration", () => { describe("comment-checker apply_patch integration", () => {

View File

@@ -1,4 +1,6 @@
import { describe, expect, it, mock } from "bun:test" import { describe, expect, it, mock, afterAll } from "bun:test"
const realSystemDirective = await import("../../shared/system-directive")
mock.module("../../shared/system-directive", () => ({ mock.module("../../shared/system-directive", () => ({
createSystemDirective: (type: string) => `[DIRECTIVE:${type}]`, createSystemDirective: (type: string) => `[DIRECTIVE:${type}]`,
@@ -14,6 +16,10 @@ mock.module("../../shared/system-directive", () => ({
}, },
})) }))
afterAll(() => {
mock.module("../../shared/system-directive", () => ({ ...realSystemDirective }))
})
import { createCompactionContextInjector } from "./index" import { createCompactionContextInjector } from "./index"
import { TaskHistory } from "../../features/background-agent/task-history" import { TaskHistory } from "../../features/background-agent/task-history"

View File

@@ -30,7 +30,7 @@ function createMockContext(todoResponses: TodoSnapshot[][]): PluginInput {
}, },
}, },
directory: "/tmp/test", directory: "/tmp/test",
} as PluginInput } as unknown as PluginInput
} }
describe("compaction-todo-preserver", () => { describe("compaction-todo-preserver", () => {
@@ -38,7 +38,7 @@ describe("compaction-todo-preserver", () => {
//#given //#given
updateMock.mockClear() updateMock.mockClear()
const sessionID = "session-compaction-missing" const sessionID = "session-compaction-missing"
const todos = [ const todos: TodoSnapshot[] = [
{ id: "1", content: "Task 1", status: "pending", priority: "high" }, { id: "1", content: "Task 1", status: "pending", priority: "high" },
{ id: "2", content: "Task 2", status: "in_progress", priority: "medium" }, { id: "2", content: "Task 2", status: "in_progress", priority: "medium" },
] ]
@@ -58,7 +58,7 @@ describe("compaction-todo-preserver", () => {
//#given //#given
updateMock.mockClear() updateMock.mockClear()
const sessionID = "session-compaction-present" const sessionID = "session-compaction-present"
const todos = [ const todos: TodoSnapshot[] = [
{ id: "1", content: "Task 1", status: "pending", priority: "high" }, { id: "1", content: "Task 1", status: "pending", priority: "high" },
] ]
const ctx = createMockContext([todos, todos]) const ctx = createMockContext([todos, todos])

View File

@@ -1,12 +1,39 @@
import { beforeEach, describe, expect, it, mock } from "bun:test" import { beforeEach, afterEach, describe, expect, it, mock, afterAll } from "bun:test"
const readFileSyncMock = mock((_: string, __: string) => "# AGENTS") const realNodeFs = await import("node:fs")
const realFinder = await import("./finder")
const realStorage = await import("./storage")
const originalReadFileSync = realNodeFs.readFileSync
const readFileSyncMock = mock((filePath: string, encoding?: string) => {
if (String(filePath).endsWith("AGENTS.md")) {
return "# AGENTS"
}
return originalReadFileSync(filePath as never, encoding as never)
})
const findAgentsMdUpMock = mock((_: { startDir: string; rootDir: string }) => [] as string[]) const findAgentsMdUpMock = mock((_: { startDir: string; rootDir: string }) => [] as string[])
const resolveFilePathMock = mock((_: string, path: string) => path) const resolveFilePathMock = mock((_: string, path: string) => path)
const loadInjectedPathsMock = mock((_: string) => new Set<string>()) const loadInjectedPathsMock = mock((_: string) => new Set<string>())
const saveInjectedPathsMock = mock((_: string, __: Set<string>) => {}) const saveInjectedPathsMock = mock((_: string, __: Set<string>) => {})
afterAll(() => {
mock.module("node:fs", () => ({ ...realNodeFs }))
mock.module("./finder", () => ({ ...realFinder }))
mock.module("./storage", () => ({ ...realStorage }))
})
let processFilePathForAgentsInjection: typeof import("./injector").processFilePathForAgentsInjection
describe("processFilePathForAgentsInjection", () => {
beforeEach(async () => {
readFileSyncMock.mockClear()
findAgentsMdUpMock.mockClear()
resolveFilePathMock.mockClear()
loadInjectedPathsMock.mockClear()
saveInjectedPathsMock.mockClear()
mock.module("node:fs", () => ({ mock.module("node:fs", () => ({
...realNodeFs,
readFileSync: readFileSyncMock, readFileSync: readFileSyncMock,
})) }))
@@ -20,15 +47,13 @@ mock.module("./storage", () => ({
saveInjectedPaths: saveInjectedPathsMock, saveInjectedPaths: saveInjectedPathsMock,
})) }))
const { processFilePathForAgentsInjection } = await import("./injector") ;({ processFilePathForAgentsInjection } = await import(`./injector?${Date.now()}`))
})
describe("processFilePathForAgentsInjection", () => { afterEach(() => {
beforeEach(() => { mock.module("node:fs", () => ({ ...realNodeFs }))
readFileSyncMock.mockClear() mock.module("./finder", () => ({ ...realFinder }))
findAgentsMdUpMock.mockClear() mock.module("./storage", () => ({ ...realStorage }))
resolveFilePathMock.mockClear()
loadInjectedPathsMock.mockClear()
saveInjectedPathsMock.mockClear()
}) })
it("does not save when all discovered paths are already cached", async () => { it("does not save when all discovered paths are already cached", async () => {

View File

@@ -1,12 +1,39 @@
import { beforeEach, describe, expect, it, mock } from "bun:test" import { beforeEach, afterEach, describe, expect, it, mock, afterAll } from "bun:test"
const readFileSyncMock = mock((_: string, __: string) => "# README") const realNodeFs = await import("node:fs")
const realFinder = await import("./finder")
const realStorage = await import("./storage")
const originalReadFileSync = realNodeFs.readFileSync
const readFileSyncMock = mock((filePath: string, encoding?: string) => {
if (String(filePath).endsWith("README.md")) {
return "# README"
}
return originalReadFileSync(filePath as never, encoding as never)
})
const findReadmeMdUpMock = mock((_: { startDir: string; rootDir: string }) => [] as string[]) const findReadmeMdUpMock = mock((_: { startDir: string; rootDir: string }) => [] as string[])
const resolveFilePathMock = mock((_: string, path: string) => path) const resolveFilePathMock = mock((_: string, path: string) => path)
const loadInjectedPathsMock = mock((_: string) => new Set<string>()) const loadInjectedPathsMock = mock((_: string) => new Set<string>())
const saveInjectedPathsMock = mock((_: string, __: Set<string>) => {}) const saveInjectedPathsMock = mock((_: string, __: Set<string>) => {})
afterAll(() => {
mock.module("node:fs", () => ({ ...realNodeFs }))
mock.module("./finder", () => ({ ...realFinder }))
mock.module("./storage", () => ({ ...realStorage }))
})
let processFilePathForReadmeInjection: typeof import("./injector").processFilePathForReadmeInjection
describe("processFilePathForReadmeInjection", () => {
beforeEach(async () => {
readFileSyncMock.mockClear()
findReadmeMdUpMock.mockClear()
resolveFilePathMock.mockClear()
loadInjectedPathsMock.mockClear()
saveInjectedPathsMock.mockClear()
mock.module("node:fs", () => ({ mock.module("node:fs", () => ({
...realNodeFs,
readFileSync: readFileSyncMock, readFileSync: readFileSyncMock,
})) }))
@@ -20,15 +47,13 @@ mock.module("./storage", () => ({
saveInjectedPaths: saveInjectedPathsMock, saveInjectedPaths: saveInjectedPathsMock,
})) }))
const { processFilePathForReadmeInjection } = await import("./injector") ;({ processFilePathForReadmeInjection } = await import(`./injector?${Date.now()}`))
})
describe("processFilePathForReadmeInjection", () => { afterEach(() => {
beforeEach(() => { mock.module("node:fs", () => ({ ...realNodeFs }))
readFileSyncMock.mockClear() mock.module("./finder", () => ({ ...realFinder }))
findReadmeMdUpMock.mockClear() mock.module("./storage", () => ({ ...realStorage }))
resolveFilePathMock.mockClear()
loadInjectedPathsMock.mockClear()
saveInjectedPathsMock.mockClear()
}) })
it("does not save when all discovered paths are already cached", async () => { it("does not save when all discovered paths are already cached", async () => {

View File

@@ -1,4 +1,4 @@
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test" import { describe, expect, test, beforeEach, afterEach } from "bun:test"
import { mkdirSync, rmSync, writeFileSync } from "node:fs" import { mkdirSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
@@ -6,16 +6,6 @@ import { randomUUID } from "node:crypto"
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive" import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
import { clearSessionAgent } from "../../features/claude-code-session-state" import { clearSessionAgent } from "../../features/claude-code-session-state"
const TEST_STORAGE_ROOT = join(tmpdir(), `prometheus-md-only-${randomUUID()}`)
const TEST_MESSAGE_STORAGE = join(TEST_STORAGE_ROOT, "message")
const TEST_PART_STORAGE = join(TEST_STORAGE_ROOT, "part")
mock.module("../../features/hook-message-injector/constants", () => ({
OPENCODE_STORAGE: TEST_STORAGE_ROOT,
MESSAGE_STORAGE: TEST_MESSAGE_STORAGE,
PART_STORAGE: TEST_PART_STORAGE,
}))
const { createPrometheusMdOnlyHook } = await import("./index") const { createPrometheusMdOnlyHook } = await import("./index")
const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector") const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector")
@@ -52,7 +42,6 @@ describe("prometheus-md-only", () => {
// ignore // ignore
} }
} }
rmSync(TEST_STORAGE_ROOT, { recursive: true, force: true })
}) })
describe("agent name matching", () => { describe("agent name matching", () => {

View File

@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; import { afterEach, beforeEach, describe, expect, it, mock, afterAll } from "bun:test";
import * as fs from "node:fs"; import * as fs from "node:fs";
import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import * as os from "node:os"; import * as os from "node:os";
@@ -6,6 +6,10 @@ import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { RULES_INJECTOR_STORAGE } from "./constants"; import { RULES_INJECTOR_STORAGE } from "./constants";
const realNodeFs = await import("node:fs");
const realNodeOs = await import("node:os");
const realMatcher = await import("./matcher");
type StatSnapshot = { mtimeMs: number; size: number }; type StatSnapshot = { mtimeMs: number; size: number };
let trackedRulePath = ""; let trackedRulePath = "";
@@ -56,6 +60,12 @@ mock.module("./matcher", () => ({
isDuplicateByContentHash: (hash: string, cache: Set<string>) => cache.has(hash), isDuplicateByContentHash: (hash: string, cache: Set<string>) => cache.has(hash),
})); }));
afterAll(() => {
mock.module("node:fs", () => ({ ...realNodeFs }));
mock.module("node:os", () => ({ ...realNodeOs }));
mock.module("./matcher", () => ({ ...realMatcher }));
});
function createOutput(): { title: string; output: string; metadata: unknown } { function createOutput(): { title: string; output: string; metadata: unknown } {
return { title: "tool", output: "", metadata: {} }; return { title: "tool", output: "", metadata: {} };
} }

View File

@@ -518,7 +518,7 @@ describe("todo-continuation-enforcer", () => {
//#then //#then
expect(promptCalls).toHaveLength(2) expect(promptCalls).toHaveLength(2)
}) }, 20000)
test("should keep injecting even when todos remain unchanged across cycles", async () => { test("should keep injecting even when todos remain unchanged across cycles", async () => {
//#given //#given
@@ -553,7 +553,7 @@ describe("todo-continuation-enforcer", () => {
//#then — all 5 injections should fire (no stagnation cap) //#then — all 5 injections should fire (no stagnation cap)
expect(promptCalls).toHaveLength(5) expect(promptCalls).toHaveLength(5)
}) }, 30000)
test("should skip idle handling while injection is in flight", async () => { test("should skip idle handling while injection is in flight", async () => {
//#given //#given
@@ -613,7 +613,7 @@ describe("todo-continuation-enforcer", () => {
//#then //#then
expect(promptCalls).toHaveLength(2) expect(promptCalls).toHaveLength(2)
}) }, 20000)
test("should accept skipAgents option without error", async () => { test("should accept skipAgents option without error", async () => {
// given - session with skipAgents configured for Prometheus // given - session with skipAgents configured for Prometheus

View File

@@ -0,0 +1,14 @@
import { describe, test, expect } from "bun:test"
import { getAgentsConfigDir } from "./agents-config-dir"
describe("getAgentsConfigDir", () => {
test("returns path ending with .agents", () => {
// given agents config dir is requested
// when getAgentsConfigDir is called
const result = getAgentsConfigDir()
// then returns path ending with .agents
expect(result.endsWith(".agents")).toBe(true)
})
})

View File

@@ -0,0 +1,6 @@
import { homedir } from "node:os"
import { join } from "node:path"
export function getAgentsConfigDir(): string {
return join(homedir(), ".agents")
}

View File

@@ -1,10 +1,10 @@
import { describe, it, expect, beforeAll, afterAll } from "bun:test" import { describe, it, expect, beforeAll, afterAll } from "bun:test"
import { mkdirSync, writeFileSync, symlinkSync, rmSync } from "fs" import { mkdirSync, writeFileSync, symlinkSync, rmSync, realpathSync } from "fs"
import { join } from "path" import { join } from "path"
import { tmpdir } from "os" import { tmpdir } from "os"
import { resolveSymlink, resolveSymlinkAsync, isSymbolicLink } from "./file-utils" import { resolveSymlink, resolveSymlinkAsync, isSymbolicLink } from "./file-utils"
const testDir = join(tmpdir(), "file-utils-test-" + Date.now()) const testDir = join(realpathSync(tmpdir()), "file-utils-test-" + Date.now())
// Create a directory structure that mimics the real-world scenario: // Create a directory structure that mimics the real-world scenario:
// //

View File

@@ -1,16 +1,28 @@
const { describe, test, expect, mock } = require("bun:test") const { describe, test, expect, mock, beforeEach, afterEach } = require("bun:test")
mock.module("./session-creator", () => ({ const realCompletionPoller = require("./completion-poller")
createOrGetSession: mock(async () => ({ sessionID: "ses-test-123" })), const realMessageProcessor = require("./message-processor")
}))
const waitForCompletionMock = mock(async () => {})
const processMessagesMock = mock(async () => "agent response")
beforeEach(() => {
waitForCompletionMock.mockClear()
processMessagesMock.mockClear()
mock.module("./completion-poller", () => ({ mock.module("./completion-poller", () => ({
waitForCompletion: mock(async () => {}), waitForCompletion: waitForCompletionMock,
})) }))
mock.module("./message-processor", () => ({ mock.module("./message-processor", () => ({
processMessages: mock(async () => "agent response"), processMessages: processMessagesMock,
})) }))
})
afterEach(() => {
mock.module("./completion-poller", () => ({ ...realCompletionPoller }))
mock.module("./message-processor", () => ({ ...realMessageProcessor }))
})
describe("executeSync", () => { describe("executeSync", () => {
test("passes question=false via tools parameter to block question tool", async () => { test("passes question=false via tools parameter to block question tool", async () => {
@@ -27,6 +39,7 @@ describe("executeSync", () => {
subagent_type: "explore", subagent_type: "explore",
description: "test task", description: "test task",
prompt: "find something", prompt: "find something",
session_id: "ses-test-123",
} }
const toolContext = { const toolContext = {
@@ -39,7 +52,10 @@ describe("executeSync", () => {
const ctx = { const ctx = {
client: { client: {
session: { promptAsync }, session: {
promptAsync,
get: mock(async () => ({ data: { id: "ses-test-123" } })),
},
}, },
} }
@@ -65,6 +81,7 @@ describe("executeSync", () => {
subagent_type: "librarian", subagent_type: "librarian",
description: "search docs", description: "search docs",
prompt: "find docs", prompt: "find docs",
session_id: "ses-test-123",
} }
const toolContext = { const toolContext = {
@@ -77,7 +94,10 @@ describe("executeSync", () => {
const ctx = { const ctx = {
client: { client: {
session: { promptAsync }, session: {
promptAsync,
get: mock(async () => ({ data: { id: "ses-test-123" } })),
},
}, },
} }

View File

@@ -2,7 +2,9 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
import { describe, it, expect, spyOn, mock, beforeEach, afterEach } from "bun:test" import { describe, it, expect, spyOn, mock, beforeEach, afterEach, afterAll } from "bun:test"
const realJsonRpcNode = await import("vscode-jsonrpc/node")
mock.module("vscode-jsonrpc/node", () => ({ mock.module("vscode-jsonrpc/node", () => ({
createMessageConnection: () => { createMessageConnection: () => {
@@ -12,6 +14,10 @@ mock.module("vscode-jsonrpc/node", () => ({
StreamMessageWriter: function StreamMessageWriter() {}, StreamMessageWriter: function StreamMessageWriter() {},
})) }))
afterAll(() => {
mock.module("vscode-jsonrpc/node", () => ({ ...realJsonRpcNode }))
})
import { LSPClient, lspManager, validateCwd } from "./client" import { LSPClient, lspManager, validateCwd } from "./client"
import type { ResolvedServer } from "./types" import type { ResolvedServer } from "./types"

View File

@@ -1,9 +1,11 @@
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test" import { describe, test, expect, beforeEach, afterEach, afterAll, mock } from "bun:test"
import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs" import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
import { randomUUID } from "node:crypto" import { randomUUID } from "node:crypto"
const realConstants = await import("./constants")
const TEST_DIR = join(tmpdir(), `omo-test-session-manager-${randomUUID()}`) const TEST_DIR = join(tmpdir(), `omo-test-session-manager-${randomUUID()}`)
const TEST_MESSAGE_STORAGE = join(TEST_DIR, "message") const TEST_MESSAGE_STORAGE = join(TEST_DIR, "message")
const TEST_PART_STORAGE = join(TEST_DIR, "part") const TEST_PART_STORAGE = join(TEST_DIR, "part")
@@ -26,6 +28,10 @@ mock.module("./constants", () => ({
TOOL_NAME_PREFIX: "session_", TOOL_NAME_PREFIX: "session_",
})) }))
afterAll(() => {
mock.module("./constants", () => ({ ...realConstants }))
})
const { getAllSessions, getMessageDir, sessionExists, readSessionMessages, readSessionTodos, getSessionInfo } = const { getAllSessions, getMessageDir, sessionExists, readSessionMessages, readSessionTodos, getSessionInfo } =
await import("./storage") await import("./storage")

View File

@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test" import { describe, it, expect, beforeEach, mock, spyOn, afterAll } from "bun:test"
import type { ToolContext } from "@opencode-ai/plugin/tool" import type { ToolContext } from "@opencode-ai/plugin/tool"
import * as fs from "node:fs" import * as fs from "node:fs"
import { createSkillTool } from "./tools" import { createSkillTool } from "./tools"
@@ -8,6 +8,8 @@ import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js"
const originalReadFileSync = fs.readFileSync.bind(fs) const originalReadFileSync = fs.readFileSync.bind(fs)
const realNodeFs = await import("node:fs")
mock.module("node:fs", () => ({ mock.module("node:fs", () => ({
...fs, ...fs,
readFileSync: (path: string, encoding?: string) => { readFileSync: (path: string, encoding?: string) => {
@@ -21,6 +23,10 @@ Test skill body content`
}, },
})) }))
afterAll(() => {
mock.module("node:fs", () => ({ ...realNodeFs }))
})
function createMockSkill(name: string, options: { agent?: string } = {}): LoadedSkill { function createMockSkill(name: string, options: { agent?: string } = {}): LoadedSkill {
return { return {
name, name,