test(engine): bound the inert-sync-resolution class on a live store (#2794)

## What

One new live-PostgreSQL E2E suite, 3 tests. **No production file is
touched** — evidence, per the E2E worker's remit. Closes the series:
#2789 (scheduler), #2791 (planner lanes), #2792 (custom fields), #2793
(terminal node).


`packages/engine/src/__tests__/workflow-sync-selection-blast-radius-live-e2e.pg.test.ts`

## Why this one is different

The four PRs above each proved a site broken because it resolved a
task's workflow synchronously. Read together they invite a conclusion
that is **false and would be expensive**: that every synchronous
consumer of the workflow selection is inert.

Most are not. The difference is one line of shape:

```ts
// GUARDED (correct)
store.getTaskWorkflowSelectionAsync
  ? await store.getTaskWorkflowSelectionAsync(id)
  : store.getTaskWorkflowSelection(id)

// UNGUARDED (inert)
store.resolveTaskWorkflowIrSync(id)
```

The real PostgreSQL store **does** implement the async reader, so every
guarded site takes the async arm and resolves the card's own workflow.
Only the sync IR helper — which has no async arm to fall to — is stuck
with the default.

Observed on one live store, one persisted workflow, one task:

```
hasAsyncReader   = function
SYNC  selection  = undefined
ASYNC selection  = { workflowId: "WF-001", stepIds: [] }
EFFECTIVE planReviewMaxRevisions = 9   <- the custom workflow's declared default
```

## The point

"The ternary saves them" is an inference from reading, and the whole
premise of this program is that reading is what let the class survive in
the first place. The guarded sites are exactly the ones a fleet worker
would otherwise "fix": converting a correct site costs review time,
risks behaviour, and produces a diff that looks like progress. This
makes the bound checkable in the same lane as the defects.

Guarded call sites (correct today): `workflow-settings-resolver.ts`,
`workflow-ir-resolver.ts`, `executor.ts`,
`workflow-graph-task-runner.ts`, `workflow-task-runtime.ts`, and
`board-workflows.ts` in the dashboard.

## The allow-listed family is now closed

| site | status |
|---|---|
| `scheduler.ts` | proven broken — #2789 |
| `replan-target.ts` | proven broken — #2791 |
| `task-store-helpers.ts` | proven broken — #2792 |
| `branch-and-pr-entities.ts` | proven broken — #2793 |
| `workflow-task-create-ops.ts` | **legitimately correct** — creation
runs before any selection exists, so the default IR is the right answer
|
| `lifecycle-ops.ts` | **NOT proven, stated as such** |

`lifecycle-ops.ts`'s stale-transition-pending recovery re-runs plugin
column-transition hooks against the sync IR. Driving it needs a
registered plugin hook plus a crash-simulated marker; I did not build
that harness and I am not substituting a unit test for it. Named in the
file so it is not mistaken for covered.

## Evidence discipline

- **Observed state.** Both readers called on one live store against one
persisted workflow, plus a real resolved settings value — not a spy on
which arm ran.
- **The settings default is `9`**, deliberately not the builtin's, so
the value can only have come from this workflow.
- **Mutation-verified.** Rewriting the guarded consumer to call the sync
reader directly fails **exactly** the bound arm; the two structural arms
are correctly unaffected, which is what a bound should do.

## Verification

- new suite — **3/3 passed**, mutation-verified
- full live-PG E2E surface — **136/136 passed** (133 on main + 3; #2791
landed while this branch was in flight)
- `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 10:56:48 -07:00
committed by GitHub
parent 90f6319b79
commit 1496ba9658

View File

@@ -0,0 +1,152 @@
/*
FNXC:WorkflowLifecycleColumns 2026-07-31-01:30 (E2E evidence — BOUNDING the inert-sync-resolution class):
Four PRs in this series (#2789 scheduler, #2791 planner lanes, #2792 custom fields, #2793 terminal
node) each proved a site broken because it resolved a task's workflow synchronously. Read together
they invite a conclusion that is FALSE and would be expensive: that every synchronous consumer of the
workflow selection is inert.
Most are not, and this file is the bound. The difference is one line of shape:
GUARDED (correct) store.getTaskWorkflowSelectionAsync
? await store.getTaskWorkflowSelectionAsync(id)
: store.getTaskWorkflowSelection(id)
UNGUARDED (inert) store.resolveTaskWorkflowIrSync(id)
The real PostgreSQL store DOES implement the async reader, so every guarded site takes the async arm
and resolves the card's own workflow. Only the sync IR helper — which has no async arm to fall to —
is stuck with the default. Five call sites use the guarded shape (`workflow-settings-resolver.ts`,
`workflow-ir-resolver.ts`, `executor.ts`, `workflow-graph-task-runner.ts`, `workflow-task-runtime.ts`,
plus `board-workflows.ts` in the dashboard); six use the unguarded one and are the allow-list.
WHY THIS IS WORTH A FILE RATHER THAN A SENTENCE. "The ternary saves them" is an inference from
reading, and the whole point of this program is that reading is what let the class survive. The
guarded sites are the ones a fleet worker would otherwise "fix" — converting a correct site costs
review time, risks behaviour, and produces a diff that looks like progress. This makes the bound
checkable in the same lane as the defects.
It also closes the allow-listed family: of the six unguarded sites, four are now proven broken
end-to-end (the four PRs above), one is legitimately correct (`workflow-task-create-ops.ts` — creation
runs before any selection exists, so the default IR is the right answer), and one is NOT driven here
and is stated as such rather than substituted for: `lifecycle-ops.ts`'s stale-transition-pending
recovery re-runs plugin column-transition hooks against the sync IR, which needs a registered plugin
hook plus a crash-simulated marker to reach. Unproven, deliberately, and named so it is not mistaken
for covered.
OBSERVED STATE. Both readers called on one live store against one persisted workflow, and a real
resolved settings value — not a spy on which arm ran.
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 "@fusion/core"; // registers the built-in column traits
import { resolveWorkflowIrForTask, type TaskStore } from "@fusion/core";
import { resolveEffectiveSettingsDetailed } from "../../../core/src/workflow-settings-resolver.js";
import {
pgDescribe,
createSharedPgTaskStoreTestHarness,
type SharedPgTaskStoreHarness,
} from "../../../core/src/__test-utils__/pg-test-harness.js";
import { RENAMED_VOCAB, lifecycleIr } from "./_workflow-vocabulary-fixture.js";
/** A declared setting whose default is deliberately not the builtin's, so "9" can only come from
* this workflow. */
const MAX_REVISIONS = 9;
pgDescribe("the sync-selection defect class, bounded on a live store", () => {
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
prefix: "fusion_sync_bound",
});
beforeAll(h.beforeAll);
afterAll(h.afterAll);
beforeEach(async () => { await h.beforeEach(); });
afterEach(async () => { await h.afterEach(); });
/** A task bound to a renamed workflow that also declares a settings default. */
async function taskOnCustomWorkflow(store: TaskStore, key: string): Promise<{ taskId: string; workflowId: string }> {
const ir = {
...lifecycleIr(RENAMED_VOCAB, `custom:${key}`),
settings: [{ id: "planReviewMaxRevisions", name: "Max revisions", type: "number", default: MAX_REVISIONS }],
};
const created = await store.createWorkflowDefinition({
name: `Bound ${key}`,
kind: "workflow",
ir,
} as never);
const workflowId = (created as { id: string }).id;
const task = await store.createTask({ description: `bound probe ${key}` });
await store.writeTaskWorkflowSelection(task.id, workflowId, []);
store.taskCache.delete(task.id);
return { taskId: task.id, workflowId };
}
it("the store implements BOTH readers, and only the sync one is blind", async () => {
/*
The pivot the whole class turns on, stated once at the seam. The sync reader is a cutover stub;
the async reader is real. Everything else in this file follows from which of the two a call site
can reach.
*/
const store = h.store();
const { taskId, workflowId } = await taskOnCustomWorkflow(store, "wf-readers");
expect(typeof store.getTaskWorkflowSelectionAsync).toBe("function");
expect(await store.getTaskWorkflowSelectionAsync(taskId)).toMatchObject({ workflowId });
expect(store.getTaskWorkflowSelection(taskId)).toBeUndefined();
});
it("BOUND — a GUARDED consumer resolves the card's own workflow settings", async () => {
/*
`resolveEffectiveSettingsDetailed` is one of the guarded sites: it prefers the async reader and
only falls to the sync one if the store has none. Against the real store it therefore reads THIS
workflow's declaration, not the builtin's.
This is the assertion that makes the bound checkable. If someone "converts" a guarded site and
this value stops being the workflow's own, the conversion broke something that worked.
*/
const store = h.store();
const { taskId } = await taskOnCustomWorkflow(store, "wf-guarded");
const effective = await resolveEffectiveSettingsDetailed(store as never, { id: taskId } as never);
expect(effective.effective?.planReviewMaxRevisions).toBe(MAX_REVISIONS);
});
it("CHARACTERIZATION — the UNGUARDED sync IR helper still answers with the default board", async () => {
/*
The contrast, on the same store and the same task, so the difference is attributable to the call
shape and nothing else. The async resolver names this workflow's own hold column; the sync helper
names the builtin's.
*/
const store = h.store();
const { taskId } = await taskOnCustomWorkflow(store, "wf-unguarded");
const asyncIr = await resolveWorkflowIrForTask(store, taskId);
const syncIr = store.resolveTaskWorkflowIrSync(taskId);
const asyncColumns = (asyncIr as { columns?: { id: string }[] }).columns?.map((c) => c.id) ?? [];
const syncColumns = (syncIr as { columns?: { id: string }[] }).columns?.map((c) => c.id) ?? [];
expect(asyncColumns).toContain(RENAMED_VOCAB.hold);
/*
FNXC:WorkflowLifecycleColumns 2026-07-31-04:20 (PR #2794 review — greptile):
POSITIVE first. `not.toContain(RENAMED_VOCAB.hold)` alone is satisfied by an empty, malformed, or
entirely unrelated IR — anything that merely lacks `backlog` — so it could stay green while
establishing nothing about the stated default-board behaviour. Naming the columns the sync helper
IS expected to return makes the claim "it answered with the DEFAULT board" rather than the much
weaker "it did not answer with this one".
`todo` and `in-progress` are the post-U11 default lineage's own ids, asserted here rather than
imported so this case fails loudly if that lineage changes instead of silently widening.
*/
expect(syncColumns).toContain("todo");
expect(syncColumns).toContain("in-progress");
expect(syncColumns).not.toContain(RENAMED_VOCAB.hold);
});
});