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,407 @@
1
+ import assert from 'node:assert/strict'
2
+ import { createServer } from 'node:http'
3
+ import { once } from 'node:events'
4
+ import { execFileSync } from 'node:child_process'
5
+ import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
6
+ import { join } from 'node:path'
7
+ import { tmpdir } from 'node:os'
8
+ import { createMcpHttpClient } from '../src/mcp-http.mjs'
9
+ import { toolAllowed, toolWithoutCredentialInputs, proxyToolPolicyFromEnvironment } from '../src/codex-mcp-proxy.mjs'
10
+ import { authorizePrPush, configurePrPublishing, isSafeAgentBranch, pushPrBranch, repositoryHasPrPushAuthorization, repositoryPushIdentity, revokePrPush, validatePrPushAuthorization } from '../src/pr-push.mjs'
11
+ import { createByoMemoryGraph, createMastraMemory } from '../src/memory.mjs'
12
+ import { releaseAuthorizedPause } from '../src/authorization-resume.mjs'
13
+ import { createTaskTypeLookup } from '../src/task-types.mjs'
14
+ import { mapConcurrent } from '../src/concurrency.mjs'
15
+
16
+ // Every row is independently runnable. The matrix varies actual operations,
17
+ // transport failures, policy boundaries, and persistence states, not ticket IDs
18
+ // solely to inflate the case count. HTTP and Git integration rows are local.
19
+ export const scenarios = []
20
+ const add = (id, category, description, run) => scenarios.push({ id: `transport-${id}`, category, description, run })
21
+ const response = ({ status = 200, body = '{}', sessionId = '', contentType = 'application/json' } = {}) => ({
22
+ ok: status >= 200 && status < 300, status,
23
+ headers: { get: (name) => ({ 'mcp-session-id': sessionId, 'content-type': contentType })[name.toLowerCase()] || null },
24
+ text: async () => body,
25
+ })
26
+ const fixture = ({ session = false, handler, requestTimeoutMs = 500 } = {}) => {
27
+ const calls = [], counts = new Map()
28
+ const client = createMcpHttpClient({ url: 'https://workspace.example.test/mcp', apiKey: 'fixture-secret', identifier: 'alex', clientVersion: 'scenario', requestTimeoutMs,
29
+ fetchImpl: async (_url, init) => {
30
+ const request = JSON.parse(init.body)
31
+ const count = (counts.get(request.method) || 0) + 1
32
+ counts.set(request.method, count)
33
+ calls.push({ request, headers: init.headers, signal: init.signal })
34
+ const custom = await handler?.(request, count, init)
35
+ if (custom) return custom
36
+ if (request.method === 'initialize') return response({ sessionId: session ? `session-${count}` : '', body: JSON.stringify({ id: request.id, result: {} }) })
37
+ if (request.method === 'notifications/initialized') return response({ status: 202, body: '' })
38
+ if (request.method === 'tools/list') return response({ body: JSON.stringify({ id: request.id, result: { tools: [] } }) })
39
+ return response({ body: JSON.stringify({ id: request.id, result: { content: [{ type: 'text', text: 'delivered' }] } }) })
40
+ },
41
+ })
42
+ return { client, calls, counts }
43
+ }
44
+ const temp = async (run) => {
45
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), 'openvisio-scenario-')))
46
+ try { return await run(dir) } finally { rmSync(dir, { recursive: true, force: true }) }
47
+ }
48
+ const tick = () => new Promise((resolve) => setImmediate(resolve))
49
+
50
+ const operations = ['list_projects', 'list_tasks', 'get_ticket', 'get_channel_messages', 'post_message', 'update_ticket', 'comment_ticket', 'react_message']
51
+ for (const name of operations) for (const session of [false, true]) for (const status of [400, 401, 403, 404, 409, 410, 422, 429, 500, 503]) {
52
+ add(`http-${name}-${session ? 'stateful' : 'stateless'}-${status}`, 'MCP HTTP failures', `${name} receives HTTP ${status} in ${session ? 'stateful' : 'stateless'} mode; only an expired session may replay the request`, async () => {
53
+ const { client, counts } = fixture({ session, handler: (request, count) => request.method === 'tools/call' && count === 1 ? response({ status }) : null })
54
+ if (session && status === 404) {
55
+ assert.equal((await client.callTool(name)).content[0].text, 'delivered')
56
+ assert.equal(counts.get('initialize'), 2)
57
+ assert.equal(counts.get('tools/call'), 2)
58
+ } else {
59
+ await assert.rejects(client.callTool(name), new RegExp(`HTTP ${status}`))
60
+ assert.equal(counts.get('initialize'), 1)
61
+ assert.equal(counts.get('tools/call'), 1)
62
+ }
63
+ })
64
+ }
65
+
66
+ const encodings = {
67
+ json: (payload) => JSON.stringify(payload),
68
+ sse: (payload) => `event: message\ndata: ${JSON.stringify(payload)}\n\n`,
69
+ 'sse-crlf': (payload) => `event: message\r\ndata: ${JSON.stringify(payload)}\r\n\r\n`,
70
+ 'sse-multiline': (payload) => `data: {"id":${payload.id},\ndata: "result":${JSON.stringify(payload.result)}}\n\n`,
71
+ 'sse-trailing-notification': (payload) => `data: ${JSON.stringify(payload)}\n\ndata: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progress":99}}\n\n`,
72
+ 'sse-other-response': (payload) => `: heartbeat\n\ndata: {"id":9999,"result":{"content":[]}}\n\ndata: ${JSON.stringify(payload)}\n\n`,
73
+ }
74
+ for (const [encoding, encode] of Object.entries(encodings)) for (const name of ['get_ticket', 'post_message', 'update_ticket', 'comment_ticket']) {
75
+ add(`framing-${name}-${encoding}`, 'MCP framing', `${name} recognizes its exact response in ${encoding} framing`, async () => {
76
+ const { client } = fixture({ handler: (request) => request.method === 'tools/call' ? response({ body: encode({ id: request.id, result: { content: [{ type: 'text', text: 'correct response ✓' }] } }) }) : null })
77
+ assert.equal((await client.callTool(name)).content[0].text, 'correct response ✓')
78
+ })
79
+ }
80
+
81
+ for (const stage of ['initialize', 'tools/list', 'tools/call']) for (const kind of ['rpc', 'network', 'malformed']) for (const session of [false, true]) {
82
+ add(`errors-${stage.replace('/', '-')}-${kind}-${session}`, 'MCP diagnostics', `${stage} ${kind} failure remains retryable by its caller and never exposes watcher credentials in ${session ? 'stateful' : 'stateless'} mode`, async () => {
83
+ let fail = true
84
+ const { client } = fixture({ session, handler: (request) => {
85
+ if (request.method !== stage || !fail) return null
86
+ if (kind === 'network') throw new Error('connection failed fixture-secret')
87
+ return response({ body: kind === 'rpc' ? JSON.stringify({ id: request.id, error: { message: 'fixture-secret denied' } }) : 'broken fixture-secret JSON' })
88
+ } })
89
+ const run = () => stage === 'initialize' ? client.initialize() : stage === 'tools/list' ? client.listTools() : client.callTool('post_message')
90
+ await assert.rejects(run(), (error) => !error.message.includes('fixture-secret'))
91
+ fail = false
92
+ await run()
93
+ })
94
+ }
95
+
96
+ for (const name of operations) for (const schemaKey of ['inputSchema', 'input_schema', 'both']) {
97
+ add(`credentials-${name}-${schemaKey}`, 'MCP credential ownership', `${name} ${schemaKey} exposes business inputs while trusted transport replaces forged identity fields`, async () => {
98
+ const schema = { type: 'object', properties: { project_id: { type: 'number' }, agent_api_key: { type: 'string' }, agent_identifier: { type: 'string' } }, required: ['project_id', 'agent_api_key', 'agent_identifier'] }
99
+ const original = { name, ...(schemaKey !== 'input_schema' ? { inputSchema: structuredClone(schema) } : {}), ...(schemaKey !== 'inputSchema' ? { input_schema: structuredClone(schema) } : {}) }
100
+ const stripped = toolWithoutCredentialInputs(original)
101
+ for (const key of ['inputSchema', 'input_schema']) if (original[key]) {
102
+ assert.deepEqual(stripped[key].required, ['project_id'])
103
+ assert.deepEqual(Object.keys(stripped[key].properties), ['project_id'])
104
+ assert.ok(original[key].properties.agent_api_key)
105
+ }
106
+ const { client, calls } = fixture()
107
+ await client.callTool(name, { project_id: 4, agent_api_key: 'forged', agent_identifier: 'other-agent' })
108
+ const last = calls.at(-1)
109
+ assert.equal(last.headers['x-agent-api-key'], 'fixture-secret')
110
+ assert.deepEqual(last.request.params.arguments, { project_id: 4, agent_api_key: 'fixture-secret', agent_identifier: 'alex' })
111
+ })
112
+ }
113
+
114
+ for (const key of ['OPENVISIO_CODEX_ALLOWED_TOOLS', 'OPENVISIO_CODEX_DISABLED_TOOLS']) for (const malformed of ['', '{', '{}', 'true', '"get_ticket"', '[42]', '[null]', '[""]']) {
115
+ add(`policy-${key.toLowerCase()}-${encodeURIComponent(malformed) || 'empty'}`, 'MCP lane policy', `Malformed ${key} value ${JSON.stringify(malformed)} fails closed before a proxy can expose tools`, () => {
116
+ assert.throws(() => proxyToolPolicyFromEnvironment({ [key]: malformed }), /Invalid MCP tool policy/)
117
+ })
118
+ }
119
+ for (const name of operations) for (const lane of ['read', 'coordinate', 'coding']) {
120
+ add(`lane-${lane}-${name}`, 'MCP lane policy', `${lane} lane exposes ${name} only when its explicit capabilities permit it`, () => {
121
+ const allowed = lane === 'coding' ? null : new Set(lane === 'read' ? operations.slice(0, 4) : operations)
122
+ const disabled = new Set(['react_message'])
123
+ const policy = proxyToolPolicyFromEnvironment({ OPENVISIO_CODEX_ALLOWED_TOOLS: JSON.stringify(allowed ? [...allowed] : null), OPENVISIO_CODEX_DISABLED_TOOLS: JSON.stringify([...disabled]) })
124
+ assert.equal(toolAllowed(name, policy), name !== 'react_message' && (lane !== 'read' || operations.slice(0, 4).includes(name)))
125
+ assert.equal(toolAllowed('', policy), false)
126
+ })
127
+ }
128
+
129
+ const branchCases = [
130
+ ['agent/fix', true], ['agent/ticket-77', true], ['agent/ui/comments-v2', true], ['agent/OPEN-77', true], ['agent/fix_loader.2', true], ['agent/a/b/c', true],
131
+ ['main', false], ['master', false], ['develop', false], ['release/v1', false], ['agent/', false], ['agent/../main', false], ['agent/.hidden/x', false],
132
+ ['agent/x/.hidden', false], ['agent/a.lock', false], ['agent/a.lock/x', false], ['agent/a..b', false], ['agent/a//b', false], ['agent/a/', false],
133
+ ['agent/a.', false], ['agent/a@{1}', false], ['agent/a:main', false], ['agent/a b', false], ['agent/a\nb', false], ['agent/a?b', false],
134
+ ['agent/a*b', false], ['agent/a[b', false], ['agent/a\\b', false], ['agent/~a', false], ['agent/-delete', false],
135
+ ]
136
+ for (const [index, [branch, valid]] of branchCases.entries()) for (const grant of ['exact', 'workspace', 'common-dir']) {
137
+ add(`branch-${index}-${grant}`, 'PR authorization', `${grant} permission ${valid ? 'allows' : 'refuses'} Git branch ${JSON.stringify(branch)} without broadening the agent/* rule`, () => {
138
+ const root = '/coding/repo', commonDir = '/coding/repo/.git', remote = 'git@github.com:org/repo.git'
139
+ const policy = grant === 'workspace' ? { workspaces: [{ root: '/coding' }] } : { authorizations: [{ root: grant === 'exact' ? root : '/external/worktree', commonDir: grant === 'common-dir' ? commonDir : undefined, remote }] }
140
+ assert.equal(isSafeAgentBranch(branch), valid)
141
+ const run = () => validatePrPushAuthorization({ root, commonDir, remote, branch, ...policy })
142
+ if (valid) assert.equal(run(), true)
143
+ else assert.throws(run, /Refusing to push branch/)
144
+ })
145
+ }
146
+ for (const root of ['/coding/repo', '/coding/repo/nested', '/coding-repo', '/coding/../elsewhere', '/elsewhere', '/coding']) for (const revoked of [false, true]) {
147
+ add(`workspace-boundary-${encodeURIComponent(root)}-${revoked}`, 'PR authorization', `Workspace grant checks canonical containment for ${root} with revocation ${revoked}`, () => {
148
+ const allowed = ['/coding/repo', '/coding/repo/nested', '/coding'].includes(root) && !revoked
149
+ const run = () => validatePrPushAuthorization({ root, commonDir: root + '/.git', remote: 'git@github.com:org/repo.git', branch: 'agent/fix', workspaces: [{ root: '/coding' }], revoked: revoked ? [root] : [] })
150
+ if (allowed) assert.equal(run(), true)
151
+ else assert.throws(run, /AUTH_REQUIRED/)
152
+ })
153
+ }
154
+
155
+ for (let mask = 0; mask < 16; mask++) for (const authorized of [false, true]) {
156
+ add(`authorization-resume-${mask}-${authorized}`, 'Authorization recovery', `Resume checks policy and preserves unrelated ticket gates for state mask ${mask} and local permission ${authorized}`, () => {
157
+ const state = {
158
+ blocked: new Set(['unrelated', ...(mask & 1 ? ['4:77'] : [])]),
159
+ repos: new Map([['unrelated', '/other'], ...(mask & 2 ? [['4:77', '/coding/repo']] : [])]),
160
+ failed: new Map([['unrelated', 'revision'], ...(mask & 4 ? [['4:77', 'revision']] : [])]),
161
+ seen: new Set(['unrelated', ...(mask & 8 ? ['4:77'] : [])]),
162
+ authorized: (repo) => { assert.equal(repo, '/coding/repo'); return authorized },
163
+ }
164
+ const shouldResume = !!(mask & 1) && !!(mask & 2) && authorized
165
+ assert.equal(releaseAuthorizedPause('4:77', state), shouldResume)
166
+ for (const collection of [state.blocked, state.repos, state.failed, state.seen]) assert.equal(collection.has('unrelated'), true)
167
+ if (shouldResume) for (const collection of [state.blocked, state.repos, state.failed, state.seen]) assert.equal(collection.has('4:77'), false)
168
+ assert.equal(releaseAuthorizedPause('absent', state), false)
169
+ })
170
+ }
171
+
172
+ const columns = [['Done', 'done'], ['Completed', 'done'], ['Closed', 'done'], ['Cancelled', 'done'], ['Archived', 'done'], ['Resolved', 'done'], ['In Review', 'review'], ['Testing', 'review'], ['QA', 'review'], ['Quality Assurance', 'review'], ['Approval', 'review'], ['In Progress', 'active'], ['Todo', 'active'], ['Incomplete', 'active']]
173
+ for (const [label, classification] of columns) for (const field of ['types', 'task_types', 'taskTypes']) for (const stringId of [false, true]) {
174
+ add(`columns-${label.toLowerCase().replaceAll(' ', '-')}-${field}-${stringId}`, 'Board column discovery', `${field} recognizes ${JSON.stringify(label)} with a ${stringId ? 'string' : 'numeric'} ID and caches only its project`, async () => {
175
+ const calls = []
176
+ const lookup = createTaskTypeLookup({ load: async (project) => { calls.push(project); return { [field]: [{ id: stringId ? '7' : 7, name: label }] } } })
177
+ const [first, duplicate] = await Promise.all([lookup(4), lookup('4')])
178
+ assert.equal(first, duplicate)
179
+ assert.equal(first.done.has(7), classification === 'done')
180
+ assert.equal(first.review.has(7), classification === 'review')
181
+ assert.equal(calls.length, 1)
182
+ await lookup(5)
183
+ assert.equal(calls.length, 2)
184
+ })
185
+ }
186
+ for (const [index, id] of [null, undefined, '', 'abc', -1, 0, 1.5, Infinity, NaN, '9007199254740992', true, false, ['7']].entries()) {
187
+ add(`invalid-column-id-${index}`, 'Board column discovery', `Malformed terminal column ID ${String(id)} cannot suppress an actionable ticket`, async () => {
188
+ const lookup = createTaskTypeLookup({ load: async () => ({ types: [null, { id, name: 'Done' }, { id, name: 'Testing' }] }) })
189
+ const value = await lookup(4)
190
+ assert.equal(value.done.size, 0)
191
+ assert.equal(value.review.size, 0)
192
+ })
193
+ }
194
+
195
+ for (const count of [0, 1, 2, 7, 30]) for (const concurrency of [1, 2, 3, 8]) {
196
+ add(`concurrent-${count}-${concurrency}`, 'Bounded reconciliation', `${count} projects preserve ordering under concurrency ${concurrency}`, async () => {
197
+ let active = 0, peak = 0
198
+ const inputs = Array.from({ length: count }, (_, index) => index)
199
+ const result = await mapConcurrent(inputs, concurrency, async (value, index) => {
200
+ assert.equal(value, index)
201
+ peak = Math.max(peak, ++active)
202
+ await tick()
203
+ active--
204
+ return value * 3
205
+ })
206
+ assert.deepEqual(result, inputs.map((value) => value * 3))
207
+ assert.equal(peak, Math.min(count, concurrency))
208
+ assert.equal(active, 0)
209
+ })
210
+ }
211
+
212
+ for (const kind of ['ticket', 'thread']) for (const state of ['received', 'queued', 'running', 'blocked', 'completed', 'rendered']) for (const restarted of [false, true]) {
213
+ add(`memory-${kind}-${state}-${restarted}`, 'Durable replay memory', `${kind} ${state} observation stays deduplicated and isolated ${restarted ? 'after recreation' : 'in its current runtime'}`, () => temp(async (dir) => {
214
+ const path = join(dir, 'ledger.json')
215
+ const refs = kind === 'ticket' ? { projectId: 4, ticketId: 77 } : { channelId: 21, threadId: 600 }
216
+ const otherRefs = kind === 'ticket' ? { projectId: 5, ticketId: 77 } : { channelId: 21, threadId: 601 }
217
+ let stamp = 0
218
+ let graph = createByoMemoryGraph({ path, now: () => ++stamp })
219
+ const entry = { key: 'event:source', kind, state, summary: 'source acceptance criteria', refs }
220
+ const first = graph.remember(entry)
221
+ assert.equal(graph.remember(entry), first)
222
+ graph.remember({ ...entry, key: 'event:other', summary: 'unrelated private context', refs: otherRefs })
223
+ if (restarted) graph = createByoMemoryGraph({ path })
224
+ assert.equal(graph.has(entry.key, state), true)
225
+ assert.equal(graph.recall(refs).length, 1)
226
+ assert.match(graph.context(refs), /source acceptance criteria/)
227
+ assert.doesNotMatch(graph.context(refs), /unrelated private context/)
228
+ }))
229
+ }
230
+
231
+ add('cache-expiration-and-recovery', 'Board column discovery', 'Column changes become visible at TTL and failed reads do not poison the next lookup', async () => {
232
+ let now = 0, calls = 0, fail = false
233
+ const lookup = createTaskTypeLookup({ now: () => now, ttlMs: 10, load: async () => {
234
+ calls++
235
+ if (fail) throw new Error('temporary outage')
236
+ return { types: [{ id: calls, name: 'Done' }] }
237
+ } })
238
+ assert.equal((await lookup(4)).done.has(1), true)
239
+ now = 9
240
+ assert.equal((await lookup(4)).done.has(1), true)
241
+ now = 10; fail = true
242
+ await assert.rejects(lookup(4), /temporary outage/)
243
+ fail = false
244
+ assert.equal((await lookup(4)).done.has(3), true)
245
+ })
246
+ add('cache-capacity', 'Board column discovery', 'Column cache evicts older projects after 100 entries instead of growing without bound', async () => {
247
+ let calls = 0
248
+ const lookup = createTaskTypeLookup({ load: async () => { calls++; return { types: [] } } })
249
+ for (let project = 0; project <= 100; project++) await lookup(project)
250
+ await lookup(0)
251
+ assert.equal(calls, 102)
252
+ })
253
+ add('authorization-awaiting-decision', 'Authorization recovery', 'An asynchronous or non-boolean permission result cannot unlock a policy block prematurely', () => {
254
+ for (const result of [Promise.resolve(false), 'yes', {}, 1, null]) {
255
+ const state = { blocked: new Set(['4:77']), repos: new Map([['4:77', '/repo']]), failed: new Map([['4:77', 'revision']]), seen: new Set(['4:77']), authorized: () => result }
256
+ assert.equal(releaseAuthorizedPause('4:77', state), false)
257
+ assert.equal(state.blocked.has('4:77'), true)
258
+ }
259
+ })
260
+ add('concurrency-failure-drains-active', 'Bounded reconciliation', 'A failed project stops new scheduling and drains already-started work before returning the error', async () => {
261
+ const started = [], finished = []
262
+ await assert.rejects(mapConcurrent([0, 1, 2, 3, 4], 2, async (value) => {
263
+ started.push(value)
264
+ if (value === 0) throw new Error('project unavailable')
265
+ await tick(); finished.push(value)
266
+ }), /project unavailable/)
267
+ assert.deepEqual(started, [0, 1])
268
+ assert.deepEqual(finished, [1])
269
+ })
270
+ add('memory-bounded-edges', 'Durable replay memory', 'Memory capacity removes incident edges and keeps the newest verified states', () => temp(async (dir) => {
271
+ let stamp = 0
272
+ const graph = createByoMemoryGraph({ path: join(dir, 'ledger.json'), maxNodes: 2, now: () => ++stamp })
273
+ graph.remember({ key: 'old', refs: { channelId: 1, threadId: 1 } })
274
+ graph.remember({ key: 'middle', refs: { channelId: 1, threadId: 2 } })
275
+ graph.connect('old', 'middle', 'answered')
276
+ graph.remember({ key: 'new', refs: { channelId: 1, threadId: 3 } })
277
+ assert.equal(graph.has('old'), false)
278
+ const saved = JSON.parse(readFileSync(join(dir, 'ledger.json'), 'utf8'))
279
+ assert.equal(saved.nodes.length, 2)
280
+ assert.equal(saved.edges.length, 0)
281
+ }))
282
+ add('memory-shared-database-resource-isolation', 'Mastra persistence integration', 'Two agents sharing a database can remember the same ticket without overwriting each other’s thread ownership', () => temp(async (dir) => {
283
+ const databasePath = join(dir, 'shared.db'), refs = { projectId: 4, ticketId: 77 }
284
+ const a = createMastraMemory({ ledgerPath: join(dir, 'alex.json'), databasePath, resourceId: 'alex' })
285
+ const b = createMastraMemory({ ledgerPath: join(dir, 'robin.json'), databasePath, resourceId: 'robin' })
286
+ a.remember({ key: 'same', summary: 'Alex private verification', refs })
287
+ await a.settled()
288
+ b.remember({ key: 'same', summary: 'Robin private verification', refs })
289
+ await b.settled()
290
+ // Empty ledgers make this test prove database isolation, not JSON fallback.
291
+ const restored = (resourceId) => createMastraMemory({ ledgerPath: join(dir, `${resourceId}-empty.json`), databasePath, resourceId })
292
+ const a2 = restored('alex'), b2 = restored('robin')
293
+ assert.match(await a2.context(refs), /Alex private verification/)
294
+ assert.doesNotMatch(await a2.context(refs), /Robin private verification/)
295
+ assert.match(await b2.context(refs), /Robin private verification/)
296
+ await Promise.all([a2.settled(), b2.settled()])
297
+ }))
298
+
299
+ add('git-setup-worktree-revoke-regrant', 'Git authorization integration', 'Real Git worktrees inherit setup, revocation survives restart, and reauthorization works even after origin is restored', () => temp(async (dir) => {
300
+ const workspace = join(dir, 'workspace'), repo = join(workspace, 'repo'), worktree = join(dir, 'ticket-77')
301
+ const options = { authPath: join(dir, 'policy.json'), rulePath: join(dir, 'rules') }
302
+ configurePrPublishing({ workspace, ...options }); mkdirSync(repo)
303
+ const git = (...args) => execFileSync('git', args, { cwd: repo, stdio: 'pipe' })
304
+ git('init'); git('-c', 'core.hooksPath=/dev/null', '-c', 'user.name=Scenario', '-c', 'user.email=scenario@example.test', 'commit', '--allow-empty', '-m', 'fixture')
305
+ git('remote', 'add', 'origin', 'ssh://git@github.com/org/repo.git')
306
+ git('worktree', 'add', '-b', 'agent/ticket-77', worktree)
307
+ assert.equal(repositoryHasPrPushAuthorization({ cwd: worktree, ...options }), true)
308
+ git('remote', 'remove', 'origin')
309
+ revokePrPush({ cwd: worktree, ...options })
310
+ git('remote', 'add', 'origin', 'ssh://git@github.com/org/repo.git')
311
+ configurePrPublishing({ workspace, ...options })
312
+ assert.equal(repositoryHasPrPushAuthorization({ cwd: repo, ...options }), false)
313
+ authorizePrPush({ cwd: repo, ...options })
314
+ assert.equal(repositoryHasPrPushAuthorization({ cwd: worktree, ...options }), true)
315
+ }))
316
+ add('git-symlink-and-multiple-pushurls', 'Git authorization integration', 'Symlinks cannot escape workspace scope and the push helper pins one verified origin URL with hooks disabled', () => temp(async (dir) => {
317
+ const workspace = join(dir, 'workspace'), outside = join(dir, 'outside'), options = { authPath: join(dir, 'policy.json'), rulePath: join(dir, 'rules') }
318
+ configurePrPublishing({ workspace, ...options }); mkdirSync(outside)
319
+ execFileSync('git', ['init', outside], { stdio: 'pipe' })
320
+ execFileSync('git', ['-C', outside, 'remote', 'add', 'origin', 'https://github.com/org/repo.git'], { stdio: 'pipe' })
321
+ symlinkSync(outside, join(workspace, 'escape'))
322
+ assert.equal(repositoryHasPrPushAuthorization({ cwd: join(workspace, 'escape'), ...options }), false)
323
+ const calls = []
324
+ const spawn = (_command, args) => {
325
+ calls.push(args)
326
+ return { status: 0, stdout: args[0] === 'rev-parse' ? outside : args[0] === 'remote' ? 'https://github.com/org/repo.git' : args[0] === 'branch' ? 'agent/fix' : '' }
327
+ }
328
+ authorizePrPush({ cwd: outside, spawn, ...options })
329
+ pushPrBranch({ cwd: outside, spawn, ...options })
330
+ assert.deepEqual(calls.at(-1), ['-c', `core.hooksPath=${process.platform === 'win32' ? 'NUL' : '/dev/null'}`, 'push', '-u', 'https://github.com/org/repo.git', 'HEAD:refs/heads/agent/fix'])
331
+ }))
332
+
333
+ for (const [index, remote] of ['https://user:secret@github.com/org/repo.git', 'ssh://git:secret@github.com/org/repo.git', 'http://github.com/org/repo.git', 'file:///repo', '/repo', 'ext::command'].entries()) {
334
+ add(`unsafe-origin-${index}`, 'PR authorization', `Repository origin form ${remote.split('@').at(-1)} cannot broaden the constrained push helper`, () => temp(async (dir) => {
335
+ const spawn = (_command, args) => ({ status: 0, stdout: args[0] === 'rev-parse' ? dir : args[0] === 'remote' ? remote : 'agent/fix' })
336
+ assert.throws(() => repositoryPushIdentity(dir, spawn), /credentials|HTTPS or SSH/)
337
+ }))
338
+ }
339
+ add('malformed-authorization-file', 'PR authorization', 'Malformed policy collections fail closed and main setup can repair them without throwing', () => temp(async (dir) => {
340
+ const workspace = join(dir, 'workspace'), authPath = join(dir, 'policy.json'), rulePath = join(dir, 'rules')
341
+ for (const value of [null, [], { workspaces: {} }, { authorizations: [null], workspaces: [null], revoked: {} }]) {
342
+ writeFileSync(authPath, JSON.stringify(value))
343
+ configurePrPublishing({ workspace, authPath, rulePath })
344
+ const saved = JSON.parse(readFileSync(authPath, 'utf8'))
345
+ assert.deepEqual(saved.workspaces, [{ root: workspace }])
346
+ }
347
+ }))
348
+
349
+ const localHttp = async (handle, run) => {
350
+ const server = createServer((request, response) => {
351
+ let body = ''
352
+ request.on('data', (chunk) => { body += chunk })
353
+ request.on('end', () => { void Promise.resolve(handle(JSON.parse(body), request, response)).catch((error) => { response.statusCode = 500; response.end(error.message) }) })
354
+ })
355
+ server.listen(0, '127.0.0.1'); await once(server, 'listening')
356
+ try { await run(`http://127.0.0.1:${server.address().port}/mcp`) }
357
+ finally { server.closeAllConnections(); await new Promise((resolve) => server.close(resolve)) }
358
+ }
359
+ add('local-http-open-sse-stream', 'MCP HTTP integration', 'A streamed tool result completes before a server closes its SSE connection', async () => {
360
+ await localHttp((message, _request, res) => {
361
+ if (message.method === 'notifications/initialized') { res.writeHead(202); res.end(); return }
362
+ if (message.method === 'initialize') { res.setHeader('content-type', 'application/json'); res.end(JSON.stringify({ id: message.id, result: {} })); return }
363
+ res.writeHead(200, { 'content-type': 'text/event-stream' })
364
+ res.write('event: message\ndata: {"method":"notifications/progress","params":{}}\n\n')
365
+ res.write(`data: {"id":${message.id},\ndata: "result":{"content":[{"type":"text","text":"stream delivered ✓"}]}}\n\n`)
366
+ // Intentionally never end: receipt of the matching response is completion.
367
+ }, async (url) => {
368
+ const client = createMcpHttpClient({ url, apiKey: 'local-only', identifier: 'alex', requestTimeoutMs: 500 })
369
+ assert.equal((await client.callTool('post_message')).content[0].text, 'stream delivered ✓')
370
+ })
371
+ })
372
+ add('local-http-ambiguous-mutation-timeout', 'MCP HTTP integration', 'An accepted mutation whose response stalls is reported once without an automatic duplicate write', async () => {
373
+ let mutations = 0
374
+ await localHttp((message, _request, res) => {
375
+ if (message.method === 'tools/call') { mutations++; res.writeHead(200, { 'content-type': 'application/json' }); res.write('{'); return }
376
+ res.end('{}')
377
+ }, async (url) => {
378
+ const client = createMcpHttpClient({ url, apiKey: 'local-only', identifier: 'alex', requestTimeoutMs: 200 })
379
+ await assert.rejects(client.callTool('post_message'), /timed out/)
380
+ assert.equal(mutations, 1)
381
+ })
382
+ })
383
+ add('reset-during-initialize', 'MCP reset races', 'A stale initialize response cannot restore the session that a reset invalidated', async () => {
384
+ let release
385
+ const held = new Promise((resolve) => { release = resolve })
386
+ const { client, calls, counts } = fixture({ session: true, handler: async (request, count) => {
387
+ if (request.method === 'initialize' && count === 1) { await held; return response({ sessionId: 'stale-session', body: JSON.stringify({ id: request.id, result: {} }) }) }
388
+ } })
389
+ const old = client.initialize(); await tick()
390
+ client.reset(); await client.initialize(); release(); await old
391
+ await client.callTool('get_ticket')
392
+ assert.equal(counts.get('initialize'), 2)
393
+ assert.equal(calls.at(-1).headers['mcp-session-id'], 'session-2')
394
+ })
395
+ add('reset-during-tool-discovery', 'MCP reset races', 'An old tool discovery cannot overwrite the new session’s capability cache', async () => {
396
+ let release
397
+ const held = new Promise((resolve) => { release = resolve })
398
+ const { client } = fixture({ session: true, handler: async (request, count) => {
399
+ if (request.method !== 'tools/list') return null
400
+ if (count === 1) await held
401
+ return response({ body: JSON.stringify({ id: request.id, result: { tools: [{ name: count === 1 ? 'old_tool' : 'new_tool' }] } }) })
402
+ } })
403
+ const old = client.listTools(); await tick(); client.reset()
404
+ assert.equal((await client.listTools())[0].name, 'new_tool')
405
+ release(); assert.equal((await old)[0].name, 'new_tool')
406
+ assert.equal((await client.listTools())[0].name, 'new_tool')
407
+ })