Files
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

84 lines
3.4 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import type { Generation } from '../db';
import { imagesResponse, isSettled, jobStatus } from './response';
const row = (over: Partial<Generation> = {}): 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');
});
});