import Database from 'better-sqlite3'; import { describe, expect, it } from 'vitest'; import { migrateDatabase } from '../../infrastructure/database/migrations.js'; import { JobQueryError, JobQueryService } from './job-query-service.js'; const t1 = '2026-07-17T10:00:00.000Z'; const t2 = '2026-07-17T11:00:00.000Z'; function setup(): { db: Database.Database; subject: JobQueryService } { const db = new Database(':memory:'); migrateDatabase(db); return { db, subject: new JobQueryService(db) }; } function insertJob( db: Database.Database, values: Partial> = {}, ): void { const id = values.id ?? 'job-1'; const root = Object.prototype.hasOwnProperty.call(values, 'root') ? values.root : id; db.prepare( `INSERT INTO jobs (id, root_job_id, operation_id, risk_level, status, requested_by, request_id, parameters_digest, created_at, updated_at) VALUES (?, ?, ?, 'R1', ?, 'actor', 'request-1', 'digest', ?, ?)`, ).run( id, root, values.operation ?? 'op.one', values.status ?? 'succeeded', values.created ?? t1, values.created ?? t1, ); } function insertItem(db: Database.Database, overrides: Record = {}): void { const row = { id: 'item-1', job: 'job-1', instance: 'instance-1', status: 'succeeded', source: null, error: null, created: t1, ...overrides, }; db.prepare( `INSERT INTO job_items (id, job_id, instance_id, attempt_number, status, error_json, source_job_item_id, created_at, updated_at) VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?)`, ).run(row.id, row.job, row.instance, row.status, row.error, row.source, row.created, row.created); } function insertAttempt(db: Database.Database, overrides: Record = {}): void { const row = { id: 'attempt-1', job: 'job-1', status: 'succeeded', started: t1, finished: t2, created: t1, ...overrides, }; db.prepare( `INSERT INTO job_attempts (id, job_id, status, started_at, finished_at, created_at) VALUES (?, ?, ?, ?, ?, ?)`, ).run(row.id, row.job, row.status, row.started, row.finished, row.created); } function expectCode(action: () => unknown, code: string): void { try { action(); } catch (error) { expect(error).toBeInstanceOf(JobQueryError); expect((error as JobQueryError).code).toBe(code); return; } throw new Error('expected JobQueryError'); } describe('JobQueryService', () => { it.each(['queued', 'running', 'cancelling'])( 'projects only terminal items while a %s job is active and retains its running attempt', (status) => { const { db, subject } = setup(); insertJob(db, { status }); insertItem(db, { id: 'terminal', instance: 'done', status: 'succeeded' }); insertItem(db, { id: 'internal', instance: 'pending', status: 'running' }); insertAttempt(db, { status: 'running', finished: null }); expect(subject.get('job-1')).toMatchObject({ status, items: [{ id: 'terminal', targetId: 'done', state: 'succeeded' }], attempts: [{ id: 'attempt-1', state: 'running', startedAt: t1 }], }); }, ); it('returns the exact frozen projection with deterministic child ordering', () => { const { db, subject } = setup(); insertJob(db); insertItem(db, { id: 'item-z', instance: 'target-z' }); insertItem(db, { id: 'item-a', instance: 'target-a', source: 'item-z' }); insertAttempt(db, { id: 'attempt-z', started: t1 }); insertAttempt(db, { id: 'attempt-a', started: t1, status: 'running', finished: null }); expect(subject.get('job-1')).toEqual({ id: 'job-1', operationId: 'op.one', status: 'succeeded', rootJobId: 'job-1', items: [ { id: 'item-a', targetId: 'target-a', state: 'succeeded', sourceJobItemId: 'item-z' }, { id: 'item-z', targetId: 'target-z', state: 'succeeded' }, ], attempts: [ { id: 'attempt-a', state: 'running', startedAt: t1 }, { id: 'attempt-z', state: 'succeeded', startedAt: t1, finishedAt: t2 }, ], createdAt: t1, }); }); it('paginates in SQL with filters and an ascending id tie-break independent of direction', () => { const { db, subject } = setup(); insertJob(db, { id: 'z', operation: 'same', created: t2 }); insertJob(db, { id: 'a', operation: 'same', created: t2 }); insertJob(db, { id: 'other', operation: 'other', created: t1 }); insertItem(db, { id: 'z-item', job: 'z', instance: 'wanted' }); insertItem(db, { id: 'a-item', job: 'a', instance: 'wanted' }); expect( subject.list({ page: 1, pageSize: 1, sort: 'createdAt', direction: 'desc', operationId: 'same', status: 'succeeded', rootJobId: 'z', instanceId: 'wanted', }), ).toEqual({ items: [expect.objectContaining({ id: 'z' })], page: { page: 1, pageSize: 1, total: 1 }, }); expect( subject.list({ sort: 'createdAt', direction: 'desc' }).items.map(({ id }) => id), ).toEqual(['a', 'z', 'other']); }); it('reports not found without exposing database details', () => { const { subject } = setup(); expect(() => subject.get('missing')).toThrow('Job was not found'); expectCode(() => subject.get('missing'), 'NOT_FOUND'); }); it.each([ [{ page: 0 }, 'page'], [{ page: 1.5 }, 'page'], [{ pageSize: 0 }, 'pageSize'], [{ pageSize: 101 }, 'pageSize'], [{ sort: 'id' }, 'sort'], [{ direction: 'sideways' }, 'direction'], ])('rejects invalid query input: %j', (query, field) => { const { subject } = setup(); expect(() => subject.list(query as never)).toThrow(new RegExp(field, 'i')); expectCode(() => subject.list(query as never), 'VALIDATION_FAILED'); }); it.each([ [ 'job status', () => { const x = setup(); insertJob(x.db, { status: 'invented' }); return x; }, ], [ 'missing root', () => { const x = setup(); insertJob(x.db, { root: null }); return x; }, ], [ 'running item', () => { const x = setup(); insertJob(x.db); insertItem(x.db, { status: 'running' }); return x; }, ], [ 'attempt state', () => { const x = setup(); insertJob(x.db); x.db.pragma('ignore_check_constraints = ON'); insertAttempt(x.db, { status: 'queued' }); return x; }, ], [ 'timestamp', () => { const x = setup(); insertJob(x.db, { created: '2026-07-17T10:00:00Z' }); return x; }, ], ])('fails the whole selection as UNREPRESENTABLE for invalid persisted %s', (_name, make) => { const { subject } = make(); expectCode(() => subject.list(), 'UNREPRESENTABLE'); }); it.each([ [ 'items', (db: Database.Database, count: number) => { const insert = db.prepare( `INSERT INTO job_items (id, job_id, instance_id, attempt_number, status, created_at, updated_at) VALUES (?, 'job-1', 'instance-1', ?, 'succeeded', ?, ?)`, ); db.transaction(() => { for (let index = 1; index <= count; index += 1) insert.run(`item-${index}`, index, t1, t1); })(); }, ], [ 'attempts', (db: Database.Database, count: number) => { const insert = db.prepare( `INSERT INTO job_attempts (id, job_id, status, started_at, finished_at, created_at) VALUES (?, 'job-1', 'succeeded', ?, ?, ?)`, ); db.transaction(() => { for (let index = 1; index <= count; index += 1) insert.run(`attempt-${index}`, t1, t2, t1); })(); }, ], ])('bounds hydrated child %s at 1000 rows per selected job', (_name, populate) => { const atBound = setup(); insertJob(atBound.db); populate(atBound.db, 1000); expect(atBound.subject.get('job-1')).toBeDefined(); const overBound = setup(); insertJob(overBound.db); populate(overBound.db, 1001); expectCode(() => overBound.subject.get('job-1'), 'UNREPRESENTABLE'); }); it('emits only a closed redacted item error and omits malformed optional errors', () => { const { db, subject } = setup(); insertJob(db); insertItem(db, { id: 'good', instance: 'instance-good', error: JSON.stringify({ type: 'https://evil/secret', title: 'database password leaked', status: 502, detail: 'password=secret', code: 'UPSTREAM_FAILED', requestId: 'req-safe', extra: 'secret', }), }); insertItem(db, { id: 'bad', instance: 'instance-bad', error: '{bad json' }); expect(subject.get('job-1').items).toEqual([ { id: 'bad', targetId: 'instance-bad', state: 'succeeded' }, { id: 'good', targetId: 'instance-good', state: 'succeeded', error: { type: 'about:blank', title: 'Job item failed', status: 502, detail: 'The job item did not complete successfully.', code: 'UPSTREAM_FAILED', requestId: 'req-safe', }, }, ]); }); });