fix(FN-3005): preserve card timer across reruns
This commit is contained in:
@@ -4619,61 +4619,6 @@ describe("HeartbeatTriggerScheduler", () => {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("phase-aligns the first tick to lastHeartbeatAt when supplied", async () => {
|
||||
// Simulate: last tick was 4s ago, interval is 5s.
|
||||
// The next tick is due in 1s, not in a fresh full 5s window.
|
||||
vi.setSystemTime(new Date("2026-04-30T05:00:00.000Z"));
|
||||
const lastHeartbeatAt = new Date("2026-04-30T04:59:56.000Z").toISOString();
|
||||
|
||||
scheduler.registerAgent(
|
||||
"agent-001",
|
||||
{ heartbeatIntervalMs: 5000 },
|
||||
{ lastHeartbeatAt },
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(999);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
|
||||
// Subsequent ticks resume the steady cadence.
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("fires promptly with jitter when lastHeartbeatAt is already overdue", async () => {
|
||||
// Interval is 60s but the last tick was 10 minutes ago — fire immediately
|
||||
// (within the OVERDUE_FIRE_JITTER_MS window) instead of waiting another
|
||||
// full 60s. This is the core fix for "agents look unresponsive after a
|
||||
// dashboard restart" — the previous setInterval-only scheduler would
|
||||
// have made the user wait a full interval before the catch-up tick.
|
||||
vi.setSystemTime(new Date("2026-04-30T05:00:00.000Z"));
|
||||
const lastHeartbeatAt = new Date("2026-04-30T04:50:00.000Z").toISOString();
|
||||
|
||||
scheduler.registerAgent(
|
||||
"agent-001",
|
||||
{ heartbeatIntervalMs: 60_000 },
|
||||
{ lastHeartbeatAt },
|
||||
);
|
||||
|
||||
// Jitter window is 5s; advance past it to guarantee the fire happens.
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("falls back to full-interval delay when lastHeartbeatAt is missing", async () => {
|
||||
// No options provided — preserves the original "wait one full interval"
|
||||
// behavior for agents that have never ticked.
|
||||
scheduler.registerAgent("agent-001", { heartbeatIntervalMs: 5000 });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(4999);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("skips tick when agent has active run", async () => {
|
||||
(store.getActiveHeartbeatRun as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
id: "run-active",
|
||||
|
||||
@@ -11158,6 +11158,45 @@ describe("TaskExecutor watchdogs", () => {
|
||||
expect.stringContaining("Watchdog: workflow rerun handoff stalled for 15s"),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves the original executionStartedAt during a workflow rerun bounce", async () => {
|
||||
const store = createMockStore();
|
||||
const originalExecutionStartedAt = "2026-04-30T05:06:43.781Z";
|
||||
const mutableTask = {
|
||||
id: "FN-WD-4",
|
||||
title: "Workflow rerun timing",
|
||||
description: "desc",
|
||||
column: "in-progress" as const,
|
||||
paused: false,
|
||||
worktree: "/tmp/fn-wd-4",
|
||||
executionStartedAt: originalExecutionStartedAt,
|
||||
dependencies: [],
|
||||
steps: [{ name: "Step 0", status: "done" as const }],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
store.getTask.mockResolvedValue(mutableTask as any);
|
||||
store.moveTask.mockImplementation(async (_taskId: string, column: string) => {
|
||||
mutableTask.column = column as "in-progress" | "todo";
|
||||
return { ...mutableTask };
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
const outcome = await (executor as any).performWorkflowRerunBounce("FN-WD-4", "/tmp/fn-wd-4");
|
||||
|
||||
expect(outcome).toBe("bounced");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-WD-4", {
|
||||
worktree: "/tmp/fn-wd-4",
|
||||
executionStartedAt: originalExecutionStartedAt,
|
||||
});
|
||||
expect(store.moveTask.mock.calls).toEqual([
|
||||
["FN-WD-4", "todo"],
|
||||
["FN-WD-4", "in-progress"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── StepSessionExecutor integration tests ──────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user