From f05c7729528c15dc6cec4fe6ce209ed5b86bca98 Mon Sep 17 00:00:00 2001 From: Giancarmine Salucci Date: Thu, 9 Jul 2026 04:24:16 +0200 Subject: [PATCH] D2: verify GET /api/jobs returns error field for failed jobs Verification tests for outcome-contract clause failed-job-error-exposed-for-ui-banner. - db.test.ts: listJobs includes error for failed jobs, null for non-failed - jobs-list-endpoint.test.ts: route-level integration with real DB, exercises GET handler end-to-end D2 already merged (f03b757+); non-blocking verification only. --- src/tests/db.test.ts | 18 +++++++ src/tests/jobs-list-endpoint.test.ts | 81 ++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/tests/jobs-list-endpoint.test.ts diff --git a/src/tests/db.test.ts b/src/tests/db.test.ts index 5b69661..fd7a26f 100644 --- a/src/tests/db.test.ts +++ b/src/tests/db.test.ts @@ -174,6 +174,24 @@ describe('listJobs', () => { it('returns an array (possibly empty)', () => { expect(Array.isArray(listJobs())).toBe(true); }); + + it('includes error field for failed jobs — D2 error banner data contract', () => { + const job = createJob('src', 'Failed Job', 'auto'); + updateJob({ id: job.id, status: 'failed', error: 'simulated transcription failure for outcome-contract probe' }); + const jobs = listJobs(); + const found = jobs.find((j) => j.id === job.id); + expect(found).toBeDefined(); + expect(found!.error).toBe('simulated transcription failure for outcome-contract probe'); + }); + + it('returns null error for non-failed jobs', () => { + const job = createJob('src', 'Done Job', 'auto'); + updateJob({ id: job.id, status: 'done', error: null }); + const jobs = listJobs(); + const found = jobs.find((j) => j.id === job.id); + expect(found).toBeDefined(); + expect(found!.error).toBeNull(); + }); }); // ── Push subscriptions ──────────────────────────────────────────────────────── diff --git a/src/tests/jobs-list-endpoint.test.ts b/src/tests/jobs-list-endpoint.test.ts new file mode 100644 index 0000000..bc1fafa --- /dev/null +++ b/src/tests/jobs-list-endpoint.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, afterAll, vi } from 'vitest'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { rm } from 'fs/promises'; + +/** + * Integration test for GET /api/jobs — verifies the error field is + * exposed for failed jobs and null for non-failed jobs, exercising + * the full db→API path with a real SQLite database. + * + * Covers outcome-contract clause failed-job-error-exposed-for-ui-banner. + * D2 already merged (f03b757+); this is non-blocking verification. + */ + +const TEST_DATA_DIR = join(tmpdir(), 'whisper-pwa-jobs-list-int-' + process.pid); +vi.stubEnv('DATA_DIR', TEST_DATA_DIR); + +const { createJob, getJob, updateJob, listJobs } = await import('$lib/server/db.js'); +const { GET } = await import('$lib/../routes/api/jobs/+server.js'); + +afterAll(async () => { + await rm(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +describe('GET /api/jobs — error field exposure', () => { + it('includes error field for a failed job', async () => { + const job = createJob('probe-upload.webm', 'UI error probe', 'auto'); + updateJob({ + id: job.id, + status: 'failed', + error: 'simulated transcription failure for outcome-contract probe' + }); + + const res = await GET(); + expect(res.status).toBe(200); + const body = await res.json(); + expect(Array.isArray(body)).toBe(true); + + const found = body.find((j: any) => j.id === job.id); + expect(found).toBeDefined(); + expect(found.status).toBe('failed'); + expect(found.error).toBe('simulated transcription failure for outcome-contract probe'); + }); + + it('returns null error for a non-failed job', async () => { + const job = createJob('probe-done.webm', 'Done probe', 'auto'); + updateJob({ id: job.id, status: 'done', error: null }); + + const res = await GET(); + expect(res.status).toBe(200); + const body = await res.json(); + + const found = body.find((j: any) => j.id === job.id); + expect(found).toBeDefined(); + expect(found.error).toBeNull(); + }); + + it('preserves other job fields when error is present', async () => { + const job = createJob('my-recording.webm', 'My Recording', 'aggressive'); + updateJob({ + id: job.id, + status: 'failed', + error: 'transcription failed', + progress: 50, + whisperJobId: 'whisper-123' + }); + + const res = await GET(); + const body = await res.json(); + const found = body.find((j: any) => j.id === job.id); + + expect(found).toBeDefined(); + expect(found.status).toBe('failed'); + expect(found.error).toBe('transcription failed'); + expect(found.title).toBe('My Recording'); + expect(found.source).toBe('my-recording.webm'); + expect(found.audioMode).toBe('aggressive'); + expect(found.progress).toBe(50); + expect(found.whisperJobId).toBe('whisper-123'); + }); +});