test(engine): third instance of the optional-role-parameter class, intra-file (#2799)

## What

One new live-PostgreSQL E2E suite, 4 tests. **No production file is
touched** — evidence, per the E2E worker's remit. Third measured
instance of the pattern from #2795 and #2798.


`packages/engine/src/__tests__/workflow-planning-continuation-terminal-gap-live-e2e.pg.test.ts`

## The finding — and why it is the sharpest form yet

The converted and unconverted call sites are **in the same file**, and
one is nested inside the other's call tree.

`in-process-runtime.ts` resolves terminal columns through an optional
parameter defaulting to `LEGACY_TERMINAL_PAIR` (`done` + `archived`):

| site | passes the resolved set? |
|---|---|
| `drainDuePlanningContinuations:386` | ✅ `{ terminalColumns }` |
| `selectActionablePlanningContinuations:413` | ❌ nothing |
| `resolvePlanningContinuationCandidate:199` → inner predicate | ❌ not
threaded |

**What breaks.** `selectActionablePlanningContinuations` documents its
own purpose as excluding *"soft-deleted / archived / done tasks so
archive-fallback rows returned by getTask cannot re-enter plan-review
after the card left the board."* On a renamed board its terminal test is
against ids that board does not have, so a card sitting in its
**complete** column is classified `actionable` and re-enters plan-review
— precisely the thing the function exists to prevent, silently, on every
custom board.

**The third site matters too**, because it shows the conversion is not
whole even along the *converted* path:
`resolvePlanningContinuationCandidate` applies the caller's resolved set
to its own terminal test, then delegates to
`isPlanningContinuationTaskDispatchable(task)` without passing it, so
that inner predicate re-tests against the legacy pair. A partially
threaded conversion is indistinguishable from a complete one at every
call site that looks converted.

### The class so far

| seam | call sites passing the resolved answer |
|---|---|
| `shouldHoldActiveFileScopeLease` | 2 of 4 — #2795 |
| `evaluateParkedAgentTaskLink` | 2 of 6 — #2798 |
| `resolvePlanningContinuationCandidate` | **1 of 2**, plus one
unthreaded inner call — this PR |

Verified clean and reported as such: `restart-recovery-coordinator.ts`,
the `isRecoverableMissingWorktreeReviewFailure` family,
`spec-staleness.ts`'s `plannerColumns`, `task-revert.ts`'s
`revertableColumns`, `agent-assignment.ts`'s `activeColumns`. Audited
since this PR was opened and also clean: `surfacing-sweeps.ts`'s
`roleColumn` (resolved internally through the async resolver — not a
caller-supplied parameter at all) and `auto-claim-snapshot.ts`'s role
trio (both `isRunnableAutoClaimCandidate` call sites pass
`rolesByTask`). **The class audit is therefore complete**: four seams
have unconverted callers (#2795, #2798, this PR, #2802); every other
seam is correct.

None of it is visible to the lifecycle-column census: there is no column
literal at any unconverted call site — the literal lives one function
away, where it is correct for an unconverted caller and correctly
annotated.

## Scope, stated honestly

The three behavioural cases are driven end to end with real persisted
rows from a live store and the real exported functions. The **call-site
split is asserted against source text** — driving the drain needs the
runtime's full dependency set, which I did not build — and the audit
case says so rather than dressing it up. It is an alarm in both
directions.

## Mutation-verified

Flipping `LEGACY_TERMINAL_PAIR` from `done` to the renamed complete id:

| case | result |
|---|---|
| CONTROL (default board) | **fails** |
| CHARACTERIZATION (renamed board) | **fails** |
| BOUND (resolved set passed) | passes — correct, the argument overrides
the default |
| AUDIT | passes — correct, it is a source assertion |

## Not done, and why

**No fix.** Threading the resolved set into
`selectActionablePlanningContinuations` changes its signature and every
caller; threading it into the inner predicate changes behaviour along
the already-converted path. Both are decisions for the file's owner. The
differential says exactly what the fix should make true.

## Verification

- new suite — **4/4 passed**, mutation matrix above
- full live-PG E2E surface — **137/137 passed** (133 on main + 4)
- `pnpm lint` — clean

Lane: `.pg.test.ts`, skipped via `pgDescribe` when no PostgreSQL is
reachable, so the merge gate is unaffected. Throwaway per-file database;
never port 4040.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-30 11:15:56 -07:00
committed by GitHub
parent ed0df8b0e2
commit ea0af4826f

View File

@@ -0,0 +1,211 @@
/*
FNXC:WorkflowScheduling 2026-07-31-07:10 (E2E evidence — the optional-role-parameter class, third instance):
Third measured instance of the pattern from #2795 and #2798, and the sharpest form of it: the
converted and unconverted call sites are in the SAME FILE, and one is nested inside the other's call
tree.
`in-process-runtime.ts` resolves terminal columns through an optional parameter defaulting to the
legacy pair (`done` + `archived`). Three internal call sites:
drainDuePlanningContinuations:386 passes { terminalColumns } CONVERTED
selectActionablePlanningContinuations:413 passes NOTHING unconverted
resolvePlanningContinuationCandidate:199 calls the inner dispatchable predicate
without threading its own set unconverted
WHAT BREAKS. `selectActionablePlanningContinuations` documents its own purpose as excluding
"soft-deleted / archived / done tasks so archive-fallback rows returned by getTask cannot re-enter
plan-review after the card left the board". On a renamed board its terminal check is against ids that
board does not have, so a card sitting in its COMPLETE column is classified `actionable` and re-enters
plan-review — precisely the thing the function exists to prevent, silently, on every custom board.
The third site is subtler and worth naming because it shows the conversion is not even whole along
the converted path: `resolvePlanningContinuationCandidate` applies the caller's resolved set to its
own terminal test, then delegates to `isPlanningContinuationTaskDispatchable(task)` WITHOUT passing
it, so that inner predicate re-tests against the legacy pair. A partially threaded conversion is
indistinguishable from a complete one at every call site that looks converted.
WHY THE CENSUS CANNOT SEE ANY OF IT. There is no column literal at the unconverted call sites — the
literal is `LEGACY_TERMINAL_PAIR`, one function away, where it is correct for an unconverted caller
and correctly annotated. Same mechanism as #2795 and #2798; this file adds the intra-file variant.
SCOPE, STATED HONESTLY. The behavioural cases are driven end to end with real persisted rows from a
live PostgreSQL store and the real exported functions. The call-site split is asserted against the
runtime module's SYNTAX (parsed, not string-matched — see the note on that case) because driving the
drain needs the runtime's full dependency set, which I did not build; the audit case says so rather
than dressing it up.
LANE. `.pg.test.ts`, skipped via `pgDescribe` when no PostgreSQL is reachable, so the merge gate is
unaffected. Throwaway per-file database; never port 4040.
*/
import { beforeAll, beforeEach, afterEach, afterAll, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import ts from "typescript";
import "@fusion/core"; // registers the built-in column traits
import type { Task, TaskStore, WorkflowWorkItem } from "@fusion/core";
import {
pgDescribe,
createSharedPgTaskStoreTestHarness,
type SharedPgTaskStoreHarness,
} from "../../../core/src/__test-utils__/pg-test-harness.js";
import {
resolvePlanningContinuationCandidate,
selectActionablePlanningContinuations,
} from "../runtimes/in-process-runtime.js";
import { DEFAULT_VOCAB, RENAMED_VOCAB, lifecycleIr, type Vocabulary } from "./_workflow-vocabulary-fixture.js";
/** A due planning continuation for a task. Only the fields the classifier reads matter. */
function planningItem(taskId: string): WorkflowWorkItem {
return {
id: `WI-${taskId}`,
runId: "RUN-1",
taskId,
nodeId: "plan",
kind: "node",
state: "pending",
attempt: 0,
retryAfter: null,
leaseOwner: null,
leaseExpiresAt: null,
lastError: null,
blockedReason: null,
stableWorkflowRunId: null,
continuationSequence: 1,
waitReason: "planning",
sourceColumn: null,
targetColumn: null,
irHash: null,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
} as WorkflowWorkItem;
}
pgDescribe("planning-continuation terminal columns, measured on a live store", () => {
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
prefix: "fusion_plan_cont",
});
beforeAll(h.beforeAll);
afterAll(h.afterAll);
beforeEach(async () => { await h.beforeEach(); });
afterEach(async () => { await h.afterEach(); });
/** A real persisted card parked in its workflow's COMPLETE column — a card that has left the board. */
async function completedCard(store: TaskStore, v: Vocabulary, key: string): Promise<Task> {
const created = await store.createWorkflowDefinition({
name: `Cont ${key}`,
kind: "workflow",
ir: lifecycleIr(v, `custom:${key}`),
} as never);
const workflowId = (created as { id: string }).id;
const task = await store.createTask({ description: `continuation probe ${key}` });
await store.writeTaskWorkflowSelection(task.id, workflowId, []);
store.taskCache.delete(task.id);
await store.moveTask(task.id, v.complete as never, { recoveryRehome: true } as never);
store.taskCache.delete(task.id);
const row = await store.getTask(task.id);
if (!row) throw new Error("fixture: card not persisted");
return row;
}
it("CONTROL — on the DEFAULT board a completed card is correctly excluded", async () => {
/* The legacy default is right here, which is why the omission went unnoticed. */
const store = h.store();
const card = await completedCard(store, DEFAULT_VOCAB, "wf-default-cont");
expect(card.column).toBe(DEFAULT_VOCAB.complete); // ...which is literally "done"
expect(selectActionablePlanningContinuations([{ item: planningItem(card.id), task: card }])).toEqual([]);
});
it("CHARACTERIZATION — on a RENAMED board a completed card re-enters plan-review", async () => {
/*
The card has left the board: it sits in its workflow's COMPLETE column. The function whose stated
job is to stop exactly this returns it as actionable, because its terminal test is against ids
this board does not contain.
*/
const store = h.store();
const card = await completedCard(store, RENAMED_VOCAB, "wf-renamed-cont");
expect(card.column).toBe(RENAMED_VOCAB.complete);
const actionable = selectActionablePlanningContinuations([{ item: planningItem(card.id), task: card }]);
expect(actionable).toHaveLength(1);
expect(actionable[0]?.task.id).toBe(card.id);
});
it("BOUND — the SAME card is an orphan once the resolved terminal columns are passed", async () => {
/* Attributes the failure above to the call shape and nothing else: identical card, identical
classifier, one extra argument — what the drain's converted call site does. */
const store = h.store();
const card = await completedCard(store, RENAMED_VOCAB, "wf-renamed-resolved");
const resolved = resolvePlanningContinuationCandidate(planningItem(card.id), card, {
terminalColumns: new Set([RENAMED_VOCAB.complete]),
});
expect(resolved.kind).toBe("orphan");
expect(resolved.kind === "orphan" ? resolved.reason : null).toBe("task-terminal");
});
it("AUDIT — one of the two classifier call sites is converted, and the inner predicate is not", async () => {
/*
NOT driven: reaching the drain needs the runtime's full dependency set. Asserted against the
module's SYNTAX and labelled as such.
FNXC:WorkflowScheduling 2026-07-31-10:15 (PR #2799 review — greptile P2):
AST, not string splitting. The first version found call sites by splitting on the callee name and
then reasoning about the text that followed — whitespace, the next `;`, whether the slice started
with a parameter name. A formatting-only change to the runtime could fail this suite or, worse,
silently misclassify a site, which is the failure mode this whole series is about. Matching the
repo's existing precedent for the same problem (`core/.../sync-workflow-ir-callsite-allowlist.test.ts`),
the check now parses the module and inspects real call expressions and their argument lists, so
it depends on the code's structure rather than its layout.
An alarm in both directions, like #2798's: converting the remaining site fails this case, which
is the moment to read the three behavioural cases above and update it deliberately.
*/
const file = join(__dirname, "..", "runtimes", "in-process-runtime.ts");
const sf = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
/** Every call of `name`, as its argument-expression list. Declarations are not call expressions,
* so they are excluded structurally rather than by guessing at their text. */
function callArguments(name: string): ts.NodeArray<ts.Expression>[] {
const found: ts.NodeArray<ts.Expression>[] = [];
const visit = (node: ts.Node) => {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name) {
found.push(node.arguments);
}
ts.forEachChild(node, visit);
};
ts.forEachChild(sf, visit);
return found;
}
/** Does this call pass a resolved terminal-column set? Checked by looking for the property in the
* options-object argument, or an identifier of that name, rather than by substring. */
const passesTerminalColumns = (args: ts.NodeArray<ts.Expression>): boolean =>
args.some((arg) => {
if (ts.isObjectLiteralExpression(arg)) {
return arg.properties.some((prop) => prop.name && ts.isIdentifier(prop.name)
&& prop.name.text === "terminalColumns");
}
return ts.isIdentifier(arg) && arg.text === "terminalColumns";
});
const classifierCalls = callArguments("resolvePlanningContinuationCandidate");
expect(classifierCalls).toHaveLength(2);
expect(classifierCalls.filter(passesTerminalColumns)).toHaveLength(1);
/* The inner predicate, called from the classifier without threading its resolved set: exactly one
call, and it takes only the task. Arity is the property asserted, so a renamed local cannot
spell around it. */
const innerCalls = callArguments("isPlanningContinuationTaskDispatchable");
expect(innerCalls).toHaveLength(1);
expect(innerCalls[0]?.length).toBe(1);
});
});