feat(FN-5034): complete Step 2 — add stale paused todo detector
Fusion-Task-Id: FN-5034 Fusion-Task-Lineage: 1e315656-30d8-41d2-9802-0d9e155f0ba8
This commit is contained in:
committed by
gsxdsm
parent
e53cf7e0b1
commit
8e1ee3efa0
49
packages/core/src/__tests__/stale-paused-todo.test.ts
Normal file
49
packages/core/src/__tests__/stale-paused-todo.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_STALE_PAUSED_TODO_THRESHOLD_MS, getStalePausedTodoSignal } from "../stale-paused-todo.js";
|
||||
|
||||
const NOW = Date.parse("2026-05-14T12:00:00.000Z");
|
||||
|
||||
const baseTask = {
|
||||
column: "todo" as const,
|
||||
paused: true,
|
||||
columnMovedAt: new Date(NOW - DEFAULT_STALE_PAUSED_TODO_THRESHOLD_MS).toISOString(),
|
||||
updatedAt: new Date(NOW - DEFAULT_STALE_PAUSED_TODO_THRESHOLD_MS).toISOString(),
|
||||
pausedReason: "manual-hold",
|
||||
pausedByAgentId: "agent-1",
|
||||
};
|
||||
|
||||
describe("getStalePausedTodoSignal", () => {
|
||||
it("returns signal for paused todo older than threshold", () => {
|
||||
const signal = getStalePausedTodoSignal({ ...baseTask }, { now: NOW });
|
||||
expect(signal?.code).toBe("stale-paused-todo");
|
||||
expect(signal?.ageMs).toBe(DEFAULT_STALE_PAUSED_TODO_THRESHOLD_MS);
|
||||
expect(signal?.thresholdMs).toBe(DEFAULT_STALE_PAUSED_TODO_THRESHOLD_MS);
|
||||
});
|
||||
|
||||
it("returns undefined when not paused", () => {
|
||||
expect(getStalePausedTodoSignal({ ...baseTask, paused: false }, { now: NOW })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for non-todo columns", () => {
|
||||
expect(getStalePausedTodoSignal({ ...baseTask, column: "in-progress" }, { now: NOW })).toBeUndefined();
|
||||
expect(getStalePausedTodoSignal({ ...baseTask, column: "in-review" }, { now: NOW })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when age is under threshold", () => {
|
||||
const signal = getStalePausedTodoSignal(
|
||||
{ ...baseTask, columnMovedAt: new Date(NOW - DEFAULT_STALE_PAUSED_TODO_THRESHOLD_MS + 1).toISOString() },
|
||||
{ now: NOW },
|
||||
);
|
||||
expect(signal).toBeUndefined();
|
||||
});
|
||||
|
||||
it("respects custom threshold override", () => {
|
||||
const signal = getStalePausedTodoSignal({ ...baseTask }, { now: NOW, thresholdMs: DEFAULT_STALE_PAUSED_TODO_THRESHOLD_MS + 1_000 });
|
||||
expect(signal).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when threshold is zero or negative", () => {
|
||||
expect(getStalePausedTodoSignal({ ...baseTask }, { now: NOW, thresholdMs: 0 })).toBeUndefined();
|
||||
expect(getStalePausedTodoSignal({ ...baseTask }, { now: NOW, thresholdMs: -1 })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
47
packages/core/src/stale-paused-todo.ts
Normal file
47
packages/core/src/stale-paused-todo.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Task } from "./types.js";
|
||||
|
||||
export type StalePausedTodoCode = "stale-paused-todo";
|
||||
|
||||
export interface StalePausedTodoSignal {
|
||||
code: StalePausedTodoCode;
|
||||
reason: string;
|
||||
observedAt: string;
|
||||
ageMs: number;
|
||||
thresholdMs: number;
|
||||
pausedReason?: string;
|
||||
pausedByAgentId?: string;
|
||||
}
|
||||
|
||||
export interface StalePausedTodoContext {
|
||||
now?: number;
|
||||
thresholdMs?: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_STALE_PAUSED_TODO_THRESHOLD_MS = 24 * 60 * 60_000;
|
||||
|
||||
export function getStalePausedTodoSignal(
|
||||
task: Pick<Task, "column" | "paused" | "columnMovedAt" | "updatedAt" | "pausedReason" | "pausedByAgentId">,
|
||||
context: StalePausedTodoContext = {},
|
||||
): StalePausedTodoSignal | undefined {
|
||||
if (task.column !== "todo" || task.paused !== true) return undefined;
|
||||
|
||||
const thresholdMs = context.thresholdMs ?? DEFAULT_STALE_PAUSED_TODO_THRESHOLD_MS;
|
||||
if (!Number.isFinite(thresholdMs) || thresholdMs <= 0) return undefined;
|
||||
|
||||
const now = context.now ?? Date.now();
|
||||
const anchor = Date.parse(task.columnMovedAt ?? task.updatedAt);
|
||||
if (!Number.isFinite(anchor)) return undefined;
|
||||
|
||||
const ageMs = now - anchor;
|
||||
if (ageMs < thresholdMs) return undefined;
|
||||
|
||||
return {
|
||||
code: "stale-paused-todo",
|
||||
reason: "Task has remained paused in todo beyond threshold",
|
||||
observedAt: new Date(now).toISOString(),
|
||||
ageMs,
|
||||
thresholdMs,
|
||||
pausedReason: task.pausedReason,
|
||||
pausedByAgentId: task.pausedByAgentId,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user