Files
atelier/sidecar/serialize.test.ts
pastilhas f23ec1283b manifest, thin proxy router, and the sidecar backend
ported from the standalone officer-atelier service: providers (adapter-shaped, openai the only
adapter today), the generation queue, collections, and the database layer — kept on its own
Postgres database rather than folded into officer_db, since both collections and provider_settings
had rows on the machine this was extracted from.

what changed rather than just moved:
- images are served from an authenticated GET /files/:filename instead of an unauthenticated
  PUBLIC_URL-prefixed static route
- the OpenAI-key bootstrap fallback reads the secret store (getSecret) before .env, per-adapter
- provider_settings (the one-row-per-adapter shape providers replaced) did not come along — it has
  no callers left anywhere in the server code, only a migration script that also did not move

ping/echo (smoke-test routes with no caller) did not move either; the sidecar's own /_health covers
liveness.

not yet wired into the platform: sidecar/index.ts exists but nothing mounts it, and web/ has not
been written, so this is not yet a discoverable plugin.
2026-08-20 03:53:53 +00:00

111 lines
4.1 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import * as serializers from './serialize';
import type { Collection, CollectionWithCount, Generation } from './db';
const generation = (over: Partial<Generation> = {}): 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> = {}): 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<string, () => 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<string, unknown>;
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<string, unknown>;
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();
});
});