test(billing): cover durable report settlement

This commit is contained in:
Jesse_Chen
2026-08-31 04:29:38 +08:00
parent a73d996f65
commit 8edae211ff
@@ -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<GeneratePersonalReportResult>;
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<ReportBillingPort["complete"]>[0]) => {
billingCompleteCount += 1;
return input.billing!.complete(completeInput);
},
release: async (releaseInput: Parameters<ReportBillingPort["release"]>[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");