import { describe, expect, test } from 'bun:test'; import * as serializers from './serialize'; import type { Collection, CollectionWithCount, Generation } from './db'; const generation = (over: Partial = {}): Generation => ({ id: '11111111-1111-1111-1111-111111111111', requestId: '22222222-2222-2222-2222-222222222222', index: 0, operation: 'generate', status: 'succeeded', provider: 'openai', model: 'gpt-image-1', prompt: 'a duck', options: { size: '1024x1024' }, inputImages: null, maskImage: null, imagePath: '11111111-1111-1111-1111-111111111111.png', width: 1024, height: 1024, hasAlpha: true, opaque: false, costUsd: '0.167075', usage: null, result: null, durationMs: 40903, error: null, createdAt: new Date(), startedAt: null, finishedAt: null, ...over, }) as Generation; const collection = (over: Partial = {}): CollectionWithCount => ({ id: '33333333-3333-3333-3333-333333333333', name: 'OfficerAppIcons', description: null, createdAt: new Date(), updatedAt: new Date(), itemCount: 6, coverImagePath: '11111111-1111-1111-1111-111111111111.png', ...over, }) as CollectionWithCount; /** * One sample per serialiser. * * The test below fails if this map does not cover every function exported from ./serialize. That is * the actual fix for the bug this file exists because of: the previous version tested the two * generation serialisers by name, so when a collections payload started going out raw it sailed past * a suite that was green. Now a new serialiser cannot be added without a case. */ const CASES: Record unknown> = { serializeGeneration: () => serializers.serializeGeneration(generation()), serializeGenerationImage: () => serializers.serializeGenerationImage(generation()), serializeCollection: () => serializers.serializeCollection(collection() as Collection), serializeCollectionSummary: () => serializers.serializeCollectionSummary(collection()), }; const exported = Object.entries(serializers) .filter(([, value]) => typeof value === 'function') .map(([name]) => name); const CAMEL = /[a-z][A-Z]/; describe('every serialiser', () => { test('has a case in this file', () => { expect(exported.filter((name) => !(name in CASES))).toEqual([]); }); test.each(exported)('%s emits no camelCase keys', (name) => { const output = CASES[name]!() as Record; expect(Object.keys(output).filter((key) => CAMEL.test(key))).toEqual([]); }); test.each(exported)('%s emits no key ending in Id, At or Path', (name) => { // The specific spellings that leaked: createdAt, updatedAt, itemCount, coverImagePath. const output = CASES[name]!() as Record; expect(Object.keys(output).filter((key) => /(Id|At|Path|Count|Url)$/.test(key))).toEqual([]); }); }); describe('shapes that must agree', () => { test("the compact image form uses the full form's spellings", () => { const full = Object.keys(serializers.serializeGeneration(generation())); const compact = Object.keys(serializers.serializeGenerationImage(generation())); expect(compact.filter((key) => !full.includes(key))).toEqual([]); }); test('a collection summary is a collection plus counts', () => { const base = Object.keys(serializers.serializeCollection(collection() as Collection)); const summary = Object.keys(serializers.serializeCollectionSummary(collection())); expect(base.filter((key) => !summary.includes(key))).toEqual([]); }); }); describe('values', () => { test('cost stays a string, so totals do not round', () => { expect(serializers.serializeGeneration(generation()).cost_usd).toBe('0.167075'); }); test('a row with no file has a null url rather than a broken one', () => { expect(serializers.serializeGeneration(generation({ imagePath: null })).url).toBeNull(); }); test('an empty collection has a null cover, not a url to nothing', () => { expect(serializers.serializeCollectionSummary(collection({ coverImagePath: null })).cover_url).toBeNull(); }); });