Files
fusion/packages/dashboard/app/utils/__tests__/taskTiming.test.ts
gsxdsm e84e9d7f60 fix: the caller audit — five unwired parameters, five defects in their callers (#2803)
Seven fixes that were sitting on separate handoff branches with no owner
while `main` moved. Consolidated, rebased onto current `main`, and
verified **together** rather than only per-branch. The individual
branches remain if a subset is preferred.

This is the same consolidation that got `batch-core` and #2787 adopted.
**Close it if it breaks queue policy** — the branch keeps the work safe
either way.

## Where these came from

#2787's review found an optional parameter whose production caller never
passed it. That is a class, so I ran it against everything I had landed
and found five more. **All five turned out to have their real defect in
the CALLER, not the parameter** — in four of them the parameter was
unreachable:

| unwired parameter | what was actually wrong |
|---|---|
| `blocker-fanout.escalationColumns` | the hold default made the count
zero — **no bottleneck warning was emitted at all** |
| analytics `columnFlagsByName` | routes never built a map — **0
in-progress / 0 in-review beside correct cost totals** |
| `isLegacyAutoMergeStampCandidate` | the read **queried a column a
renamed board does not have**, so the backfill iterated nothing |
| `rankAssignedTasksForWakeDelta` | `getTasksByAssignedAgent`'s
`excludeArchived` used the literal — **archived cards returned as open
work** |
| `duplicate-intake.columnFlagsByColumnId` | intake could **archive or
soft-delete a newly created task** as a duplicate of finished work |

The heuristic worth keeping: **an optional parameter no production
caller fills is a marker pointing at an unexamined caller.** The census
cannot see any of these five — every gate is a `Set`/array literal or a
query filter, i.e. a definition rather than a comparison.

## Also included

- **`executor.ts`** — the stale-spec guard did the exact thing its own
comment forbids: on a renamed board it ran on a LIVE task and pulled it
out of execution into replan. `activeMergeStatuses` protected merging
cards *by accident*, which is why the symptom looked arbitrary.
- **`register-project-routes.ts`** — project health reported **0 active
tasks**; its list also still contained `triage`, dead since U11.
- **`dashboard/app/utils/taskTiming.ts`** — a **second copy** of
`getTotalAgentActiveMs`. Core's was converted; the card chip imports
this one, so the census counted the site as done while the rendered
number stayed keyed on `"in-progress"`.

## Verification

Verified as a set: `pnpm test:gate` **161 / 13 / 487 / 71** · core
suites **15 passed** · engine **7** · dashboard **12** · four `tsc`
targets clean · lint clean · census `--strict` exits 0.

Each fix is revert-proven individually; the specific case that fails is
named in each test header.

## Two honesty notes

**Three guards here are structural, not behavioural, and say so in their
headers.** `sanitizeAgentTaskLinks` is a closure inside
`createApiRoutes`; the analytics aggregators need a live
`AsyncDataLayer`; the stale-spec guard sits deep inside `execute()`.
Each ratchet fails on revert — verified — but none is an end-to-end
proof, and the headers state which half they cover.

**One of my behavioural test sets would have lied.** The intake-dedup
cases drive `findSameAgentDuplicates` directly; I removed the wiring to
measure the revert and **they stayed green**, because they pin the
predicate and not the caller. That is the exact illusion this audit was
chasing, reproduced in my own file. The forward now has its own
structural check.

## Deliberately not included

`worktree-pool.ts:1205` — it **fails safe** (a missed match protects a
branch from cleanup rather than deleting it) and sits in the merger's
branch-reaping path where the opposite error destroys work. That
deserves its owner's judgement, not a drive-by conversion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 12:02:53 -07:00

119 lines
4.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { getActiveRuntimeMs, getTotalAgentActiveMs, getWallClockSinceFirstExecutionMs } from "../taskTiming";
describe("taskTiming helpers", () => {
it("returns persisted plus live segment for in-progress tasks", () => {
const nowMs = Date.parse("2026-05-15T13:16:00.000Z");
const runtime = getActiveRuntimeMs(
{
column: "in-progress",
cumulativeActiveMs: 240_000,
executionStartedAt: "2026-05-15T13:15:00.000Z",
columnMovedAt: "2026-05-15T13:15:00.000Z",
},
nowMs,
);
expect(runtime).toBe(300_000);
});
it("sums planning and execution segments without using idle dwell", () => {
expect(getTotalAgentActiveMs({
column: "done", cumulativeActiveMs: 120_000, executionStartedAt: undefined,
cumulativePlanningMs: 180_000, planningStartedAt: undefined,
}, Date.parse("2026-05-15T13:16:00.000Z"))).toBe(300_000);
});
it("returns null when there is no active-runtime signal", () => {
const runtime = getActiveRuntimeMs(
{
column: "todo",
cumulativeActiveMs: undefined,
executionStartedAt: undefined,
columnMovedAt: undefined,
},
Date.now(),
);
expect(runtime).toBeNull();
});
it("uses shifted executionStartedAt so the active badge excludes engine-down time", () => {
const t0 = Date.parse("2026-06-25T00:00:00.000Z");
const runtime = getActiveRuntimeMs(
{
column: "in-progress",
cumulativeActiveMs: undefined,
executionStartedAt: new Date(t0 + 60 * 60_000).toISOString(),
columnMovedAt: new Date(t0).toISOString(),
},
t0 + 65 * 60_000,
);
expect(runtime).toBe(5 * 60_000);
expect(getActiveRuntimeMs({ column: "in-progress", cumulativeActiveMs: undefined, executionStartedAt: undefined, columnMovedAt: undefined }, t0)).toBeNull();
});
it("returns wall-clock runtime since first execution", () => {
const wallClock = getWallClockSinceFirstExecutionMs(
"2026-05-15T08:42:00.000Z",
"2026-05-15T13:17:00.000Z",
Date.parse("2026-05-15T13:20:00.000Z"),
);
expect(wallClock).toBe(16_500_000);
});
});
/*
FNXC:WorkflowLifecycleColumns 2026-07-31-10:10:
THE INVARIANT: the card's active-time chip counts the live run from the card's OWN wip lane.
THE FINDING THAT MATTERS MORE THAN THE FIX: `@fusion/core` exports its own `getTotalAgentActiveMs`,
and it was already converted onto `isWipColumnRole`. The card chip imports THIS module instead — a
second implementation of the same calculation in a different package — so that conversion never
reached the surface an operator looks at. The census counted core's site as done while the rendered
number stayed wrong. Two implementations of one rule, one converted and one not, is exactly the drift
`column-roles.ts` exists to end.
Keyed on the literal, the live execution segment was dropped on a renamed board: the chip
under-reported the run in flight by exactly its elapsed time, then healed itself the moment the card
moved on and the segment was persisted into `cumulativeActiveMs`. A number that is wrong only while
you are watching it.
REVERT PROOF, measured: restore `task.column === "in-progress"` in `getActiveRuntimeMs` and the
renamed-lane cases below fail.
*/
describe("active-time resolves the card's own wip lane", () => {
const WIP_FLAGS = { countsTowardWip: true } as never;
const NOW = Date.parse("2026-07-31T12:00:00Z");
const STARTED = "2026-07-31T11:00:00Z";
const HOUR = 60 * 60 * 1000;
it("counts the in-flight run for a RENAMED wip lane", () => {
expect(getActiveRuntimeMs(
{ column: "building", cumulativeActiveMs: 0, executionStartedAt: STARTED } as never, NOW, WIP_FLAGS,
)).toBe(HOUR);
});
it("includes it in the rendered total", () => {
expect(getTotalAgentActiveMs(
{ column: "building", cumulativeActiveMs: 0, executionStartedAt: STARTED } as never, NOW, WIP_FLAGS,
)).toBe(HOUR);
});
it("does NOT count a live segment outside the wip lane", () => {
// A stale executionStartedAt on a review card is not active time.
expect(getActiveRuntimeMs(
{ column: "signoff", cumulativeActiveMs: 0, executionStartedAt: STARTED } as never, NOW, { mergeBlocker: true } as never,
)).toBe(0);
});
it("keeps the legacy id when no flags are supplied", () => {
expect(getActiveRuntimeMs(
{ column: "in-progress", cumulativeActiveMs: 0, executionStartedAt: STARTED } as never, NOW,
)).toBe(HOUR);
});
});