Skip to content

Commit 08182e2

Browse files
committed
fix(workboard): harden stale lifecycle proof
1 parent 9a8ae5a commit 08182e2

3 files changed

Lines changed: 560 additions & 3 deletions

File tree

extensions/workboard/src/store.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,13 +367,15 @@ describe("WorkboardStore", () => {
367367
status: "review",
368368
metadata: { lifecycleStatusSourceUpdatedAt: 0 },
369369
});
370+
expect(staleZeroLifecycle).toEqual(manual);
370371
expect(staleZeroLifecycle.status).toBe("running");
371372
expect(staleZeroLifecycle.metadata?.lifecycleStatusSourceUpdatedAt).toBeUndefined();
372373

373374
const staleLifecycle = await store.update(card.id, {
374375
status: "review",
375376
metadata: { lifecycleStatusSourceUpdatedAt: 2000 },
376377
});
378+
expect(staleLifecycle).toEqual(manual);
377379
expect(staleLifecycle.status).toBe("running");
378380
expect(staleLifecycle.updatedAt).toBe(manual.updatedAt);
379381
expect(staleLifecycle.events).toHaveLength(manual.events?.length ?? 0);
@@ -390,6 +392,74 @@ describe("WorkboardStore", () => {
390392
);
391393
});
392394

395+
it("keeps non-status fields from stale lifecycle patches", async () => {
396+
const store = new WorkboardStore(createMemoryStore());
397+
const card = await store.create({
398+
title: "Keep stale sync details",
399+
execution: {
400+
id: "exec-1",
401+
kind: "agent-session",
402+
engine: "codex",
403+
mode: "autonomous",
404+
status: "running",
405+
model: "openai/gpt-5.5",
406+
sessionKey: "agent:main:dashboard:1",
407+
runId: "run-1",
408+
startedAt: 1,
409+
updatedAt: 1000,
410+
},
411+
});
412+
const lifecycleMoved = await store.update(card.id, {
413+
status: "review",
414+
metadata: {
415+
lifecycleStatusSourceUpdatedAt: 1000,
416+
stale: {
417+
detectedAt: 1000,
418+
lastSessionUpdatedAt: 1000,
419+
reason: "Session has not reported recent activity.",
420+
},
421+
},
422+
});
423+
const manual = await store.update(card.id, {
424+
status: "running",
425+
metadata: lifecycleMoved.metadata,
426+
});
427+
428+
const synced = await store.update(card.id, {
429+
status: "review",
430+
execution: {
431+
id: "exec-1",
432+
kind: "agent-session",
433+
engine: "codex",
434+
mode: "autonomous",
435+
status: "done",
436+
model: "openai/gpt-5.5",
437+
sessionKey: "agent:main:dashboard:1",
438+
runId: "run-1",
439+
startedAt: 1,
440+
updatedAt: 2000,
441+
},
442+
metadata: {
443+
lifecycleStatusSourceUpdatedAt: 1000,
444+
stale: null,
445+
},
446+
});
447+
448+
expect(manual.metadata?.stale).toBeDefined();
449+
expect(synced.status).toBe("running");
450+
expect(synced.execution).toMatchObject({
451+
runId: "run-1",
452+
status: "done",
453+
updatedAt: 2000,
454+
});
455+
expect(synced.metadata?.stale).toBeUndefined();
456+
expect(synced.metadata?.lifecycleStatusSourceUpdatedAt).toBeUndefined();
457+
expect(synced.events?.at(-1)).toMatchObject({
458+
kind: "attempt_updated",
459+
runId: "run-1",
460+
});
461+
});
462+
393463
it("clears copied lifecycle provenance on manual status patches", async () => {
394464
const store = new WorkboardStore(createMemoryStore());
395465
const card = await store.create({ title: "Clear copied provenance" });

extensions/workboard/src/store.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2649,9 +2649,7 @@ export class WorkboardStore {
26492649
if (patch.metadata && typeof patch.metadata === "object" && !Array.isArray(patch.metadata)) {
26502650
const metadataPatch = patch.metadata as Record<string, unknown>;
26512651
const { lifecycleStatusSourceUpdatedAt: _ignored, ...rest } = metadataPatch;
2652-
patch.metadata = Object.values(rest).some((value) => value !== undefined)
2653-
? rest
2654-
: undefined;
2652+
patch.metadata = Object.keys(rest).length > 0 ? rest : undefined;
26552653
}
26562654
const hasSemanticPatch = Object.entries(patch).some(
26572655
([key, value]) => key !== "status" && key !== "metadata" && value !== undefined,

0 commit comments

Comments
 (0)