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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user