opencastle 0.26.1 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/bin/cli.mjs +10 -0
- package/dist/cli/agents.d.ts +3 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +161 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/baselines.d.ts +3 -0
- package/dist/cli/baselines.d.ts.map +1 -0
- package/dist/cli/baselines.js +128 -0
- package/dist/cli/baselines.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts +68 -2
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +2102 -26
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +1572 -70
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/events.d.ts +4 -1
- package/dist/cli/convoy/events.d.ts.map +1 -1
- package/dist/cli/convoy/events.js +74 -13
- package/dist/cli/convoy/events.js.map +1 -1
- package/dist/cli/convoy/events.test.js +154 -27
- package/dist/cli/convoy/events.test.js.map +1 -1
- package/dist/cli/convoy/expertise.d.ts +16 -0
- package/dist/cli/convoy/expertise.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.js +121 -0
- package/dist/cli/convoy/expertise.js.map +1 -0
- package/dist/cli/convoy/expertise.test.d.ts +2 -0
- package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.test.js +96 -0
- package/dist/cli/convoy/expertise.test.js.map +1 -0
- package/dist/cli/convoy/export.test.js +1 -0
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/formula.d.ts +19 -0
- package/dist/cli/convoy/formula.d.ts.map +1 -0
- package/dist/cli/convoy/formula.js +142 -0
- package/dist/cli/convoy/formula.js.map +1 -0
- package/dist/cli/convoy/formula.test.d.ts +2 -0
- package/dist/cli/convoy/formula.test.d.ts.map +1 -0
- package/dist/cli/convoy/formula.test.js +342 -0
- package/dist/cli/convoy/formula.test.js.map +1 -0
- package/dist/cli/convoy/gates.d.ts +128 -0
- package/dist/cli/convoy/gates.d.ts.map +1 -0
- package/dist/cli/convoy/gates.js +606 -0
- package/dist/cli/convoy/gates.js.map +1 -0
- package/dist/cli/convoy/gates.test.d.ts +2 -0
- package/dist/cli/convoy/gates.test.d.ts.map +1 -0
- package/dist/cli/convoy/gates.test.js +976 -0
- package/dist/cli/convoy/gates.test.js.map +1 -0
- package/dist/cli/convoy/health.d.ts +11 -0
- package/dist/cli/convoy/health.d.ts.map +1 -1
- package/dist/cli/convoy/health.js +54 -0
- package/dist/cli/convoy/health.js.map +1 -1
- package/dist/cli/convoy/health.test.js +56 -1
- package/dist/cli/convoy/health.test.js.map +1 -1
- package/dist/cli/convoy/issues.d.ts +8 -0
- package/dist/cli/convoy/issues.d.ts.map +1 -0
- package/dist/cli/convoy/issues.js +98 -0
- package/dist/cli/convoy/issues.js.map +1 -0
- package/dist/cli/convoy/issues.test.d.ts +2 -0
- package/dist/cli/convoy/issues.test.d.ts.map +1 -0
- package/dist/cli/convoy/issues.test.js +107 -0
- package/dist/cli/convoy/issues.test.js.map +1 -0
- package/dist/cli/convoy/knowledge.d.ts +5 -0
- package/dist/cli/convoy/knowledge.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.js +116 -0
- package/dist/cli/convoy/knowledge.js.map +1 -0
- package/dist/cli/convoy/knowledge.test.d.ts +2 -0
- package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.test.js +87 -0
- package/dist/cli/convoy/knowledge.test.js.map +1 -0
- package/dist/cli/convoy/lessons.d.ts +17 -0
- package/dist/cli/convoy/lessons.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.js +149 -0
- package/dist/cli/convoy/lessons.js.map +1 -0
- package/dist/cli/convoy/lessons.test.d.ts +2 -0
- package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.test.js +135 -0
- package/dist/cli/convoy/lessons.test.js.map +1 -0
- package/dist/cli/convoy/lock.d.ts +13 -0
- package/dist/cli/convoy/lock.d.ts.map +1 -0
- package/dist/cli/convoy/lock.js +88 -0
- package/dist/cli/convoy/lock.js.map +1 -0
- package/dist/cli/convoy/lock.test.d.ts +2 -0
- package/dist/cli/convoy/lock.test.d.ts.map +1 -0
- package/dist/cli/convoy/lock.test.js +136 -0
- package/dist/cli/convoy/lock.test.js.map +1 -0
- package/dist/cli/convoy/merge.d.ts +4 -0
- package/dist/cli/convoy/merge.d.ts.map +1 -1
- package/dist/cli/convoy/merge.js +18 -1
- package/dist/cli/convoy/merge.js.map +1 -1
- package/dist/cli/convoy/merge.test.js +6 -7
- package/dist/cli/convoy/merge.test.js.map +1 -1
- package/dist/cli/convoy/partition.d.ts +51 -0
- package/dist/cli/convoy/partition.d.ts.map +1 -0
- package/dist/cli/convoy/partition.js +186 -0
- package/dist/cli/convoy/partition.js.map +1 -0
- package/dist/cli/convoy/partition.test.d.ts +2 -0
- package/dist/cli/convoy/partition.test.d.ts.map +1 -0
- package/dist/cli/convoy/partition.test.js +315 -0
- package/dist/cli/convoy/partition.test.js.map +1 -0
- package/dist/cli/convoy/pipeline.test.js +6 -0
- package/dist/cli/convoy/pipeline.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +47 -5
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +525 -19
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +1345 -12
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +156 -2
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/destroy.d.ts +3 -0
- package/dist/cli/destroy.d.ts.map +1 -0
- package/dist/cli/destroy.js +69 -0
- package/dist/cli/destroy.js.map +1 -0
- package/dist/cli/destroy.test.d.ts +2 -0
- package/dist/cli/destroy.test.d.ts.map +1 -0
- package/dist/cli/destroy.test.js +116 -0
- package/dist/cli/destroy.test.js.map +1 -0
- package/dist/cli/gitignore.d.ts +9 -0
- package/dist/cli/gitignore.d.ts.map +1 -1
- package/dist/cli/gitignore.js +29 -0
- package/dist/cli/gitignore.js.map +1 -1
- package/dist/cli/plan.d.ts +3 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +288 -0
- package/dist/cli/plan.js.map +1 -0
- package/dist/cli/run/adapters/claude.d.ts +2 -0
- package/dist/cli/run/adapters/claude.d.ts.map +1 -1
- package/dist/cli/run/adapters/claude.js +89 -49
- package/dist/cli/run/adapters/claude.js.map +1 -1
- package/dist/cli/run/adapters/claude.test.d.ts +2 -0
- package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/claude.test.js +205 -0
- package/dist/cli/run/adapters/claude.test.js.map +1 -0
- package/dist/cli/run/adapters/copilot.d.ts +1 -0
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +84 -46
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
- package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/copilot.test.js +195 -0
- package/dist/cli/run/adapters/copilot.test.js.map +1 -0
- package/dist/cli/run/adapters/cursor.d.ts +1 -0
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/run/adapters/cursor.js +83 -47
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
- package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/cursor.test.js +129 -0
- package/dist/cli/run/adapters/cursor.test.js.map +1 -0
- package/dist/cli/run/adapters/opencode.d.ts +1 -0
- package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/run/adapters/opencode.js +81 -47
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
- package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/opencode.test.js +119 -0
- package/dist/cli/run/adapters/opencode.test.js.map +1 -0
- package/dist/cli/run/executor.js +1 -1
- package/dist/cli/run/executor.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +245 -4
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +669 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +362 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +85 -2
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js.map +1 -1
- package/dist/cli/watch.d.ts +15 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +279 -0
- package/dist/cli/watch.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/agents.ts +177 -0
- package/src/cli/baselines.ts +143 -0
- package/src/cli/convoy/engine.test.ts +1839 -70
- package/src/cli/convoy/engine.ts +2417 -38
- package/src/cli/convoy/events.test.ts +179 -38
- package/src/cli/convoy/events.ts +88 -16
- package/src/cli/convoy/expertise.test.ts +128 -0
- package/src/cli/convoy/expertise.ts +163 -0
- package/src/cli/convoy/export.test.ts +1 -0
- package/src/cli/convoy/formula.test.ts +405 -0
- package/src/cli/convoy/formula.ts +174 -0
- package/src/cli/convoy/gates.test.ts +1169 -0
- package/src/cli/convoy/gates.ts +774 -0
- package/src/cli/convoy/health.test.ts +64 -2
- package/src/cli/convoy/health.ts +80 -2
- package/src/cli/convoy/issues.test.ts +143 -0
- package/src/cli/convoy/issues.ts +136 -0
- package/src/cli/convoy/knowledge.test.ts +101 -0
- package/src/cli/convoy/knowledge.ts +132 -0
- package/src/cli/convoy/lessons.test.ts +188 -0
- package/src/cli/convoy/lessons.ts +164 -0
- package/src/cli/convoy/lock.test.ts +181 -0
- package/src/cli/convoy/lock.ts +103 -0
- package/src/cli/convoy/merge.test.ts +6 -7
- package/src/cli/convoy/merge.ts +19 -1
- package/src/cli/convoy/partition.test.ts +423 -0
- package/src/cli/convoy/partition.ts +232 -0
- package/src/cli/convoy/pipeline.test.ts +6 -0
- package/src/cli/convoy/store.test.ts +1512 -14
- package/src/cli/convoy/store.ts +676 -30
- package/src/cli/convoy/types.ts +170 -1
- package/src/cli/destroy.test.ts +141 -0
- package/src/cli/destroy.ts +88 -0
- package/src/cli/gitignore.ts +36 -0
- package/src/cli/plan.ts +316 -0
- package/src/cli/run/adapters/claude.test.ts +234 -0
- package/src/cli/run/adapters/claude.ts +45 -5
- package/src/cli/run/adapters/copilot.test.ts +224 -0
- package/src/cli/run/adapters/copilot.ts +34 -4
- package/src/cli/run/adapters/cursor.test.ts +144 -0
- package/src/cli/run/adapters/cursor.ts +33 -2
- package/src/cli/run/adapters/opencode.test.ts +135 -0
- package/src/cli/run/adapters/opencode.ts +30 -2
- package/src/cli/run/executor.ts +1 -1
- package/src/cli/run/schema.test.ts +758 -0
- package/src/cli/run/schema.ts +300 -25
- package/src/cli/run.ts +341 -21
- package/src/cli/types.ts +86 -1
- package/src/cli/watch.ts +298 -0
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { EventEmitter } from 'node:events'
|
|
6
|
+
import type { Task } from '../../types.js'
|
|
7
|
+
|
|
8
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeTask(): Task {
|
|
11
|
+
return {
|
|
12
|
+
id: 'test-task',
|
|
13
|
+
agent: 'developer',
|
|
14
|
+
prompt: 'Do something',
|
|
15
|
+
files: [],
|
|
16
|
+
timeout: '5m',
|
|
17
|
+
depends_on: [],
|
|
18
|
+
description: 'test task',
|
|
19
|
+
max_retries: 0,
|
|
20
|
+
} as unknown as Task
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeMockProc(exitCode = 0, stdoutData = '{"result":"ok"}') {
|
|
24
|
+
const proc = new EventEmitter() as EventEmitter & {
|
|
25
|
+
stdout: EventEmitter
|
|
26
|
+
stderr: EventEmitter
|
|
27
|
+
killed: boolean
|
|
28
|
+
kill: ReturnType<typeof vi.fn>
|
|
29
|
+
}
|
|
30
|
+
proc.stdout = new EventEmitter()
|
|
31
|
+
proc.stderr = new EventEmitter()
|
|
32
|
+
proc.killed = false
|
|
33
|
+
proc.kill = vi.fn()
|
|
34
|
+
process.nextTick(() => {
|
|
35
|
+
if (stdoutData) proc.stdout.emit('data', Buffer.from(stdoutData))
|
|
36
|
+
proc.emit('close', exitCode)
|
|
37
|
+
})
|
|
38
|
+
return proc
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── SDK mode ──────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
describe('copilot adapter — SDK mode', () => {
|
|
44
|
+
let mockCreateSession: ReturnType<typeof vi.fn>
|
|
45
|
+
let mockSession: {
|
|
46
|
+
sendAndWait: ReturnType<typeof vi.fn>
|
|
47
|
+
on: ReturnType<typeof vi.fn>
|
|
48
|
+
destroy: ReturnType<typeof vi.fn>
|
|
49
|
+
abort: ReturnType<typeof vi.fn>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.resetModules()
|
|
54
|
+
mockSession = {
|
|
55
|
+
sendAndWait: vi.fn().mockResolvedValue({ data: { content: 'I did the task' } }),
|
|
56
|
+
on: vi.fn(),
|
|
57
|
+
destroy: vi.fn().mockResolvedValue(undefined),
|
|
58
|
+
abort: vi.fn().mockResolvedValue(undefined),
|
|
59
|
+
}
|
|
60
|
+
mockCreateSession = vi.fn().mockResolvedValue(mockSession)
|
|
61
|
+
vi.doMock('@github/copilot-sdk', () => {
|
|
62
|
+
// Must use a regular function (not arrow) so `new CopilotClient()` works
|
|
63
|
+
function MockCopilotClient(this: Record<string, unknown>) {
|
|
64
|
+
this.start = vi.fn().mockResolvedValue(undefined)
|
|
65
|
+
this.createSession = mockCreateSession
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
CopilotClient: MockCopilotClient,
|
|
69
|
+
approveAll: vi.fn(),
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
vi.restoreAllMocks()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('passes mcpServers to createSession when provided', async () => {
|
|
79
|
+
const { execute } = await import('./copilot.js')
|
|
80
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
81
|
+
await execute(makeTask(), { mcpServers })
|
|
82
|
+
expect(mockCreateSession).toHaveBeenCalledWith(
|
|
83
|
+
expect.objectContaining({ mcpServers }),
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('does NOT include mcpServers in createSession when not provided', async () => {
|
|
88
|
+
const { execute } = await import('./copilot.js')
|
|
89
|
+
await execute(makeTask(), {})
|
|
90
|
+
const callArg = mockCreateSession.mock.calls[0]?.[0] as Record<string, unknown>
|
|
91
|
+
expect(callArg).not.toHaveProperty('mcpServers')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('does NOT include mcpServers when mcpServers is empty array', async () => {
|
|
95
|
+
const { execute } = await import('./copilot.js')
|
|
96
|
+
await execute(makeTask(), { mcpServers: [] })
|
|
97
|
+
const callArg = mockCreateSession.mock.calls[0]?.[0] as Record<string, unknown>
|
|
98
|
+
expect(callArg).not.toHaveProperty('mcpServers')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// ── CLI mode ──────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
describe('copilot adapter — CLI mode', () => {
|
|
105
|
+
let tmpDir: string
|
|
106
|
+
let mockSpawn: ReturnType<typeof vi.fn>
|
|
107
|
+
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
vi.resetModules()
|
|
110
|
+
tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'copilot-test-')))
|
|
111
|
+
|
|
112
|
+
// Make SDK unavailable so the adapter falls through to CLI
|
|
113
|
+
vi.doMock('@github/copilot-sdk', () => {
|
|
114
|
+
throw new Error('Module not found: @github/copilot-sdk')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
mockSpawn = vi.fn().mockImplementation((cmd: string) => {
|
|
118
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
119
|
+
return makeMockProc(0, '{"result":"ok"}')
|
|
120
|
+
})
|
|
121
|
+
vi.doMock('node:child_process', () => ({ spawn: mockSpawn }))
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
126
|
+
vi.restoreAllMocks()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('writes mcp.json to cwd with correct format when mcpServers provided', async () => {
|
|
130
|
+
let capturedContent: string | null = null
|
|
131
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
132
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
133
|
+
// mcp.json should exist at this point
|
|
134
|
+
const mcpPath = join(tmpDir, 'mcp.json')
|
|
135
|
+
if (existsSync(mcpPath)) {
|
|
136
|
+
capturedContent = readFileSync(mcpPath, 'utf8')
|
|
137
|
+
}
|
|
138
|
+
return makeMockProc(0, '{}')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const { execute } = await import('./copilot.js')
|
|
142
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
143
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
144
|
+
|
|
145
|
+
expect(capturedContent).not.toBeNull()
|
|
146
|
+
expect(JSON.parse(capturedContent!)).toEqual({
|
|
147
|
+
mcpServers: { 'my-mcp': { command: 'node', args: ['server.js'] } },
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('cleans up mcp.json after successful execution', async () => {
|
|
152
|
+
const { execute } = await import('./copilot.js')
|
|
153
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
154
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
155
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('cleans up mcp.json after failed execution (non-zero exit)', async () => {
|
|
159
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
160
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
161
|
+
return makeMockProc(1, '') // non-zero exit
|
|
162
|
+
})
|
|
163
|
+
const { execute } = await import('./copilot.js')
|
|
164
|
+
const mcpServers = [{ name: 'err-mcp', type: 'local', command: 'node', args: [] }]
|
|
165
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
166
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('includes --approve-mcps flag when mcp_approve_all is true', async () => {
|
|
170
|
+
const capturedArgs: string[] = []
|
|
171
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
172
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
173
|
+
capturedArgs.push(...args)
|
|
174
|
+
return makeMockProc(0, '{}')
|
|
175
|
+
})
|
|
176
|
+
const { execute } = await import('./copilot.js')
|
|
177
|
+
await execute(makeTask(), { mcp_approve_all: true, cwd: tmpDir })
|
|
178
|
+
expect(capturedArgs).toContain('--approve-mcps')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('does NOT write mcp.json when mcpServers not configured', async () => {
|
|
182
|
+
const { execute } = await import('./copilot.js')
|
|
183
|
+
await execute(makeTask(), { cwd: tmpDir })
|
|
184
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('does NOT add --approve-mcps when mcp_approve_all is not set', async () => {
|
|
188
|
+
const capturedArgs: string[] = []
|
|
189
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
190
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
191
|
+
capturedArgs.push(...args)
|
|
192
|
+
return makeMockProc(0, '{}')
|
|
193
|
+
})
|
|
194
|
+
const { execute } = await import('./copilot.js')
|
|
195
|
+
await execute(makeTask(), { cwd: tmpDir })
|
|
196
|
+
expect(capturedArgs).not.toContain('--approve-mcps')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('maps mcpServers with url and config into mcp.json', async () => {
|
|
200
|
+
let capturedContent: string | null = null
|
|
201
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
202
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
203
|
+
const mcpPath = join(tmpDir, 'mcp.json')
|
|
204
|
+
if (existsSync(mcpPath)) capturedContent = readFileSync(mcpPath, 'utf8')
|
|
205
|
+
return makeMockProc(0, '{}')
|
|
206
|
+
})
|
|
207
|
+
const { execute } = await import('./copilot.js')
|
|
208
|
+
const mcpServers = [
|
|
209
|
+
{
|
|
210
|
+
name: 'remote-mcp',
|
|
211
|
+
type: 'remote',
|
|
212
|
+
url: 'http://localhost:9000',
|
|
213
|
+
config: { token: 'abc' },
|
|
214
|
+
},
|
|
215
|
+
]
|
|
216
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
217
|
+
expect(capturedContent).not.toBeNull()
|
|
218
|
+
const parsed = JSON.parse(capturedContent!) as { mcpServers: Record<string, Record<string, unknown>> }
|
|
219
|
+
expect(parsed.mcpServers['remote-mcp']).toMatchObject({
|
|
220
|
+
url: 'http://localhost:9000',
|
|
221
|
+
token: 'abc',
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
})
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
|
|
2
2
|
import { spawn } from 'node:child_process'
|
|
3
|
-
import
|
|
3
|
+
import { writeFileSync, unlinkSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import type { CopilotClient as CopilotClientType, CopilotSession, PermissionHandler, SessionConfig } from '@github/copilot-sdk'
|
|
4
6
|
import { parseTimeout } from '../schema.js'
|
|
5
7
|
import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
|
|
6
8
|
|
|
7
9
|
// Adapter name
|
|
8
10
|
export const name = 'copilot'
|
|
9
11
|
|
|
12
|
+
export function supportsSessionContinuity(): boolean { return true }
|
|
10
13
|
// --- Unified adapter: SDK first, fallback to CLI ---
|
|
11
14
|
let mode: 'sdk' | 'cli' | null = null
|
|
12
15
|
|
|
@@ -83,7 +86,9 @@ async function executeViaSdk(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
83
86
|
},
|
|
84
87
|
infiniteSessions: { enabled: false },
|
|
85
88
|
...(options.verbose ? { streaming: true } : {}),
|
|
86
|
-
|
|
89
|
+
// mcpServers is forward-compatible: field will be recognised by future SDK versions
|
|
90
|
+
...(options.mcpServers?.length ? { mcpServers: options.mcpServers } : {}),
|
|
91
|
+
} as SessionConfig)
|
|
87
92
|
activeSessions.set(task.id, session)
|
|
88
93
|
if (options.verbose) {
|
|
89
94
|
session.on('assistant.message_delta', (event: { data: { deltaContent: string } }) => {
|
|
@@ -143,11 +148,31 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
143
148
|
'--max-turns',
|
|
144
149
|
'50',
|
|
145
150
|
]
|
|
146
|
-
|
|
151
|
+
const cwd = options?.cwd ?? process.cwd()
|
|
152
|
+
const mcpJsonPath = join(cwd, 'mcp.json')
|
|
153
|
+
let wroteJson = false
|
|
154
|
+
if (options.mcpServers?.length) {
|
|
155
|
+
const mcpJson: Record<string, Record<string, unknown>> = {}
|
|
156
|
+
for (const server of options.mcpServers) {
|
|
157
|
+
const entry: Record<string, unknown> = {}
|
|
158
|
+
if (server.command) entry.command = server.command
|
|
159
|
+
if (server.args) entry.args = server.args
|
|
160
|
+
if (server.url) entry.url = server.url
|
|
161
|
+
if (server.config) Object.assign(entry, server.config)
|
|
162
|
+
mcpJson[server.name] = entry
|
|
163
|
+
}
|
|
164
|
+
writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: mcpJson }, null, 2), 'utf8')
|
|
165
|
+
wroteJson = true
|
|
166
|
+
}
|
|
167
|
+
if (options.mcp_approve_all) {
|
|
168
|
+
args.push('--approve-mcps')
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
return await new Promise<ExecuteResult>((resolve) => {
|
|
147
172
|
const proc = spawn('copilot', args, {
|
|
148
173
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
149
174
|
env: { ...process.env },
|
|
150
|
-
cwd
|
|
175
|
+
cwd,
|
|
151
176
|
})
|
|
152
177
|
let stdout = ''
|
|
153
178
|
let stderr = ''
|
|
@@ -192,6 +217,11 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
192
217
|
})
|
|
193
218
|
task._process = proc
|
|
194
219
|
})
|
|
220
|
+
} finally {
|
|
221
|
+
if (wroteJson) {
|
|
222
|
+
try { unlinkSync(mcpJsonPath) } catch { /* ignore */ }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
195
225
|
}
|
|
196
226
|
|
|
197
227
|
function killCli(task: Task): void {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { EventEmitter } from 'node:events'
|
|
6
|
+
import type { Task } from '../../types.js'
|
|
7
|
+
|
|
8
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeTask(): Task {
|
|
11
|
+
return {
|
|
12
|
+
id: 'test-task',
|
|
13
|
+
agent: 'developer',
|
|
14
|
+
prompt: 'Do something',
|
|
15
|
+
files: [],
|
|
16
|
+
timeout: '5m',
|
|
17
|
+
depends_on: [],
|
|
18
|
+
description: 'test task',
|
|
19
|
+
max_retries: 0,
|
|
20
|
+
} as unknown as Task
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeMockProc(exitCode = 0, stdoutData = '{"result":"ok"}') {
|
|
24
|
+
const proc = new EventEmitter() as EventEmitter & {
|
|
25
|
+
stdout: EventEmitter
|
|
26
|
+
stderr: EventEmitter
|
|
27
|
+
killed: boolean
|
|
28
|
+
kill: ReturnType<typeof vi.fn>
|
|
29
|
+
}
|
|
30
|
+
proc.stdout = new EventEmitter()
|
|
31
|
+
proc.stderr = new EventEmitter()
|
|
32
|
+
proc.killed = false
|
|
33
|
+
proc.kill = vi.fn()
|
|
34
|
+
process.nextTick(() => {
|
|
35
|
+
if (stdoutData) proc.stdout.emit('data', Buffer.from(stdoutData))
|
|
36
|
+
proc.emit('close', exitCode)
|
|
37
|
+
})
|
|
38
|
+
return proc
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── CLI mode ──────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
describe('cursor adapter — MCP support', () => {
|
|
44
|
+
let tmpDir: string
|
|
45
|
+
let mockSpawn: ReturnType<typeof vi.fn>
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.resetModules()
|
|
49
|
+
tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'cursor-test-')))
|
|
50
|
+
|
|
51
|
+
mockSpawn = vi.fn().mockImplementation((cmd: string) => {
|
|
52
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
53
|
+
return makeMockProc(0, '{"result":"ok"}')
|
|
54
|
+
})
|
|
55
|
+
vi.doMock('node:child_process', () => ({ spawn: mockSpawn }))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
60
|
+
vi.restoreAllMocks()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('writes mcp.json to cwd with correct format when mcpServers provided', async () => {
|
|
64
|
+
let capturedContent: string | null = null
|
|
65
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
66
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
67
|
+
const mcpPath = join(tmpDir, 'mcp.json')
|
|
68
|
+
if (existsSync(mcpPath)) {
|
|
69
|
+
capturedContent = readFileSync(mcpPath, 'utf8')
|
|
70
|
+
}
|
|
71
|
+
return makeMockProc(0, '{}')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const { execute } = await import('./cursor.js')
|
|
75
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
76
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
77
|
+
|
|
78
|
+
expect(capturedContent).not.toBeNull()
|
|
79
|
+
expect(JSON.parse(capturedContent!)).toEqual({
|
|
80
|
+
mcpServers: { 'my-mcp': { command: 'node', args: ['server.js'] } },
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('passes --approve-mcps when mcp_approve_all is true', async () => {
|
|
85
|
+
const capturedArgs: string[] = []
|
|
86
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
87
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
88
|
+
capturedArgs.push(...args)
|
|
89
|
+
return makeMockProc(0, '{}')
|
|
90
|
+
})
|
|
91
|
+
const { execute } = await import('./cursor.js')
|
|
92
|
+
await execute(makeTask(), { mcp_approve_all: true, cwd: tmpDir })
|
|
93
|
+
expect(capturedArgs).toContain('--approve-mcps')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('cleans up mcp.json after successful execution', async () => {
|
|
97
|
+
const { execute } = await import('./cursor.js')
|
|
98
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
99
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
100
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('cleans up mcp.json after failed execution (non-zero exit)', async () => {
|
|
104
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
105
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
106
|
+
return makeMockProc(1, '')
|
|
107
|
+
})
|
|
108
|
+
const { execute } = await import('./cursor.js')
|
|
109
|
+
const mcpServers = [{ name: 'err-mcp', type: 'local', command: 'node', args: [] }]
|
|
110
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
111
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('does NOT write mcp.json when mcpServers not configured', async () => {
|
|
115
|
+
const { execute } = await import('./cursor.js')
|
|
116
|
+
await execute(makeTask(), { cwd: tmpDir })
|
|
117
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('does NOT add --approve-mcps when mcp_approve_all is not set', async () => {
|
|
121
|
+
const capturedArgs: string[] = []
|
|
122
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
123
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
124
|
+
capturedArgs.push(...args)
|
|
125
|
+
return makeMockProc(0, '{}')
|
|
126
|
+
})
|
|
127
|
+
const { execute } = await import('./cursor.js')
|
|
128
|
+
await execute(makeTask(), { cwd: tmpDir })
|
|
129
|
+
expect(capturedArgs).not.toContain('--approve-mcps')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('includes --approve-mcps when mcp_approve_all is true (no mcpServers)', async () => {
|
|
133
|
+
const capturedArgs: string[] = []
|
|
134
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
135
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
136
|
+
capturedArgs.push(...args)
|
|
137
|
+
return makeMockProc(0, '{}')
|
|
138
|
+
})
|
|
139
|
+
const { execute } = await import('./cursor.js')
|
|
140
|
+
await execute(makeTask(), { mcp_approve_all: true, cwd: tmpDir })
|
|
141
|
+
expect(capturedArgs).toContain('--approve-mcps')
|
|
142
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
+
import { writeFileSync, unlinkSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
2
4
|
import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
|
|
3
5
|
|
|
4
6
|
/** Adapter name */
|
|
5
7
|
export const name = 'cursor'
|
|
6
8
|
|
|
9
|
+
export function supportsSessionContinuity(): boolean { return false }
|
|
7
10
|
/**
|
|
8
11
|
* Check if the Cursor CLI (`agent`) is available on the system PATH.
|
|
9
12
|
*/
|
|
@@ -33,11 +36,34 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
33
36
|
'json',
|
|
34
37
|
]
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
const cwd = options?.cwd ?? process.cwd()
|
|
40
|
+
const mcpJsonPath = join(cwd, 'mcp.json')
|
|
41
|
+
let wroteJson = false
|
|
42
|
+
|
|
43
|
+
if (options.mcpServers?.length) {
|
|
44
|
+
const mcpJson: Record<string, Record<string, unknown>> = {}
|
|
45
|
+
for (const server of options.mcpServers) {
|
|
46
|
+
const entry: Record<string, unknown> = {}
|
|
47
|
+
if (server.command) entry.command = server.command
|
|
48
|
+
if (server.args) entry.args = server.args
|
|
49
|
+
if (server.url) entry.url = server.url
|
|
50
|
+
if (server.config) Object.assign(entry, server.config)
|
|
51
|
+
mcpJson[server.name] = entry
|
|
52
|
+
}
|
|
53
|
+
writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: mcpJson }, null, 2), 'utf8')
|
|
54
|
+
wroteJson = true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (options.mcp_approve_all) {
|
|
58
|
+
args.push('--approve-mcps')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return await new Promise<ExecuteResult>((resolve) => {
|
|
37
63
|
const proc = spawn('agent', args, {
|
|
38
64
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
65
|
env: { ...process.env },
|
|
40
|
-
cwd
|
|
66
|
+
cwd,
|
|
41
67
|
})
|
|
42
68
|
|
|
43
69
|
let stdout = ''
|
|
@@ -89,6 +115,11 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
89
115
|
// Store process ref for potential timeout kill
|
|
90
116
|
task._process = proc
|
|
91
117
|
})
|
|
118
|
+
} finally {
|
|
119
|
+
if (wroteJson) {
|
|
120
|
+
try { unlinkSync(mcpJsonPath) } catch { /* ignore */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
92
123
|
}
|
|
93
124
|
|
|
94
125
|
/**
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { EventEmitter } from 'node:events'
|
|
6
|
+
import type { Task } from '../../types.js'
|
|
7
|
+
|
|
8
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeTask(): Task {
|
|
11
|
+
return {
|
|
12
|
+
id: 'test-task',
|
|
13
|
+
agent: 'developer',
|
|
14
|
+
prompt: 'Do something',
|
|
15
|
+
files: [],
|
|
16
|
+
timeout: '5m',
|
|
17
|
+
depends_on: [],
|
|
18
|
+
description: 'test task',
|
|
19
|
+
max_retries: 0,
|
|
20
|
+
} as unknown as Task
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeMockProc(exitCode = 0, stdoutData = '{"result":"ok"}') {
|
|
24
|
+
const proc = new EventEmitter() as EventEmitter & {
|
|
25
|
+
stdout: EventEmitter
|
|
26
|
+
stderr: EventEmitter
|
|
27
|
+
killed: boolean
|
|
28
|
+
kill: ReturnType<typeof vi.fn>
|
|
29
|
+
}
|
|
30
|
+
proc.stdout = new EventEmitter()
|
|
31
|
+
proc.stderr = new EventEmitter()
|
|
32
|
+
proc.killed = false
|
|
33
|
+
proc.kill = vi.fn()
|
|
34
|
+
process.nextTick(() => {
|
|
35
|
+
if (stdoutData) proc.stdout.emit('data', Buffer.from(stdoutData))
|
|
36
|
+
proc.emit('close', exitCode)
|
|
37
|
+
})
|
|
38
|
+
return proc
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── CLI mode ──────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
describe('opencode adapter — MCP support', () => {
|
|
44
|
+
let tmpDir: string
|
|
45
|
+
let mockSpawn: ReturnType<typeof vi.fn>
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.resetModules()
|
|
49
|
+
tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'opencode-test-')))
|
|
50
|
+
|
|
51
|
+
mockSpawn = vi.fn().mockImplementation((cmd: string) => {
|
|
52
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
53
|
+
return makeMockProc(0, '{"result":"ok"}')
|
|
54
|
+
})
|
|
55
|
+
vi.doMock('node:child_process', () => ({ spawn: mockSpawn }))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
60
|
+
vi.restoreAllMocks()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('writes mcp.json to cwd with correct format when mcpServers provided', async () => {
|
|
64
|
+
let capturedContent: string | null = null
|
|
65
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
66
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
67
|
+
const mcpPath = join(tmpDir, 'mcp.json')
|
|
68
|
+
if (existsSync(mcpPath)) {
|
|
69
|
+
capturedContent = readFileSync(mcpPath, 'utf8')
|
|
70
|
+
}
|
|
71
|
+
return makeMockProc(0, '{}')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const { execute } = await import('./opencode.js')
|
|
75
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
76
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
77
|
+
|
|
78
|
+
expect(capturedContent).not.toBeNull()
|
|
79
|
+
expect(JSON.parse(capturedContent!)).toEqual({
|
|
80
|
+
mcpServers: { 'my-mcp': { command: 'node', args: ['server.js'] } },
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('passes --mcp-config flag pointing to mcp.json path', async () => {
|
|
85
|
+
const capturedArgs: string[] = []
|
|
86
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
87
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
88
|
+
capturedArgs.push(...args)
|
|
89
|
+
return makeMockProc(0, '{}')
|
|
90
|
+
})
|
|
91
|
+
const { execute } = await import('./opencode.js')
|
|
92
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
93
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
94
|
+
|
|
95
|
+
const idx = capturedArgs.indexOf('--mcp-config')
|
|
96
|
+
expect(idx).toBeGreaterThanOrEqual(0)
|
|
97
|
+
expect(capturedArgs[idx + 1]).toBe(join(tmpDir, 'mcp.json'))
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('cleans up mcp.json after successful execution', async () => {
|
|
101
|
+
const { execute } = await import('./opencode.js')
|
|
102
|
+
const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
|
|
103
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
104
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('cleans up mcp.json after failed execution (non-zero exit)', async () => {
|
|
108
|
+
mockSpawn.mockImplementation((cmd: string) => {
|
|
109
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
110
|
+
return makeMockProc(1, '')
|
|
111
|
+
})
|
|
112
|
+
const { execute } = await import('./opencode.js')
|
|
113
|
+
const mcpServers = [{ name: 'err-mcp', type: 'local', command: 'node', args: [] }]
|
|
114
|
+
await execute(makeTask(), { mcpServers, cwd: tmpDir })
|
|
115
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('does NOT write mcp.json when mcpServers not configured', async () => {
|
|
119
|
+
const { execute } = await import('./opencode.js')
|
|
120
|
+
await execute(makeTask(), { cwd: tmpDir })
|
|
121
|
+
expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('does NOT add --mcp-config when mcpServers not provided', async () => {
|
|
125
|
+
const capturedArgs: string[] = []
|
|
126
|
+
mockSpawn.mockImplementation((cmd: string, args: string[]) => {
|
|
127
|
+
if (cmd === 'which') return makeMockProc(0, '')
|
|
128
|
+
capturedArgs.push(...args)
|
|
129
|
+
return makeMockProc(0, '{}')
|
|
130
|
+
})
|
|
131
|
+
const { execute } = await import('./opencode.js')
|
|
132
|
+
await execute(makeTask(), { cwd: tmpDir })
|
|
133
|
+
expect(capturedArgs).not.toContain('--mcp-config')
|
|
134
|
+
})
|
|
135
|
+
})
|