import { mkdtemp, readdir, rm } from 'node:fs/promises'; import type { Server } from 'node:http'; import type { AddressInfo } from 'node:net'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { FileSystemBackendArtifactStore } from '../../server/artifacts/backend-artifact-store'; import { createAssessorHttpServer } from '../../server/assessor-http-server'; import { GpoBackupManagementService } from '../../server/gpo/gpo-backup-management-service'; import { GpoBackupSubmissionService } from '../../server/gpo/gpo-backup-submission-service'; import { directGpoInventoryExecutor } from '../../server/gpo/gpo-inventory-executor'; import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository'; import { syntheticGpoBackupZip } from './gpo-synthetic-fixture'; const initial = Date.parse('2026-07-21T17:00:00.000Z'); const project = { id: 'gpo-management-http', name: 'Synthetic management HTTP', createdAt: new Date(initial).toISOString(), updatedAt: new Date(initial).toISOString(), attributes: {} }; describe('GPO dead-letter management HTTP boundary', () => { let directory: string; let server: Server; let baseUrl: string; let repository: LocalAssessorRepository; let store: FileSystemBackendArtifactStore; let artifactId: string; let jobId: string; let currentTime: number; async function deadLetter(): Promise { for (let attempt = 0; attempt < 3; attempt += 1) { const claimedAt = new Date(currentTime).toISOString(); await repository.claimNextIngestionJob({ workerId: 'http-worker', now: claimedAt, leaseDurationMs: 60_000, kinds: ['analyze-gpo-backup'] }); currentTime += 1_000; const failedAt = new Date(currentTime).toISOString(); await repository.failIngestionJob(jobId, 'http-worker', failedAt, 'synthetic-memory-limit', failedAt); } } beforeEach(async () => { directory = await mkdtemp(join(tmpdir(), 'assessor-gpo-management-http-')); repository = new LocalAssessorRepository(); store = new FileSystemBackendArtifactStore(join(directory, 'artifacts')); currentTime = initial; await repository.saveProject(project); const submission = new GpoBackupSubmissionService(repository, store, directGpoInventoryExecutor, 1024 * 1024); const submitted = await submission.submit({ projectId: project.id, fileName: 'failed.zip', bytes: syntheticGpoBackupZip(), receivedAt: new Date(currentTime).toISOString() }); artifactId = submitted.artifactId; jobId = submitted.jobId; await deadLetter(); server = createAssessorHttpServer({ repository, staticRoot: join(directory, 'dist'), now: () => new Date(currentTime), gpoBackupSubmissionService: submission, gpoBackupManagementService: new GpoBackupManagementService(repository, store, () => new Date(currentTime)) }); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; }); afterEach(async () => { await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); await rm(directory, { recursive: true, force: true }); }); it('retries and deletes only through exact project/artifact mutation routes with confirmation', async () => { const retry = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups/${artifactId}/retry`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl }, body: '{}' }); expect(retry.status).toBe(200); await expect(retry.json()).resolves.toMatchObject({ artifactId, jobId, status: 'queued', attempts: 0 }); await deadLetter(); const unconfirmed = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups/${artifactId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl }, body: JSON.stringify({ artifactId, confirmDelete: false }) }); expect(unconfirmed.status).toBe(400); const deleted = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups/${artifactId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl }, body: JSON.stringify({ artifactId, confirmDelete: true }) }); expect(deleted.status).toBe(200); await expect(deleted.json()).resolves.toMatchObject({ artifactId, deleted: true, storageRemoval: 'complete' }); await expect(readdir(join(directory, 'artifacts', 'objects'))).resolves.toEqual([]); }); });