feat(FN-4646): complete Step 4 — prefer landed file metadata in dashboard

Fusion-Task-Id: FN-4646
Fusion-Task-Lineage: f921098e-3407-40fa-984d-720e0c3248c7
This commit is contained in:
Fusion
2026-05-15 13:13:51 -07:00
committed by gsxdsm
parent b6f2e9b4d1
commit 67c2fbc182
4 changed files with 68 additions and 10 deletions

View File

@@ -1404,7 +1404,10 @@ function TaskCardComponent({
);
}
const modifiedCount = task.modifiedFiles?.length;
const landedFallbackCount = task.mergeDetails?.landedFiles?.length;
const modifiedCount = landedFallbackCount && landedFallbackCount > 0
? landedFallbackCount
: task.modifiedFiles?.length;
if (!diffLoading && (modifiedCount ?? 0) > 0) {
return (
<button
@@ -1415,7 +1418,7 @@ function TaskCardComponent({
title="Captured from worktree during execution; may not match the landed diff. Open the task to view the recorded merge."
>
<Folder size={12} />
<span>{modifiedCount} {modifiedCount === 1 ? "file" : "files"} touched during execution</span>
<span>{modifiedCount} {modifiedCount === 1 ? "file" : "files"} {landedFallbackCount ? "in merged commit" : "touched during execution"}</span>
</button>
);
}

View File

@@ -49,9 +49,10 @@ function getStatusLabel(status: "added" | "modified" | "deleted" | "unknown"): s
}
function renderModifiedFilesFallback(
modifiedFiles: string[],
fileList: string[],
isDone: boolean,
mergeDetails?: MergeDetails,
source: "landed" | "execution" = "execution",
) {
return (
<div className="detail-section task-changes-tab">
@@ -72,16 +73,18 @@ function renderModifiedFilesFallback(
)}
<div className="task-changes-state task-changes-state--empty">
<FileCode size={24} />
<p>{modifiedFiles.length} file{modifiedFiles.length === 1 ? "" : "s"} modified during execution.</p>
<p>{fileList.length} file{fileList.length === 1 ? "" : "s"} {source === "landed" ? "in the merged commit" : "modified during execution"}.</p>
<span className="task-changes-state-hint">
{isDone
// FN-4647: done-task fallback must explicitly describe executor-captured scope.
? "These are files captured from the worktree during execution. They may differ from the files that actually landed on main. The lineage-backed diff is unavailable for this task."
? source === "landed"
? "These are files captured from the merged commit metadata. The lineage-backed diff is unavailable for this task."
: "These are files captured from the worktree during execution. They may differ from the files that actually landed on main. The lineage-backed diff is unavailable for this task."
: "The live worktree diff is empty. Showing the last file paths captured during execution — patches unavailable."}
</span>
</div>
<div className="changes-file-list task-changes-file-list--compact">
{modifiedFiles.map((path) => (
{fileList.map((path) => (
<div key={path} className="changes-file-item">
<div className="changes-file-header changes-file-header--static">
<span
@@ -302,8 +305,11 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
if (files.length === 0) {
if (isDone && !isDoneWithCommit) {
if (modifiedFiles && modifiedFiles.length > 0) {
return renderModifiedFilesFallback(modifiedFiles, true, mergeDetails);
const doneFallbackFiles = mergeDetails?.landedFiles && mergeDetails.landedFiles.length > 0
? mergeDetails.landedFiles
: modifiedFiles;
if (doneFallbackFiles && doneFallbackFiles.length > 0) {
return renderModifiedFilesFallback(doneFallbackFiles, true, mergeDetails, mergeDetails?.landedFiles?.length ? "landed" : "execution");
}
const summaryFiles = mergeDetails?.filesChanged;
@@ -326,7 +332,7 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
);
}
if (modifiedFiles && modifiedFiles.length > 0) {
if (!isDone && modifiedFiles && modifiedFiles.length > 0) {
return renderModifiedFilesFallback(modifiedFiles, isDone, mergeDetails);
}

View File

@@ -2444,6 +2444,31 @@ describe("TaskCard", () => {
expect(filesChangedButton).toBeNull();
});
it("prefers landedFiles fallback label for done tasks when lineage stats are unavailable", () => {
const onOpenDetailWithTab = vi.fn();
useTaskDiffStatsMock.mockReturnValue({ stats: null, loading: false });
render(
<TaskCard
task={makeTask({
column: "done",
mergeDetails: { landedFiles: ["a.ts", "b.ts"] },
modifiedFiles: ["a.ts", "b.ts", "c.ts", "d.ts", "e.ts", "f.ts"],
})}
onOpenDetail={noop}
addToast={noop}
onOpenDetailWithTab={onOpenDetailWithTab}
/>,
);
const landedButton = screen.getByRole("button", { name: "2 files in merged commit" });
expect(landedButton).toBeDefined();
fireEvent.click(landedButton);
expect(onOpenDetailWithTab).toHaveBeenCalledTimes(1);
expect(onOpenDetailWithTab.mock.calls[0]?.[1]).toBe("changes");
});
it("shows execution-touched fallback label for done tasks when lineage stats are unavailable", () => {
const onOpenDetailWithTab = vi.fn();
useTaskDiffStatsMock.mockReturnValue({ stats: null, loading: false });

View File

@@ -570,7 +570,31 @@ describe("TaskChangesTab — regression: non-done tasks and done-without-commitS
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-001", undefined, undefined);
});
it("done task without commitSha falls back to modifiedFiles when available", async () => {
it("done task without commitSha prefers landedFiles over stale modifiedFiles", async () => {
mockFetchTaskDiff.mockResolvedValue({ files: [], stats: { filesChanged: 0, additions: 0, deletions: 0 } });
render(
<TaskChangesTab
taskId="FN-001"
worktree={undefined}
column="done"
mergeDetails={{ filesChanged: 0, insertions: 0, deletions: 0, landedFiles: ["packages/cli/src/commands/__tests__/settings.test.ts"] }}
modifiedFiles={["packages/cli/src/commands/__tests__/settings.test.ts", "packages/cli/src/commands/__tests__/task.test.ts"]}
/>,
);
await waitFor(() => {
expect(screen.getByText("1 file in the merged commit.")).toBeTruthy();
});
expect(screen.getByText("These are files captured from the merged commit metadata. The lineage-backed diff is unavailable for this task.")).toBeTruthy();
expect(screen.getByText("packages/cli/src/commands/__tests__/settings.test.ts")).toBeTruthy();
expect(screen.queryByText("packages/cli/src/commands/__tests__/task.test.ts")).toBeNull();
expect(screen.queryByText(/Error loading changes:/)).toBeNull();
expect(screen.queryByText("Files Changed (2)")).toBeNull();
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-001", undefined, undefined);
});
it("done task without commitSha falls back to modifiedFiles when landedFiles unavailable", async () => {
mockFetchTaskDiff.mockResolvedValue({ files: [], stats: { filesChanged: 0, additions: 0, deletions: 0 } });
render(