import { describe, expect, test } from 'bun:test'; import type { Generation } from '../db'; import { imagesResponse, isSettled, jobStatus } from './response'; const row = (over: Partial = {}): Generation => ({ id: '11111111-1111-1111-1111-111111111111', requestId: '22222222-2222-2222-2222-222222222222', index: 0, status: 'succeeded', provider: 'openai', model: 'gpt-image-1', prompt: 'a voxel duck', options: {}, imagePath: '11111111-1111-1111-1111-111111111111.png', costUsd: '0.010000', usage: { total_tokens: 100, input_tokens: 20, output_tokens: 80, input_tokens_details: { text_tokens: 20, image_tokens: 0 }, }, result: {}, durationMs: 4200, error: null, createdAt: new Date('2026-08-16T12:00:00Z'), startedAt: new Date('2026-08-16T12:00:01Z'), finishedAt: new Date('2026-08-16T12:00:05Z'), ...over, }) as Generation; describe('jobStatus', () => { test('running beats queued', () => expect(jobStatus([row({ status: 'queued' }), row({ status: 'running' })])).toBe('running')); test('queued while any is waiting', () => expect(jobStatus([row(), row({ status: 'queued' })])).toBe('queued')); test('succeeded when all did', () => expect(jobStatus([row(), row()])).toBe('succeeded')); test('failed when all did', () => expect(jobStatus([row({ status: 'failed' }), row({ status: 'failed' })])).toBe('failed')); // The case n>1 exists for: three images made, one refused. Calling that "failed" discards work that // was paid for; calling it "succeeded" hides a failure. test('partial when some failed and some did not', () => expect(jobStatus([row(), row({ status: 'failed' })])).toBe('partial')); }); describe('isSettled', () => { test.each(['succeeded', 'failed', 'partial'] as const)('%s is settled', (s) => expect(isSettled(s)).toBe(true)); test.each(['queued', 'running'] as const)('%s is not', (s) => expect(isSettled(s)).toBe(false)); }); describe('imagesResponse', () => { test('shapes an OpenAI response, with url populated', async () => { const body = await imagesResponse([row()], false); expect(body.created).toBe(Math.floor(new Date('2026-08-16T12:00:00Z').getTime() / 1000)); expect(body.data).toHaveLength(1); // The divergence that is the point of Atelier: OpenAI never returns url for GPT image models. expect(body.data[0]!.url).toMatch(/\/files\/11111111-1111-1111-1111-111111111111\.png$/); expect(body.data[0]).not.toHaveProperty('b64_json'); }); test('omits failed images from data but keeps them in the atelier block', async () => { const body = await imagesResponse([row(), row({ status: 'failed', imagePath: null, error: 'refused' })], false); expect(body.data).toHaveLength(1); expect(body.atelier.images).toHaveLength(2); expect(body.atelier.status).toBe('partial'); expect(body.atelier.images[1]!.error).toBe('refused'); }); test('sums usage and cost across images', async () => { const body = await imagesResponse([row(), row({ costUsd: '0.020000' })], false); expect(body.usage.total_tokens).toBe(200); expect(body.usage.output_tokens).toBe(160); expect(body.atelier.cost_usd).toBe('0.030000'); }); test('reports revised_prompt when the provider rewrote it', async () => { const body = await imagesResponse([row({ result: { revised_prompt: 'a cheerful voxel duck' } })], false); expect(body.data[0]!.revised_prompt).toBe('a cheerful voxel duck'); }); });