Compare commits
14 Commits
feat/nativ
...
fix/inheri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
271929a9e4 | ||
|
|
945329e261 | ||
|
|
f27733eae2 | ||
|
|
e9c9cb696d | ||
|
|
3de05f6442 | ||
|
|
8514906c3d | ||
|
|
f20e1aa0d0 | ||
|
|
936b51de79 | ||
|
|
38a4bbc75f | ||
|
|
daf011c616 | ||
|
|
c8bc267127 | ||
|
|
c41b38990c | ||
|
|
a4a5502e61 | ||
|
|
1511886c0c |
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@@ -52,12 +52,31 @@ jobs:
|
||||
bun test src/hooks/atlas
|
||||
bun test src/hooks/compaction-context-injector
|
||||
bun test src/features/tmux-subagent
|
||||
bun test src/cli/doctor/formatter.test.ts
|
||||
bun test src/cli/doctor/format-default.test.ts
|
||||
bun test src/tools/call-omo-agent/sync-executor.test.ts
|
||||
bun test src/tools/call-omo-agent/session-creator.test.ts
|
||||
bun test src/features/opencode-skill-loader/loader.test.ts
|
||||
|
||||
- name: Run remaining tests
|
||||
run: |
|
||||
# Run all other tests (mock-heavy ones are re-run but that's acceptable)
|
||||
bun test bin script src/cli src/config src/mcp src/index.test.ts \
|
||||
src/agents src/tools src/shared \
|
||||
# Enumerate subdirectories/files explicitly to EXCLUDE mock-heavy files
|
||||
# that were already run in isolation above.
|
||||
# Excluded from src/cli: doctor/formatter.test.ts, doctor/format-default.test.ts
|
||||
# Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts
|
||||
bun test bin script src/config src/mcp src/index.test.ts \
|
||||
src/agents src/shared \
|
||||
src/cli/run src/cli/config-manager src/cli/mcp-oauth \
|
||||
src/cli/index.test.ts src/cli/install.test.ts src/cli/model-fallback.test.ts \
|
||||
src/cli/config-manager.test.ts \
|
||||
src/cli/doctor/runner.test.ts src/cli/doctor/checks \
|
||||
src/tools/ast-grep src/tools/background-task src/tools/delegate-task \
|
||||
src/tools/glob src/tools/grep src/tools/interactive-bash \
|
||||
src/tools/look-at src/tools/lsp src/tools/session-manager \
|
||||
src/tools/skill src/tools/skill-mcp src/tools/slashcommand src/tools/task \
|
||||
src/tools/call-omo-agent/background-agent-executor.test.ts \
|
||||
src/tools/call-omo-agent/background-executor.test.ts \
|
||||
src/tools/call-omo-agent/subagent-session-creator.test.ts \
|
||||
src/hooks/anthropic-context-window-limit-recovery \
|
||||
src/hooks/claude-code-compatibility \
|
||||
src/hooks/context-injection \
|
||||
@@ -70,7 +89,11 @@ jobs:
|
||||
src/features/builtin-skills \
|
||||
src/features/claude-code-session-state \
|
||||
src/features/hook-message-injector \
|
||||
src/features/opencode-skill-loader \
|
||||
src/features/opencode-skill-loader/config-source-discovery.test.ts \
|
||||
src/features/opencode-skill-loader/merger.test.ts \
|
||||
src/features/opencode-skill-loader/skill-content.test.ts \
|
||||
src/features/opencode-skill-loader/blocking.test.ts \
|
||||
src/features/opencode-skill-loader/async-loader.test.ts \
|
||||
src/features/skill-mcp-manager
|
||||
|
||||
typecheck:
|
||||
|
||||
@@ -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"
|
||||
|
||||
const realFormatDefault = await import("./format-default")
|
||||
const realFormatStatus = await import("./format-status")
|
||||
const realFormatVerbose = await import("./format-verbose")
|
||||
|
||||
function createDoctorResult(): DoctorResult {
|
||||
return {
|
||||
results: [
|
||||
@@ -44,6 +48,12 @@ describe("formatter", () => {
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("./format-default", () => ({ ...realFormatDefault }))
|
||||
mock.module("./format-status", () => ({ ...realFormatStatus }))
|
||||
mock.module("./format-verbose", () => ({ ...realFormatVerbose }))
|
||||
})
|
||||
|
||||
describe("formatDoctorOutput", () => {
|
||||
it("dispatches to default formatter for default mode", async () => {
|
||||
//#given
|
||||
|
||||
@@ -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"
|
||||
|
||||
const realChecks = await import("./checks")
|
||||
const realFormatter = await import("./formatter")
|
||||
|
||||
function createSystemInfo(): SystemInfo {
|
||||
return {
|
||||
opencodeVersion: "1.0.200",
|
||||
@@ -47,6 +50,11 @@ describe("runner", () => {
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("./checks", () => ({ ...realChecks }))
|
||||
mock.module("./formatter", () => ({ ...realFormatter }))
|
||||
})
|
||||
|
||||
describe("runCheck", () => {
|
||||
it("returns fail result with issue when check throws", async () => {
|
||||
//#given
|
||||
|
||||
@@ -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 }))
|
||||
|
||||
@@ -11,6 +13,10 @@ mock.module("../../features/mcp-oauth/provider", () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("../../features/mcp-oauth/provider", () => ({ ...realMcpOauthProvider }))
|
||||
})
|
||||
|
||||
const { login } = await import("./login")
|
||||
|
||||
describe("login command", () => {
|
||||
|
||||
@@ -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 { createJsonOutputManager } from "./json-output"
|
||||
import { resolveSession } from "./session-resolver"
|
||||
import { executeOnCompleteHook } from "./on-complete-hook"
|
||||
import type { OpencodeClient } from "./types"
|
||||
|
||||
const realSdk = await import("@opencode-ai/sdk")
|
||||
const realPortUtils = await import("../../shared/port-utils")
|
||||
|
||||
const mockServerClose = mock(() => {})
|
||||
const mockCreateOpencode = mock(() =>
|
||||
Promise.resolve({
|
||||
@@ -17,16 +20,23 @@ const mockIsPortAvailable = mock(() => Promise.resolve(true))
|
||||
const mockGetAvailableServerPort = mock(() => Promise.resolve({ port: 9999, wasAutoSelected: false }))
|
||||
|
||||
mock.module("@opencode-ai/sdk", () => ({
|
||||
...realSdk,
|
||||
createOpencode: mockCreateOpencode,
|
||||
createOpencodeClient: mockCreateOpencodeClient,
|
||||
}))
|
||||
|
||||
mock.module("../../shared/port-utils", () => ({
|
||||
...realPortUtils,
|
||||
isPortAvailable: mockIsPortAvailable,
|
||||
getAvailableServerPort: mockGetAvailableServerPort,
|
||||
DEFAULT_SERVER_PORT: 4096,
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("@opencode-ai/sdk", () => ({ ...realSdk }))
|
||||
mock.module("../../shared/port-utils", () => ({ ...realPortUtils }))
|
||||
})
|
||||
|
||||
const { createServerConnection } = await import("./server-connection")
|
||||
|
||||
interface MockWriteStream {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,16 +18,23 @@ const mockGetAvailableServerPort = mock(() => Promise.resolve({ port: 4096, wasA
|
||||
const mockConsoleLog = mock(() => {})
|
||||
|
||||
mock.module("@opencode-ai/sdk", () => ({
|
||||
...realSdk,
|
||||
createOpencode: mockCreateOpencode,
|
||||
createOpencodeClient: mockCreateOpencodeClient,
|
||||
}))
|
||||
|
||||
mock.module("../../shared/port-utils", () => ({
|
||||
...realPortUtils,
|
||||
isPortAvailable: mockIsPortAvailable,
|
||||
getAvailableServerPort: mockGetAvailableServerPort,
|
||||
DEFAULT_SERVER_PORT: 4096,
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("@opencode-ai/sdk", () => ({ ...realSdk }))
|
||||
mock.module("../../shared/port-utils", () => ({ ...realPortUtils }))
|
||||
})
|
||||
|
||||
const { createServerConnection } = await import("./server-connection")
|
||||
|
||||
describe("createServerConnection", () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
} from "./types"
|
||||
import { TaskHistory } from "./task-history"
|
||||
import { log, getAgentToolRestrictions, promptWithModelSuggestionRetry } from "../../shared"
|
||||
import { setSessionTools } from "../../shared/session-tools-store"
|
||||
import { ConcurrencyManager } from "./concurrency"
|
||||
import type { BackgroundTaskConfig, TmuxConfig } from "../../config/schema"
|
||||
import { isInsideTmux } from "../../shared/tmux"
|
||||
@@ -141,6 +142,7 @@ export class BackgroundManager {
|
||||
parentMessageID: input.parentMessageID,
|
||||
parentModel: input.parentModel,
|
||||
parentAgent: input.parentAgent,
|
||||
parentTools: input.parentTools,
|
||||
model: input.model,
|
||||
category: input.category,
|
||||
}
|
||||
@@ -328,12 +330,16 @@ export class BackgroundManager {
|
||||
...(launchModel ? { model: launchModel } : {}),
|
||||
...(launchVariant ? { variant: launchVariant } : {}),
|
||||
system: input.skillContent,
|
||||
tools: {
|
||||
...getAgentToolRestrictions(input.agent),
|
||||
task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
},
|
||||
tools: (() => {
|
||||
const tools = {
|
||||
...getAgentToolRestrictions(input.agent),
|
||||
task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
}
|
||||
setSessionTools(sessionID, tools)
|
||||
return tools
|
||||
})(),
|
||||
parts: [{ type: "text", text: input.prompt }],
|
||||
},
|
||||
}).catch((error) => {
|
||||
@@ -535,6 +541,9 @@ export class BackgroundManager {
|
||||
existingTask.parentMessageID = input.parentMessageID
|
||||
existingTask.parentModel = input.parentModel
|
||||
existingTask.parentAgent = input.parentAgent
|
||||
if (input.parentTools) {
|
||||
existingTask.parentTools = input.parentTools
|
||||
}
|
||||
// Reset startedAt on resume to prevent immediate completion
|
||||
// The MIN_IDLE_TIME_MS check uses startedAt, so resumed tasks need fresh timing
|
||||
existingTask.startedAt = new Date()
|
||||
@@ -588,12 +597,16 @@ export class BackgroundManager {
|
||||
agent: existingTask.agent,
|
||||
...(resumeModel ? { model: resumeModel } : {}),
|
||||
...(resumeVariant ? { variant: resumeVariant } : {}),
|
||||
tools: {
|
||||
...getAgentToolRestrictions(existingTask.agent),
|
||||
task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
},
|
||||
tools: (() => {
|
||||
const tools = {
|
||||
...getAgentToolRestrictions(existingTask.agent),
|
||||
task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
}
|
||||
setSessionTools(existingTask.sessionID!, tools)
|
||||
return tools
|
||||
})(),
|
||||
parts: [{ type: "text", text: input.prompt }],
|
||||
},
|
||||
}).catch((error) => {
|
||||
@@ -1252,6 +1265,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
||||
noReply: !allComplete,
|
||||
...(agent !== undefined ? { agent } : {}),
|
||||
...(model !== undefined ? { model } : {}),
|
||||
...(task.parentTools ? { tools: task.parentTools } : {}),
|
||||
parts: [{ type: "text", text: notification }],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -148,6 +148,7 @@ export async function notifyParentSession(args: {
|
||||
noReply: !allComplete,
|
||||
...(agent !== undefined ? { agent } : {}),
|
||||
...(model !== undefined ? { model } : {}),
|
||||
...(task.parentTools ? { tools: task.parentTools } : {}),
|
||||
parts: [{ type: "text", text: notification }],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -71,6 +71,7 @@ export async function notifyParentSession(
|
||||
noReply: !allComplete,
|
||||
...(agent !== undefined ? { agent } : {}),
|
||||
...(model !== undefined ? { model } : {}),
|
||||
...(task.parentTools ? { tools: task.parentTools } : {}),
|
||||
parts: [{ type: "text", text: notification }],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ export function createTask(input: LaunchInput): BackgroundTask {
|
||||
parentMessageID: input.parentMessageID,
|
||||
parentModel: input.parentModel,
|
||||
parentAgent: input.parentAgent,
|
||||
parentTools: input.parentTools,
|
||||
model: input.model,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { BackgroundTask, ResumeInput } from "../types"
|
||||
import { log, getAgentToolRestrictions } from "../../../shared"
|
||||
import { setSessionTools } from "../../../shared/session-tools-store"
|
||||
import type { SpawnerContext } from "./spawner-context"
|
||||
import { subagentSessions } from "../../claude-code-session-state"
|
||||
import { getTaskToastManager } from "../../task-toast-manager"
|
||||
@@ -35,6 +36,9 @@ export async function resumeTask(
|
||||
task.parentMessageID = input.parentMessageID
|
||||
task.parentModel = input.parentModel
|
||||
task.parentAgent = input.parentAgent
|
||||
if (input.parentTools) {
|
||||
task.parentTools = input.parentTools
|
||||
}
|
||||
task.startedAt = new Date()
|
||||
|
||||
task.progress = {
|
||||
@@ -75,12 +79,16 @@ export async function resumeTask(
|
||||
agent: task.agent,
|
||||
...(resumeModel ? { model: resumeModel } : {}),
|
||||
...(resumeVariant ? { variant: resumeVariant } : {}),
|
||||
tools: {
|
||||
...getAgentToolRestrictions(task.agent),
|
||||
task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
},
|
||||
tools: (() => {
|
||||
const tools = {
|
||||
...getAgentToolRestrictions(task.agent),
|
||||
task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
}
|
||||
setSessionTools(task.sessionID!, tools)
|
||||
return tools
|
||||
})(),
|
||||
parts: [{ type: "text", text: input.prompt }],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { QueueItem } from "../constants"
|
||||
import { log, getAgentToolRestrictions, promptWithModelSuggestionRetry } from "../../../shared"
|
||||
import { setSessionTools } from "../../../shared/session-tools-store"
|
||||
import { subagentSessions } from "../../claude-code-session-state"
|
||||
import { getTaskToastManager } from "../../task-toast-manager"
|
||||
import { createBackgroundSession } from "./background-session-creator"
|
||||
@@ -79,12 +80,16 @@ export async function startTask(item: QueueItem, ctx: SpawnerContext): Promise<v
|
||||
...(launchModel ? { model: launchModel } : {}),
|
||||
...(launchVariant ? { variant: launchVariant } : {}),
|
||||
system: input.skillContent,
|
||||
tools: {
|
||||
...getAgentToolRestrictions(input.agent),
|
||||
task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
},
|
||||
tools: (() => {
|
||||
const tools = {
|
||||
...getAgentToolRestrictions(input.agent),
|
||||
task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
}
|
||||
setSessionTools(sessionID, tools)
|
||||
return tools
|
||||
})(),
|
||||
parts: [{ type: "text", text: input.prompt }],
|
||||
},
|
||||
}).catch((error: unknown) => {
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface BackgroundTask {
|
||||
concurrencyGroup?: string
|
||||
/** Parent session's agent name for notification */
|
||||
parentAgent?: string
|
||||
/** Parent session's tool restrictions for notification prompts */
|
||||
parentTools?: Record<string, boolean>
|
||||
/** Marks if the task was launched from an unstable agent/category */
|
||||
isUnstableAgent?: boolean
|
||||
/** Category used for this task (e.g., 'quick', 'visual-engineering') */
|
||||
@@ -56,6 +58,7 @@ export interface LaunchInput {
|
||||
parentMessageID: string
|
||||
parentModel?: { providerID: string; modelID: string }
|
||||
parentAgent?: string
|
||||
parentTools?: Record<string, boolean>
|
||||
model?: { providerID: string; modelID: string; variant?: string }
|
||||
isUnstableAgent?: boolean
|
||||
skills?: string[]
|
||||
@@ -70,4 +73,5 @@ export interface ResumeInput {
|
||||
parentMessageID: string
|
||||
parentModel?: { providerID: string; modelID: string }
|
||||
parentAgent?: string
|
||||
parentTools?: Record<string, boolean>
|
||||
}
|
||||
|
||||
@@ -6,15 +6,20 @@ import { tmpdir } from "os"
|
||||
const TEST_DIR = join(tmpdir(), "mcp-loader-test-" + Date.now())
|
||||
const TEST_HOME = join(TEST_DIR, "home")
|
||||
|
||||
const realOs = await import("os")
|
||||
const realShared = await import("../../shared")
|
||||
|
||||
describe("getSystemMcpServerNames", () => {
|
||||
beforeEach(() => {
|
||||
mkdirSync(TEST_DIR, { recursive: true })
|
||||
mkdirSync(TEST_HOME, { recursive: true })
|
||||
mock.module("os", () => ({
|
||||
...realOs,
|
||||
homedir: () => TEST_HOME,
|
||||
tmpdir,
|
||||
}))
|
||||
mock.module("../../shared", () => ({
|
||||
...realShared,
|
||||
getClaudeConfigDir: () => join(TEST_HOME, ".claude"),
|
||||
}))
|
||||
})
|
||||
@@ -22,6 +27,9 @@ describe("getSystemMcpServerNames", () => {
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
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 () => {
|
||||
|
||||
@@ -53,26 +53,28 @@ async function loadSourcePath(options: {
|
||||
const stat = await fs.stat(absolutePath).catch(() => null)
|
||||
if (!stat) return []
|
||||
|
||||
const realBasePath = await fs.realpath(absolutePath).catch(() => absolutePath)
|
||||
|
||||
if (stat.isFile()) {
|
||||
if (!isMarkdownPath(absolutePath)) return []
|
||||
if (!isMarkdownPath(realBasePath)) return []
|
||||
const loaded = await loadSkillFromPath({
|
||||
skillPath: absolutePath,
|
||||
resolvedPath: dirname(absolutePath),
|
||||
defaultName: inferSkillNameFromFileName(absolutePath),
|
||||
skillPath: realBasePath,
|
||||
resolvedPath: dirname(realBasePath),
|
||||
defaultName: inferSkillNameFromFileName(realBasePath),
|
||||
scope: "config",
|
||||
})
|
||||
if (!loaded) return []
|
||||
return filterByGlob([loaded], dirname(absolutePath), options.globPattern)
|
||||
return filterByGlob([loaded], dirname(realBasePath), options.globPattern)
|
||||
}
|
||||
|
||||
if (!stat.isDirectory()) return []
|
||||
|
||||
const directorySkills = await loadSkillsFromDir({
|
||||
skillsDir: absolutePath,
|
||||
skillsDir: realBasePath,
|
||||
scope: "config",
|
||||
maxDepth: options.recursive ? MAX_RECURSIVE_DEPTH : 0,
|
||||
})
|
||||
return filterByGlob(directorySkills, absolutePath, options.globPattern)
|
||||
return filterByGlob(directorySkills, realBasePath, options.globPattern)
|
||||
}
|
||||
|
||||
export async function discoverConfigSourceSkills(options: {
|
||||
|
||||
@@ -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 type { SkillMcpClientInfo, SkillMcpServerContext } from "./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
|
||||
const mockHttpConnect = mock(() => Promise.reject(new Error("Mocked HTTP connection failure")))
|
||||
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 }))
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 { WindowState, PaneAction } from './types'
|
||||
import type { ActionResult, ExecuteContext } from './action-executor'
|
||||
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 = {
|
||||
success: boolean
|
||||
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>()
|
||||
|
||||
function createMockContext(overrides?: {
|
||||
|
||||
@@ -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 { 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", () => ({
|
||||
attemptDeduplicationRecovery: attemptDeduplicationRecoveryMock,
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("./deduplication-recovery", () => ({ ...realDeduplicationRecovery }))
|
||||
})
|
||||
|
||||
function createImmediateTimeouts(): () => void {
|
||||
const originalSetTimeout = globalThis.setTimeout
|
||||
const originalClearTimeout = globalThis.clearTimeout
|
||||
@@ -37,13 +45,15 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
||||
const experimental = {
|
||||
dynamic_context_pruning: {
|
||||
enabled: true,
|
||||
notification: "off",
|
||||
protected_tools: [],
|
||||
strategies: {
|
||||
deduplication: { enabled: true },
|
||||
},
|
||||
},
|
||||
} satisfies ExperimentalConfig
|
||||
|
||||
let resolveSummarize: (() => void) | null = null
|
||||
let resolveSummarize: ((value?: void) => void) | null = null
|
||||
const summarizePromise = new Promise<void>((resolve) => {
|
||||
resolveSummarize = resolve
|
||||
})
|
||||
@@ -62,7 +72,7 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
||||
|
||||
try {
|
||||
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 })
|
||||
|
||||
// first error triggers compaction (setTimeout runs immediately due to mock)
|
||||
@@ -105,7 +115,7 @@ describe("createAnthropicContextWindowLimitRecoveryHook", () => {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
//#when - single error (no compaction in progress)
|
||||
|
||||
@@ -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 * as storage from "./storage"
|
||||
|
||||
@@ -11,6 +11,10 @@ mock.module("./storage", () => {
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("./storage", () => ({ ...storage }))
|
||||
})
|
||||
|
||||
describe("truncateUntilTargetTokens", () => {
|
||||
const sessionID = "test-session"
|
||||
|
||||
|
||||
@@ -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 { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
@@ -9,20 +9,19 @@ import {
|
||||
readBoulderState,
|
||||
} from "../../features/boulder-state"
|
||||
import type { BoulderState } from "../../features/boulder-state"
|
||||
|
||||
const TEST_STORAGE_ROOT = join(tmpdir(), `atlas-message-storage-${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 realClaudeCodeSessionState = await import(
|
||||
"../../features/claude-code-session-state"
|
||||
)
|
||||
|
||||
const { createAtlasHook } = await import("./index")
|
||||
const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector")
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("../../features/claude-code-session-state", () => ({
|
||||
...realClaudeCodeSessionState,
|
||||
}))
|
||||
})
|
||||
|
||||
describe("atlas hook", () => {
|
||||
let TEST_DIR: string
|
||||
let SISYPHUS_DIR: string
|
||||
@@ -77,7 +76,6 @@ describe("atlas hook", () => {
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true })
|
||||
}
|
||||
rmSync(TEST_STORAGE_ROOT, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe("tool.execute.after handler", () => {
|
||||
|
||||
@@ -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
|
||||
const mockFindPluginEntry = mock(() => null as any)
|
||||
@@ -39,6 +46,15 @@ mock.module("../../../shared/logger", () => ({
|
||||
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")
|
||||
|
||||
describe("runBackgroundUpdateCheck", () => {
|
||||
|
||||
@@ -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 { StopContext } from "./stop"
|
||||
|
||||
const realCommandExecutor = await import("../../shared/command-executor")
|
||||
const realLogger = await import("../../shared/logger")
|
||||
|
||||
const mockExecuteHookCommand = mock(() =>
|
||||
Promise.resolve({ exitCode: 0, stdout: "", stderr: "" })
|
||||
)
|
||||
@@ -17,6 +20,11 @@ mock.module("../../shared/logger", () => ({
|
||||
getLogFilePath: () => "/tmp/test.log",
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("../../shared/command-executor", () => ({ ...realCommandExecutor }))
|
||||
mock.module("../../shared/logger", () => ({ ...realLogger }))
|
||||
})
|
||||
|
||||
const { executeStopHooks } = await import("./stop")
|
||||
|
||||
function createStopContext(overrides?: Partial<StopContext>): StopContext {
|
||||
|
||||
@@ -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 { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
|
||||
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() {
|
||||
return {
|
||||
session_id: "test",
|
||||
|
||||
@@ -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 () => {})
|
||||
|
||||
@@ -10,6 +12,10 @@ mock.module("./cli-runner", () => ({
|
||||
processApplyPatchEditsWithCli,
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("./cli-runner", () => ({ ...realCliRunner }))
|
||||
})
|
||||
|
||||
const { createCommentCheckerHooks } = await import("./hook")
|
||||
|
||||
describe("comment-checker apply_patch integration", () => {
|
||||
|
||||
@@ -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", () => ({
|
||||
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 { TaskHistory } from "../../features/background-agent/task-history"
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ function createMockContext(todoResponses: TodoSnapshot[][]): PluginInput {
|
||||
},
|
||||
},
|
||||
directory: "/tmp/test",
|
||||
} as PluginInput
|
||||
} as unknown as PluginInput
|
||||
}
|
||||
|
||||
describe("compaction-todo-preserver", () => {
|
||||
@@ -38,7 +38,7 @@ describe("compaction-todo-preserver", () => {
|
||||
//#given
|
||||
updateMock.mockClear()
|
||||
const sessionID = "session-compaction-missing"
|
||||
const todos = [
|
||||
const todos: TodoSnapshot[] = [
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
{ id: "2", content: "Task 2", status: "in_progress", priority: "medium" },
|
||||
]
|
||||
@@ -58,7 +58,7 @@ describe("compaction-todo-preserver", () => {
|
||||
//#given
|
||||
updateMock.mockClear()
|
||||
const sessionID = "session-compaction-present"
|
||||
const todos = [
|
||||
const todos: TodoSnapshot[] = [
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
]
|
||||
const ctx = createMockContext([todos, todos])
|
||||
|
||||
@@ -1,34 +1,59 @@
|
||||
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 resolveFilePathMock = mock((_: string, path: string) => path)
|
||||
const loadInjectedPathsMock = mock((_: string) => new Set<string>())
|
||||
const saveInjectedPathsMock = mock((_: string, __: Set<string>) => {})
|
||||
|
||||
mock.module("node:fs", () => ({
|
||||
readFileSync: readFileSyncMock,
|
||||
}))
|
||||
afterAll(() => {
|
||||
mock.module("node:fs", () => ({ ...realNodeFs }))
|
||||
mock.module("./finder", () => ({ ...realFinder }))
|
||||
mock.module("./storage", () => ({ ...realStorage }))
|
||||
})
|
||||
|
||||
mock.module("./finder", () => ({
|
||||
findAgentsMdUp: findAgentsMdUpMock,
|
||||
resolveFilePath: resolveFilePathMock,
|
||||
}))
|
||||
|
||||
mock.module("./storage", () => ({
|
||||
loadInjectedPaths: loadInjectedPathsMock,
|
||||
saveInjectedPaths: saveInjectedPathsMock,
|
||||
}))
|
||||
|
||||
const { processFilePathForAgentsInjection } = await import("./injector")
|
||||
let processFilePathForAgentsInjection: typeof import("./injector").processFilePathForAgentsInjection
|
||||
|
||||
describe("processFilePathForAgentsInjection", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
readFileSyncMock.mockClear()
|
||||
findAgentsMdUpMock.mockClear()
|
||||
resolveFilePathMock.mockClear()
|
||||
loadInjectedPathsMock.mockClear()
|
||||
saveInjectedPathsMock.mockClear()
|
||||
|
||||
mock.module("node:fs", () => ({
|
||||
...realNodeFs,
|
||||
readFileSync: readFileSyncMock,
|
||||
}))
|
||||
|
||||
mock.module("./finder", () => ({
|
||||
findAgentsMdUp: findAgentsMdUpMock,
|
||||
resolveFilePath: resolveFilePathMock,
|
||||
}))
|
||||
|
||||
mock.module("./storage", () => ({
|
||||
loadInjectedPaths: loadInjectedPathsMock,
|
||||
saveInjectedPaths: saveInjectedPathsMock,
|
||||
}))
|
||||
|
||||
;({ processFilePathForAgentsInjection } = await import(`./injector?${Date.now()}`))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.module("node:fs", () => ({ ...realNodeFs }))
|
||||
mock.module("./finder", () => ({ ...realFinder }))
|
||||
mock.module("./storage", () => ({ ...realStorage }))
|
||||
})
|
||||
|
||||
it("does not save when all discovered paths are already cached", async () => {
|
||||
|
||||
@@ -1,34 +1,59 @@
|
||||
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 resolveFilePathMock = mock((_: string, path: string) => path)
|
||||
const loadInjectedPathsMock = mock((_: string) => new Set<string>())
|
||||
const saveInjectedPathsMock = mock((_: string, __: Set<string>) => {})
|
||||
|
||||
mock.module("node:fs", () => ({
|
||||
readFileSync: readFileSyncMock,
|
||||
}))
|
||||
afterAll(() => {
|
||||
mock.module("node:fs", () => ({ ...realNodeFs }))
|
||||
mock.module("./finder", () => ({ ...realFinder }))
|
||||
mock.module("./storage", () => ({ ...realStorage }))
|
||||
})
|
||||
|
||||
mock.module("./finder", () => ({
|
||||
findReadmeMdUp: findReadmeMdUpMock,
|
||||
resolveFilePath: resolveFilePathMock,
|
||||
}))
|
||||
|
||||
mock.module("./storage", () => ({
|
||||
loadInjectedPaths: loadInjectedPathsMock,
|
||||
saveInjectedPaths: saveInjectedPathsMock,
|
||||
}))
|
||||
|
||||
const { processFilePathForReadmeInjection } = await import("./injector")
|
||||
let processFilePathForReadmeInjection: typeof import("./injector").processFilePathForReadmeInjection
|
||||
|
||||
describe("processFilePathForReadmeInjection", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
readFileSyncMock.mockClear()
|
||||
findReadmeMdUpMock.mockClear()
|
||||
resolveFilePathMock.mockClear()
|
||||
loadInjectedPathsMock.mockClear()
|
||||
saveInjectedPathsMock.mockClear()
|
||||
|
||||
mock.module("node:fs", () => ({
|
||||
...realNodeFs,
|
||||
readFileSync: readFileSyncMock,
|
||||
}))
|
||||
|
||||
mock.module("./finder", () => ({
|
||||
findReadmeMdUp: findReadmeMdUpMock,
|
||||
resolveFilePath: resolveFilePathMock,
|
||||
}))
|
||||
|
||||
mock.module("./storage", () => ({
|
||||
loadInjectedPaths: loadInjectedPathsMock,
|
||||
saveInjectedPaths: saveInjectedPathsMock,
|
||||
}))
|
||||
|
||||
;({ processFilePathForReadmeInjection } = await import(`./injector?${Date.now()}`))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.module("node:fs", () => ({ ...realNodeFs }))
|
||||
mock.module("./finder", () => ({ ...realFinder }))
|
||||
mock.module("./storage", () => ({ ...realStorage }))
|
||||
})
|
||||
|
||||
it("does not save when all discovered paths are already cached", async () => {
|
||||
|
||||
@@ -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 { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
@@ -6,16 +6,6 @@ import { randomUUID } from "node:crypto"
|
||||
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
|
||||
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 { MESSAGE_STORAGE } = await import("../../features/hook-message-injector")
|
||||
|
||||
@@ -52,7 +42,6 @@ describe("prometheus-md-only", () => {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
rmSync(TEST_STORAGE_ROOT, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe("agent name matching", () => {
|
||||
|
||||
@@ -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 { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import * as os from "node:os";
|
||||
@@ -6,6 +6,10 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
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 };
|
||||
|
||||
let trackedRulePath = "";
|
||||
@@ -56,6 +60,12 @@ mock.module("./matcher", () => ({
|
||||
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 } {
|
||||
return { title: "tool", output: "", metadata: {} };
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
|
||||
//#then
|
||||
expect(promptCalls).toHaveLength(2)
|
||||
})
|
||||
}, 20000)
|
||||
|
||||
test("should keep injecting even when todos remain unchanged across cycles", async () => {
|
||||
//#given
|
||||
@@ -553,7 +553,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
|
||||
//#then — all 5 injections should fire (no stagnation cap)
|
||||
expect(promptCalls).toHaveLength(5)
|
||||
})
|
||||
}, 30000)
|
||||
|
||||
test("should skip idle handling while injection is in flight", async () => {
|
||||
//#given
|
||||
@@ -613,7 +613,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
|
||||
//#then
|
||||
expect(promptCalls).toHaveLength(2)
|
||||
})
|
||||
}, 20000)
|
||||
|
||||
test("should accept skipAgents option without error", async () => {
|
||||
// given - session with skipAgents configured for Prometheus
|
||||
|
||||
14
src/shared/agents-config-dir.test.ts
Normal file
14
src/shared/agents-config-dir.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
6
src/shared/agents-config-dir.ts
Normal file
6
src/shared/agents-config-dir.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
export function getAgentsConfigDir(): string {
|
||||
return join(homedir(), ".agents")
|
||||
}
|
||||
103
src/shared/file-utils.test.ts
Normal file
103
src/shared/file-utils.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test"
|
||||
import { mkdirSync, writeFileSync, symlinkSync, rmSync, realpathSync } from "fs"
|
||||
import { join } from "path"
|
||||
import { tmpdir } from "os"
|
||||
import { resolveSymlink, resolveSymlinkAsync, isSymbolicLink } from "./file-utils"
|
||||
|
||||
const testDir = join(realpathSync(tmpdir()), "file-utils-test-" + Date.now())
|
||||
|
||||
// Create a directory structure that mimics the real-world scenario:
|
||||
//
|
||||
// testDir/
|
||||
// ├── repo/
|
||||
// │ ├── skills/
|
||||
// │ │ └── category/
|
||||
// │ │ └── my-skill/
|
||||
// │ │ └── SKILL.md
|
||||
// │ └── .opencode/
|
||||
// │ └── skills/
|
||||
// │ └── my-skill -> ../../skills/category/my-skill (relative symlink)
|
||||
// └── config/
|
||||
// └── skills -> ../repo/.opencode/skills (absolute symlink)
|
||||
|
||||
const realSkillDir = join(testDir, "repo", "skills", "category", "my-skill")
|
||||
const repoOpencodeSkills = join(testDir, "repo", ".opencode", "skills")
|
||||
const configSkills = join(testDir, "config", "skills")
|
||||
|
||||
beforeAll(() => {
|
||||
// Create real skill directory with a file
|
||||
mkdirSync(realSkillDir, { recursive: true })
|
||||
writeFileSync(join(realSkillDir, "SKILL.md"), "# My Skill")
|
||||
|
||||
// Create .opencode/skills/ with a relative symlink to the real skill
|
||||
mkdirSync(repoOpencodeSkills, { recursive: true })
|
||||
symlinkSync("../../skills/category/my-skill", join(repoOpencodeSkills, "my-skill"))
|
||||
|
||||
// Create config/skills as an absolute symlink to .opencode/skills
|
||||
mkdirSync(join(testDir, "config"), { recursive: true })
|
||||
symlinkSync(repoOpencodeSkills, configSkills)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(testDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe("resolveSymlink", () => {
|
||||
it("resolves a regular file path to itself", () => {
|
||||
const filePath = join(realSkillDir, "SKILL.md")
|
||||
expect(resolveSymlink(filePath)).toBe(filePath)
|
||||
})
|
||||
|
||||
it("resolves a relative symlink to its real path", () => {
|
||||
const symlinkPath = join(repoOpencodeSkills, "my-skill")
|
||||
expect(resolveSymlink(symlinkPath)).toBe(realSkillDir)
|
||||
})
|
||||
|
||||
it("resolves a chained symlink (symlink-to-dir-containing-symlinks) to the real path", () => {
|
||||
// This is the real-world scenario:
|
||||
// config/skills/my-skill -> (follows config/skills) -> repo/.opencode/skills/my-skill -> repo/skills/category/my-skill
|
||||
const chainedPath = join(configSkills, "my-skill")
|
||||
expect(resolveSymlink(chainedPath)).toBe(realSkillDir)
|
||||
})
|
||||
|
||||
it("returns the original path for non-existent paths", () => {
|
||||
const fakePath = join(testDir, "does-not-exist")
|
||||
expect(resolveSymlink(fakePath)).toBe(fakePath)
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveSymlinkAsync", () => {
|
||||
it("resolves a regular file path to itself", async () => {
|
||||
const filePath = join(realSkillDir, "SKILL.md")
|
||||
expect(await resolveSymlinkAsync(filePath)).toBe(filePath)
|
||||
})
|
||||
|
||||
it("resolves a relative symlink to its real path", async () => {
|
||||
const symlinkPath = join(repoOpencodeSkills, "my-skill")
|
||||
expect(await resolveSymlinkAsync(symlinkPath)).toBe(realSkillDir)
|
||||
})
|
||||
|
||||
it("resolves a chained symlink (symlink-to-dir-containing-symlinks) to the real path", async () => {
|
||||
const chainedPath = join(configSkills, "my-skill")
|
||||
expect(await resolveSymlinkAsync(chainedPath)).toBe(realSkillDir)
|
||||
})
|
||||
|
||||
it("returns the original path for non-existent paths", async () => {
|
||||
const fakePath = join(testDir, "does-not-exist")
|
||||
expect(await resolveSymlinkAsync(fakePath)).toBe(fakePath)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isSymbolicLink", () => {
|
||||
it("returns true for a symlink", () => {
|
||||
expect(isSymbolicLink(join(repoOpencodeSkills, "my-skill"))).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false for a regular directory", () => {
|
||||
expect(isSymbolicLink(realSkillDir)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns false for a non-existent path", () => {
|
||||
expect(isSymbolicLink(join(testDir, "does-not-exist"))).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import { lstatSync, readlinkSync } from "fs"
|
||||
import { lstatSync, realpathSync } from "fs"
|
||||
import { promises as fs } from "fs"
|
||||
import { resolve } from "path"
|
||||
|
||||
export function isMarkdownFile(entry: { name: string; isFile: () => boolean }): boolean {
|
||||
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile()
|
||||
@@ -16,11 +15,7 @@ export function isSymbolicLink(filePath: string): boolean {
|
||||
|
||||
export function resolveSymlink(filePath: string): string {
|
||||
try {
|
||||
const stats = lstatSync(filePath, { throwIfNoEntry: false })
|
||||
if (stats?.isSymbolicLink()) {
|
||||
return resolve(filePath, "..", readlinkSync(filePath))
|
||||
}
|
||||
return filePath
|
||||
return realpathSync(filePath)
|
||||
} catch {
|
||||
return filePath
|
||||
}
|
||||
@@ -28,12 +23,7 @@ export function resolveSymlink(filePath: string): string {
|
||||
|
||||
export async function resolveSymlinkAsync(filePath: string): Promise<string> {
|
||||
try {
|
||||
const stats = await fs.lstat(filePath)
|
||||
if (stats.isSymbolicLink()) {
|
||||
const linkTarget = await fs.readlink(filePath)
|
||||
return resolve(filePath, "..", linkTarget)
|
||||
}
|
||||
return filePath
|
||||
return await fs.realpath(filePath)
|
||||
} catch {
|
||||
return filePath
|
||||
}
|
||||
|
||||
72
src/shared/session-tools-store.test.ts
Normal file
72
src/shared/session-tools-store.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, test, expect, beforeEach } from "bun:test"
|
||||
import { setSessionTools, getSessionTools, clearSessionTools } from "./session-tools-store"
|
||||
|
||||
describe("session-tools-store", () => {
|
||||
beforeEach(() => {
|
||||
clearSessionTools()
|
||||
})
|
||||
|
||||
test("returns undefined for unknown session", () => {
|
||||
//#given
|
||||
const sessionID = "ses_unknown"
|
||||
|
||||
//#when
|
||||
const result = getSessionTools(sessionID)
|
||||
|
||||
//#then
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
test("stores and retrieves tools for a session", () => {
|
||||
//#given
|
||||
const sessionID = "ses_abc123"
|
||||
const tools = { question: false, task: true, call_omo_agent: true }
|
||||
|
||||
//#when
|
||||
setSessionTools(sessionID, tools)
|
||||
const result = getSessionTools(sessionID)
|
||||
|
||||
//#then
|
||||
expect(result).toEqual({ question: false, task: true, call_omo_agent: true })
|
||||
})
|
||||
|
||||
test("overwrites existing tools for same session", () => {
|
||||
//#given
|
||||
const sessionID = "ses_abc123"
|
||||
setSessionTools(sessionID, { question: false })
|
||||
|
||||
//#when
|
||||
setSessionTools(sessionID, { question: true, task: false })
|
||||
const result = getSessionTools(sessionID)
|
||||
|
||||
//#then
|
||||
expect(result).toEqual({ question: true, task: false })
|
||||
})
|
||||
|
||||
test("clearSessionTools removes all entries", () => {
|
||||
//#given
|
||||
setSessionTools("ses_1", { question: false })
|
||||
setSessionTools("ses_2", { task: true })
|
||||
|
||||
//#when
|
||||
clearSessionTools()
|
||||
|
||||
//#then
|
||||
expect(getSessionTools("ses_1")).toBeUndefined()
|
||||
expect(getSessionTools("ses_2")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("returns a copy, not a reference", () => {
|
||||
//#given
|
||||
const sessionID = "ses_abc123"
|
||||
const tools = { question: false }
|
||||
setSessionTools(sessionID, tools)
|
||||
|
||||
//#when
|
||||
const result = getSessionTools(sessionID)!
|
||||
result.question = true
|
||||
|
||||
//#then
|
||||
expect(getSessionTools(sessionID)).toEqual({ question: false })
|
||||
})
|
||||
})
|
||||
14
src/shared/session-tools-store.ts
Normal file
14
src/shared/session-tools-store.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const store = new Map<string, Record<string, boolean>>()
|
||||
|
||||
export function setSessionTools(sessionID: string, tools: Record<string, boolean>): void {
|
||||
store.set(sessionID, { ...tools })
|
||||
}
|
||||
|
||||
export function getSessionTools(sessionID: string): Record<string, boolean> | undefined {
|
||||
const tools = store.get(sessionID)
|
||||
return tools ? { ...tools } : undefined
|
||||
}
|
||||
|
||||
export function clearSessionTools(): void {
|
||||
store.clear()
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { log } from "../../shared"
|
||||
import type { CallOmoAgentArgs } from "./types"
|
||||
import type { ToolContextWithMetadata } from "./tool-context-with-metadata"
|
||||
import { getMessageDir } from "./message-storage-directory"
|
||||
import { getSessionTools } from "../../shared/session-tools-store"
|
||||
|
||||
export async function executeBackgroundAgent(
|
||||
args: CallOmoAgentArgs,
|
||||
@@ -36,6 +37,7 @@ export async function executeBackgroundAgent(
|
||||
parentSessionID: toolContext.sessionID,
|
||||
parentMessageID: toolContext.messageID,
|
||||
parentAgent,
|
||||
parentTools: getSessionTools(toolContext.sessionID),
|
||||
})
|
||||
|
||||
const waitStart = Date.now()
|
||||
|
||||
@@ -5,6 +5,7 @@ import { consumeNewMessages } from "../../shared/session-cursor"
|
||||
import { findFirstMessageWithAgent, findNearestMessageWithFields } from "../../features/hook-message-injector"
|
||||
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||
import { getMessageDir } from "./message-dir"
|
||||
import { getSessionTools } from "../../shared/session-tools-store"
|
||||
|
||||
export async function executeBackground(
|
||||
args: CallOmoAgentArgs,
|
||||
@@ -41,6 +42,7 @@ export async function executeBackground(
|
||||
parentSessionID: toolContext.sessionID,
|
||||
parentMessageID: toolContext.messageID,
|
||||
parentAgent,
|
||||
parentTools: getSessionTools(toolContext.sessionID),
|
||||
})
|
||||
|
||||
const WAIT_FOR_SESSION_INTERVAL_MS = 50
|
||||
|
||||
@@ -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", () => ({
|
||||
createOrGetSession: mock(async () => ({ sessionID: "ses-test-123" })),
|
||||
}))
|
||||
const realCompletionPoller = require("./completion-poller")
|
||||
const realMessageProcessor = require("./message-processor")
|
||||
|
||||
mock.module("./completion-poller", () => ({
|
||||
waitForCompletion: mock(async () => {}),
|
||||
}))
|
||||
const waitForCompletionMock = mock(async () => {})
|
||||
const processMessagesMock = mock(async () => "agent response")
|
||||
|
||||
mock.module("./message-processor", () => ({
|
||||
processMessages: mock(async () => "agent response"),
|
||||
}))
|
||||
beforeEach(() => {
|
||||
waitForCompletionMock.mockClear()
|
||||
processMessagesMock.mockClear()
|
||||
|
||||
mock.module("./completion-poller", () => ({
|
||||
waitForCompletion: waitForCompletionMock,
|
||||
}))
|
||||
|
||||
mock.module("./message-processor", () => ({
|
||||
processMessages: processMessagesMock,
|
||||
}))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.module("./completion-poller", () => ({ ...realCompletionPoller }))
|
||||
mock.module("./message-processor", () => ({ ...realMessageProcessor }))
|
||||
})
|
||||
|
||||
describe("executeSync", () => {
|
||||
test("passes question=false via tools parameter to block question tool", async () => {
|
||||
@@ -27,6 +39,7 @@ describe("executeSync", () => {
|
||||
subagent_type: "explore",
|
||||
description: "test task",
|
||||
prompt: "find something",
|
||||
session_id: "ses-test-123",
|
||||
}
|
||||
|
||||
const toolContext = {
|
||||
@@ -39,7 +52,10 @@ describe("executeSync", () => {
|
||||
|
||||
const ctx = {
|
||||
client: {
|
||||
session: { promptAsync },
|
||||
session: {
|
||||
promptAsync,
|
||||
get: mock(async () => ({ data: { id: "ses-test-123" } })),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -65,6 +81,7 @@ describe("executeSync", () => {
|
||||
subagent_type: "librarian",
|
||||
description: "search docs",
|
||||
prompt: "find docs",
|
||||
session_id: "ses-test-123",
|
||||
}
|
||||
|
||||
const toolContext = {
|
||||
@@ -77,7 +94,10 @@ describe("executeSync", () => {
|
||||
|
||||
const ctx = {
|
||||
client: {
|
||||
session: { promptAsync },
|
||||
session: {
|
||||
promptAsync,
|
||||
get: mock(async () => ({ data: { id: "ses-test-123" } })),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types"
|
||||
import type { ExecutorContext, ParentContext } from "./executor-types"
|
||||
import { storeToolMetadata } from "../../features/tool-metadata-store"
|
||||
import { formatDetailedError } from "./error-formatting"
|
||||
import { getSessionTools } from "../../shared/session-tools-store"
|
||||
|
||||
export async function executeBackgroundContinuation(
|
||||
args: DelegateTaskArgs,
|
||||
@@ -19,6 +20,7 @@ export async function executeBackgroundContinuation(
|
||||
parentMessageID: parentContext.messageID,
|
||||
parentModel: parentContext.model,
|
||||
parentAgent: parentContext.agent,
|
||||
parentTools: getSessionTools(parentContext.sessionID),
|
||||
})
|
||||
|
||||
const bgContMeta = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ExecutorContext, ParentContext } from "./executor-types"
|
||||
import { getTimingConfig } from "./timing"
|
||||
import { storeToolMetadata } from "../../features/tool-metadata-store"
|
||||
import { formatDetailedError } from "./error-formatting"
|
||||
import { getSessionTools } from "../../shared/session-tools-store"
|
||||
|
||||
export async function executeBackgroundTask(
|
||||
args: DelegateTaskArgs,
|
||||
@@ -24,6 +25,7 @@ export async function executeBackgroundTask(
|
||||
parentMessageID: parentContext.messageID,
|
||||
parentModel: parentContext.model,
|
||||
parentAgent: parentContext.agent,
|
||||
parentTools: getSessionTools(parentContext.sessionID),
|
||||
model: categoryModel,
|
||||
skills: args.load_skills.length > 0 ? args.load_skills : undefined,
|
||||
skillContent: systemContent,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-re
|
||||
import { findNearestMessageWithFields } from "../../features/hook-message-injector"
|
||||
import { formatDuration } from "./time-formatter"
|
||||
import { syncContinuationDeps, type SyncContinuationDeps } from "./sync-continuation-deps"
|
||||
import { setSessionTools } from "../../shared/session-tools-store"
|
||||
|
||||
export async function executeSyncContinuation(
|
||||
args: DelegateTaskArgs,
|
||||
@@ -77,6 +78,13 @@ export async function executeSyncContinuation(
|
||||
}
|
||||
|
||||
const allowTask = isPlanFamily(resumeAgent)
|
||||
const tools = {
|
||||
...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}),
|
||||
task: allowTask,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
}
|
||||
setSessionTools(args.session_id!, tools)
|
||||
|
||||
await promptWithModelSuggestionRetry(client, {
|
||||
path: { id: args.session_id! },
|
||||
@@ -84,12 +92,7 @@ export async function executeSyncContinuation(
|
||||
...(resumeAgent !== undefined ? { agent: resumeAgent } : {}),
|
||||
...(resumeModel !== undefined ? { model: resumeModel } : {}),
|
||||
...(resumeVariant !== undefined ? { variant: resumeVariant } : {}),
|
||||
tools: {
|
||||
...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}),
|
||||
task: allowTask,
|
||||
call_omo_agent: true, // Intentionally overrides restrictions - continuation context needs delegation capability even for restricted agents
|
||||
question: false,
|
||||
},
|
||||
tools,
|
||||
parts: [{ type: "text", text: args.prompt }],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isPlanFamily } from "./constants"
|
||||
import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry"
|
||||
import { formatDetailedError } from "./error-formatting"
|
||||
import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions"
|
||||
import { setSessionTools } from "../../shared/session-tools-store"
|
||||
|
||||
export async function sendSyncPrompt(
|
||||
client: OpencodeClient,
|
||||
@@ -18,17 +19,19 @@ export async function sendSyncPrompt(
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const allowTask = isPlanFamily(input.agentToUse)
|
||||
const tools = {
|
||||
task: allowTask,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
...getAgentToolRestrictions(input.agentToUse),
|
||||
}
|
||||
setSessionTools(input.sessionID, tools)
|
||||
await promptWithModelSuggestionRetry(client, {
|
||||
path: { id: input.sessionID },
|
||||
body: {
|
||||
agent: input.agentToUse,
|
||||
system: input.systemContent,
|
||||
tools: {
|
||||
task: allowTask,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
...getAgentToolRestrictions(input.agentToUse),
|
||||
},
|
||||
tools,
|
||||
parts: [{ type: "text", text: input.args.prompt }],
|
||||
...(input.categoryModel ? { model: { providerID: input.categoryModel.providerID, modelID: input.categoryModel.modelID } } : {}),
|
||||
...(input.categoryModel?.variant ? { variant: input.categoryModel.variant } : {}),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getTimingConfig } from "./timing"
|
||||
import { storeToolMetadata } from "../../features/tool-metadata-store"
|
||||
import { formatDuration } from "./time-formatter"
|
||||
import { formatDetailedError } from "./error-formatting"
|
||||
import { getSessionTools } from "../../shared/session-tools-store"
|
||||
|
||||
export async function executeUnstableAgentTask(
|
||||
args: DelegateTaskArgs,
|
||||
@@ -26,6 +27,7 @@ export async function executeUnstableAgentTask(
|
||||
parentMessageID: parentContext.messageID,
|
||||
parentModel: parentContext.model,
|
||||
parentAgent: parentContext.agent,
|
||||
parentTools: getSessionTools(parentContext.sessionID),
|
||||
model: categoryModel,
|
||||
skills: args.load_skills.length > 0 ? args.load_skills : undefined,
|
||||
skillContent: systemContent,
|
||||
|
||||
@@ -2,7 +2,9 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
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", () => ({
|
||||
createMessageConnection: () => {
|
||||
@@ -12,6 +14,10 @@ mock.module("vscode-jsonrpc/node", () => ({
|
||||
StreamMessageWriter: function StreamMessageWriter() {},
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("vscode-jsonrpc/node", () => ({ ...realJsonRpcNode }))
|
||||
})
|
||||
|
||||
import { LSPClient, lspManager, validateCwd } from "./client"
|
||||
import type { ResolvedServer } from "./types"
|
||||
|
||||
|
||||
@@ -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 { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
import { randomUUID } from "node:crypto"
|
||||
|
||||
const realConstants = await import("./constants")
|
||||
|
||||
const TEST_DIR = join(tmpdir(), `omo-test-session-manager-${randomUUID()}`)
|
||||
const TEST_MESSAGE_STORAGE = join(TEST_DIR, "message")
|
||||
const TEST_PART_STORAGE = join(TEST_DIR, "part")
|
||||
@@ -26,6 +28,10 @@ mock.module("./constants", () => ({
|
||||
TOOL_NAME_PREFIX: "session_",
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
mock.module("./constants", () => ({ ...realConstants }))
|
||||
})
|
||||
|
||||
const { getAllSessions, getMessageDir, sessionExists, readSessionMessages, readSessionTodos, getSessionInfo } =
|
||||
await import("./storage")
|
||||
|
||||
|
||||
@@ -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 * as fs from "node:fs"
|
||||
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 realNodeFs = await import("node:fs")
|
||||
|
||||
mock.module("node:fs", () => ({
|
||||
...fs,
|
||||
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 {
|
||||
return {
|
||||
name,
|
||||
|
||||
Reference in New Issue
Block a user