import { randomUUID } from "node:crypto"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { ModelConfigurationService } from "../../apps/api/src/model-configuration.js"; import { RegistrationService } from "../../apps/api/src/registration.js"; import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js"; import { GatewayBalanceRuntime } from "../../apps/worker/src/gateway-balance-runtime.js"; const roots: string[] = []; const services: RegistrationService[] = []; const runtimes: GatewayBalanceRuntime[] = []; const now = Date.parse("2026-08-02T15:00:00.000Z"); const ids = ["gemini-3.1-flash-image-preview", "gemini-3-pro-image-preview", "gpt-image-2"] as const; function evidence(caseId: string, file: string, value: unknown) { const root = process.env.DADA_EVIDENCE_DIR_RUNTIME; if (!root) return; const directory = resolve(root, caseId); mkdirSync(directory, { recursive: true }); writeFileSync(resolve(directory, file), `${JSON.stringify(value, null, 2)}\n`); } function harness() { const root = mkdtempSync(join(tmpdir(), "dada-wp3-02-recovery-")); roots.push(root); const registration = new RegistrationService({ challengePepper: Buffer.alloc(32, 0x81), clock: () => now, currentPrivacyNoticeVersion: "p0a-registration-notice-v1", databasePath: join(root, "dada.sqlite3"), invitePepper: Buffer.alloc(32, 0x82), resend: new MockResendAdapter(), sessionPepper: Buffer.alloc(32, 0x83), }); services.push(registration); const models = new ModelConfigurationService({ database: registration.database, clock: () => now }); const candidates = structuredClone(models.read().models); for (const candidate of candidates) { candidate.contract_validation_status = "verified"; candidate.contract_evidence_ref = `fixture-${candidate.model_id}`; } models.replace({ actorId: "wp3-02-recovery", expectedConfigSetVersion: 1, idempotencyKey: "wp3-02-recovery-verified-00000000000000000001", models: candidates }); registration.database.prepare("UPDATE model_runtime_availability SET available_for_new_jobs = 1, reason = 'available', checked_at = ?").run(now); const runtime = new GatewayBalanceRuntime({ clock: () => now, database: registration.database }); runtimes.push(runtime); runtime.seedModels(ids.map((modelId) => ({ gatewayAccountRef: "gateway-account-primary", modelId }))); return { models, registration, runtime }; } afterEach(() => { for (const runtime of runtimes.splice(0).reverse()) runtime.close(); for (const service of services.splice(0).reverse()) service.close(); for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true }); }); describe("TDD-WP3-BAL-001-confirmed-recovery", () => { it.each(["model", "account", "unknown"] as const)("requires health check and explicit admin confirmation for %s impact", (impactScope) => { const { models, registration, runtime } = harness(); const before = registration.database.prepare("SELECT model_id, config_version, contract_fingerprint FROM model_config_versions ORDER BY model_id, config_version").all(); const event = runtime.recordInsufficient({ eventId: randomUUID(), gatewayAccountRef: "gateway-account-primary", impactScope, modelId: ids[0], sourceCategory: "adapter_balance_signal", }); expect(event.recoveryStatus).toBe("awaiting_confirmation"); expect(() => runtime.confirmRecovery({ actorId: randomUUID(), gatewayAccountRef: "gateway-account-primary" })) .toThrow("gateway_balance_recovery_unconfirmed"); const check = runtime.runRecoveryCheck("gateway-account-primary"); expect(check).toMatchObject({ gateway_account_ref: "gateway-account-primary", status: "passed" }); const recovered = runtime.confirmRecovery({ actorId: randomUUID(), gatewayAccountRef: "gateway-account-primary", recoveryCheckId: check.check_id, }); expect(recovered).toMatchObject({ recoveryStatus: "confirmed", balanceStatus: "available" }); expect(runtime.listRuntime().every((model) => model.availableForNewJobs && model.runtimeReason === "available")).toBe(true); expect(models.read()).toMatchObject({ configured_default_model_id: ids[0], recommended_model_id: ids[0] }); expect(registration.database.prepare("SELECT COUNT(*) AS count FROM admin_operation_logs WHERE operation_type = 'gateway_balance_recovery' AND result = 'succeeded'").get()).toEqual({ count: 1 }); expect(registration.database.prepare("SELECT COUNT(*) AS count FROM service_recovery_checks WHERE check_id = ?").get(check.check_id)).toEqual({ count: 1 }); const after = registration.database.prepare("SELECT model_id, config_version, contract_fingerprint FROM model_config_versions ORDER BY model_id, config_version").all(); expect(after).toEqual(before); evidence("TDD-WP3-BAL-001-confirmed-recovery", "response.json", { impact_scope: impactScope, event, check, recovered }); evidence("TDD-WP3-BAL-001-confirmed-recovery", "db-diff.json", { config_before: before, config_after: after, config_set_version: models.read().config_set_version }); evidence("TDD-WP3-BAL-001-confirmed-recovery", "external-calls.json", { automatic_recharge_calls: 0, recovery_calls: 0, user_credit_adjustments: 0 }); }); it("keeps a contract blocker after balance recovery", () => { const { models, runtime } = harness(); runtime.recordInsufficient({ eventId: randomUUID(), gatewayAccountRef: "gateway-account-primary", impactScope: "model", modelId: ids[0], sourceCategory: "adapter_balance_signal", }); const changed = structuredClone(models.read().models); changed[0].route_profile.endpoint = "https://mock.invalid/v2/images"; models.replace({ actorId: "wp3-02-contract", expectedConfigSetVersion: 2, idempotencyKey: "wp3-02-contract-change-00000000000000000001", models: changed }); const check = runtime.runRecoveryCheck("gateway-account-primary"); runtime.confirmRecovery({ actorId: randomUUID(), gatewayAccountRef: "gateway-account-primary", recoveryCheckId: check.check_id }); expect(runtime.listRuntime().find((model) => model.modelId === ids[0])).toMatchObject({ availableForNewJobs: false, runtimeReason: "contract_unverified" }); expect(models.read()).toMatchObject({ configured_default_model_id: ids[0], recommended_model_id: ids[1] }); }); });