import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { FIXTURE_IMAGE_BASE64, FIXTURE_IMAGE_PATH, MAX_JSON_BODY_BYTES, createFixtureServer } from './local-image-upstream-fixture.mjs'; describe('local image upstream fixture', () => { it('serves Images API JSON responses', async () => { const fixture = await startFixture(); try { const response = await fetch(`${fixture.baseUrl}/v1/images/generations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-image-2', prompt: 'fixture json smoke', stream: false }) }); assert.equal(response.status, 200); assert.match(response.headers.get('content-type') || '', /^application\/json\b/); const body = await response.json(); assert.equal(body.data[0].b64_json, FIXTURE_IMAGE_BASE64); assert.equal(body.fixture_request.model, 'gpt-image-2'); assert.equal(body.fixture_request.stream, false); } finally { await fixture.close(); } }); it('serves Images API SSE responses', async () => { const fixture = await startFixture(); try { const response = await fetch(`${fixture.baseUrl}/v1/images/generations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-image-2', prompt: 'fixture sse smoke', stream: true }) }); assert.equal(response.status, 200); assert.match(response.headers.get('content-type') || '', /^text\/event-stream\b/); const events = readSseEvents(await response.text()); assert.equal(events[0].comment, 'keepalive'); assert.deepEqual( events.filter((event) => event.event).map((event) => event.event), ['image_generation.partial_image', 'image_generation.completed'] ); assert.equal(events[1].data.type, 'image_generation.partial_image'); assert.equal(events[1].data.b64_json, FIXTURE_IMAGE_BASE64); assert.equal(events[2].data.type, 'image_generation.completed'); assert.equal(events[2].data.b64_json, FIXTURE_IMAGE_BASE64); assert.equal(events[3].done, true); } finally { await fixture.close(); } }); it('serves Responses API JSON image outputs', async () => { const fixture = await startFixture(); try { const response = await fetch(`${fixture.baseUrl}/v1/responses`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-5.4', input: 'fixture responses json smoke', stream: false, tools: [{ type: 'image_generation' }], tool_choice: { type: 'image_generation' } }) }); assert.equal(response.status, 200); assert.match(response.headers.get('content-type') || '', /^application\/json\b/); const body = await response.json(); assert.equal(body.id, 'resp_fixture'); assert.equal(body.output[0].type, 'image_generation_call'); assert.equal(body.output[0].result, FIXTURE_IMAGE_PATH); const imageResponse = await fetch(`${fixture.baseUrl}${FIXTURE_IMAGE_PATH}`); assert.equal(imageResponse.status, 200); assert.match(imageResponse.headers.get('content-type') || '', /^image\/png\b/); assert.equal(Buffer.from(await imageResponse.arrayBuffer()).toString('base64'), FIXTURE_IMAGE_BASE64); } finally { await fixture.close(); } }); it('serves Responses API SSE image events', async () => { const fixture = await startFixture(); try { const response = await fetch(`${fixture.baseUrl}/v1/responses`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-5.4', input: 'fixture responses sse smoke', stream: true, tools: [{ type: 'image_generation' }], tool_choice: { type: 'image_generation' } }) }); assert.equal(response.status, 200); assert.match(response.headers.get('content-type') || '', /^text\/event-stream\b/); const events = readSseEvents(await response.text()); assert.deepEqual( events.filter((event) => event.event).map((event) => event.event), [ 'response.image_generation_call.partial_image', 'response.output_item.done', 'response.completed' ] ); assert.equal(events[0].data.partial_image_b64, FIXTURE_IMAGE_BASE64); assert.equal(events[1].data.item.type, 'image_generation_call'); assert.equal(events[1].data.item.result, FIXTURE_IMAGE_PATH); assert.equal(events[2].data.response.output[0].result, FIXTURE_IMAGE_PATH); assert.equal(events[3].done, true); } finally { await fixture.close(); } }); it('serves model discovery and health checks', async () => { const fixture = await startFixture(); try { const healthResponse = await fetch(`${fixture.baseUrl}/health`); const modelsResponse = await fetch(`${fixture.baseUrl}/v1/models`); assert.equal(healthResponse.status, 200); assert.deepEqual(await healthResponse.json(), { ok: true }); assert.equal(modelsResponse.status, 200); const models = await modelsResponse.json(); assert.deepEqual( models.data.map((model) => model.id), ['gpt-image-2', 'gpt-5.4'] ); } finally { await fixture.close(); } }); it('rejects oversized JSON request bodies', async () => { const fixture = await startFixture(); try { const response = await fetch(`${fixture.baseUrl}/v1/images/generations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-image-2', prompt: 'x'.repeat(MAX_JSON_BODY_BYTES) }) }); assert.equal(response.status, 413); const body = await response.json(); assert.match(body.error.message, /too large/); } finally { await fixture.close(); } }); }); async function startFixture() { const server = createFixtureServer(); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); const address = server.address(); assert.ok(address && typeof address === 'object'); return { baseUrl: `http://127.0.0.1:${address.port}`, close: () => closeServer(server) }; } function closeServer(server) { return new Promise((resolve, reject) => { server.close((error) => { if (error) reject(error); else resolve(); }); }); } function readSseEvents(raw) { return raw .trim() .split(/\n\n+/) .map((chunk) => readSseEvent(chunk)) .filter(Boolean); } function readSseEvent(chunk) { const lines = chunk.split(/\n/); const comment = lines.find((line) => line.startsWith(': ')); const event = lines.find((line) => line.startsWith('event: ')); const data = lines.find((line) => line.startsWith('data: ')); if (comment) return { comment: comment.slice(2) }; if (!data) return undefined; if (data === 'data: [DONE]') return { done: true }; return { event: event ? event.slice('event: '.length) : undefined, data: JSON.parse(data.slice('data: '.length)) }; }