@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.
- package/README.md +41 -21
- package/package.json +3 -2
- package/src/proxy.mjs +29 -427
- package/src/relay.mjs +440 -0
- package/src/setup/cli.mjs +130 -0
- package/src/setup/codex.mjs +110 -0
- package/src/setup/config.mjs +530 -0
- package/src/setup/configFile.mjs +155 -0
- package/src/setup/environment.mjs +79 -0
- package/src/setup/secret.mjs +91 -0
- package/src/__tests__/backfill.test.ts +0 -394
- package/src/__tests__/failFast.test.ts +0 -356
- package/src/__tests__/fixtures/proxy-runtime-loader.mjs +0 -216
- package/src/__tests__/mcpHeaders.test.ts +0 -32
- package/src/__tests__/proxyMessages.test.ts +0 -81
- package/src/__tests__/route.integration.test.ts +0 -68
- package/src/__tests__/startup.test.ts +0 -752
- package/src/__tests__/upstreamHttp.real.test.ts +0 -37
- package/src/__tests__/upstreamHttp.test.ts +0 -151
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @gemus/mcp-proxy backfill orchestration (#1751, Mode B). Mocks only the core's NETWORK + rollout-IO
|
|
5
|
-
* (deliverImageToNode / addImageToCanvas / readRollout / findRolloutFile); keeps extractImageOutputs +
|
|
6
|
-
* ExecuteClaimTracker + unwrapMcpContent REAL so the tap→tracker→claim/salvage wiring is exercised
|
|
7
|
-
* end-to-end. Shares the same @modelcontextprotocol/sdk instance as bridge tests via the vitest alias.
|
|
8
|
-
*/
|
|
9
|
-
const mocks = vi.hoisted(() => ({
|
|
10
|
-
deliverImageToNode: vi.fn(),
|
|
11
|
-
addImageToCanvas: vi.fn(),
|
|
12
|
-
readRollout: vi.fn(),
|
|
13
|
-
findRolloutFile: vi.fn(),
|
|
14
|
-
readGeneratedImageFiles: vi.fn(),
|
|
15
|
-
}))
|
|
16
|
-
|
|
17
|
-
vi.mock('@gemus/codex-backfill-core', async (importOriginal) => {
|
|
18
|
-
const actual = await importOriginal<typeof import('@gemus/codex-backfill-core')>()
|
|
19
|
-
return {
|
|
20
|
-
...actual, // real extractImageOutputs / ExecuteClaimTracker / EXECUTE_TOOL_NAMES / unwrapMcpContent
|
|
21
|
-
deliverImageToNode: mocks.deliverImageToNode,
|
|
22
|
-
addImageToCanvas: mocks.addImageToCanvas,
|
|
23
|
-
readRollout: mocks.readRollout,
|
|
24
|
-
findRolloutFile: mocks.findRolloutFile,
|
|
25
|
-
readGeneratedImageFiles: mocks.readGeneratedImageFiles,
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
import { createBackfiller, normalizeToolName, injectSteering, COMPANION_STEERING } from '../backfill.mjs'
|
|
30
|
-
import { EXECUTE_TOOL_NAMES } from '@gemus/codex-backfill-core'
|
|
31
|
-
|
|
32
|
-
/** rollout JSONL with N imagegen images (distinct call_ids, PNG-magic base64 so mime→png). */
|
|
33
|
-
function rollout(images: Array<[string, string]>): string {
|
|
34
|
-
return images
|
|
35
|
-
.map(([callId, b64]) =>
|
|
36
|
-
JSON.stringify({ payload: { type: 'image_generation_end', call_id: callId, result: b64 } }),
|
|
37
|
-
)
|
|
38
|
-
.join('\n')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** MCP ContentBlock[] wrap of a structured tool result (what the wire carries; unwrapMcpContent restores it). */
|
|
42
|
-
function wrap(obj: unknown) {
|
|
43
|
-
return [{ type: 'text', text: JSON.stringify(obj) }]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const IMG_A: [string, string] = ['ig_a', 'iVBORw0KGgoAAAA1']
|
|
47
|
-
const IMG_B: [string, string] = ['ig_b', 'iVBORw0KGgoAAAA2']
|
|
48
|
-
|
|
49
|
-
/** Simulate one `execute` Phase-1 awaiting_client_content anchor for `nodeId` on a turn. */
|
|
50
|
-
function observeExecuteAnchor(
|
|
51
|
-
bf: ReturnType<typeof createBackfiller>,
|
|
52
|
-
jsonrpcId: number,
|
|
53
|
-
turnKey: string,
|
|
54
|
-
sessionId: string,
|
|
55
|
-
workflowId: string,
|
|
56
|
-
nodeId: string,
|
|
57
|
-
) {
|
|
58
|
-
bf.observeCall({ jsonrpcId, turnKey, sessionId, name: 'execute', args: { workflowId, nodeId, dryRun: true } })
|
|
59
|
-
bf.observeResult({
|
|
60
|
-
jsonrpcId,
|
|
61
|
-
turnKey,
|
|
62
|
-
name: 'execute',
|
|
63
|
-
resultContent: wrap({ status: 'awaiting_client_content', nodeId }),
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function makeBackfiller() {
|
|
68
|
-
return createBackfiller({ server: 'http://localhost:3000', apiKey: 'mak_test', codexHome: '/fake/.codex' })
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
beforeEach(() => {
|
|
72
|
-
mocks.deliverImageToNode.mockReset().mockResolvedValue({ ok: true, status: 'processing' })
|
|
73
|
-
mocks.addImageToCanvas.mockReset().mockResolvedValue({ ok: true, entityId: 'agent_x' })
|
|
74
|
-
mocks.readRollout.mockReset().mockReturnValue('')
|
|
75
|
-
mocks.findRolloutFile.mockReset().mockReturnValue('/fake/rollout.jsonl')
|
|
76
|
-
// Default: generated_images unavailable → backfill falls back to the rollout source (existing tests).
|
|
77
|
-
mocks.readGeneratedImageFiles.mockReset().mockReturnValue({ available: false, images: [], failedCallIds: [] })
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
describe('normalizeToolName (tap → tracker)', () => {
|
|
81
|
-
it('maps wire execute names to the tracker constants', () => {
|
|
82
|
-
expect(normalizeToolName('execute')).toBe('mcp__gemus__execute')
|
|
83
|
-
expect(normalizeToolName('batch_execute')).toBe('mcp__gemus__batch_execute')
|
|
84
|
-
// the whole point: the normalized name must be one the tracker actually matches.
|
|
85
|
-
expect(EXECUTE_TOOL_NAMES.has(normalizeToolName('execute'))).toBe(true)
|
|
86
|
-
expect(EXECUTE_TOOL_NAMES.has(normalizeToolName('batch_execute'))).toBe(true)
|
|
87
|
-
})
|
|
88
|
-
it('passes non-execute tool names through unchanged', () => {
|
|
89
|
-
expect(normalizeToolName('canvas_read')).toBe('canvas_read')
|
|
90
|
-
expect(normalizeToolName('workflow_list')).toBe('workflow_list')
|
|
91
|
-
})
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
describe('injectSteering (Mode-B initialize instructions)', () => {
|
|
95
|
-
it('appends a search-neutral skill bootstrap to initialize instructions', () => {
|
|
96
|
-
const msg = { jsonrpc: '2.0', id: 1, result: { protocolVersion: '2024-11-05', serverInfo: { name: 'gemus' } } }
|
|
97
|
-
expect(injectSteering(msg)).toBe(true)
|
|
98
|
-
expect(msg.result.instructions).toContain('gemus-getting-started')
|
|
99
|
-
expect(msg.result.instructions).toContain('gemus-codex-imagen')
|
|
100
|
-
expect(msg.result.instructions).toBe(COMPANION_STEERING)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('does not pollute deferred search documents with registered tool names', () => {
|
|
104
|
-
const toolNames = [
|
|
105
|
-
'canvas_read', 'view_image', 'node_list', 'workflow_list', 'canvas_edit', 'ppt_edit',
|
|
106
|
-
'open_canvas', 'blueprint', 'execute', 'batch_execute', 'recall_history',
|
|
107
|
-
]
|
|
108
|
-
for (const toolName of toolNames) {
|
|
109
|
-
const exactName = new RegExp(`(?<![A-Za-z0-9_])${toolName}(?![A-Za-z0-9_])`, 'i')
|
|
110
|
-
expect(COMPANION_STEERING, `companion steering references ${toolName}`).not.toMatch(exactName)
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('preserves any existing server instructions (appends, not replaces)', () => {
|
|
115
|
-
const msg = { id: 1, result: { protocolVersion: '1', instructions: 'server says hi' } }
|
|
116
|
-
injectSteering(msg)
|
|
117
|
-
expect(msg.result.instructions).toContain('server says hi')
|
|
118
|
-
expect(msg.result.instructions).toContain('Gemus companion')
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('is a no-op for non-initialize messages (tool results, notifications)', () => {
|
|
122
|
-
const toolResult = { id: 5, result: { content: [{ type: 'text', text: '{}' }] } }
|
|
123
|
-
expect(injectSteering(toolResult)).toBe(false)
|
|
124
|
-
expect(toolResult.result.instructions).toBeUndefined()
|
|
125
|
-
expect(injectSteering({ method: 'notifications/message' })).toBe(false)
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
describe('backfillTurn — claim path (Stop hook)', () => {
|
|
130
|
-
it('delivers each scavenged image to its planned node (key-only: real workflowId, no sessionId header)', async () => {
|
|
131
|
-
const bf = makeBackfiller()
|
|
132
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
133
|
-
observeExecuteAnchor(bf, 2, 't1', 's1', 'wf_real', 'nodeB')
|
|
134
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A, IMG_B]))
|
|
135
|
-
|
|
136
|
-
const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/precise.jsonl', salvageOnly: false })
|
|
137
|
-
|
|
138
|
-
expect(r).toEqual({ delivered: 2, orphaned: 0, dropped: 0 })
|
|
139
|
-
// Stop hook path reads the PRECISE rollout (transcript_path), not findRolloutFile.
|
|
140
|
-
expect(mocks.readRollout).toHaveBeenCalledWith('/precise.jsonl')
|
|
141
|
-
expect(mocks.findRolloutFile).not.toHaveBeenCalled()
|
|
142
|
-
// N:N ordered claim: nodeA→first image, nodeB→second.
|
|
143
|
-
expect(mocks.deliverImageToNode).toHaveBeenCalledTimes(2)
|
|
144
|
-
const calls = mocks.deliverImageToNode.mock.calls.map((c) => c[0])
|
|
145
|
-
expect(calls[0]).toMatchObject({ workflowId: 'wf_real', nodeId: 'nodeA', image: { callId: 'ig_a' } })
|
|
146
|
-
expect(calls[1]).toMatchObject({ workflowId: 'wf_real', nodeId: 'nodeB', image: { callId: 'ig_b' } })
|
|
147
|
-
// key-only invariant: no X-Gemus-Session-Id (no sessionId passed to the delivery fn).
|
|
148
|
-
expect(calls[0].sessionId).toBeUndefined()
|
|
149
|
-
expect(mocks.addImageToCanvas).not.toHaveBeenCalled()
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('falls back to orphan when deliver fails (e.g. already_completed) — image not lost', async () => {
|
|
153
|
-
const bf = makeBackfiller()
|
|
154
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
155
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A]))
|
|
156
|
-
mocks.deliverImageToNode.mockResolvedValue({ ok: false, code: 'ALREADY_COMPLETED' })
|
|
157
|
-
|
|
158
|
-
const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
159
|
-
|
|
160
|
-
expect(mocks.deliverImageToNode).toHaveBeenCalledTimes(1)
|
|
161
|
-
expect(mocks.addImageToCanvas).toHaveBeenCalledTimes(1) // orphan fallback
|
|
162
|
-
expect(r).toEqual({ delivered: 0, orphaned: 1, dropped: 0 })
|
|
163
|
-
})
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
describe('backfillTurn — salvageOnly fallback (untrusted hook / idle timer)', () => {
|
|
167
|
-
it('orphans every fresh image and NEVER calls claim/deliver, even with pending anchors', async () => {
|
|
168
|
-
const bf = makeBackfiller()
|
|
169
|
-
// A legit multi-image turn with pending anchors — salvage must NOT mis-claim them.
|
|
170
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
171
|
-
observeExecuteAnchor(bf, 2, 't1', 's1', 'wf_real', 'nodeB')
|
|
172
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A, IMG_B]))
|
|
173
|
-
|
|
174
|
-
const r = await bf.backfillTurn({ turnKey: 't1', rolloutPath: null, salvageOnly: true })
|
|
175
|
-
|
|
176
|
-
// idle fallback has no transcript_path → locates rollout via findRolloutFile(session_id).
|
|
177
|
-
expect(mocks.findRolloutFile).toHaveBeenCalledWith('/fake/.codex', 's1')
|
|
178
|
-
expect(mocks.deliverImageToNode).not.toHaveBeenCalled() // never claim in salvage
|
|
179
|
-
expect(mocks.addImageToCanvas).toHaveBeenCalledTimes(2)
|
|
180
|
-
expect(r).toEqual({ delivered: 0, orphaned: 2, dropped: 0 })
|
|
181
|
-
})
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
describe('backfillTurn — idempotency + guards', () => {
|
|
185
|
-
it('is idempotent across the two triggers via the seen-set (2nd pass no-ops)', async () => {
|
|
186
|
-
const bf = makeBackfiller()
|
|
187
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
188
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A]))
|
|
189
|
-
|
|
190
|
-
await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
191
|
-
mocks.deliverImageToNode.mockClear()
|
|
192
|
-
mocks.addImageToCanvas.mockClear()
|
|
193
|
-
// A late idle-timer fire for the same turn: all call_ids already seen → nothing to do.
|
|
194
|
-
const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: null, salvageOnly: true })
|
|
195
|
-
|
|
196
|
-
expect(r).toEqual({ delivered: 0, orphaned: 0, dropped: 0 })
|
|
197
|
-
expect(mocks.deliverImageToNode).not.toHaveBeenCalled()
|
|
198
|
-
expect(mocks.addImageToCanvas).not.toHaveBeenCalled()
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('a FAILED salvage does not burn the image — a later Stop hook still claims it (#1751 seen-on-success)', async () => {
|
|
202
|
-
const bf = makeBackfiller()
|
|
203
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
204
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A]))
|
|
205
|
-
// 1) an idle salvage fires but the orphan fails (e.g. session cap) → image must NOT be marked seen.
|
|
206
|
-
mocks.addImageToCanvas.mockResolvedValueOnce({ ok: false, error: 'session cap reached' })
|
|
207
|
-
const r1 = await bf.backfillTurn({ turnKey: 't1', rolloutPath: null, salvageOnly: true })
|
|
208
|
-
expect(r1.orphaned).toBe(0)
|
|
209
|
-
// 2) the real Stop hook fires → the image is still fresh → claim to the planned node succeeds.
|
|
210
|
-
const r2 = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
211
|
-
expect(mocks.deliverImageToNode).toHaveBeenCalledTimes(1)
|
|
212
|
-
expect(mocks.deliverImageToNode.mock.calls[0][0]).toMatchObject({ nodeId: 'nodeA' })
|
|
213
|
-
expect(r2.delivered).toBe(1)
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
it('an early idle tick (image not flushed yet) keeps turn state for the later hook (#1751 no-delete-on-empty)', async () => {
|
|
217
|
-
const bf = makeBackfiller()
|
|
218
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
219
|
-
// 1) idle fires mid-generation, before imagegen flushed to rollout → empty, must be a pure no-op.
|
|
220
|
-
mocks.readRollout.mockReturnValueOnce('')
|
|
221
|
-
const r1 = await bf.backfillTurn({ turnKey: 't1', rolloutPath: null, salvageOnly: true })
|
|
222
|
-
expect(r1).toEqual({ delivered: 0, orphaned: 0, dropped: 0 })
|
|
223
|
-
expect(mocks.addImageToCanvas).not.toHaveBeenCalled()
|
|
224
|
-
// 2) hook fires after flush → the pending anchor + workflowId survived → claim succeeds.
|
|
225
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A]))
|
|
226
|
-
const r2 = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
227
|
-
expect(mocks.deliverImageToNode).toHaveBeenCalledTimes(1)
|
|
228
|
-
expect(r2.delivered).toBe(1)
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
it('serializes concurrent passes so a racing salvage does not double-place an in-flight claim (#1751)', async () => {
|
|
232
|
-
// Real-machine bug (dev.log 1400–1544): a claim pass (Stop hook) and a salvage pass (idle timer)
|
|
233
|
-
// ran concurrently over the same session rollout. Because `seen` is only marked AFTER each slow
|
|
234
|
-
// awaited deliver, the salvage pass read the still-in-flight images as "fresh" and orphaned them —
|
|
235
|
-
// the same image landed BOTH on its planned gen node AND as a duplicate image-upload node.
|
|
236
|
-
const bf = makeBackfiller()
|
|
237
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
238
|
-
observeExecuteAnchor(bf, 2, 't1', 's1', 'wf_real', 'nodeB')
|
|
239
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A, IMG_B]))
|
|
240
|
-
|
|
241
|
-
// deliver resolves only when released — models the 6-20s awaited media job seen in the logs.
|
|
242
|
-
let releaseDeliver!: () => void
|
|
243
|
-
const gate = new Promise<void>((res) => {
|
|
244
|
-
releaseDeliver = res
|
|
245
|
-
})
|
|
246
|
-
mocks.deliverImageToNode.mockImplementation(async () => {
|
|
247
|
-
await gate
|
|
248
|
-
return { ok: true, status: 'processing' }
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
// Fire the claim pass (parks on the deliver gate), then a racing salvage pass — do NOT await between.
|
|
252
|
-
const claimPass = bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
253
|
-
const salvagePass = bf.backfillTurn({ turnKey: 't1', rolloutPath: null, salvageOnly: true })
|
|
254
|
-
releaseDeliver()
|
|
255
|
-
const [claimResult, salvageResult] = await Promise.all([claimPass, salvagePass])
|
|
256
|
-
|
|
257
|
-
// Both images delivered to their planned nodes; the racing salvage placed NO duplicate orphans.
|
|
258
|
-
expect(claimResult).toEqual({ delivered: 2, orphaned: 0, dropped: 0 })
|
|
259
|
-
expect(salvageResult).toEqual({ delivered: 0, orphaned: 0, dropped: 0 })
|
|
260
|
-
expect(mocks.addImageToCanvas).not.toHaveBeenCalled()
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it('does nothing (no placement) when no workflowId was ever captured', async () => {
|
|
264
|
-
const bf = makeBackfiller()
|
|
265
|
-
// No tool call carried a workflowId this turn.
|
|
266
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A]))
|
|
267
|
-
const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
268
|
-
expect(r).toEqual({ delivered: 0, orphaned: 0, dropped: 0 })
|
|
269
|
-
expect(mocks.deliverImageToNode).not.toHaveBeenCalled()
|
|
270
|
-
expect(mocks.addImageToCanvas).not.toHaveBeenCalled()
|
|
271
|
-
})
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
describe('backfillTurn — footprint-0 call passthrough + timeout semantics (#1756 Layer 1a)', () => {
|
|
275
|
-
it('threads the injected `call` into deliver AND orphan (footprint-0)', async () => {
|
|
276
|
-
const call = vi.fn()
|
|
277
|
-
const bf = createBackfiller({
|
|
278
|
-
server: 'http://localhost:3000',
|
|
279
|
-
apiKey: 'mak_test',
|
|
280
|
-
codexHome: '/fake/.codex',
|
|
281
|
-
call,
|
|
282
|
-
})
|
|
283
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
284
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A]))
|
|
285
|
-
mocks.deliverImageToNode.mockResolvedValue({ ok: false, code: 'ALREADY_COMPLETED' }) // force orphan fallback
|
|
286
|
-
|
|
287
|
-
await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
288
|
-
|
|
289
|
-
expect(mocks.deliverImageToNode.mock.calls[0][0]).toMatchObject({ call })
|
|
290
|
-
expect(mocks.addImageToCanvas.mock.calls[0][0]).toMatchObject({ call })
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
it('a timed-out deliver leaves the image FRESH — no orphan, not marked seen (no duplicate)', async () => {
|
|
294
|
-
const bf = makeBackfiller()
|
|
295
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
296
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A]))
|
|
297
|
-
// deliver times out → result unknown; the platform job may still land, so we must NOT orphan.
|
|
298
|
-
mocks.deliverImageToNode.mockResolvedValue({ ok: false, timedOut: true, error: 'inject timeout' })
|
|
299
|
-
|
|
300
|
-
const r1 = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
301
|
-
expect(r1).toEqual({ delivered: 0, orphaned: 0, dropped: 0 })
|
|
302
|
-
expect(mocks.addImageToCanvas).not.toHaveBeenCalled() // NO orphan on timeout
|
|
303
|
-
|
|
304
|
-
// Image stayed fresh (not seen): a later Stop-hook pass can still deliver it once the job lands.
|
|
305
|
-
mocks.deliverImageToNode.mockResolvedValue({ ok: true, status: 'processing' })
|
|
306
|
-
const r2 = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
307
|
-
expect(r2.delivered).toBe(1)
|
|
308
|
-
})
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
describe('backfillTurn — Layer 2 file byte source (#1756)', () => {
|
|
312
|
-
it('uses generated_images when available and does NOT read the rollout', async () => {
|
|
313
|
-
const bf = makeBackfiller()
|
|
314
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
315
|
-
// generated_images available with one fresh image → file source wins, rollout untouched.
|
|
316
|
-
mocks.readGeneratedImageFiles.mockReturnValue({
|
|
317
|
-
available: true,
|
|
318
|
-
images: [{ callId: 'call_current', base64: 'iVBORw0KGgoAAAA1', mimeType: 'image/png' }],
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
322
|
-
|
|
323
|
-
expect(mocks.readGeneratedImageFiles).toHaveBeenCalled()
|
|
324
|
-
expect(mocks.readRollout).not.toHaveBeenCalled() // file source is authoritative when available
|
|
325
|
-
expect(r.delivered).toBe(1)
|
|
326
|
-
expect(mocks.deliverImageToNode.mock.calls[0][0]).toMatchObject({
|
|
327
|
-
nodeId: 'nodeA',
|
|
328
|
-
image: { callId: 'call_current' },
|
|
329
|
-
})
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
it('available but empty → no delivery, and does NOT fall back to rollout (mtime guard preserved)', async () => {
|
|
333
|
-
const bf = makeBackfiller()
|
|
334
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
335
|
-
mocks.readGeneratedImageFiles.mockReturnValue({ available: true, images: [] })
|
|
336
|
-
// Even though the rollout has an image, availability=true must lock us to the (empty) file source.
|
|
337
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A]))
|
|
338
|
-
|
|
339
|
-
const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
340
|
-
|
|
341
|
-
expect(mocks.readRollout).not.toHaveBeenCalled()
|
|
342
|
-
expect(r).toEqual({ delivered: 0, orphaned: 0, dropped: 0 })
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
it('unavailable → falls back to rollout-scrape (older codex)', async () => {
|
|
346
|
-
const bf = makeBackfiller()
|
|
347
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
348
|
-
mocks.readGeneratedImageFiles.mockReturnValue({ available: false, images: [] })
|
|
349
|
-
mocks.readRollout.mockReturnValue(rollout([IMG_A]))
|
|
350
|
-
|
|
351
|
-
const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
352
|
-
|
|
353
|
-
expect(mocks.readRollout).toHaveBeenCalledWith('/x')
|
|
354
|
-
expect(r.delivered).toBe(1)
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
it('exec-* file-source image (opaque call-id) is delivered exactly once (#1968)', async () => {
|
|
358
|
-
const bf = makeBackfiller()
|
|
359
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
360
|
-
mocks.readGeneratedImageFiles.mockReturnValue({
|
|
361
|
-
available: true,
|
|
362
|
-
images: [{ callId: 'exec-3eb2a29c', base64: 'iVBORw0KGgoAAAA1', mimeType: 'image/png' }],
|
|
363
|
-
failedCallIds: [],
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
367
|
-
|
|
368
|
-
expect(mocks.readRollout).not.toHaveBeenCalled()
|
|
369
|
-
expect(r.delivered).toBe(1)
|
|
370
|
-
expect(mocks.deliverImageToNode).toHaveBeenCalledTimes(1)
|
|
371
|
-
expect(mocks.deliverImageToNode.mock.calls[0][0]).toMatchObject({
|
|
372
|
-
nodeId: 'nodeA',
|
|
373
|
-
image: { callId: 'exec-3eb2a29c' },
|
|
374
|
-
})
|
|
375
|
-
})
|
|
376
|
-
|
|
377
|
-
it('unreadable fresh candidate is recovered from rollout by EXACT call-id only — no history bypass (#1968)', async () => {
|
|
378
|
-
const bf = makeBackfiller()
|
|
379
|
-
observeExecuteAnchor(bf, 1, 't1', 's1', 'wf_real', 'nodeA')
|
|
380
|
-
// File source: nothing readable, but one fresh candidate failed to read (torn write).
|
|
381
|
-
mocks.readGeneratedImageFiles.mockReturnValue({ available: true, images: [], failedCallIds: ['exec-torn'] })
|
|
382
|
-
// Rollout holds the failed id AND an unrelated historical image that must NOT be surfaced.
|
|
383
|
-
mocks.readRollout.mockReturnValue(rollout([['exec-torn', 'iVBORw0KGgoAAAA9'], ['ig_history', 'iVBORw0KGgoAAAA8']]))
|
|
384
|
-
|
|
385
|
-
const r = await bf.backfillTurn({ turnKey: 't1', sessionId: 's1', rolloutPath: '/x', salvageOnly: false })
|
|
386
|
-
|
|
387
|
-
expect(mocks.readRollout).toHaveBeenCalledWith('/x')
|
|
388
|
-
expect(r.delivered).toBe(1)
|
|
389
|
-
expect(mocks.deliverImageToNode).toHaveBeenCalledTimes(1)
|
|
390
|
-
expect(mocks.deliverImageToNode.mock.calls[0][0]).toMatchObject({ image: { callId: 'exec-torn' } })
|
|
391
|
-
// The historical image was in the rollout but is NOT in failedCallIds → never delivered or orphaned.
|
|
392
|
-
expect(mocks.addImageToCanvas).not.toHaveBeenCalled()
|
|
393
|
-
})
|
|
394
|
-
})
|