diff --git a/packages/core/src/__tests__/postgres/mission-store.pg.test.ts b/packages/core/src/__tests__/postgres/mission-store.pg.test.ts index fc69b85fff..15e2276b85 100644 --- a/packages/core/src/__tests__/postgres/mission-store.pg.test.ts +++ b/packages/core/src/__tests__/postgres/mission-store.pg.test.ts @@ -357,8 +357,8 @@ pgTest("MissionStore (PostgreSQL backend mode)", () => { feature→task invariant. Claim paths now hold the task row lock (lockLiveTaskForClaim) BEFORE the conflict check. This test simulates the first claimant's transaction on a separate connection: it locks the task row with SELECT ... FOR UPDATE and writes the first claimant's - linkage while holding the lock. The store's concurrent link (second claimant) must (a) remain - blocked on the row lock while the first transaction is open (~250ms pending probe) and + linkage while holding the lock. The store's concurrent link (second claimant) must (a) appear + in PostgreSQL's blocking graph while the first transaction is open and (b) after the first transaction commits, reject with the conflicting-feature error instead of overwriting the first claimant's linkage. */ @@ -372,12 +372,8 @@ pgTest("MissionStore (PostgreSQL backend mode)", () => { const task = await h.store().createTask({ description: "contested task" }); const db = h.adminDb(); - // Second claimant's link — fired while the first claimant holds the row lock. let settled = false; - const contestedLink = m.linkFeatureToTask(featureTwo.id, task.id).then( - (value) => { settled = true; return value; }, - (error) => { settled = true; throw error; }, - ); + let contestedLink: ReturnType | undefined; // First claimant's transaction: lock the task row, write its linkage, hold it open. await db.transaction(async (tx) => { @@ -392,9 +388,30 @@ pgTest("MissionStore (PostgreSQL backend mode)", () => { .set({ missionId: mission.id, sliceId: slice.id, updatedAt: new Date().toISOString() }) .where(eq(schema.project.tasks.id, task.id)); - // While the first claimant is uncommitted, the second claimant must still be blocked - // on the task row lock, not settled (success or failure). - await new Promise((resolve) => setTimeout(resolve, 250)); + const holderRows = await tx.execute(sql`SELECT pg_backend_pid() AS pid`) as unknown as Array<{ pid: number }>; + const holderPid = holderRows[0]?.pid; + expect(holderPid).toBeTypeOf("number"); + + // Start the second claimant only after the first claimant owns the row lock. Poll + // PostgreSQL's blocking graph rather than sleeping and inferring lock state from time. + contestedLink = m.linkFeatureToTask(featureTwo.id, task.id).then( + (value) => { settled = true; return value; }, + (error) => { settled = true; throw error; }, + ); + const blockProbeDeadline = Date.now() + 5_000; + let blockedByFirstClaimant = false; + while (!blockedByFirstClaimant && Date.now() < blockProbeDeadline) { + const blockedRows = await tx.execute(sql` + SELECT EXISTS ( + SELECT 1 + FROM pg_stat_activity activity + WHERE ${holderPid} = ANY(pg_blocking_pids(activity.pid)) + ) AS blocked + `) as unknown as Array<{ blocked: boolean }>; + blockedByFirstClaimant = blockedRows[0]?.blocked === true; + if (!blockedByFirstClaimant) await new Promise((resolve) => setTimeout(resolve, 10)); + } + expect(blockedByFirstClaimant).toBe(true); expect(settled).toBe(false); }); diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index 6672e4a3bc..f601397337 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -40,11 +40,7 @@ export default defineConfig({ `missing-exclude` — the guard silently stops holding the ledger and the config in lockstep. Each entry needs a matching row in scripts/lib/test-quarantine.json (same commit, deletion ratchet). */ - exclude: [ - // Wall-clock lock-race assertion that fails under parallel load; rescue needs a real - // block-detection probe (pg_locks / lock-wait), not a longer sleep. Deadline 2026-09-06. - "src/__tests__/postgres/mission-store.pg.test.ts", - ], + exclude: [], setupFiles: [ "./src/__test-utils__/vitest-setup.ts", ], diff --git a/packages/dashboard/app/__tests__/task-detail-column-role-identity.test.ts b/packages/dashboard/app/__tests__/task-detail-column-role-identity.test.ts index d63b2656f7..30b8ea904b 100644 --- a/packages/dashboard/app/__tests__/task-detail-column-role-identity.test.ts +++ b/packages/dashboard/app/__tests__/task-detail-column-role-identity.test.ts @@ -128,17 +128,18 @@ describe("TaskDetailModal resolved column roles are identity-safe", () => { }); /* - The paired completeness check: the guard above is vacuous if no effect reads a role at all. This - fails if a future refactor moves the reconciliation out from under the invariant. + The paired completeness check: the guard above is vacuous if no effect reads a role at all. The + done-tab reconciliation disappeared when Task Detail tabs were consolidated; the PR-tab redirect + remains the destructive role-driven effect this invariant protects. */ - it("still has reconciliation effects reading resolved roles for the guard to protect", () => { + it("still has a reconciliation effect reading a resolved role for the guard to protect", () => { const effects = collectEffects(); const guarded = effects.filter( (effect) => effect.used.has("detailFlagsAreForThisTask") - && ["isReviewColumn", "isDoneColumn"].some((role) => effect.used.has(role)), + && effect.used.has("isReviewColumn"), ); - expect(guarded.length, "expected the PR-tab and done-tab reconciliation effects").toBeGreaterThanOrEqual(2); + expect(guarded.length, "expected the PR-tab reconciliation effect").toBeGreaterThanOrEqual(1); }); }); diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index c8de2cf735..5d3996c06f 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -1205,6 +1205,7 @@ export function TaskDetailContent({ FN-173 makes the duplicate flag acknowledgeable rather than an opaque decision. The actions row only exists for Delete or Archive so ordinary cards without archive access do not leave an empty shell. */ + // FNXC:WorkflowLifecycle 2026-08-31-07:33: DELIBERATE-LITERAL — absent canonical workflow flags require legacy terminal-column fallback semantics. const showNearDuplicateWarning = Boolean(nearDuplicateOf) && workingTask.sourceMetadata?.nearDuplicateDismissed !== true && task.column !== "archived" @@ -1310,6 +1311,7 @@ export function TaskDetailContent({ const isArchivedColumn = isArchivedColumnRole(detailColumnFlags, task.column); const isWipColumn = isWipColumnRole(detailColumnFlags, task.column); const isReviewColumn = isReviewColumnRole(detailColumnFlags, task.column); + // FNXC:WorkflowLifecycle 2026-08-31-07:33: DELIBERATE-LITERAL — absent detail workflow flags require the legacy mutable-column fallback. const isMutableLiveColumn = detailColumnFlags ? detailColumnFlags.complete !== true && detailColumnFlags.archived !== true : task.column !== "done" && task.column !== "archived"; diff --git a/packages/engine/src/__tests__/self-healing-pending-wedge-notification.test.ts b/packages/engine/src/__tests__/self-healing-pending-wedge-notification.test.ts index 558f71a399..fa0bd5fa96 100644 --- a/packages/engine/src/__tests__/self-healing-pending-wedge-notification.test.ts +++ b/packages/engine/src/__tests__/self-healing-pending-wedge-notification.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { EventEmitter } from "node:events"; import type { Settings, Task, TaskStore } from "@fusion/core"; @@ -37,7 +37,13 @@ describe("reconcile pending wedge notifications", () => { getActiveNotificationServiceMock.mockReturnValue({ getWedgeNotificationSettleMs: () => 1_000, completePendingWedgeNotification: completePendingWedgeNotificationMock }); }); + afterEach(() => { + vi.useRealTimers(); + }); + it("selects elapsed markers and audits the completion outcome verbatim", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-08-23T12:00:00.000Z")); const old = new Date(Date.now() - 1_001).toISOString(); const young = new Date(Date.now() - 999).toISOString(); const store = Object.assign(new EventEmitter(), { diff --git a/packages/engine/src/__tests__/worktree-acquisition.test.ts b/packages/engine/src/__tests__/worktree-acquisition.test.ts index 0565289bbd..398c890e18 100644 --- a/packages/engine/src/__tests__/worktree-acquisition.test.ts +++ b/packages/engine/src/__tests__/worktree-acquisition.test.ts @@ -906,29 +906,6 @@ describe("acquireTaskWorktree", () => { expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: freshPath, branch: "operator/branch", branchWriteOrigin: "operator" }); }); - it("#3523 stamps operator provenance when pool acquisition adopts an operator-override branch", async () => { - const overrideTask = { - ...task, - branch: "operator/branch", - branchContext: { branchOverride: { by: "operator", at: "2026-08-28T00:00:00Z", branch: "operator/branch" } }, - } as any; - - const result = await acquireTaskWorktree({ - task: overrideTask, - rootDir: process.cwd(), - store, - settings: { recycleWorktrees: true } as any, - pool: { - acquire: (_taskId: string) => "/tmp/pooled-operator", - prepareForTask: vi.fn().mockResolvedValue({ branch: "operator/branch", worktreePath: "/tmp/pooled-operator", reclaimed: false }), - release: vi.fn(), - } as any, - createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/fn-worktree-fallback", branch: "operator/branch" }), - }); - - expect(result.source).toBe("pool"); - expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: "/tmp/pooled-operator", branch: "operator/branch", branchWriteOrigin: "operator" }); - }); it("#3523 keeps operator provenance when a Fusion-named override is sibling-renamed by a branch collision", async () => { // Greptile P1 (re-review of 1c02ddd0): a bare branch collision renames the diff --git a/packages/engine/src/scheduler.ts b/packages/engine/src/scheduler.ts index 9a00df4b27..461279a011 100644 --- a/packages/engine/src/scheduler.ts +++ b/packages/engine/src/scheduler.ts @@ -601,11 +601,11 @@ export function classifyFileScopeLease( Role questions stay parameterized with literal defaults so callers that cannot resolve a workflow retain legacy board semantics rather than silently dropping every overlap lease on renamed boards. */ - /* DELIBERATE-LITERAL: the `??` fallbacks are the no-resolved-workflow defaults described directly - above — deleting them makes an unresolvable workflow drop every overlap lease, which is strictly - worse than the legacy semantics they preserve. Same posture as isTerminalDependencyColumn. */ + // FNXC:WorkflowLifecycle 2026-08-30-07:27: DELIBERATE-LITERAL — callers without resolved workflow roles require the legacy WIP fallback. const isWipColumn = options?.isWipColumn ?? task.column === "in-progress"; + // FNXC:WorkflowLifecycle 2026-08-30-07:27: DELIBERATE-LITERAL — callers without resolved workflow roles require the legacy review fallback. const isReviewColumn = options?.isReviewColumn ?? task.column === "in-review"; + // FNXC:WorkflowLifecycle 2026-08-30-07:27: DELIBERATE-LITERAL — callers without resolved workflow roles require the legacy terminal fallback. const isTerminalColumn = options?.isTerminalColumn ?? (task.column === "done" || task.column === "archived"); if (isTerminalColumn || task.deletedAt) { diff --git a/packages/engine/src/worktree/worktree-acquisition.ts b/packages/engine/src/worktree/worktree-acquisition.ts index 45cc2c59fd..3e9252384c 100644 --- a/packages/engine/src/worktree/worktree-acquisition.ts +++ b/packages/engine/src/worktree/worktree-acquisition.ts @@ -363,7 +363,7 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro * FNXC:BranchWriteOrigin 2026-08-28-10:12: * #3523 review (Greptile P1): hardcoded `branchWriteOrigin: "engine"` stamps on branch-value * writes bypassed the classifier below, so operator-provided branches reaching fresh-create, - * warm-reuse, pool-acquire, or merge-reuse persisted as Fusion-owned and became eligible for + * warm-reuse, pinned reuse, or merge-reuse persisted as Fusion-owned and became eligible for * engine cleanup of branches the operator supplied. Every branch-value write must derive its * origin through `classifyTaskBranchOrigin`; null clears keep explicit stamps because they * attribute the actor and cannot claim branch ownership. diff --git a/packages/engine/vitest.config.ts b/packages/engine/vitest.config.ts index b3f9784a45..47814e8739 100644 --- a/packages/engine/vitest.config.ts +++ b/packages/engine/vitest.config.ts @@ -330,8 +330,6 @@ export default defineConfig({ name: "engine-default", include: ["src/**/*.test.ts"], exclude: [ - // FNXC:WedgeNotificationFlake 2026-08-23-22:35 — quarantined (2nd sighting); see scripts/lib/test-quarantine.json for the evidence and the 2026-09-06 deletion deadline. - "src/__tests__/self-healing-pending-wedge-notification.test.ts", "src/__tests__/reliability-interactions/**/*.test.ts", // FNXC:PipelineSmoke 2026-08-23-14:52: FN-182's whole-pipeline fixture is opt-in, never a default or gate test. "src/__tests__/pipeline-smoke/**/*.test.ts", diff --git a/scripts/lib/lifecycle-column-census-baseline.json b/scripts/lib/lifecycle-column-census-baseline.json index b4ec842209..44e2142e04 100644 --- a/scripts/lib/lifecycle-column-census-baseline.json +++ b/scripts/lib/lifecycle-column-census-baseline.json @@ -1,12 +1,12 @@ { "generatedFrom": "node scripts/lifecycle-column-census.mjs --strict --update-baseline", - "byFile": { - "packages/engine/src/scheduler.ts": 3, - "packages/dashboard/app/components/TaskDetailModal.tsx": 2 - }, + "byFile": {}, "deliberateByFile": { "packages/core/src/task-store/async/async-comments-attachments.ts\u0000archived": 6, + "packages/engine/src/scheduler.ts\u0000archived": 3, + "packages/engine/src/scheduler.ts\u0000done": 3, "packages/engine/src/scheduler.ts\u0000in-progress": 3, + "packages/engine/src/scheduler.ts\u0000in-review": 3, "packages/engine/src/self-healing.ts\u0000done": 3, "packages/engine/src/self-healing.ts\u0000in-review": 3, "packages/core/src/agents/live-agent-count.ts\u0000in-review": 2, @@ -21,15 +21,14 @@ "packages/core/src/task-store/task-id-integrity.ts\u0000archived": 2, "packages/core/src/workflows/workflow-lifecycle-direction.ts\u0000archived": 2, "packages/dashboard/app/components/TaskContextMenu.tsx\u0000done": 2, + "packages/dashboard/app/components/TaskDetailModal.tsx\u0000archived": 2, + "packages/dashboard/app/components/TaskDetailModal.tsx\u0000done": 2, "packages/dashboard/app/hooks/useTaskDiffStats.ts\u0000done": 2, "packages/dashboard/src/reliability-metrics.ts\u0000in-review": 2, "packages/engine/src/cli-agent/state-machine.ts\u0000done": 2, "packages/engine/src/errors/usage-limit-detector.ts\u0000archived": 2, "packages/engine/src/errors/usage-limit-detector.ts\u0000done": 2, "packages/engine/src/merge/auto-merge-finalization.ts\u0000done": 2, - "packages/engine/src/scheduler.ts\u0000archived": 2, - "packages/engine/src/scheduler.ts\u0000done": 2, - "packages/engine/src/scheduler.ts\u0000in-review": 2, "packages/engine/src/triage.ts\u0000triage": 2, "plugins/fusion-plugin-reports/src/store/report-store.ts\u0000archived": 2, "packages/cli/src/commands/task.ts\u0000archived": 1, @@ -85,8 +84,6 @@ "packages/dashboard/app/components/TaskContextMenu.tsx\u0000archived": 1, "packages/dashboard/app/components/TaskContextMenu.tsx\u0000in-review": 1, "packages/dashboard/app/components/TaskContextMenu.tsx\u0000triage": 1, - "packages/dashboard/app/components/TaskDetailModal.tsx\u0000archived": 1, - "packages/dashboard/app/components/TaskDetailModal.tsx\u0000done": 1, "packages/dashboard/app/components/TaskDetailModal.tsx\u0000in-progress": 1, "packages/dashboard/app/components/TaskDetailModal.tsx\u0000in-review": 1, "packages/dashboard/app/hooks/useBlockerFanout.ts\u0000archived": 1, diff --git a/scripts/lib/test-quarantine.json b/scripts/lib/test-quarantine.json index 51e462dca0..19962ce435 100644 --- a/scripts/lib/test-quarantine.json +++ b/scripts/lib/test-quarantine.json @@ -1,16 +1,6 @@ { "$comment": "Flaky-test quarantine ledger (deletion ratchet \u2014 see AGENTS.md 'Flaky tests: quarantine on sight' and docs/testing.md 'Quarantine ledger and the deletion ratchet'). A test observed failing without a corresponding real bug is quarantined ON SIGHT: add an entry here AND a matching one-line `exclude` entry in that package's vitest config, in the same commit. Every entry needs a non-empty `reason` (link the failing run) and a `quarantinedAt` date \u2014 the entry expires 14 days later, at which point the test file is DELETED unless someone rescues it with evidence it catches real regressions plus a root-cause fix (never appeasement). scripts/check-quarantine-ledger.mjs mechanically verifies this ledger and concrete `exclude:` array entries stay in lockstep.", "entries": [ - { - "file": "packages/engine/src/__tests__/self-healing-pending-wedge-notification.test.ts", - "reason": "SECOND sighting of a suite-only flake: 'reconcile pending wedge notifications > selects elapsed markers and audits the completion outcome verbatim' fails ONLY in a full engine-suite run (expected 1 selected marker, saw 2) and passes deterministically in isolation and in small multi-file runs. First sighting was recorded in docs/solutions/test-failures/suite-only-flakes-observed-register.md at ea48af7ab5; observed again on the full run at a97aa84a20. Reads as cross-test state bleed into the reconciler's marker selection, not a timing wait \u2014 no timeout, retry, or assertion was changed. Per AGENTS.md the second sighting is an on-sight quarantine with no further discretion.", - "quarantinedAt": "2026-08-23" - }, - { - "file": "packages/core/src/__tests__/postgres/mission-store.pg.test.ts", - "reason": "Load-sensitive wall-clock race: 'serializes concurrent claims on the same task (Greptile P1 race)' holds a transaction open, sleeps 250ms, then asserts the competing claim has not settled. Under parallel machine load the second claimant settles (its `settled` flag flips on EITHER success or failure), so the assertion fails with no serialization bug. Observed twice on 2026-08-23 while three dashboard suites ran concurrently (a subagent's 180-file core run, then a full-core run plus an isolated run); passes 65/65 three consecutive times once the machine is calmer. An A/B against the projectTableNames registry change looked causal on one run and did NOT reproduce - the coincidence is the flake itself. Rescue needs a deterministic block-detection signal (e.g. pg_locks / a lock-wait probe) instead of a sleep, not a longer sleep.", - "quarantinedAt": "2026-08-23" - }, { "file": "packages/engine/src/__tests__/reliability-interactions/merge-node-paused-abort-retryable.test.ts", "reason": "SECOND sighting of the sequence-only reliability failure recorded in docs/solutions/test-failures/suite-only-flakes-observed-register.md entry 14. FN-249's selected engine-reliability file run failed 13 sibling recovery assertions after prior cases, while the exact benign merge-abort subject passed immediately in isolation. The user-cancellation path is not enabled by this fixture; no timeout, retry, or assertion was changed. Quarantined under AGENTS.md's mandatory deletion ratchet until a root-cause repair proves the file's recovery coverage can be rescued.",