Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions packages/core/src/__tests__/postgres/mission-store.pg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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<AsyncMissionStore["linkFeatureToTask"]> | undefined;

// First claimant's transaction: lock the task row, write its linkage, hold it open.
await db.transaction(async (tx) => {
Expand All @@ -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);
});

Expand Down
6 changes: 1 addition & 5 deletions packages/core/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
2 changes: 2 additions & 0 deletions packages/dashboard/app/components/TaskDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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(), {
Expand Down
23 changes: 0 additions & 23 deletions packages/engine/src/__tests__/worktree-acquisition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/engine/src/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/engine/src/worktree/worktree-acquisition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions packages/engine/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 6 additions & 9 deletions scripts/lib/lifecycle-column-census-baseline.json
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 0 additions & 10 deletions scripts/lib/test-quarantine.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
Loading