fix(run-files): refresh patch diffs when scope changes

This commit is contained in:
Bryan Helmkamp 2026-05-09 14:00:34 -04:00
parent 8b7d5bd16c
commit bfb633ba6e
No known key found for this signature in database
2 changed files with 109 additions and 24 deletions

View file

@ -1,4 +1,5 @@
import { afterEach, describe, expect, mock, test } from "bun:test";
import { useRef } from "react";
import TestRenderer, { act } from "react-test-renderer";
import { MemoryRouter, Route, Routes } from "react-router";
@ -10,6 +11,7 @@ const useRunFilesCalls: any[] = [];
const multiFileDiffCalls: any[] = [];
const patchDiffCalls: any[] = [];
let patchDiffMountSeq = 0;
const virtualizerCalls: any[] = [];
const providerCalls: any[] = [];
const mountedRenderers: TestRenderer.ReactTestRenderer[] = [];
@ -20,8 +22,13 @@ mock.module("@pierre/diffs/react", () => ({
return <div data-pierre-multi="true">{props.newFile.name}</div>;
},
PatchDiff: (props: any) => {
patchDiffCalls.push(props);
return <div data-pierre-patch="true">{props.patch}</div>;
const mountId = useRef(++patchDiffMountSeq);
patchDiffCalls.push({ ...props, mountId: mountId.current });
return (
<div data-pierre-patch="true" data-mount-id={mountId.current}>
{props.patch}
</div>
);
},
Virtualizer: (props: any) => {
virtualizerCalls.push(props);
@ -89,19 +96,46 @@ function makePayload(count: number, source = "sandbox") {
};
}
function makePatchPayload(patch: string) {
return {
data: [
{
change_kind: "modified",
old_file: { name: "docs/live.md", contents: null },
new_file: { name: "docs/live.md", contents: null },
unified_patch: patch,
},
],
source: "sandbox",
meta: {
degraded: false,
degraded_reason: null,
total_changed: 1,
stats: { additions: 1, deletions: 0 },
truncated: false,
to_sha: "abc1234",
to_sha_committed_at: "2026-05-05T12:00:00Z",
},
};
}
function runFilesTree(initialEntry = "/runs/run_1/files") {
return (
<ToastProvider>
<MemoryRouter initialEntries={[initialEntry]}>
<Routes>
<Route path="/runs/:id/files" element={<RunFiles />} />
</Routes>
</MemoryRouter>
</ToastProvider>
);
}
function renderRunFiles(initialEntry = "/runs/run_1/files") {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
let renderer: TestRenderer.ReactTestRenderer | undefined;
act(() => {
renderer = TestRenderer.create(
<ToastProvider>
<MemoryRouter initialEntries={[initialEntry]}>
<Routes>
<Route path="/runs/:id/files" element={<RunFiles />} />
</Routes>
</MemoryRouter>
</ToastProvider>,
);
renderer = TestRenderer.create(runFilesTree(initialEntry));
});
mountedRenderers.push(renderer!);
return renderer!;
@ -118,6 +152,7 @@ describe("RunFiles rendering", () => {
currentRunStatus = "succeeded";
multiFileDiffCalls.length = 0;
patchDiffCalls.length = 0;
patchDiffMountSeq = 0;
virtualizerCalls.length = 0;
providerCalls.length = 0;
useRunFilesCalls.length = 0;
@ -147,7 +182,7 @@ describe("RunFiles rendering", () => {
const sandboxRenderer = renderRunFiles();
expect(
sandboxRenderer.root.findAllByProps({ "aria-label": "Diff scope" }),
).toHaveLength(1);
).not.toHaveLength(0);
act(() => sandboxRenderer.unmount());
mountedRenderers.pop();
@ -176,15 +211,7 @@ describe("RunFiles rendering", () => {
const firstNewKey = multiFileDiffCalls[0].newFile.cacheKey;
act(() => {
renderer.update(
<ToastProvider>
<MemoryRouter initialEntries={["/runs/run_1/files"]}>
<Routes>
<Route path="/runs/:id/files" element={<RunFiles />} />
</Routes>
</MemoryRouter>
</ToastProvider>,
);
renderer.update(runFilesTree());
});
const lastCall = multiFileDiffCalls[multiFileDiffCalls.length - 1];
@ -194,4 +221,24 @@ describe("RunFiles rendering", () => {
expect(firstNewKey).toContain("fabro-run-file:run_1:abc1234:new:src/file-0.ts:");
expect(lastCall.options).not.toHaveProperty("theme");
});
test("remounts patch diffs when the patch changes for the same file", () => {
currentFilesPayload = makePatchPayload(
"diff --git a/docs/live.md b/docs/live.md\n@@ -1 +1 @@\n+committed\n",
);
const renderer = renderRunFiles("/runs/run_1/files?scope=all");
const firstMountId = patchDiffCalls[0].mountId;
currentFilesPayload = makePatchPayload(
"diff --git a/docs/live.md b/docs/live.md\n@@ -1,0 +1,2 @@\n+committed\n+uncommitted\n",
);
act(() => {
renderer.update(runFilesTree("/runs/run_1/files?scope=all"));
});
const lastCall = patchDiffCalls[patchDiffCalls.length - 1];
expect(lastCall.patch).toContain("+uncommitted");
expect(lastCall.mountId).not.toBe(firstMountId);
});
});

View file

@ -35,7 +35,7 @@ import {
} from "./run-files/states";
import { useFileKeyboardNav } from "./run-files/keyboard";
import { Toolbar, type DiffStyle } from "./run-files/toolbar";
import { fileCacheKey } from "./run-files/cache-keys";
import { fileCacheKey, stringHash } from "./run-files/cache-keys";
import { VirtualizedDiffList } from "./run-files/virtualized-diff-list";
import { ApiError, extractRequestId } from "../lib/api-client";
import { useRun, useRunFiles } from "../lib/queries";
@ -211,6 +211,33 @@ interface RunFileRowProps {
toSha: string | null | undefined;
}
function fileDiffRenderKey({
file,
index,
scope,
toSha,
}: {
file: ApiFileDiff;
index: number;
scope: RunFileScope;
toSha: string | null | undefined;
}): string {
const display = file.new_file.name || file.old_file.name || `file-${index}`;
const oldContents = file.old_file.contents ?? "";
const newContents = file.new_file.contents ?? "";
const contentFingerprint = file.unified_patch
? `patch:${stringHash(file.unified_patch)}`
: `contents:${stringHash(oldContents)}:${stringHash(newContents)}`;
return [
scope,
toSha ?? "no-sha",
display,
index,
file.change_kind ?? "modified",
contentFingerprint,
].join(":");
}
const RunFileRow = memo(function RunFileRow({
file,
diffStyle,
@ -280,7 +307,13 @@ const RunFileRow = memo(function RunFileRow({
/>
);
} else if (patch) {
body = <PatchDiff patch={patch} options={patchOptions} />;
body = (
<PatchDiff
key={stringHash(patch)}
patch={patch}
options={patchOptions}
/>
);
}
return (
@ -564,7 +597,12 @@ export default function RunFiles() {
(file.new_file.name === hashFile || file.old_file.name === hashFile);
return (
<RunFileRow
key={`${display}-${idx}`}
key={fileDiffRenderKey({
file,
index: idx,
scope: effectiveScope,
toSha: meta.to_sha,
})}
file={file}
diffStyle={diffStyle}
isDeepLinkTarget={isDeepLinkTarget}