openvisio-agent 0.19.12 → 0.20.0

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.
@@ -0,0 +1,451 @@
1
+ import assert from 'node:assert/strict'
2
+ import { acpPermissionResponse, createMastraAcpRunner } from '../src/mastra-harness.mjs'
3
+ import { createCycleQueue } from '../src/cycle-queue.mjs'
4
+ import { createRunnerPool } from '../src/runner-pool.mjs'
5
+ import { resolveAvailableModel, resolveClaudeModel, claudeFallbackModels } from '../src/model-selection.mjs'
6
+ import { buildOpencodeConfig } from '../src/opencode-config.mjs'
7
+
8
+ export const scenarios = []
9
+ const add = (id, category, description, run) => scenarios.push({ id: `runtime-${id}`, category, description, run })
10
+ const deferred = () => { let resolve; const promise = new Promise((done) => { resolve = done }); return { promise, resolve } }
11
+ const tick = () => new Promise((done) => setImmediate(done))
12
+ const permissionOptions = [{ optionId: 'yes', kind: 'allow_once' }, { optionId: 'no', kind: 'reject_once' }]
13
+ const modes = [
14
+ { name: 'reply', canCode: false, canCoordinate: false },
15
+ { name: 'coordination', canCode: false, canCoordinate: true },
16
+ { name: 'coding', canCode: true, canCoordinate: false },
17
+ ]
18
+ const readTools = ['list_agents', 'list_projects', 'list_tasks', 'list_task_types', 'get_ticket', 'list_channels', 'list_message_thread', 'list_activity', 'list_codebases', 'get_codebase', 'codebase_tree']
19
+ const coordinationTools = ['update_ticket', 'post_message', 'react_message', 'comment_ticket']
20
+ const repositoryTools = ['create_codebase_branch', 'create_codebase_commit', 'create_pull_request', 'write_codebase_file']
21
+ const localTools = ['bash', 'read', 'edit', 'task']
22
+ const formats = [
23
+ { name: 'codex-title', call: (tool) => ({ title: `mcp__openvisio-team-watcher__${tool}` }) },
24
+ { name: 'opencode-title', call: (tool) => ({ title: `openvisio-team-watcher_${tool}` }) },
25
+ { name: 'structured-name', call: (tool) => ({ title: 'Tool invocation', rawInput: { toolName: tool } }) },
26
+ ]
27
+
28
+ // Tool identity, runtime representation, and lane capability are independent
29
+ // dimensions: every row checks the actual permission boundary, not a snapshot.
30
+ for (const tool of [...readTools, ...coordinationTools, ...repositoryTools, ...localTools]) {
31
+ for (const mode of modes) for (const format of formats) {
32
+ add(`permission-${mode.name}-${format.name}-${tool}`, 'runtime-permissions', `${mode.name} handles ${tool} from ${format.name} with only that lane's authority`, async () => {
33
+ const permitted = mode.canCode || readTools.includes(tool) || (mode.canCoordinate && coordinationTools.includes(tool))
34
+ const result = acpPermissionResponse({ options: permissionOptions, toolCall: format.call(tool) }, mode)
35
+ assert.equal(result.outcome.optionId, permitted ? 'yes' : 'no')
36
+ })
37
+ }
38
+ }
39
+
40
+ for (const mode of modes) for (const proxyProtected of [false, true]) for (const metadata of ['missing', 'true', 'false', 'string']) {
41
+ add(`opaque-${mode.name}-${proxyProtected}-${metadata}`, 'runtime-permissions', `Opaque ${metadata} MCP approval in ${mode.name}, private proxy ${proxyProtected}`, async () => {
42
+ const flag = { true: true, false: false, string: 'true' }[metadata]
43
+ const request = { options: permissionOptions, toolCall: { title: 'unknown local tool' }, ...(metadata === 'missing' ? {} : { _meta: { is_mcp_tool_approval: flag } }) }
44
+ const result = acpPermissionResponse(request, { ...mode, proxyProtected })
45
+ assert.equal(result.outcome.optionId, mode.canCode || (proxyProtected && flag === true) ? 'yes' : 'no')
46
+ })
47
+ }
48
+
49
+ const optionCases = [
50
+ { name: 'empty', options: [], yes: null, no: null },
51
+ { name: 'reversed', options: [...permissionOptions].reverse(), yes: 'yes', no: 'no' },
52
+ { name: 'allow-only', options: [permissionOptions[0]], yes: 'yes', no: null },
53
+ { name: 'reject-only', options: [permissionOptions[1]], yes: null, no: 'no' },
54
+ { name: 'persistent-fallback', options: [{ optionId: 'a', kind: 'allow_always' }, { optionId: 'r', kind: 'reject_always' }], yes: 'a', no: 'r' },
55
+ { name: 'once-preferred', options: [{ optionId: 'persistent', kind: 'allow_always' }, ...permissionOptions], yes: 'yes', no: 'no' },
56
+ { name: 'malformed-before-valid', options: [null, {}, { kind: 4 }, ...permissionOptions], yes: 'yes', no: 'no' },
57
+ { name: 'invalid-id', options: [{ optionId: null, kind: 'allow_once' }, { optionId: 5, kind: 'reject_once' }], yes: null, no: null },
58
+ { name: 'unknown-kind', options: [{ optionId: 'a', kind: 'allow_future' }, { optionId: 'r', kind: 'reject_future' }], yes: null, no: null },
59
+ { name: 'empty-id', options: [{ optionId: '', kind: 'allow_once' }, { optionId: '', kind: 'reject_once' }], yes: null, no: null },
60
+ ]
61
+ for (const canCode of [false, true]) for (const row of optionCases) {
62
+ add(`permission-options-${canCode}-${row.name}`, 'runtime-permissions', `Permission options ${row.name} fail closed or select the appropriate ${canCode ? 'allow' : 'reject'} choice`, async () => {
63
+ const result = acpPermissionResponse({ options: row.options, toolCall: { title: 'shell' } }, { canCode })
64
+ const expected = canCode ? row.yes : row.no
65
+ assert.deepEqual(result, expected == null ? { outcome: { outcome: 'cancelled' } } : { outcome: { outcome: 'selected', optionId: expected } })
66
+ })
67
+ }
68
+
69
+ const efforts = ['low', 'medium', 'high', 'xhigh', 'max', 'ultra']
70
+ for (const provider of ['', 'vendor-a/', 'vendor-b/']) for (const effort of efforts) for (const representation of ['string', 'object']) {
71
+ const requested = `${provider}fixture-model[${effort}]`
72
+ add(`model-exact-${provider || 'bare'}-${effort}-${representation}`, 'runtime-models', `Preserve ${requested} when advertised as ${representation}; never substitute another effort or provider`, async () => {
73
+ const ids = ['vendor-c/fixture-model[medium]', `${provider}different-generation[${effort}]`, requested]
74
+ const available = representation === 'object' ? ids.map((modelId) => ({ modelId })) : ids
75
+ const result = resolveAvailableModel(requested, available)
76
+ assert.equal(result.selected, requested)
77
+ assert.equal(result.reason, 'exact')
78
+ })
79
+ }
80
+ for (const provider of ['', 'vendor-a/', 'vendor-b/']) for (const preferredEffort of efforts) {
81
+ add(`model-implicit-effort-${provider || 'bare'}-${preferredEffort}`, 'runtime-models', `Unspecified effort for ${provider || 'bare'} fixture uses configured ${preferredEffort} and preserves model identity`, async () => {
82
+ const selected = `${provider}fixture-model[${preferredEffort}]`
83
+ const result = resolveAvailableModel(`${provider}fixture-model`, efforts.map((effort) => `${provider}fixture-model[${effort}]`), { preferredEffort })
84
+ assert.equal(result.selected, selected)
85
+ })
86
+ }
87
+ for (const effort of efforts) for (const mismatch of ['provider', 'generation', 'effort']) {
88
+ add(`model-no-substitution-${effort}-${mismatch}`, 'runtime-models', `Reject an advertised ${mismatch} mismatch for an explicit ${effort} model`, async () => {
89
+ const requested = `vendor-a/fixture-one[${effort}]`
90
+ const choice = mismatch === 'provider' ? `vendor-b/fixture-one[${effort}]` : mismatch === 'generation' ? `vendor-a/fixture-two[${effort}]` : `vendor-a/fixture-one[${effort === 'low' ? 'high' : 'low'}]`
91
+ assert.throws(() => resolveAvailableModel(requested, [choice]), /substitution is disabled/)
92
+ })
93
+ }
94
+ for (const model of ['vendor-a/model-v1', 'vendor-b/model-v2[high]', 'fixture-alias', '']) {
95
+ add(`model-unadvertised-${model || 'default'}`, 'runtime-models', `Runtime with no advertised list preserves explicit model ${model || '(default)'}`, async () => {
96
+ assert.equal(resolveAvailableModel(model, []).selected, model)
97
+ assert.equal(resolveClaudeModel(` ${model} `), model)
98
+ assert.deepEqual(claudeFallbackModels(model), [])
99
+ })
100
+ }
101
+
102
+ const groupPatterns = {
103
+ distinct: [1, 2, 3, 4, 5, 6, 7, 8],
104
+ shared: [1, 1, 1, 1, 1, 1, 1, 1],
105
+ alternating: [1, 2, 1, 2, 1, 2, 1, 2],
106
+ 'zero-resource': [0, 0, 1, 0, 1, 2, 2, 0],
107
+ 'blocked-head': [1, 1, 1, 2, 3, 1, 2, 4],
108
+ 'ungrouped-mixed': ['', 1, '', 1, 2, '', 2, ''],
109
+ }
110
+ for (const [pattern, groups] of Object.entries(groupPatterns)) for (const concurrency of [1, 2, 3, 6]) for (const outcome of ['success', 'failure', 'cancel']) {
111
+ add(`queue-${pattern}-${concurrency}-${outcome}`, 'runtime-queue', `Queue ${pattern} resources at concurrency ${concurrency} under ${outcome} preserves isolation, ordering, and progress`, async () => {
112
+ const gates = groups.map(deferred), started = [], running = new Map(), errors = [], canceled = []
113
+ const queue = createCycleQueue({ concurrency, groupKey: (item) => item.group, onError: (error) => errors.push(error.message), run: async (item) => {
114
+ assert.ok(running.size < concurrency)
115
+ if (item.group !== '') assert.equal([...running.values()].includes(item.group), false, 'same mutable resource overlapped')
116
+ running.set(item.id, item.group)
117
+ started.push(item.id)
118
+ await gates[item.id].promise
119
+ running.delete(item.id)
120
+ if (outcome === 'failure' && item.id % 3 === 1) throw new Error(`failure-${item.id}`)
121
+ return { status: 'done', id: item.id }
122
+ } })
123
+ const promises = groups.map((group, id) => queue.enqueue({ id, group }, `ticket:${id}`))
124
+ assert.equal(queue.enqueue({ id: 999 }, 'ticket:0'), promises[0], 'replayed active source has a single promise')
125
+ if (outcome === 'cancel') queue.cancel((item) => item.id % 2 === 1, (item) => canceled.push(item.id))
126
+ for (let round = 0; queue.size && round < 20; round++) {
127
+ assert.ok(running.size > 0, 'pending queue must always make progress when no group is busy')
128
+ const id = [...running.keys()].at(outcome === 'failure' ? -1 : 0)
129
+ gates[id].resolve()
130
+ await tick()
131
+ }
132
+ assert.equal(queue.size, 0)
133
+ const results = await Promise.all(promises)
134
+ assert.equal(new Set(started).size, started.length)
135
+ for (const group of new Set(groups.filter((group) => group !== ''))) {
136
+ const order = started.filter((id) => groups[id] === group)
137
+ assert.deepEqual(order, [...order].sort((a, b) => a - b), 'same-resource FIFO changed')
138
+ }
139
+ for (let id = 0; id < groups.length; id++) {
140
+ assert.equal(queue.has(`ticket:${id}`), false)
141
+ const expected = outcome === 'cancel' && !started.includes(id) ? 'canceled' : outcome === 'failure' && id % 3 === 1 ? 'failed' : 'done'
142
+ assert.equal(results[id].status, expected)
143
+ }
144
+ assert.equal(errors.length, outcome === 'failure' ? 3 : 0)
145
+ assert.ok(canceled.every((id) => id % 2 === 1))
146
+ })
147
+ }
148
+
149
+ for (const failureKind of ['sync', 'async']) for (const phase of ['run', 'cancel']) {
150
+ add(`queue-hook-failure-${phase}-${failureKind}`, 'runtime-queue', `${failureKind} ${phase} hook failure cannot strand workers or create unhandled rejections`, async () => {
151
+ const gates = [deferred(), deferred()], errors = []
152
+ const queue = createCycleQueue({ concurrency: 2, run: async ({ id }) => { await gates[id].promise; if (phase === 'run') throw new Error(`run-${id}`) }, onError: (error) => {
153
+ errors.push(error.message)
154
+ if (failureKind === 'sync') throw new Error('logger failed')
155
+ return Promise.reject(new Error('logger failed'))
156
+ } })
157
+ const tasks = [queue.enqueue({ id: 0 }), queue.enqueue({ id: 1 })]
158
+ if (phase === 'cancel') queue.cancel(() => true, ({ id }) => {
159
+ if (failureKind === 'sync') throw new Error(`cancel-${id}`)
160
+ return Promise.reject(new Error(`cancel-${id}`))
161
+ })
162
+ gates.forEach((gate) => gate.resolve())
163
+ await Promise.all(tasks)
164
+ await tick()
165
+ assert.equal(errors.length, 2)
166
+ assert.equal(queue.size, 0)
167
+ })
168
+ }
169
+
170
+ for (const capacity of [1, 2, 4]) for (const state of ['idle', 'busy', 'recently-released', 'lru']) for (const elapsed of [0, 99, 100]) {
171
+ add(`pool-${capacity}-${state}-${elapsed}`, 'runtime-pool', `Warm pool capacity ${capacity}, ${state} session at ${elapsed} ms obeys expiry and pins active work`, async () => {
172
+ let now = 0, created = 0
173
+ const closed = []
174
+ const pool = createRunnerPool({ capacity, idleMs: 100, now: () => now, create: (options) => ({ id: ++created, options, close() { closed.push(this.id) } }) })
175
+ try {
176
+ const first = pool.acquire('project:1:ticket:1:cwd:a', { cwd: 'a' })
177
+ if (state !== 'busy') pool.release('project:1:ticket:1:cwd:a')
178
+ now = elapsed
179
+ if (state === 'busy') {
180
+ assert.throws(() => pool.acquire('project:1:ticket:1:cwd:a'), /active/)
181
+ pool.acquire('project:1:ticket:1:cwd:b', { cwd: 'b' })
182
+ assert.equal(closed.includes(first.id), false)
183
+ } else if (state === 'recently-released') {
184
+ const latest = pool.acquire('project:1:ticket:1:cwd:a')
185
+ pool.release('project:1:ticket:1:cwd:a')
186
+ now += 99
187
+ assert.equal(pool.acquire('project:1:ticket:1:cwd:a'), latest)
188
+ } else if (state === 'lru') {
189
+ for (let id = 0; id < capacity; id++) { pool.acquire(`other:${id}`); pool.release(`other:${id}`) }
190
+ assert.ok(closed.includes(first.id), 'least recently acquired idle session must leave a full cache')
191
+ } else {
192
+ const again = pool.acquire('project:1:ticket:1:cwd:a')
193
+ assert.equal(again === first, elapsed < 100)
194
+ }
195
+ } finally { pool.close() }
196
+ assert.equal(closed.length, created)
197
+ assert.equal(new Set(closed).size, created, 'session closed more than once')
198
+ })
199
+ }
200
+ for (const phase of ['eviction', 'close']) for (const failureKind of ['sync', 'async']) {
201
+ add(`pool-cleanup-${phase}-${failureKind}`, 'runtime-pool', `${failureKind} session close error during ${phase} does not retain stale sessions or skip remaining cleanup`, async () => {
202
+ let closed = 0
203
+ const errors = []
204
+ const pool = createRunnerPool({ capacity: 1, create: () => ({ close() { closed++; if (failureKind === 'sync') throw new Error('close failed'); return Promise.reject(new Error('close failed')) } }), onError: (error) => errors.push(error.message) })
205
+ pool.acquire('a')
206
+ if (phase === 'eviction') pool.release('a')
207
+ pool.acquire('b')
208
+ pool.close()
209
+ pool.close()
210
+ await tick()
211
+ assert.equal(closed, 2)
212
+ assert.equal(errors.length, 2)
213
+ assert.throws(() => pool.acquire('c'), /closed/)
214
+ })
215
+ }
216
+ for (const [name, options] of [
217
+ ['zero-capacity', { capacity: 0 }], ['negative-capacity', { capacity: -1 }], ['fractional-capacity', { capacity: 1.5 }],
218
+ ['infinite-capacity', { capacity: Infinity }], ['zero-expiry', { idleMs: 0 }], ['negative-expiry', { idleMs: -1 }],
219
+ ['infinite-expiry', { idleMs: Infinity }], ['nan-expiry', { idleMs: NaN }],
220
+ ]) add(`pool-invalid-${name}`, 'runtime-pool', `Invalid ${name} configuration fails before starting any timer`, async () => { assert.throws(() => createRunnerPool({ ...options, create: () => ({}) }), /positive/) })
221
+
222
+ function fakeAcp({ events = [], negotiate, select, stream, cancel } = {}) {
223
+ const state = { instances: [], prompts: [], modelReads: 0, modelSets: [], disconnects: 0, cancels: 0 }
224
+ class FakeAcp {
225
+ constructor(options) {
226
+ state.instances.push(options)
227
+ this.connection = {
228
+ async *promptStream(prompt, signal) {
229
+ state.prompts.push(prompt)
230
+ if (stream) { yield* stream(prompt, signal); return }
231
+ for (const event of events) yield event
232
+ },
233
+ disconnect() { state.disconnects++ },
234
+ async cancel() { state.cancels++; if (cancel) await cancel() },
235
+ }
236
+ }
237
+ async getAvailableModels() { state.modelReads++; return negotiate ? negotiate() : [{ modelId: 'fixture-model[medium]' }] }
238
+ async setModel(id) { state.modelSets.push(id); if (select) await select(id) }
239
+ }
240
+ return { state, FakeAcp }
241
+ }
242
+ const toolEvent = (id, title, status, extras = {}) => ({ type: 'session-update', update: { sessionUpdate: 'tool_call', toolCallId: id, title, status, ...extras } })
243
+ const headers = { 'x-agent-api-key': 'fixture-secret-key', 'x-agent-identifier': 'fixture-agent' }
244
+ async function withRunner(fake, options, run) {
245
+ const runner = createMastraAcpRunner({ agent: 'codex', maxCycleMs: 1000, AcpAgentClass: fake.FakeAcp, ...options })
246
+ try { return await run(runner) } finally { runner.close() }
247
+ }
248
+
249
+ const evidenceTools = [
250
+ { name: 'get_ticket', expected: 'didMcpTaskRead' }, { name: 'list_tasks', expected: 'didMcpTaskRead' },
251
+ { name: 'update_ticket', expected: 'didMcpTaskUpdate' }, { name: 'post_message', expected: 'didMessage' },
252
+ { name: 'write_codebase_file', expected: 'didRepoMutation' }, { name: 'create_codebase_commit', expected: 'didRepoMutation' },
253
+ { name: 'create_pull_request', expected: null }, { name: 'create_codebase_branch', expected: null },
254
+ ]
255
+ for (const tool of evidenceTools) for (const status of ['pending', 'in_progress', 'completed', 'failed']) {
256
+ add(`evidence-${tool.name}-${status}`, 'runtime-evidence', `${status} ${tool.name} grants only completed, appropriate evidence`, async () => {
257
+ const fake = fakeAcp({ events: [toolEvent('call', `openvisio-team ${tool.name}`, status, { rawOutput: status === 'failed' ? 'server unavailable' : {} })] })
258
+ await withRunner(fake, {}, async (runner) => {
259
+ const result = await runner.runCycle('fixture task')
260
+ for (const flag of ['didMcpTaskRead', 'didMcpTaskUpdate', 'didRepoMutation', 'didMessage']) {
261
+ assert.equal(result[flag], status === 'completed' && flag === tool.expected, flag)
262
+ }
263
+ assert.equal(result.mcpErrors.includes(tool.name), status === 'failed')
264
+ assert.deepEqual(result.mcpCalls, [tool.name])
265
+ })
266
+ })
267
+ }
268
+
269
+ for (const terminal of ['completed', 'failed', 'in_progress']) for (const ordering of ['message-before-edit', 'message-after-edit', 'same-call-update']) {
270
+ add(`evidence-order-${ordering}-${terminal}`, 'runtime-evidence', `${ordering} with ${terminal} repository tool derives delivery evidence from final successful ordering`, async () => {
271
+ const edit = toolEvent('edit', 'write_codebase_file', terminal)
272
+ const message = toolEvent('message', 'post_message', 'completed')
273
+ const events = ordering === 'message-before-edit' ? [message, edit] : ordering === 'message-after-edit' ? [edit, message] : [toolEvent('edit', 'write_codebase_file', 'in_progress'), message, { update: { sessionUpdate: 'tool_call_update', toolCallId: 'edit', status: terminal } }]
274
+ const fake = fakeAcp({ events })
275
+ await withRunner(fake, {}, async (runner) => {
276
+ const result = await runner.runCycle('task')
277
+ assert.equal(result.didRepoMutation, terminal === 'completed')
278
+ assert.equal(result.didMessage, true)
279
+ assert.equal(result.didResultMessage, terminal === 'completed' && ordering === 'message-after-edit')
280
+ })
281
+ })
282
+ }
283
+
284
+ for (const agent of ['codex', 'opencode']) for (const phase of ['model-list', 'model-set', 'stream']) for (const action of ['timeout', 'cancel', 'close']) {
285
+ add(`interruption-${agent}-${phase}-${action}`, 'runtime-lifecycle', `${agent} ${action} releases a cycle stalled in ${phase} and never reports success`, async () => {
286
+ const entered = deferred(), never = deferred()
287
+ const stall = () => { entered.resolve(); return never.promise }
288
+ const fake = fakeAcp({
289
+ ...(phase === 'model-list' ? { negotiate: stall } : {}),
290
+ ...(phase === 'model-set' ? { select: stall } : {}),
291
+ ...(phase === 'stream' ? { stream: async function* () { await stall() } } : {}),
292
+ cancel: () => never.promise,
293
+ })
294
+ await withRunner(fake, { agent, maxCycleMs: action === 'timeout' ? 20 : 1000 }, async (runner) => {
295
+ const task = runner.runCycle('task', phase === 'stream' ? undefined : 'fixture-model')
296
+ await entered.promise
297
+ if (action === 'cancel') await runner.cancelCurrent()
298
+ if (action === 'close') runner.close()
299
+ const result = await task
300
+ assert.equal(result.subtype, action === 'timeout' ? 'timeout' : 'canceled')
301
+ assert.ok(fake.state.disconnects >= 1)
302
+ assert.equal(result.didCode, false)
303
+ })
304
+ })
305
+ }
306
+
307
+ for (const agent of ['codex', 'opencode']) for (const mode of modes) for (const transport of ['private', 'http', 'none']) {
308
+ add(`session-config-${agent}-${mode.name}-${transport}`, 'runtime-session-config', `${agent} ${mode.name} session exposes the correct ${transport} MCP transport and local tool policy`, async () => {
309
+ const fake = fakeAcp()
310
+ await withRunner(fake, { agent, ...mode, workdir: '/fixture/isolated-runtime-test', ...(transport !== 'none' ? { mcpUrl: 'https://fixture.invalid/mcp' } : {}), ...(transport === 'private' ? { mcpHeaders: headers } : {}) }, async (runner) => {
311
+ await runner.runCycle('task')
312
+ const config = fake.state.instances[0]
313
+ const server = config.session.mcpServers[0]
314
+ if (transport === 'none') assert.equal(config.session.mcpServers.length, 0)
315
+ if (transport === 'http') assert.equal(server.type, 'http')
316
+ if (transport === 'private') {
317
+ assert.equal(server.command, process.execPath)
318
+ const env = Object.fromEntries(server.env.map(({ name, value }) => [name, value]))
319
+ const allowed = JSON.parse(env.OPENVISIO_CODEX_ALLOWED_TOOLS)
320
+ if (mode.canCode) assert.equal(allowed, null)
321
+ else {
322
+ assert.ok(allowed.includes('get_ticket'))
323
+ assert.equal(allowed.includes('update_ticket'), mode.canCoordinate)
324
+ assert.equal(allowed.includes('write_codebase_file'), false)
325
+ }
326
+ }
327
+ if (agent === 'opencode') {
328
+ const value = JSON.parse(config.env.OPENCODE_CONFIG_CONTENT)
329
+ if (mode.canCode) assert.equal(value.permission, undefined)
330
+ else {
331
+ assert.equal(value.permission.bash, 'deny')
332
+ assert.equal(value.permission['openvisio-team-watcher_*'], 'allow')
333
+ }
334
+ assert.equal(value.mcp['openvisio-team'].enabled, false)
335
+ } else assert.equal(config.env.INITIAL_AGENT_MODE, mode.canCode ? 'agent' : 'read-only')
336
+ })
337
+ })
338
+ }
339
+
340
+ for (const change of ['same', 'cwd', 'disabled-tools', 'model']) {
341
+ add(`session-reuse-${change}`, 'runtime-lifecycle', `Warm ACP ${change} change preserves only compatible context and model negotiation`, async () => {
342
+ const fake = fakeAcp({ negotiate: async () => [{ modelId: 'fixture-model[medium]' }, { modelId: 'fixture-model[high]' }] })
343
+ await withRunner(fake, { systemPrompt: 'stable charter' }, async (runner) => {
344
+ await runner.runCycle('first', 'fixture-model[medium]')
345
+ await runner.runCycle('second', change === 'model' ? 'fixture-model[high]' : 'fixture-model[medium]', change === 'cwd' ? { workdir: '/fixture/other-worktree' } : change === 'disabled-tools' ? { disabledMcpTools: ['post_message'] } : {})
346
+ const reset = change === 'cwd' || change === 'disabled-tools'
347
+ assert.equal(fake.state.instances.length, reset ? 2 : 1)
348
+ assert.equal(fake.state.modelReads, change === 'same' ? 1 : 2)
349
+ assert.equal(fake.state.prompts[0], 'stable charter\n\nfirst')
350
+ assert.equal(fake.state.prompts[1], reset ? 'stable charter\n\nsecond' : 'second')
351
+ })
352
+ })
353
+ }
354
+
355
+ for (const localTool of localTools) {
356
+ add(`opencode-local-denial-${localTool}`, 'runtime-session-config', `OpenCode reply configuration denies inherited ${localTool} access globally and at agent level`, async () => {
357
+ const config = buildOpencodeConfig({ canCode: false, mcpUrl: 'https://fixture.invalid/mcp', mcpHeaders: headers })
358
+ assert.equal(config.permission[localTool], 'deny')
359
+ assert.equal(config.agent['openvisio-reply'].permission[localTool], 'deny')
360
+ assert.equal(config.mcp['openvisio-team'].oauth, false)
361
+ assert.equal(config.mcp['openvisio-team'].headers['x-agent-api-key'], 'fixture-secret-key')
362
+ })
363
+ }
364
+
365
+ for (const ordering of ['message-first', 'edit-first']) for (const metadata of ['status-only', 'output-only']) {
366
+ add(`evidence-late-metadata-${ordering}-${metadata}`, 'runtime-evidence', `Late ${metadata} for a completed tool preserves ${ordering} execution order`, async () => {
367
+ const edit = toolEvent('edit', 'write_codebase_file', 'completed')
368
+ const message = toolEvent('message', 'post_message', 'completed')
369
+ const first = ordering === 'message-first' ? message : edit
370
+ const second = ordering === 'message-first' ? edit : message
371
+ const fake = fakeAcp({ events: [first, second, { update: { sessionUpdate: 'tool_call_update', toolCallId: first.update.toolCallId, ...(metadata === 'status-only' ? { status: 'completed' } : { rawOutput: { duration: 3 } }) } }] })
372
+ await withRunner(fake, {}, async (runner) => {
373
+ const result = await runner.runCycle('task')
374
+ assert.equal(result.didResultMessage, ordering === 'edit-first')
375
+ })
376
+ })
377
+ }
378
+
379
+ for (const secretKind of ['key', 'identifier']) {
380
+ add(`error-redaction-${secretKind}`, 'runtime-evidence', `Failed MCP ${secretKind} is redacted in structured result details as well as logs`, async () => {
381
+ const secret = secretKind === 'key' ? headers['x-agent-api-key'] : headers['x-agent-identifier']
382
+ const logs = []
383
+ const fake = fakeAcp({ events: [toolEvent('failed', 'get_ticket', 'failed', { rawOutput: `Server error exposed ${secret}` })] })
384
+ await withRunner(fake, { mcpHeaders: headers, log: (line) => logs.push(line) }, async (runner) => {
385
+ const result = await runner.runCycle('task')
386
+ assert.equal(JSON.stringify(result.mcpErrorDetails).includes(secret), false)
387
+ assert.equal(logs.join('\n').includes(secret), false)
388
+ assert.match(result.mcpErrorDetails.get_ticket, /\[redacted\]/)
389
+ })
390
+ })
391
+ }
392
+
393
+ for (const action of ['cancel', 'close']) {
394
+ add(`graceful-stream-${action}`, 'runtime-lifecycle', `A stream that ends normally after ${action} must still report cancellation`, async () => {
395
+ const entered = deferred()
396
+ const fake = fakeAcp({ stream: async function* (_, signal) {
397
+ entered.resolve()
398
+ await new Promise((resolve) => signal.addEventListener('abort', resolve, { once: true }))
399
+ } })
400
+ await withRunner(fake, {}, async (runner) => {
401
+ const task = runner.runCycle('task')
402
+ await entered.promise
403
+ if (action === 'cancel') await runner.cancelCurrent()
404
+ else runner.close()
405
+ assert.equal((await task).subtype, 'canceled')
406
+ })
407
+ })
408
+ }
409
+
410
+ add('same-session-concurrent-cycle', 'runtime-lifecycle', 'Concurrent direct calls cannot share a warm session or overwrite the first cancellation handle', async () => {
411
+ const entered = deferred(), gate = deferred()
412
+ const fake = fakeAcp({ stream: async function* () { entered.resolve(); await gate.promise; yield { type: 'text', text: 'first result' } } })
413
+ await withRunner(fake, {}, async (runner) => {
414
+ const task = runner.runCycle('first')
415
+ await entered.promise
416
+ await assert.rejects(runner.runCycle('second'), /already has an active cycle/)
417
+ gate.resolve()
418
+ assert.equal((await task).outputText, 'first result')
419
+ assert.deepEqual(fake.state.prompts, ['first'])
420
+ })
421
+ })
422
+
423
+ for (const [name, maxCycleMs] of [['zero', 0], ['negative', -1], ['nan', NaN], ['infinite', Infinity], ['overflow', 2_147_483_648]]) {
424
+ add(`timeout-config-${name}`, 'runtime-lifecycle', `Invalid ${name} deadline cannot silently become Node's one-millisecond timeout`, async () => {
425
+ const fake = fakeAcp()
426
+ assert.throws(() => createMastraAcpRunner({ agent: 'codex', maxCycleMs, AcpAgentClass: fake.FakeAcp }), /positive supported timeout/)
427
+ assert.equal(fake.state.instances.length, 0)
428
+ })
429
+ }
430
+
431
+ for (const concurrency of [1, 2, 3, 6]) for (const outcome of ['success', 'failure', 'cancel']) {
432
+ add(`queue-settlement-${concurrency}-${outcome}`, 'runtime-lifecycle', `Shutdown at concurrency ${concurrency} waits for ${outcome} worker cleanup without polling or losing queued promises`, async () => {
433
+ const gate = deferred()
434
+ let cleaned = 0, settled = false
435
+ const queue = createCycleQueue({ concurrency, run: async () => {
436
+ try { await gate.promise; if (outcome === 'failure') throw new Error('runtime failed during shutdown') }
437
+ finally { cleaned++ }
438
+ } })
439
+ const jobs = Array.from({ length: concurrency + 2 }, (_, id) => queue.enqueue({ id }))
440
+ if (outcome === 'cancel') queue.cancel(() => true)
441
+ const completion = queue.settled().then(() => { settled = true })
442
+ await tick()
443
+ assert.equal(settled, false, 'settlement resolved before active worker cleanup')
444
+ gate.resolve()
445
+ await completion
446
+ await Promise.all(jobs)
447
+ assert.equal(cleaned, outcome === 'cancel' ? concurrency : concurrency + 2)
448
+ assert.equal(queue.size, 0)
449
+ await queue.settled()
450
+ })
451
+ }