@gemus/mcp-proxy 0.1.9 → 0.1.10

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.
@@ -1,356 +0,0 @@
1
- /**
2
- * @gemus/mcp-proxy fail-fast (#2068) — single-pass upstream-disconnect correlation.
3
- *
4
- * Pure logic; the socket is an injected `realFetch` returning controllable `Response`s. Cases that
5
- * involve the final-safety timer use fake timers with advanceTimersByTimeAsync (lesson: fake timers
6
- * + real reader.read() promises need the async advance to flush continuations).
7
- */
8
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
9
- import { parseRequestIds, isCodexOriginated, createSseIdScanner, createFailFast, isJsonRpcResponse } from '../failFast.mjs'
10
-
11
- const enc = new TextEncoder()
12
-
13
- /** A response body that emits the given SSE frames then closes. */
14
- function bodyOf(...frames: string[]): ReadableStream<Uint8Array> {
15
- return new ReadableStream<Uint8Array>({
16
- start(c) {
17
- for (const f of frames) c.enqueue(enc.encode(f))
18
- c.close()
19
- },
20
- })
21
- }
22
- /** A response body that never emits and never closes (zombie stream). */
23
- function neverBody(): ReadableStream<Uint8Array> {
24
- return new ReadableStream<Uint8Array>({ start() {} })
25
- }
26
- function sseRes(body: ReadableStream<Uint8Array> | null, init?: { status?: number; contentType?: string }) {
27
- const status = init?.status ?? 200
28
- return new Response(body, { status, headers: { 'content-type': init?.contentType ?? 'text/event-stream' } })
29
- }
30
- function resultFrame(id: number | string): string {
31
- return `event: message\ndata: ${JSON.stringify({ jsonrpc: '2.0', id, result: { ok: true } })}\n\n`
32
- }
33
- function post(id: number | string, method = 'tools/call') {
34
- return { method: 'POST', body: JSON.stringify({ jsonrpc: '2.0', id, method, params: {} }) }
35
- }
36
- async function drain(res: Response): Promise<void> {
37
- const r = res.body!.getReader()
38
- for (;;) { const { done } = await r.read(); if (done) break }
39
- }
40
-
41
- describe('parseRequestIds', () => {
42
- it('extracts the id from a POST JSON-RPC request', () => {
43
- expect(parseRequestIds(post(42))).toEqual([42])
44
- })
45
- it('extracts all ids from a batch', () => {
46
- const body = JSON.stringify([
47
- { jsonrpc: '2.0', id: 1, method: 'a' },
48
- { jsonrpc: '2.0', id: 2, method: 'b' },
49
- ])
50
- expect(parseRequestIds({ method: 'POST', body })).toEqual([1, 2])
51
- })
52
- it('returns [] for GET / DELETE / bodyless requests', () => {
53
- expect(parseRequestIds({ method: 'GET' })).toEqual([])
54
- expect(parseRequestIds({ method: 'DELETE' })).toEqual([])
55
- expect(parseRequestIds({ method: 'POST' })).toEqual([])
56
- })
57
- it('returns [] for non-JSON body and for notifications (no id)', () => {
58
- expect(parseRequestIds({ method: 'POST', body: 'not json' })).toEqual([])
59
- expect(parseRequestIds({ method: 'POST', body: JSON.stringify({ jsonrpc: '2.0', method: 'x' }) })).toEqual([])
60
- })
61
- })
62
-
63
- describe('isCodexOriginated', () => {
64
- it.each([
65
- [42, true],
66
- ['abc', true],
67
- ['gemus-bf-3', false],
68
- ['gemus-ping-9', false],
69
- [null, false],
70
- [undefined, false],
71
- ])('id=%s → %s', (id, expected) => {
72
- expect(isCodexOriginated(id as never)).toBe(expected)
73
- })
74
- })
75
-
76
- describe('isJsonRpcResponse', () => {
77
- it('is true for a result/error response, false for a request or notification', () => {
78
- expect(isJsonRpcResponse({ id: 1, result: {} })).toBe(true)
79
- expect(isJsonRpcResponse({ id: 1, error: {} })).toBe(true)
80
- expect(isJsonRpcResponse({ id: 1, method: 'elicitation/create', params: {} })).toBe(false) // server→client request
81
- expect(isJsonRpcResponse({ method: 'notifications/x' })).toBe(false)
82
- expect(isJsonRpcResponse(null)).toBe(false)
83
- })
84
- })
85
-
86
- describe('createSseIdScanner', () => {
87
- it('yields the id of a complete RESPONSE frame; ignores comment lines', () => {
88
- const s = createSseIdScanner()
89
- expect(s.push(enc.encode(': keepalive\n\n'))).toEqual([])
90
- expect(s.push(enc.encode(resultFrame(7)))).toEqual([7])
91
- })
92
- it('does NOT yield a server→client REQUEST id (has a method) — avoids id-collision premature settle', () => {
93
- const s = createSseIdScanner()
94
- const reqFrame = `event: message\ndata: ${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'elicitation/create', params: {} })}\n\n`
95
- expect(s.push(enc.encode(reqFrame))).toEqual([])
96
- })
97
- it('buffers a half-frame split across chunks until its boundary arrives', () => {
98
- const s = createSseIdScanner()
99
- const frame = resultFrame(5)
100
- const mid = Math.floor(frame.length / 2)
101
- expect(s.push(enc.encode(frame.slice(0, mid)))).toEqual([])
102
- expect(s.push(enc.encode(frame.slice(mid)))).toEqual([5])
103
- })
104
- it('handles CRLF line endings (normalizes to LF)', () => {
105
- const s = createSseIdScanner()
106
- const crlf = `event: message\r\ndata: ${JSON.stringify({ jsonrpc: '2.0', id: 8, result: {} })}\r\n\r\n`
107
- expect(s.push(enc.encode(crlf))).toEqual([8])
108
- })
109
- it('ignores non-JSON data payloads', () => {
110
- const s = createSseIdScanner()
111
- expect(s.push(enc.encode('data: not-json\n\n'))).toEqual([])
112
- })
113
- })
114
-
115
- describe('createFailFast.fetch', () => {
116
- it('injects a JSON-RPC error when the stream closes before the result', async () => {
117
- const onLost = vi.fn()
118
- const realFetch = vi.fn(async () => sseRes(bodyOf(': keepalive\n\n'))) // no result frame → lost
119
- const ff = createFailFast({ onLost, realFetch })
120
-
121
- const res = await ff.fetch('u', post(42))
122
- await drain(res)
123
-
124
- expect(onLost).toHaveBeenCalledTimes(1)
125
- expect(onLost).toHaveBeenCalledWith(42, { kind: 'stream' })
126
- expect(ff.failedIds.has('42')).toBe(true)
127
- })
128
-
129
- it('does NOT inject an error when the result crosses the wire before close', async () => {
130
- const onLost = vi.fn()
131
- const realFetch = vi.fn(async () => sseRes(bodyOf(resultFrame(42))))
132
- const ff = createFailFast({ onLost, realFetch })
133
-
134
- const res = await ff.fetch('u', post(42))
135
- await drain(res)
136
-
137
- expect(onLost).not.toHaveBeenCalled()
138
- expect(ff.failedIds.has('42')).toBe(false)
139
- })
140
-
141
- it('fails fast immediately when realFetch throws before any response', async () => {
142
- const onLost = vi.fn()
143
- const realFetch = vi.fn(async () => {
144
- throw new Error('socket reset', { cause: Object.assign(new Error('inner socket failure'), { code: 'ECONNRESET' }) })
145
- })
146
- const ff = createFailFast({ onLost, realFetch })
147
-
148
- await expect(ff.fetch('u', post(7))).rejects.toThrow('socket reset')
149
- expect(onLost).toHaveBeenCalledWith(7, { kind: 'connection', code: 'ECONNRESET' })
150
- expect(realFetch).toHaveBeenCalledTimes(1) // must NOT retry — a 2nd fetch could execute the tool
151
- })
152
-
153
- it('allowlists a nested connection code without exposing hostile raw diagnostics or retrying', async () => {
154
- const hostile =
155
- 'Authorization: Bearer arbitrary-secret at '
156
- + 'https://alice:password@example.test/api/mcp?token=query-secret '
157
- + 'via 203.0.113.1,2001:db8::1'
158
- const onLost = vi.fn()
159
- const log = vi.fn()
160
- const rejection = new Error(`outer ${hostile}`, {
161
- cause: new Error(`middle ${hostile}`, {
162
- cause: Object.assign(new Error(`inner ${hostile}`), {
163
- code: 'UND_ERR_CONNECT_TIMEOUT',
164
- }),
165
- }),
166
- })
167
- const realFetch = vi.fn(async () => { throw rejection })
168
- const ff = createFailFast({ onLost, log, realFetch })
169
-
170
- await expect(ff.fetch('u', post(71))).rejects.toBe(rejection)
171
-
172
- expect(realFetch).toHaveBeenCalledTimes(1)
173
- expect(onLost).toHaveBeenCalledTimes(1)
174
- expect(onLost).toHaveBeenCalledWith(71, {
175
- kind: 'connection',
176
- code: 'UND_ERR_CONNECT_TIMEOUT',
177
- })
178
- const diagnostics = JSON.stringify(log.mock.calls)
179
- expect(diagnostics).not.toMatch(
180
- /arbitrary-secret|alice|password|query-secret|203\.0\.113\.1|2001:db8/,
181
- )
182
- })
183
-
184
- it('classifies an established SSE reader error before its result as a stream failure', async () => {
185
- const readerError = new Error('established stream failed')
186
- let pulls = 0
187
- const body = new ReadableStream<Uint8Array>({
188
- pull(controller) {
189
- if (pulls++ === 0) {
190
- controller.enqueue(enc.encode(': established\n\n'))
191
- } else {
192
- controller.error(readerError)
193
- }
194
- },
195
- })
196
- const onLost = vi.fn()
197
- const realFetch = vi.fn(async () => sseRes(body))
198
- const ff = createFailFast({ onLost, realFetch })
199
-
200
- const res = await ff.fetch('u', post(72))
201
- await expect(drain(res)).rejects.toBe(readerError)
202
-
203
- expect(onLost).toHaveBeenCalledTimes(1)
204
- expect(onLost).toHaveBeenCalledWith(72, { kind: 'stream' })
205
- })
206
-
207
- it('classifies downstream cancellation before its result as a stream failure', async () => {
208
- const onLost = vi.fn()
209
- const realFetch = vi.fn(async () => sseRes(neverBody()))
210
- const ff = createFailFast({ onLost, realFetch })
211
- const res = await ff.fetch('u', post(73))
212
- const reader = res.body!.getReader()
213
-
214
- await reader.cancel('downstream closed')
215
-
216
- expect(onLost).toHaveBeenCalledTimes(1)
217
- expect(onLost).toHaveBeenCalledWith(73, { kind: 'stream' })
218
- })
219
-
220
- it('fails fast on a non-ok HTTP response', async () => {
221
- const onLost = vi.fn()
222
- const realFetch = vi.fn(async () => sseRes(null, { status: 404, contentType: 'application/json' }))
223
- const ff = createFailFast({ onLost, realFetch })
224
-
225
- await ff.fetch('u', post(7))
226
- expect(onLost).toHaveBeenCalledWith(7, { kind: 'http', status: 404 })
227
- })
228
-
229
- it('passes GET and DELETE through untouched (no body → no monitoring, no onLost)', async () => {
230
- const onLost = vi.fn()
231
- const getRes = sseRes(bodyOf())
232
- const realFetch = vi.fn(async () => getRes)
233
- const ff = createFailFast({ onLost, realFetch })
234
-
235
- const g = await ff.fetch('u', { method: 'GET' })
236
- const d = await ff.fetch('u', { method: 'DELETE' })
237
- expect(g).toBe(getRes)
238
- expect(d).toBe(getRes)
239
- expect(onLost).not.toHaveBeenCalled()
240
- })
241
-
242
- it('does not track internal gemus-bf-* / gemus-ping-* ids', async () => {
243
- const onLost = vi.fn()
244
- const realFetch = vi.fn(async () => sseRes(bodyOf(': keepalive\n\n'))) // closes with no result
245
- const ff = createFailFast({ onLost, realFetch })
246
-
247
- await drain(await ff.fetch('u', post('gemus-bf-1')))
248
- await drain(await ff.fetch('u', post('gemus-ping-2')))
249
- expect(onLost).not.toHaveBeenCalled()
250
- })
251
-
252
- it('degrades to plain passthrough if internal logic throws', async () => {
253
- const onLost = vi.fn()
254
- const badRes = { ok: true, status: 200, headers: { get() { throw new Error('boom') } }, body: null } as unknown as Response
255
- const realFetch = vi.fn(async () => badRes)
256
- const ff = createFailFast({ onLost, realFetch })
257
-
258
- const res = await ff.fetch('u', post(1))
259
- expect(res).toBe(badRes) // returns the response we ALREADY have — no re-fetch
260
- expect(realFetch).toHaveBeenCalledTimes(1)
261
- expect(onLost).not.toHaveBeenCalled()
262
- ff.dispose()
263
- })
264
-
265
- it('isolates concurrent requests — disconnecting one does not affect the other', async () => {
266
- const onLost = vi.fn()
267
- const realFetch = vi
268
- .fn()
269
- .mockResolvedValueOnce(sseRes(bodyOf(': keepalive\n\n'))) // id 1 → dies with no result
270
- .mockResolvedValueOnce(sseRes(bodyOf(resultFrame(2)))) // id 2 → delivers
271
- const ff = createFailFast({ onLost, realFetch })
272
-
273
- await drain(await ff.fetch('u', post(1)))
274
- await drain(await ff.fetch('u', post(2)))
275
-
276
- expect(onLost).toHaveBeenCalledTimes(1)
277
- expect(onLost.mock.calls[0][0]).toBe(1)
278
- })
279
-
280
- it('does not settle a healthy concurrent request when another connection rejects before response', async () => {
281
- let healthyController!: ReadableStreamDefaultController<Uint8Array>
282
- const healthyBody = new ReadableStream<Uint8Array>({
283
- start(controller) { healthyController = controller },
284
- })
285
- const connectionError = Object.assign(new Error('connection rejected'), {
286
- code: 'ECONNREFUSED',
287
- })
288
- const onLost = vi.fn()
289
- const realFetch = vi.fn(async (_url: unknown, init: { body?: unknown }) => {
290
- const [{ id }] = [JSON.parse(String(init.body))]
291
- if (id === 74) throw connectionError
292
- return sseRes(healthyBody)
293
- })
294
- const ff = createFailFast({ onLost, realFetch })
295
-
296
- const rejecting = ff.fetch('u', post(74))
297
- const healthy = ff.fetch('u', post(75))
298
- await expect(rejecting).rejects.toBe(connectionError)
299
- const healthyResponse = await healthy
300
-
301
- expect(onLost).toHaveBeenCalledTimes(1)
302
- expect(onLost).toHaveBeenCalledWith(74, {
303
- kind: 'connection',
304
- code: 'ECONNREFUSED',
305
- })
306
- healthyController.enqueue(enc.encode(resultFrame(75)))
307
- healthyController.close()
308
- await drain(healthyResponse)
309
- expect(onLost).toHaveBeenCalledTimes(1)
310
- expect(ff.failedIds.has('75')).toBe(false)
311
- })
312
-
313
- describe('idle safety timeout', () => {
314
- beforeEach(() => vi.useFakeTimers())
315
- afterEach(() => vi.useRealTimers())
316
-
317
- it('injects an error for a zombie connection that never emits a byte', async () => {
318
- const onLost = vi.fn()
319
- const realFetch = vi.fn(async () => sseRes(neverBody()))
320
- const ff = createFailFast({ onLost, realFetch, idleTimeoutMs: 120_000 })
321
-
322
- await ff.fetch('u', post(99)) // never emits → idle timer is never reset
323
- expect(onLost).not.toHaveBeenCalled()
324
- await vi.advanceTimersByTimeAsync(120_000)
325
- expect(onLost).toHaveBeenCalledWith(99, { kind: 'idle' })
326
- ff.dispose()
327
- })
328
-
329
- it('does NOT kill a healthy-but-slow stream — heartbeat bytes reset the idle timer', async () => {
330
- const onLost = vi.fn()
331
- let ctl!: ReadableStreamDefaultController<Uint8Array>
332
- const body = new ReadableStream<Uint8Array>({ start(c) { ctl = c } })
333
- const realFetch = vi.fn(async () => sseRes(body))
334
- const ff = createFailFast({ onLost, realFetch, idleTimeoutMs: 100 })
335
-
336
- const res = await ff.fetch('u', post(1))
337
- const reader = res.body!.getReader()
338
-
339
- // Keepalive every 60ms (< 100ms idle) for 300ms total — each read touches → never fires.
340
- for (let i = 0; i < 5; i++) {
341
- ctl.enqueue(enc.encode(': keepalive\n\n'))
342
- await reader.read()
343
- await vi.advanceTimersByTimeAsync(60)
344
- }
345
- expect(onLost).not.toHaveBeenCalled() // survived 300ms > idleTimeout because bytes kept flowing
346
-
347
- // Now the result arrives and the stream closes → still no fail-fast.
348
- ctl.enqueue(enc.encode(resultFrame(1)))
349
- await reader.read()
350
- ctl.close()
351
- await reader.read()
352
- expect(onLost).not.toHaveBeenCalled()
353
- ff.dispose()
354
- })
355
- })
356
- })
@@ -1,216 +0,0 @@
1
- function moduleUrl(source) {
2
- return `data:text/javascript,${encodeURIComponent(source)}`
3
- }
4
-
5
- const markerPrelude = `
6
- import fs from 'node:fs'
7
- function mark(name) {
8
- const file = process.env.PROXY_TEST_CONSTRUCTION_LOG
9
- if (file) fs.appendFileSync(file, name + '\\n')
10
- }
11
- `
12
-
13
- const modules = new Map([
14
- ['@gemus/codex-backfill-core', `
15
- export const CLIENT_CAPABILITIES = []
16
- export const CLIENT_CAPABILITIES_HEADER = 'X-Gemus-Client-Capabilities'
17
- export function serializeClientCapabilities(value) { return JSON.stringify(value) }
18
- export function unwrapMcpContent(value) { return value }
19
- `],
20
- ['@modelcontextprotocol/sdk/server/stdio.js', `
21
- ${markerPrelude}
22
- export class StdioServerTransport {
23
- constructor() { mark('stdio-transport') }
24
- async start() {
25
- if (process.env.PROXY_TEST_STDIO_START_FAILURE === '1') {
26
- this.onclose?.()
27
- throw new Error(
28
- 'stdio failed Authorization: Bearer '
29
- + process.env.GEMUS_KEY
30
- + ' at '
31
- + process.env.GEMUS_URL
32
- )
33
- }
34
- if (process.env.PROXY_TEST_FORWARD_STDIN === '1') {
35
- let input = ''
36
- process.stdin.setEncoding('utf8')
37
- process.stdin.on('data', (chunk) => {
38
- input += chunk
39
- for (;;) {
40
- const newline = input.indexOf('\\n')
41
- if (newline < 0) break
42
- const line = input.slice(0, newline).trim()
43
- input = input.slice(newline + 1)
44
- if (line) this.onmessage?.(JSON.parse(line))
45
- }
46
- })
47
- }
48
- process.stdin.resume()
49
- process.stdin.once('end', () => this.onclose?.())
50
- if (process.env.PROXY_TEST_SIGNAL_AFTER_START === 'SIGTERM') {
51
- setTimeout(() => {
52
- if (process.platform === 'win32') {
53
- process.emit('SIGTERM')
54
- } else {
55
- process.kill(process.pid, 'SIGTERM')
56
- }
57
- }, 0)
58
- }
59
- }
60
- async send(message) {
61
- if (process.env.PROXY_TEST_STDIO_CAPTURE_SEND === '1') {
62
- process.stdout.write(JSON.stringify(message) + '\\n')
63
- }
64
- }
65
- }
66
- `],
67
- ['@modelcontextprotocol/sdk/client/streamableHttp.js', `
68
- ${markerPrelude}
69
- export class StreamableHTTPClientTransport {
70
- constructor(url, options) {
71
- mark('upstream-transport')
72
- this.url = url
73
- this.fetch = options.fetch
74
- }
75
- async start() {}
76
- async send(message) {
77
- if (
78
- process.env.PROXY_TEST_UPSTREAM_INITIALIZE_FAILURE === '1'
79
- && message?.method === 'initialize'
80
- ) {
81
- throw new Error(
82
- 'initialize fetch failed Authorization: Bearer '
83
- + process.env.GEMUS_KEY
84
- + ' at '
85
- + process.env.GEMUS_URL
86
- )
87
- }
88
- if (
89
- process.env.PROXY_TEST_UPSTREAM_INITIALIZE_FETCH_FAILURE === '1'
90
- && message?.method === 'initialize'
91
- ) {
92
- await this.fetch(this.url, {
93
- method: 'POST',
94
- headers: { Authorization: 'Bearer ' + process.env.GEMUS_KEY },
95
- body: JSON.stringify(message),
96
- })
97
- }
98
- if (
99
- process.env.PROXY_TEST_LATE_RESULT === '1'
100
- && message?.method === 'tools/call'
101
- ) {
102
- await this.fetch(this.url, {
103
- method: 'POST',
104
- body: JSON.stringify(message),
105
- })
106
- setTimeout(() => {
107
- this.onmessage?.({
108
- jsonrpc: '2.0',
109
- id: message.id,
110
- result: { content: [{ type: 'text', text: 'late-real-result' }] },
111
- })
112
- }, 80)
113
- }
114
- }
115
- async terminateSession() {
116
- mark('upstream-terminate-session')
117
- if (process.env.PROXY_TEST_TERMINATE_SESSION_HANG === '1') {
118
- return await new Promise(() => {})
119
- }
120
- if (process.env.PROXY_TEST_TERMINATE_SESSION_REJECT === '1') {
121
- throw new Error('terminate session rejected')
122
- }
123
- }
124
- async close() { mark('upstream-transport-close') }
125
- }
126
- `],
127
- ['undici', `
128
- ${markerPrelude}
129
- export class Agent {
130
- constructor() { mark('upstream-http-agent') }
131
- async close() { mark('upstream-http-shutdown') }
132
- destroy() { mark('upstream-http-destroy') }
133
- }
134
- export class Pool {
135
- destroy() { mark('upstream-origin-destroy') }
136
- }
137
- export async function fetch() {
138
- mark('undici-fetch')
139
- if (process.env.PROXY_TEST_LATE_RESULT === '1') {
140
- return new Response(
141
- new ReadableStream({ start() {} }),
142
- { headers: { 'content-type': 'text/event-stream' } },
143
- )
144
- }
145
- const error = new Error(
146
- 'connect failed Authorization: Bearer '
147
- + process.env.GEMUS_KEY
148
- + ' at '
149
- + process.env.GEMUS_URL
150
- + ' via 203.0.113.1,2001:db8::1'
151
- )
152
- error.cause = { code: 'UND_ERR_CONNECT_TIMEOUT' }
153
- throw error
154
- }
155
- `],
156
- ['node:http', `
157
- ${markerPrelude}
158
- function hookError() {
159
- return new Error(
160
- 'hook listen failed Authorization: Bearer '
161
- + process.env.GEMUS_KEY
162
- + ' at '
163
- + process.env.GEMUS_URL
164
- )
165
- }
166
- const http = {
167
- createServer() {
168
- mark('hook-server')
169
- const listeners = new Map()
170
- return {
171
- on(event, handler) { listeners.set(event, handler); return this },
172
- listen(_port, _host, callback) {
173
- if (process.env.PROXY_TEST_HOOK_LISTEN_FAILURE === '1') {
174
- queueMicrotask(() => listeners.get('error')?.(hookError()))
175
- } else {
176
- queueMicrotask(callback)
177
- }
178
- return this
179
- },
180
- address() { return { port: 43114 } },
181
- unref() {},
182
- close() {
183
- mark('hook-server-close')
184
- if (process.env.PROXY_TEST_HOOK_ERROR_AFTER_CLOSE === '1') {
185
- queueMicrotask(() => listeners.get('error')?.(hookError()))
186
- }
187
- },
188
- }
189
- },
190
- }
191
- export default http
192
- `],
193
- ['./backfill.mjs', `
194
- ${markerPrelude}
195
- export function createBackfiller() {
196
- mark('backfiller')
197
- return {
198
- observeCall() { mark('backfiller-observe-call') },
199
- observeResult() { mark('backfiller-observe-result') },
200
- async backfillTurn() {},
201
- }
202
- }
203
- export function injectSteering() { return false }
204
- `],
205
- ])
206
-
207
- export async function resolve(specifier, context, nextResolve) {
208
- const source = modules.get(specifier)
209
- if (
210
- source !== undefined
211
- && (specifier !== './backfill.mjs' || context.parentURL?.endsWith('/proxy.mjs'))
212
- ) {
213
- return { url: moduleUrl(source), shortCircuit: true }
214
- }
215
- return nextResolve(specifier, context)
216
- }
@@ -1,32 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { CLIENT_CAPABILITIES } from '@gemus/codex-backfill-core'
3
- import { buildProxyUpstreamHeaders } from '../mcpHeaders.mjs'
4
-
5
- describe('buildProxyUpstreamHeaders', () => {
6
- it('advertises only the capabilities the proxy can actually fulfil', () => {
7
- expect(buildProxyUpstreamHeaders('mak_test123')).toEqual({
8
- Authorization: 'Bearer mak_test123',
9
- 'X-Gemus-Client-Capabilities': 'agent-authored-prompts,codex-imagen',
10
- })
11
- })
12
-
13
- it('never advertises codex-instant-delivery — the proxy has no ImageScheduler to renew the lease (#2049)', () => {
14
- // 该能力门控服务端的 AWAITING_CLIENT execute 分支,要求客户端有 scheduler 每 60s renewLease。
15
- // 只有 bridge 有(且 probe 门控,见 bridge/src/cli.ts)。proxy 没有 —— 发了它就等于承诺一个
16
- // 永不到来的续租 → 5min 后 CLIENT_LEASE_EXPIRED → 图 orphan 成孤立 image-upload 节点。
17
- // absent → 服务端回退经典两阶段 client-delivered 协议,proxy 完全支持(backfill.mjs)。
18
- const header = buildProxyUpstreamHeaders('mak_test123')['X-Gemus-Client-Capabilities']
19
- expect(header).not.toContain('codex-instant-delivery')
20
- })
21
-
22
- it('stays a strict subset of the shared allowlist — a new capability must not auto-inherit (#2049)', () => {
23
- // 回归门:本 bug 的成因正是 proxy 发「整个 CLIENT_CAPABILITIES 数组」—— #1969 往数组里加了
24
- // codex-instant-delivery,proxy 就自动继承了一个它做不到的承诺,而 #1990 把红掉的断言改成
25
- // 迎合它。此断言让「加能力」与「proxy 声称支持它」变成两个独立决策,不再自动传染。
26
- const advertised = buildProxyUpstreamHeaders('mak_test123')['X-Gemus-Client-Capabilities'].split(',')
27
- for (const capability of advertised) {
28
- expect(CLIENT_CAPABILITIES).toContain(capability)
29
- }
30
- expect(advertised.length).toBeLessThan(CLIENT_CAPABILITIES.length)
31
- })
32
- })
@@ -1,81 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { createProxyFailure, publicMessageForFailure } from '../proxyMessages.mjs'
3
-
4
- describe('publicMessageForFailure', () => {
5
- it.each([
6
- [
7
- { kind: 'connection', code: 'UND_ERR_CONNECT_TIMEOUT' },
8
- 'Gemus upstream connection failed before a response was received',
9
- ],
10
- [
11
- { kind: 'http', status: 503 },
12
- 'Gemus upstream returned an HTTP error before the tool result was delivered',
13
- ],
14
- [
15
- { kind: 'stream' },
16
- 'Gemus upstream stream disconnected before the tool result was delivered',
17
- ],
18
- [
19
- { kind: 'idle' },
20
- 'Gemus upstream stream became unresponsive before the tool result was delivered',
21
- ],
22
- ])('maps $kind to its delivery-stage message', (failure, expected) => {
23
- expect(publicMessageForFailure(failure)).toBe(expected)
24
- })
25
-
26
- it('fails closed for an unknown failure kind', () => {
27
- expect(publicMessageForFailure({ kind: 'hostile-secret-kind' })).toBe(
28
- 'Gemus upstream failed before the tool result was delivered',
29
- )
30
- })
31
-
32
- it.each(['toString', 'constructor', '__proto__'])(
33
- 'fails closed for the Object.prototype property name %s',
34
- (kind) => {
35
- expect(publicMessageForFailure({ kind })).toBe(
36
- 'Gemus upstream failed before the tool result was delivered',
37
- )
38
- },
39
- )
40
- })
41
-
42
- describe('createProxyFailure', () => {
43
- it('preserves the original JSON-RPC id in the public error envelope', () => {
44
- expect(createProxyFailure('request-17', { kind: 'stream' }).response).toEqual({
45
- jsonrpc: '2.0',
46
- id: 'request-17',
47
- error: {
48
- code: -32000,
49
- message: 'Gemus upstream stream disconnected before the tool result was delivered',
50
- },
51
- })
52
- })
53
-
54
- it.each([
55
- [
56
- {
57
- kind: 'connection',
58
- code: 'UND_ERR_CONNECT_TIMEOUT',
59
- reason: 'Authorization: Bearer secret at https://alice:password@example.test/api/mcp?token=secret',
60
- addresses: ['203.0.113.1', '2001:db8::1'],
61
- },
62
- { id: 41, kind: 'connection', code: 'UND_ERR_CONNECT_TIMEOUT' },
63
- ],
64
- [
65
- {
66
- kind: 'http',
67
- status: 503,
68
- reason: 'https://alice:password@example.test/api/mcp?token=secret',
69
- },
70
- { id: 41, kind: 'http', status: 503 },
71
- ],
72
- ])('keeps only safe diagnostic fields for $kind failures', (failure, expected) => {
73
- const result = createProxyFailure(41, failure)
74
- const serialized = JSON.stringify(result)
75
-
76
- expect(result.diagnostic).toEqual(expected)
77
- expect(serialized).not.toMatch(
78
- /Authorization|Bearer|secret|alice|password|example\.test|203\.0\.113\.1|2001:db8/,
79
- )
80
- })
81
- })