import Database from 'better-sqlite3'; import { describe, expect, it } from 'vitest'; import { migrateDatabase } from '../../infrastructure/database/migrations.js'; import { DEFAULT_RECONCILE_GRACE_MS, JobReconcileService } from './job-reconcile-service.js'; const NOW = new Date('2026-07-20T12:00:00.000Z'); const minutesAgo = (minutes: number): string => new Date(NOW.getTime() - minutes * 60_000).toISOString(); function setup(): { db: Database.Database; subject: JobReconcileService } { const db = new Database(':memory:'); migrateDatabase(db); return { db, subject: new JobReconcileService({ db, now: () => NOW }), }; } function insertJob(db: Database.Database, id: string, status: string, createdAt: string): void { 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 (?, ?, 'op.one', 'R2', ?, 'actor', 'request-1', 'digest', ?, ?)`, ).run(id, id, status, createdAt, createdAt); db.prepare( `INSERT INTO job_items (id, job_id, instance_id, attempt_number, status, created_at, updated_at) VALUES (?, ?, 'instance-1', 1, ?, ?, ?)`, ).run(`${id}-item`, id, status === 'queued' ? 'queued' : 'running', createdAt, createdAt); if (status !== 'queued') { db.prepare( `INSERT INTO job_attempts (id, job_id, status, started_at, created_at) VALUES (?, ?, 'running', ?, ?)`, ).run(`${id}-attempt`, id, createdAt, createdAt); } } const rows = (db: Database.Database, table: string): Array> => db.prepare(`SELECT * FROM ${table}`).all() as Array>; describe('JobReconcileService', () => { it('closes dispatched jobs that can no longer finish', () => { const { db, subject } = setup(); insertJob(db, 'job-running', 'running', minutesAgo(60)); expect(subject.summary().pending).toBe(1); expect(subject.reconcile()).toEqual({ interrupted: 1, pending: 0 }); expect(rows(db, 'jobs')[0]).toMatchObject({ id: 'job-running', status: 'unknown-result', finished_at: NOW.toISOString(), }); expect(rows(db, 'job_items')[0]).toMatchObject({ status: 'unknown-result', result_code: 'INTERRUPTED', }); expect(rows(db, 'job_attempts')[0]).toMatchObject({ status: 'unknown-result', finished_at: NOW.toISOString(), }); }); it('cancels queued jobs that never reached a transport', () => { const { db, subject } = setup(); insertJob(db, 'job-queued', 'queued', minutesAgo(30)); expect(subject.reconcile()).toEqual({ interrupted: 1, pending: 0 }); expect(rows(db, 'jobs')[0]).toMatchObject({ status: 'cancelled' }); expect(rows(db, 'job_items')[0]).toMatchObject({ result_code: 'NEVER_DISPATCHED' }); }); it('leaves jobs inside the grace window alone', () => { const { db, subject } = setup(); insertJob(db, 'job-fresh', 'running', minutesAgo(2)); insertJob(db, 'job-old', 'cancelling', minutesAgo(DEFAULT_RECONCILE_GRACE_MS + 1)); expect(subject.reconcile()).toEqual({ interrupted: 1, pending: 1 }); const byId = new Map(rows(db, 'jobs').map((row) => [row.id, row.status])); expect(byId.get('job-fresh')).toBe('running'); expect(byId.get('job-old')).toBe('unknown-result'); }); it('reports the oldest pending job so the console can warn about it', () => { const { db, subject } = setup(); insertJob(db, 'job-newer', 'running', minutesAgo(20)); insertJob(db, 'job-older', 'running', minutesAgo(40)); insertJob(db, 'job-done', 'succeeded', minutesAgo(90)); expect(subject.summary()).toEqual({ pending: 2, dispatched: 2, queued: 0, oldestCreatedAt: minutesAgo(40), }); }); it('is idempotent and rejects a negative window', () => { const { db, subject } = setup(); insertJob(db, 'job-once', 'running', minutesAgo(60)); expect(subject.reconcile(0).interrupted).toBe(1); expect(subject.reconcile(0)).toEqual({ interrupted: 0, pending: 0 }); expect(() => subject.reconcile(-1)).toThrow(RangeError); }); });