From 8edae211ff723b16bb9a143021a98921459b0b28 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Mon, 31 Aug 2026 04:29:38 +0800 Subject: [PATCH] test(billing): cover durable report settlement --- frontend/tests/personal-report-worker.test.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/frontend/tests/personal-report-worker.test.ts b/frontend/tests/personal-report-worker.test.ts index 4e5e287a..8cab12ab 100644 --- a/frontend/tests/personal-report-worker.test.ts +++ b/frontend/tests/personal-report-worker.test.ts @@ -15,6 +15,7 @@ import { PersonalReportWorkerError, createPersonalReportWorker, } from "../src/lib/personal-report-worker-core.ts"; +import type { ReportBillingPort } from "../src/lib/personal-report-billing.ts"; const USER_ID = "11111111-1111-4111-8111-111111111111"; const REQUEST_ID = "22222222-2222-4222-8222-222222222222"; @@ -89,6 +90,7 @@ function createHarness(input: Readonly<{ generate?: () => Promise; loseLeaseBeforeCompletion?: boolean; markReadyError?: PersonalReportJobServiceError; + billing?: ReportBillingPort; }>) { let job = input.job ?? jobRecord(); let report = input.report === undefined ? reportRecord() : input.report; @@ -97,6 +99,8 @@ function createHarness(input: Readonly<{ let generateCount = 0; let completeCount = 0; let reportFailureCount = 0; + let billingCompleteCount = 0; + const billingReleaseReasons: string[] = []; function requireLease(jobId: string, leaseToken: string): PersonalReportJobRecord { assert.equal(jobId, job.id); @@ -327,6 +331,19 @@ function createHarness(input: Readonly<{ generateCount += 1; return input.generate?.() ?? { status: "ready", document: READY_DOCUMENT, evidenceHash: "c".repeat(64) }; }, + billing: input.billing + ? { + reserve: input.billing.reserve, + complete: async (completeInput: Parameters[0]) => { + billingCompleteCount += 1; + return input.billing!.complete(completeInput); + }, + release: async (releaseInput: Parameters[0]) => { + billingReleaseReasons.push(releaseInput.reason); + return input.billing!.release(releaseInput); + }, + } + : undefined, now: () => NOW, retryDelayMs: () => 1_000, heartbeatIntervalMs: 60_000, @@ -340,6 +357,8 @@ function createHarness(input: Readonly<{ get generateCount() { return generateCount; }, get completeCount() { return completeCount; }, get reportFailureCount() { return reportFailureCount; }, + get billingCompleteCount() { return billingCompleteCount; }, + billingReleaseReasons, }; } @@ -537,6 +556,86 @@ test("retryable failures use the bounded job retry budget and fail the report on assert.equal(harness.reportFailureCount, 1); }); +test("successful durable generation completes the reserved report usage", async () => { + const billing: ReportBillingPort = { + async reserve() { throw new Error("not used"); }, + async complete(input) { + assert.equal(input.userId, USER_ID); + assert.equal(input.requestId, REQUEST_ID); + assert.deepEqual(input.usage, { + actualModelId: "model-a", + modelConfigVersion: 7, + inputTokens: 120, + outputTokens: 45, + durationMs: 0, + }); + return true; + }, + async release() { throw new Error("release must not run"); }, + }; + const harness = createHarness({ + billing, + generate: async () => ({ + status: "ready", + document: READY_DOCUMENT, + evidenceHash: "c".repeat(64), + usage: { inputTokens: 120, outputTokens: 45, actualModelId: "model-a", modelConfigVersion: 7 }, + }), + }); + + const result = await harness.worker.tick(); + + assert.equal(result.outcome, "ready"); + assert.equal(harness.billingCompleteCount, 1); + assert.deepEqual(harness.billingReleaseReasons, []); +}); + +test("terminal durable generation failure releases the reserved report usage", async () => { + const billing: ReportBillingPort = { + async reserve() { throw new Error("not used"); }, + async complete() { throw new Error("complete must not run"); }, + async release(input) { + assert.equal(input.userId, USER_ID); + assert.equal(input.requestId, REQUEST_ID); + return true; + }, + }; + const harness = createHarness({ + billing, + generate: async () => ({ + status: "failed", + failureCode: "report_schema_invalid", + innerReason: "agent_output_invalid", + }), + }); + + const result = await harness.worker.tick(); + + assert.equal(result.outcome, "failed"); + assert.deepEqual(harness.billingReleaseReasons, ["report_schema_invalid"]); + assert.equal(harness.billingCompleteCount, 0); +}); + +test("retryable durable generation failure does not release before retry exhaustion", async () => { + const billing: ReportBillingPort = { + async reserve() { throw new Error("not used"); }, + async complete() { throw new Error("complete must not run"); }, + async release() { return true; }, + }; + const harness = createHarness({ + billing, + job: jobRecord({ maxAttempts: 2 }), + generate: async () => { + throw new PersonalReportWorkerError("calculation_unavailable", true); + }, + }); + + const first = await harness.worker.tick(); + + assert.equal(first.outcome, "retry_scheduled"); + assert.deepEqual(harness.billingReleaseReasons, []); +}); + test("instrumentation retains the Skill guard and starts a singleton Node worker loop", () => { const instrumentation = readFileSync(new URL("../src/instrumentation.ts", import.meta.url), "utf8"); const productionAdapter = readFileSync(new URL("../src/lib/personal-report-worker.ts", import.meta.url), "utf8");