perf(merger): skip redundant in-merge verification and lockfile-stable installs

Two cuts to wasted work in the merge verification loop:

1. After the in-merge fix agent runs, fingerprint the working tree
   (`git diff HEAD` + `git status --porcelain`, sha256). If the post-fix
   fingerprint matches pre-fix and is non-empty, the agent didn't actually
   change anything — re-running the same failing command can only yield
   the same failure, so log and report the attempt as unsuccessful without
   paying the test/build cost. Empty fingerprints (snapshot tooling failed)
   fall through to the existing re-run path so we never silently swallow a
   real fix.

2. Inside `syncDependenciesForMerge`, hash the active lockfile and compare
   against `node_modules/.fusion-install-marker` (written after each
   successful install). When they match, skip `pnpm install
   --frozen-lockfile` even if `package.json` is staged. Covers the common
   case where `package.json` changes but the lockfile doesn't, and
   amortizes install across auto-recovery re-enqueues that hit the same
   worktree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-06 12:24:57 -07:00
parent 9087239078
commit cd845d39be
11 changed files with 233 additions and 16 deletions

View File

@@ -1,5 +1,5 @@
import type { Task, TaskDetail, Column as ColumnType, TaskCreateInput, TaskPriority } from "@fusion/core";
import { COLUMNS } from "@fusion/core";
import { COLUMNS, DEFAULT_COLUMN, isColumn } from "@fusion/core";
import { Column } from "./Column";
import type { ToastType } from "../hooks/useToast";
import { useState, useMemo, useEffect, useCallback, useRef } from "react";
@@ -188,12 +188,19 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
// Keep per-column array identities stable for unchanged columns so React.memo(Column)
// can skip sibling rerenders during unrelated task updates.
const tasksByColumn = useMemo(() => {
const nextGrouped = Object.fromEntries(
COLUMNS.map((column) => [column, [] as Task[]]),
) as Record<ColumnType, Task[]>;
const nextGrouped: Record<ColumnType, Task[]> = {
triage: [],
todo: [],
"in-progress": [],
"in-review": [],
done: [],
archived: [],
};
for (const task of tasks) {
nextGrouped[task.column].push(task);
const column = isColumn(task.column) ? task.column : DEFAULT_COLUMN;
const bucket = nextGrouped[column] ?? nextGrouped[DEFAULT_COLUMN];
bucket.push(task);
}
const previousGrouped = tasksByColumnCacheRef.current;

View File

@@ -2,7 +2,7 @@ import "./ListView.css";
import { useState, useCallback, useMemo, Fragment, useEffect, useRef } from "react";
import { ArrowUpDown, ArrowUp, ArrowDown, Link, Columns3, EyeOff, Eye, ChevronRight, Zap } from "lucide-react";
import type { Task, TaskDetail, Column, TaskCreateInput, MergeResult } from "@fusion/core";
import { COLUMN_LABELS, COLUMNS, getErrorMessage } from "@fusion/core";
import { COLUMN_LABELS, COLUMNS, DEFAULT_COLUMN, getErrorMessage, isColumn } from "@fusion/core";
import { batchUpdateTaskModels, fetchNodes, fetchTaskDetail } from "../api";
import { TaskDetailContent } from "./TaskDetailModal";
import type { ModelInfo, NodeInfo } from "../api";
@@ -530,7 +530,10 @@ export function ListView({
done: [],
archived: []
};
sorted.forEach(task => groups[task.column].push(task));
sorted.forEach((task) => {
const column = isColumn(task.column) ? task.column : DEFAULT_COLUMN;
groups[column].push(task);
});
return groups;
}, [tasks, searchQuery, sortField, sortDirection, hideDoneTasks, selectedColumn]);

View File

@@ -79,6 +79,26 @@ describe("Board", () => {
}
});
it("falls back malformed task columns to triage instead of crashing", () => {
const malformedTask = {
id: "FN-404",
description: "Malformed",
column: "impossible-column",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
} as unknown as Task;
expect(() => renderBoard({ tasks: [malformedTask] })).not.toThrow();
const triageTasks = JSON.parse(screen.getByTestId("column-triage").getAttribute("data-tasks") || "[]") as Task[];
expect(triageTasks).toHaveLength(1);
expect(triageTasks[0]?.id).toBe("FN-404");
});
it("forwards board-level workflow name lookup to columns", async () => {
renderBoard();

View File

@@ -157,6 +157,21 @@ describe("ListView", () => {
expect(screen.getByText("View options")).toBeDefined();
});
it("falls back malformed task columns to Planning group instead of crashing", () => {
const malformedTask = {
...createMockTask({ id: "FN-404" }),
column: "impossible-column",
} as unknown as Task;
expect(() => renderListView({ tasks: [malformedTask] })).not.toThrow();
expect(screen.getByText("FN-404")).toBeInTheDocument();
const planningSection = screen
.getAllByRole("row")
.find((row) => row.className.includes("list-section-header") && row.textContent?.includes("Planning"));
expect(planningSection?.textContent).toContain("1");
});
it("keeps view options collapsed by default on desktop", () => {
renderListView({}, { openViewOptions: false });

View File

@@ -140,6 +140,22 @@ describe("useTasks", () => {
expect(result.current.tasks[0].id).toBe("FN-001");
});
it("normalizes invalid column values from initial fetch to triage", async () => {
const malformedTask = {
...createMockTask({ id: "FN-099" }),
column: "unknown-column",
} as unknown as Task;
mockFetchTasks.mockResolvedValueOnce([malformedTask]);
const { result } = renderHook(() => useTasks());
await waitFor(() => {
expect(result.current.tasks).toHaveLength(1);
});
expect(result.current.tasks[0].column).toBe("triage");
});
describe("SSE event: task:created", () => {
it("adds new task to the list", async () => {
mockFetchTasks.mockResolvedValueOnce([]);
@@ -158,6 +174,27 @@ describe("useTasks", () => {
expect(result.current.tasks).toHaveLength(1);
expect(result.current.tasks[0].id).toBe("FN-002");
});
it("normalizes invalid column values from SSE created events", async () => {
mockFetchTasks.mockResolvedValueOnce([]);
const { result } = renderHook(() => useTasks());
await waitFor(() => {
expect(MockEventSource.instances).toHaveLength(1);
});
const malformedTask = {
...createMockTask({ id: "FN-003" }),
column: "bad-column",
} as unknown as Task;
act(() => {
MockEventSource.instances[0]._emit("task:created", malformedTask);
});
expect(result.current.tasks).toHaveLength(1);
expect(result.current.tasks[0].column).toBe("triage");
});
});
describe("SSE event: task:moved", () => {

View File

@@ -1,11 +1,13 @@
import { useState, useEffect, useCallback, useRef } from "react";
import type { Task, Column, TaskCreateInput, MergeResult } from "@fusion/core";
import { normalizeColumn } from "@fusion/core";
import * as api from "../api";
import { subscribeSse } from "../sse-bus";
function normalizeTask(task: Task): Task {
return {
...task,
column: normalizeColumn((task as Task & { column?: unknown }).column),
dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
steps: Array.isArray(task.steps) ? task.steps : [],
log: Array.isArray((task as Task & { log?: unknown }).log)
@@ -255,7 +257,7 @@ export function useTasks(options?: UseTasksOptions) {
const normalizedTask = normalizeTask(task);
setTasks((prev) =>
prev.map((t) =>
t.id === normalizedTask.id ? { ...normalizedTask, column: to } : t
t.id === normalizedTask.id ? { ...normalizedTask, column: normalizeColumn(to, normalizedTask.column) } : t
)
);
lastFetchTimeMs.current = Date.now();