fix(report): keep personal-report heartbeats ref'd during chapter writes
Independent Staging Quality Gate / validate (push) Successful in 7m56s
Independent Staging Quality Gate / publish (push) Successful in 1m49s

Staging lost the job lease mid-chapter because the unref'd interval never
fired outside an HTTP request. Refresh the lease on section progress and
leave the heartbeat timer ref'd so a long model await cannot starve it.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-03 02:13:13 +08:00
co-authored by Cursor
parent 4329426530
commit 5c0bec0c94
5 changed files with 46 additions and 21 deletions
@@ -422,6 +422,12 @@ test("production writer and worker keep the same signal on initial and repair mo
assert.match(workerSource, /generatePersonalReport\(\{[\s\S]*signal: context\.signal,[\s\S]*\}\)/);
assert.match(workerSource, /leaseSeconds: 600/);
assert.match(workerSource, /heartbeatIntervalMs: 20_000/);
const workerCore = readFileSync(
new URL("../src/lib/personal-report-worker-core.ts", import.meta.url),
"utf8",
);
assert.match(workerCore, /onProgress: async \(progress\) => \{\s*await refreshLease\(\)/);
assert.doesNotMatch(workerCore, /timerUnref\(heartbeatTimer\)/);
});
test("an all-blocked plan still produces a valid v2 document with honest disclosures and no invented thematic section", async () => {
+21 -3
View File
@@ -87,7 +87,9 @@ function jobRecord(overrides: Partial<PersonalReportJobRecord> = {}): PersonalRe
function createHarness(input: Readonly<{
job?: PersonalReportJobRecord;
report?: PersonalReportRecord | null;
generate?: () => Promise<GeneratePersonalReportResult>;
generate?: (context: {
onProgress?: (progress: Readonly<{ phase: string; completed: number; total: number }>) => Promise<void> | void;
}) => Promise<GeneratePersonalReportResult>;
loseLeaseBeforeCompletion?: boolean;
markReadyError?: PersonalReportJobServiceError;
billing?: ReportBillingPort;
@@ -326,10 +328,10 @@ function createHarness(input: Readonly<{
assert.equal(userId, USER_ID);
return { birth_time_status: "confirmed" };
},
generate: async () => {
generate: async (context) => {
events.push("generate");
generateCount += 1;
return input.generate?.() ?? { status: "ready", document: READY_DOCUMENT, evidenceHash: "c".repeat(64) };
return input.generate?.(context) ?? { status: "ready", document: READY_DOCUMENT, evidenceHash: "c".repeat(64) };
},
billing: input.billing
? {
@@ -385,6 +387,22 @@ test("tick recovers first, heartbeats, persists the report, then closes the leas
);
});
test("section progress heartbeats the lease before recording percent", async () => {
const harness = createHarness({
generate: async (context) => {
await context.onProgress?.({ phase: "section:theme-career", completed: 1, total: 4 });
return { status: "ready", document: READY_DOCUMENT, evidenceHash: "c".repeat(64) };
},
});
const result = await harness.worker.tick();
assert.equal(result.outcome, "ready");
const generateAt = harness.events.indexOf("generate");
const progressAt = harness.events.indexOf("progress:section:theme-career:43");
assert.ok(generateAt >= 0);
assert.ok(progressAt > generateAt);
assert.ok(harness.events.slice(generateAt + 1, progressAt).includes("heartbeat"));
});
test("a restarted process reclaims an expired lease and does not create a second report or job", async () => {
const harness = createHarness({
job: jobRecord({