From 38a8032145a49c81506a3aaef0b80e13ece768ad Mon Sep 17 00:00:00 2001 From: fujiwaranosai850 Date: Fri, 8 May 2026 12:12:49 +0000 Subject: [PATCH 1/6] fix: keep blocked hold issues out of auto requeue (#225) --- lib/orchestrator-intervention/engine.test.ts | 11 ++++--- lib/orchestrator-intervention/engine.ts | 3 ++ .../tasks/orchestrator-intervention.test.ts | 33 ++++++++++++++++--- lib/tools/tasks/orchestrator-intervention.ts | 7 ++++ lib/tools/tasks/task-start.test.ts | 31 +++++++++++++++++ lib/tools/tasks/task-start.ts | 20 ++++++++++- 6 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 lib/tools/tasks/task-start.test.ts diff --git a/lib/orchestrator-intervention/engine.test.ts b/lib/orchestrator-intervention/engine.test.ts index c992258..d039fc8 100644 --- a/lib/orchestrator-intervention/engine.test.ts +++ b/lib/orchestrator-intervention/engine.test.ts @@ -48,7 +48,7 @@ describe("orchestrator intervention engine", () => { } }); - it("requeues a refining issue when a hold policy matches", async () => { + it("does not auto-requeue a refining issue when a hold policy matches", async () => { const h = await createTestHarness(); try { const issue = h.provider.seedIssue({ iid: 77, title: "Blocked", labels: ["Refining"] }); @@ -79,12 +79,13 @@ describe("orchestrator intervention engine", () => { source: "worker", }); - assert.equal(executions[0]?.executed, true); + assert.equal(executions[0]?.executed, false); + assert.match(executions[0]?.error ?? "", /not allowed from HOLD state/i); const updated = await h.provider.getIssue(77); - assert.ok(updated.labels.includes("To Do")); + assert.ok(updated.labels.includes("Refining")); + assert.ok(!updated.labels.includes("To Do")); const comments = await h.provider.listComments(77); - assert.equal(comments.length, 1); - assert.match(comments[0]!.body, /Requeued after blocked/); + assert.equal(comments.length, 0); } finally { await h.cleanup(); } diff --git a/lib/orchestrator-intervention/engine.ts b/lib/orchestrator-intervention/engine.ts index 9cfe893..0f8c551 100644 --- a/lib/orchestrator-intervention/engine.ts +++ b/lib/orchestrator-intervention/engine.ts @@ -172,6 +172,9 @@ async function executePolicyAction( if (!currentLabel) throw new Error("issue has no recognized workflow label"); const currentState = findStateByLabel(ctx.workflow, currentLabel); if (!currentState) throw new Error(`unknown state for ${currentLabel}`); + if (currentState.type === StateType.HOLD) { + throw new Error(`automatic requeue is not allowed from HOLD state ${currentLabel}; require explicit human restart via task_start(confirmHoldRestart=true)`); + } const target = resolveTarget(ctx.workflow, currentLabel, currentState); if (target.transitioned) { await ctx.provider.transitionLabel(issue.iid, currentLabel, target.targetLabel); diff --git a/lib/tools/tasks/orchestrator-intervention.test.ts b/lib/tools/tasks/orchestrator-intervention.test.ts index 37c5cb1..2dbdc9a 100644 --- a/lib/tools/tasks/orchestrator-intervention.test.ts +++ b/lib/tools/tasks/orchestrator-intervention.test.ts @@ -12,7 +12,7 @@ const pluginCtx = { }; describe("orchestrator_intervention tool", () => { - it("saves and lists policies", async () => { + it("saves and lists safe policies", async () => { const h = await createTestHarness(); try { const tool = createOrchestratorInterventionTool(pluginCtx as any)({ @@ -24,9 +24,9 @@ describe("orchestrator_intervention tool", () => { channelId: h.channelId, action: "set_policy", policy: { - title: "Requeue blocked dev", + title: "Comment on blocked dev", event: { type: "workflow.hold", role: "developer", result: "blocked" }, - action: { type: "requeue", message: "Try again" }, + action: { type: "comment", message: "Need human decision" }, }, }); @@ -37,7 +37,32 @@ describe("orchestrator_intervention tool", () => { const details = listed.details as { policies: Array<{ title: string }> }; assert.equal(details.policies.length, 1); - assert.equal(details.policies[0]?.title, "Requeue blocked dev"); + assert.equal(details.policies[0]?.title, "Comment on blocked dev"); + } finally { + await h.cleanup(); + } + }); + + it("rejects auto requeue policies for hold events", async () => { + const h = await createTestHarness(); + try { + const tool = createOrchestratorInterventionTool(pluginCtx as any)({ + workspaceDir: h.workspaceDir, + messageChannel: "telegram", + }); + + await assert.rejects( + tool.execute("1", { + channelId: h.channelId, + action: "set_policy", + policy: { + title: "Requeue blocked dev", + event: { type: "workflow.hold", role: "developer", result: "blocked" }, + action: { type: "requeue", message: "Try again" }, + }, + }), + /not allowed for workflow\.hold policies/, + ); } finally { await h.cleanup(); } diff --git a/lib/tools/tasks/orchestrator-intervention.ts b/lib/tools/tasks/orchestrator-intervention.ts index 21179f3..3b0c065 100644 --- a/lib/tools/tasks/orchestrator-intervention.ts +++ b/lib/tools/tasks/orchestrator-intervention.ts @@ -131,6 +131,13 @@ Supported action types: ${ORCHESTRATOR_INTERVENTION_ACTION_TYPES.join(", ")}`, action: payload.action as OrchestratorInterventionPolicy["action"], updatedBy: toolCtx.sessionKey ?? toolCtx.agentId, }; + if (policy.mode === "auto" + && policy.event.type === "workflow.hold" + && (policy.action.type === "requeue" || policy.action.type === "queue_issue")) { + throw new Error( + `auto ${policy.action.type} is not allowed for workflow.hold policies. Hold states like Refining require explicit human restart.`, + ); + } const saved = await upsertInterventionPolicy(workspaceDir, project.slug, policy); await auditLog(workspaceDir, "orchestrator_intervention_policy_set", { project: project.name, diff --git a/lib/tools/tasks/task-start.test.ts b/lib/tools/tasks/task-start.test.ts new file mode 100644 index 0000000..4783340 --- /dev/null +++ b/lib/tools/tasks/task-start.test.ts @@ -0,0 +1,31 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { StateType } from "../../workflow/types.js"; +import { assertExplicitHoldRestart, resolveTarget } from "./task-start.js"; +import { DEFAULT_WORKFLOW } from "../../workflow/defaults.js"; + +describe("task_start", () => { + it("requires explicit confirmation to restart an issue from Refining", () => { + assert.throws( + () => assertExplicitHoldRestart(42, "Refining", { label: "Refining", type: StateType.HOLD, description: "hold" }, false), + /confirmHoldRestart: true/, + ); + }); + + it("allows explicit restart from Refining when confirmHoldRestart is true", () => { + assert.doesNotThrow(() => { + assertExplicitHoldRestart(42, "Refining", { label: "Refining", type: StateType.HOLD, description: "hold" }, true); + }); + }); + + it("resolves Refining to the developer queue when restart is explicitly confirmed", () => { + const target = resolveTarget( + DEFAULT_WORKFLOW, + "Refining", + DEFAULT_WORKFLOW.states.refining!, + ); + + assert.equal(target.transitioned, true); + assert.equal(target.targetLabel, "To Do"); + }); +}); diff --git a/lib/tools/tasks/task-start.ts b/lib/tools/tasks/task-start.ts index 029ed04..69fbd93 100644 --- a/lib/tools/tasks/task-start.ts +++ b/lib/tools/tasks/task-start.ts @@ -39,7 +39,8 @@ Optionally set a level hint (e.g. "junior", "senior") so the heartbeat dispatche Examples: - Start work: { channelId: "-1003844794417", issueId: 42 } → advances to next queue -- With level: { channelId: "-1003844794417", issueId: 42, level: "junior" } → advances + hints junior`, +- With level: { channelId: "-1003844794417", issueId: 42, level: "junior" } → advances + hints junior +- Restart from hold: { channelId: "-1003844794417", issueId: 42, confirmHoldRestart: true } → explicitly leaves Planning/Refining`, parameters: { type: "object", required: ["channelId", "issueId"], @@ -60,6 +61,10 @@ Examples: type: "string", description: "Optional level hint for dispatch (e.g. 'junior', 'senior'). Applied as a label so the heartbeat respects it.", }, + confirmHoldRestart: { + type: "boolean", + description: "Required when restarting an issue from a HOLD state (Planning, Refining). Makes human intent explicit and prevents accidental auto-requeue.", + }, }, }, @@ -67,6 +72,7 @@ Examples: const channelId = resolveChannelId(toolCtx, params.channelId as string | undefined); const issueId = params.issueId as number; const levelHint = params.level as string | undefined; + const confirmHoldRestart = params.confirmHoldRestart === true; const workspaceDir = requireWorkspaceDir(toolCtx); const messageThreadId = params.messageThreadId as number | undefined; @@ -91,6 +97,7 @@ Examples: if (!currentState) { throw new Error(`No state config for label "${currentLabel}".`); } + assertExplicitHoldRestart(issueId, currentLabel, currentState, confirmHoldRestart); // Determine target based on current state type const { targetLabel, targetState, transitioned } = resolveTarget( @@ -172,6 +179,17 @@ Examples: * - ACTIVE: error (already being worked on) * - TERMINAL: error (issue is closed) */ +export function assertExplicitHoldRestart( + issueId: number, + currentLabel: string, + currentState: StateConfig, + confirmHoldRestart: boolean, +): void { + if (currentState.type === StateType.HOLD && !confirmHoldRestart) { + throw new Error(`Issue #${issueId} is in hold state "${currentLabel}". Restart requires explicit confirmHoldRestart: true.`); + } +} + export function resolveTarget( workflow: WorkflowConfig, currentLabel: string, From 538fb211c8a1f1d325e740309f4df726f06cb325 Mon Sep 17 00:00:00 2001 From: fujiwaranosai850 Date: Fri, 8 May 2026 12:14:24 +0000 Subject: [PATCH 2/6] test: fix hold restart regression test typing --- lib/tools/tasks/task-start.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tools/tasks/task-start.test.ts b/lib/tools/tasks/task-start.test.ts index 4783340..b3a4869 100644 --- a/lib/tools/tasks/task-start.test.ts +++ b/lib/tools/tasks/task-start.test.ts @@ -7,14 +7,14 @@ import { DEFAULT_WORKFLOW } from "../../workflow/defaults.js"; describe("task_start", () => { it("requires explicit confirmation to restart an issue from Refining", () => { assert.throws( - () => assertExplicitHoldRestart(42, "Refining", { label: "Refining", type: StateType.HOLD, description: "hold" }, false), + () => assertExplicitHoldRestart(42, "Refining", { label: "Refining", type: StateType.HOLD, description: "hold", color: "#000000" }, false), /confirmHoldRestart: true/, ); }); it("allows explicit restart from Refining when confirmHoldRestart is true", () => { assert.doesNotThrow(() => { - assertExplicitHoldRestart(42, "Refining", { label: "Refining", type: StateType.HOLD, description: "hold" }, true); + assertExplicitHoldRestart(42, "Refining", { label: "Refining", type: StateType.HOLD, description: "hold", color: "#000000" }, true); }); }); From b5f8991f6805092ab227a91c7be3675bb61b92f0 Mon Sep 17 00:00:00 2001 From: fujiwaranosai850 Date: Fri, 8 May 2026 12:22:38 +0000 Subject: [PATCH 3/6] fix: keep refining issues out of auto queue actions --- lib/orchestrator-intervention/engine.test.ts | 41 +++++++++++++++++++ lib/orchestrator-intervention/engine.ts | 3 ++ .../tasks/orchestrator-intervention.test.ts | 25 +++++++++++ 3 files changed, 69 insertions(+) diff --git a/lib/orchestrator-intervention/engine.test.ts b/lib/orchestrator-intervention/engine.test.ts index d039fc8..ca82bb7 100644 --- a/lib/orchestrator-intervention/engine.test.ts +++ b/lib/orchestrator-intervention/engine.test.ts @@ -91,6 +91,47 @@ describe("orchestrator intervention engine", () => { } }); + it("does not auto-queue a refining issue via queue_issue after a blocked hold event", async () => { + const h = await createTestHarness(); + try { + const issue = h.provider.seedIssue({ iid: 78, title: "Blocked", labels: ["Refining"] }); + await upsertInterventionPolicy(h.workspaceDir, h.project.slug, { + id: "queue-blocked", + title: "Queue blocked issues", + mode: "auto", + issueId: 78, + event: { type: "workflow.hold", result: "blocked" }, + action: { type: "queue_issue", issueId: 78 }, + }); + + const executions = await recordAndApplyInterventionEvent({ + workspaceDir: h.workspaceDir, + channelId: h.channelId, + agentId: "main", + project: h.project, + workflow: h.workflow, + provider: h.provider, + issue, + runCommand: h.runCommand, + }, { + eventType: "workflow.hold", + issueId: 78, + result: "blocked", + fromState: "Doing", + toState: "Refining", + source: "worker", + }); + + assert.equal(executions[0]?.executed, false); + assert.match(executions[0]?.error ?? "", /automatic queue_issue is not allowed from HOLD state/i); + const updated = await h.provider.getIssue(78); + assert.ok(updated.labels.includes("Refining")); + assert.ok(!updated.labels.includes("To Do")); + } finally { + await h.cleanup(); + } + }); + it("creates follow-up issues in the workflow initial hold state, not the first hold state by order", async () => { const h = await createTestHarness(); try { diff --git a/lib/orchestrator-intervention/engine.ts b/lib/orchestrator-intervention/engine.ts index 0f8c551..1e9ba62 100644 --- a/lib/orchestrator-intervention/engine.ts +++ b/lib/orchestrator-intervention/engine.ts @@ -195,6 +195,9 @@ async function executePolicyAction( if (!currentLabel) throw new Error(`issue #${targetIssueId} has no recognized workflow label`); const currentState = findStateByLabel(ctx.workflow, currentLabel); if (!currentState) throw new Error(`unknown state for ${currentLabel}`); + if (currentState.type === StateType.HOLD) { + throw new Error(`automatic queue_issue is not allowed from HOLD state ${currentLabel}; require explicit human restart via task_start(confirmHoldRestart=true)`); + } const target = resolveTarget(ctx.workflow, currentLabel, currentState); if (target.transitioned) { await ctx.provider.transitionLabel(targetIssueId, currentLabel, target.targetLabel); diff --git a/lib/tools/tasks/orchestrator-intervention.test.ts b/lib/tools/tasks/orchestrator-intervention.test.ts index 2dbdc9a..270948b 100644 --- a/lib/tools/tasks/orchestrator-intervention.test.ts +++ b/lib/tools/tasks/orchestrator-intervention.test.ts @@ -67,4 +67,29 @@ describe("orchestrator_intervention tool", () => { await h.cleanup(); } }); + + it("rejects auto queue_issue policies for hold events", async () => { + const h = await createTestHarness(); + try { + const tool = createOrchestratorInterventionTool(pluginCtx as any)({ + workspaceDir: h.workspaceDir, + messageChannel: "telegram", + }); + + await assert.rejects( + tool.execute("1", { + channelId: h.channelId, + action: "set_policy", + policy: { + title: "Queue blocked dev", + event: { type: "workflow.hold", role: "developer", result: "blocked" }, + action: { type: "queue_issue", issueId: 42 }, + }, + }), + /not allowed for workflow\.hold policies/, + ); + } finally { + await h.cleanup(); + } + }); }); From 57f69d6d09b7a2d6ea50747c7f600fa578f8c691 Mon Sep 17 00:00:00 2001 From: fujiwaranosai850 Date: Fri, 8 May 2026 12:36:04 +0000 Subject: [PATCH 4/6] fix: allow normal planning starts --- lib/tools/tasks/task-start.test.ts | 10 ++++++++-- lib/tools/tasks/task-start.ts | 19 ++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/tools/tasks/task-start.test.ts b/lib/tools/tasks/task-start.test.ts index b3a4869..4c23984 100644 --- a/lib/tools/tasks/task-start.test.ts +++ b/lib/tools/tasks/task-start.test.ts @@ -7,14 +7,20 @@ import { DEFAULT_WORKFLOW } from "../../workflow/defaults.js"; describe("task_start", () => { it("requires explicit confirmation to restart an issue from Refining", () => { assert.throws( - () => assertExplicitHoldRestart(42, "Refining", { label: "Refining", type: StateType.HOLD, description: "hold", color: "#000000" }, false), + () => assertExplicitHoldRestart(42, DEFAULT_WORKFLOW, "Refining", { label: "Refining", type: StateType.HOLD, description: "hold", color: "#000000" }, false), /confirmHoldRestart: true/, ); }); + it("allows the normal Planning start path without confirmHoldRestart", () => { + assert.doesNotThrow(() => { + assertExplicitHoldRestart(42, DEFAULT_WORKFLOW, "Planning", DEFAULT_WORKFLOW.states.planning!, false); + }); + }); + it("allows explicit restart from Refining when confirmHoldRestart is true", () => { assert.doesNotThrow(() => { - assertExplicitHoldRestart(42, "Refining", { label: "Refining", type: StateType.HOLD, description: "hold", color: "#000000" }, true); + assertExplicitHoldRestart(42, DEFAULT_WORKFLOW, "Refining", { label: "Refining", type: StateType.HOLD, description: "hold", color: "#000000" }, true); }); }); diff --git a/lib/tools/tasks/task-start.ts b/lib/tools/tasks/task-start.ts index 69fbd93..3365494 100644 --- a/lib/tools/tasks/task-start.ts +++ b/lib/tools/tasks/task-start.ts @@ -21,7 +21,6 @@ import { import { getCurrentStateLabel, findStateByLabel, - findStateKeyByLabel, getRoleLabelColor, } from "../../workflow/index.js"; import { getLevelsForRole } from "../../roles/index.js"; @@ -40,7 +39,7 @@ Optionally set a level hint (e.g. "junior", "senior") so the heartbeat dispatche Examples: - Start work: { channelId: "-1003844794417", issueId: 42 } → advances to next queue - With level: { channelId: "-1003844794417", issueId: 42, level: "junior" } → advances + hints junior -- Restart from hold: { channelId: "-1003844794417", issueId: 42, confirmHoldRestart: true } → explicitly leaves Planning/Refining`, +- Restart from Refining: { channelId: "-1003844794417", issueId: 42, confirmHoldRestart: true } → explicitly leaves blocked/rework hold`, parameters: { type: "object", required: ["channelId", "issueId"], @@ -63,7 +62,7 @@ Examples: }, confirmHoldRestart: { type: "boolean", - description: "Required when restarting an issue from a HOLD state (Planning, Refining). Makes human intent explicit and prevents accidental auto-requeue.", + description: "Required when restarting an issue from blocked/rework hold states like Refining. Normal Planning starts do not need it.", }, }, }, @@ -97,7 +96,7 @@ Examples: if (!currentState) { throw new Error(`No state config for label "${currentLabel}".`); } - assertExplicitHoldRestart(issueId, currentLabel, currentState, confirmHoldRestart); + assertExplicitHoldRestart(issueId, workflow, currentLabel, currentState, confirmHoldRestart); // Determine target based on current state type const { targetLabel, targetState, transitioned } = resolveTarget( @@ -181,13 +180,19 @@ Examples: */ export function assertExplicitHoldRestart( issueId: number, + workflow: WorkflowConfig, currentLabel: string, currentState: StateConfig, confirmHoldRestart: boolean, ): void { - if (currentState.type === StateType.HOLD && !confirmHoldRestart) { - throw new Error(`Issue #${issueId} is in hold state "${currentLabel}". Restart requires explicit confirmHoldRestart: true.`); - } + if (currentState.type !== StateType.HOLD) return; + + const initialState = workflow.states[workflow.initial]; + const initialHoldLabel = initialState?.label; + if (currentLabel === initialHoldLabel) return; + if (confirmHoldRestart) return; + + throw new Error(`Issue #${issueId} is in hold state "${currentLabel}". Restart requires explicit confirmHoldRestart: true.`); } export function resolveTarget( From a1549c94fb466f59bff0d82187897c17d20d83a1 Mon Sep 17 00:00:00 2001 From: fujiwaranosai850 Date: Fri, 8 May 2026 15:35:10 +0000 Subject: [PATCH 5/6] fix: guard heartbeat requeue against stale hold races --- lib/services/heartbeat/health.test.ts | 56 ++++++++++++++++++++++++++- lib/services/heartbeat/health.ts | 10 +++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/lib/services/heartbeat/health.test.ts b/lib/services/heartbeat/health.test.ts index 1a13b6c..cb55649 100644 --- a/lib/services/heartbeat/health.test.ts +++ b/lib/services/heartbeat/health.test.ts @@ -10,7 +10,7 @@ import { describe, it, beforeEach, afterEach } from "node:test"; import assert from "node:assert"; import { createTestHarness, type TestHarness } from "../../testing/index.js"; -import { scanOrphanedLabels } from "./health.js"; +import { checkWorkerHealth, scanOrphanedLabels } from "./health.js"; import { PrState } from "../../providers/provider.js"; import { writeProjects, type ProjectsData } from "../../projects/index.js"; @@ -18,6 +18,60 @@ import { writeProjects, type ProjectsData } from "../../projects/index.js"; // Test suite // --------------------------------------------------------------------------- +describe("checkWorkerHealth", () => { + let h: TestHarness; + + afterEach(async () => { + if (h) await h.cleanup(); + }); + + it("should not revert a blocked hold back to queue from a stale active snapshot", async () => { + h = await createTestHarness({ + workers: { + developer: { active: true, issueId: "42", sessionKey: null, previousLabel: "To Do" }, + }, + }); + + h.provider.seedIssue({ iid: 42, title: "Blocked issue", labels: ["Doing"] }); + + const originalGetIssue = h.provider.getIssue.bind(h.provider); + let firstRead = true; + h.provider.getIssue = async (issueId: number) => { + const issue = await originalGetIssue(issueId); + if (!firstRead) return issue; + firstRead = false; + + const snapshot = { ...issue, labels: [...issue.labels] }; + issue.labels = issue.labels.filter((label) => label !== "Doing"); + issue.labels.push("Refining"); + return snapshot; + }; + + const fixes = await checkWorkerHealth({ + workspaceDir: h.workspaceDir, + projectSlug: h.project.slug, + project: h.project, + role: "developer", + autoFix: true, + provider: h.provider, + sessions: null, + workflow: h.workflow, + runCommand: async () => ({ stdout: "", stderr: "", exitCode: 0 }), + }); + + assert.strictEqual(fixes.length, 1); + assert.strictEqual(fixes[0]!.fixed, true); + assert.strictEqual(fixes[0]!.labelReverted, undefined, "stale repair should skip queue revert"); + + const issue = await originalGetIssue(42); + assert.ok(issue.labels.includes("Refining"), `Expected Refining to survive, got: ${issue.labels}`); + assert.ok(!issue.labels.includes("To Do"), `Expected no To Do requeue, got: ${issue.labels}`); + + const transitions = h.provider.callsTo("transitionLabel"); + assert.strictEqual(transitions.length, 0, "should not transition back to queue from stale state"); + }); +}); + describe("scanOrphanedLabels", () => { let h: TestHarness; diff --git a/lib/services/heartbeat/health.ts b/lib/services/heartbeat/health.ts index 563d149..8555435 100644 --- a/lib/services/heartbeat/health.ts +++ b/lib/services/heartbeat/health.ts @@ -232,6 +232,16 @@ export async function checkWorkerHealth(opts: { async function revertLabel(fix: HealthFix, from: StateLabel, to: StateLabel) { if (!issueIdNum) return; try { + const latestIssue = await fetchIssue(provider, issueIdNum); + const liveLabel = latestIssue + ? getCurrentStateLabel(latestIssue.labels, workflow) + : null; + + // Do not overwrite a newer workflow state with a stale health repair. + // This specifically protects blocked HOLD transitions like Doing -> Refining + // from being clobbered back into queue by a stale heartbeat snapshot. + if (liveLabel !== from) return; + await provider.transitionLabel(issueIdNum, from, to); await recordLoopDiagnostic(workspaceDir, "health_requeue", { project: project.name, From ffa1794ab7fabe5277a12a19b2892e9a081d7424 Mon Sep 17 00:00:00 2001 From: fujiwaranosai850 Date: Fri, 8 May 2026 15:42:50 +0000 Subject: [PATCH 6/6] test: align heartbeat stale-hold regression coverage --- lib/services/heartbeat/health.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/services/heartbeat/health.test.ts b/lib/services/heartbeat/health.test.ts index cb55649..db87ed5 100644 --- a/lib/services/heartbeat/health.test.ts +++ b/lib/services/heartbeat/health.test.ts @@ -56,7 +56,14 @@ describe("checkWorkerHealth", () => { provider: h.provider, sessions: null, workflow: h.workflow, - runCommand: async () => ({ stdout: "", stderr: "", exitCode: 0 }), + runCommand: async () => ({ + stdout: "", + stderr: "", + code: 0, + signal: null, + killed: false, + termination: "exit", + }), }); assert.strictEqual(fixes.length, 1);