import { randomUUID } from "node:crypto"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { RetentionCleanup } from "../../apps/worker/src/retention-cleanup.js"; import { RegistrationService } from "../../apps/api/src/registration.js"; import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js"; import { registrationNotice } from "../../packages/shared-contracts/src/registration-notice.js"; const baseNow = Date.parse("2026-07-28T11:00:00.000Z"); const roots: string[] = []; const services: RegistrationService[] = []; function writeEvidence(value: unknown) { const directory = process.env.DADA_EVIDENCE_DIR_RETENTION; if (!directory) return; mkdirSync(directory, { recursive: true }); writeFileSync(resolve(directory, "retention-events.json"), `${JSON.stringify(value, null, 2)}\n`); } afterEach(() => { for (const service of services.splice(0)) service.close(); for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true }); }); describe("TDD-WP1-DEL-002 retention worker", () => { it("refuses early/manual deletion and removes each row only at its original expiry", () => { const root = mkdtempSync(join(tmpdir(), "dada-wp1-05-retention-")); roots.push(root); const databasePath = join(root, "dada.sqlite3"); const service = new RegistrationService({ challengePepper: Buffer.alloc(32, 0x61), clock: () => baseNow, codeGenerator: () => "120984", currentPrivacyNoticeVersion: registrationNotice.version, databasePath, invitePepper: Buffer.alloc(32, 0x62), resend: new MockResendAdapter(), sessionPepper: Buffer.alloc(32, 0x63), }); services.push(service); const accessExpiry = baseNow + 1_000; const anonymousExpiry = baseNow + 2_000; service.database.prepare(` INSERT INTO private_content_access_logs (log_id, actor_ref, subject_ref, target_ref, content_type, occurred_at, expires_at) VALUES (?, ?, ?, ?, 'prompt', ?, ?) `).run(randomUUID(), randomUUID(), randomUUID(), randomUUID(), accessExpiry - 180 * 86_400_000, accessExpiry); service.database.prepare(` INSERT INTO anonymous_retained_events ( event_id, anonymous_subject_id, event_type, model_id, outcome, error_category, credit_delta, occurred_at, expires_at ) VALUES (?, ?, 'generation_commit', 'model-a', 'succeeded', NULL, -1, ?, ?) `).run(randomUUID(), randomUUID(), baseNow - 10_000, anonymousExpiry); expect(() => service.database.prepare("DELETE FROM anonymous_retained_events").run()).toThrow(); expect(() => service.database.prepare("DELETE FROM private_content_access_logs").run()).toThrow(); const early = new RetentionCleanup({ clock: () => baseNow, databasePath }); expect(early.purgeExpired()).toEqual({ admin_operation_logs: 0, anonymous_events: 0, private_access_logs: 0 }); early.close(); const firstExpiry = new RetentionCleanup({ clock: () => accessExpiry, databasePath }); expect(firstExpiry.purgeExpired()).toEqual({ admin_operation_logs: 0, anonymous_events: 0, private_access_logs: 1 }); firstExpiry.close(); const secondExpiry = new RetentionCleanup({ clock: () => anonymousExpiry, databasePath }); expect(secondExpiry.purgeExpired()).toEqual({ admin_operation_logs: 0, anonymous_events: 1, private_access_logs: 0 }); secondExpiry.close(); writeEvidence({ anonymous_deleted_at_expiry: true, early_delete_blocked: true, private_access_deleted_at_original_expiry: true, }); }); });