fix(server): bypass diff cache on working-tree watch refresh

CheckoutDiffManager.refreshTarget was calling workspaceGitService.getCheckoutDiff
without forcing a cache bypass, so watch-fired refreshes within the 15s consumer
TTL returned the pre-mutation cached diff — fingerprint matched, no
checkout_diff_update was emitted, and the sidebar diff appeared frozen while an
agent edited files. Route watch-fired refreshes through the service with
{ force: true, reason: "working-tree-watch" } so they always recompute.
This commit is contained in:
Mohamed Boudra
2026-04-19 23:19:41 +07:00
parent 8165de7b06
commit 7b9636a22d
2 changed files with 60 additions and 7 deletions

View File

@@ -115,6 +115,7 @@ describe("CheckoutDiffManager", () => {
expect(workspaceGitService.getCheckoutDiff).toHaveBeenCalledWith(
"/tmp/repo",
expect.objectContaining({ mode: "uncommitted", includeStructured: true }),
undefined,
);
});
@@ -157,6 +158,49 @@ describe("CheckoutDiffManager", () => {
});
});
test("watch-triggered refresh forces a cache bypass on getCheckoutDiff", async () => {
const getCheckoutDiff = vi
.fn()
.mockResolvedValueOnce({
diff: "",
structured: [{ path: "a.ts", additions: 1, deletions: 0, status: "modified" }],
})
.mockResolvedValueOnce({
diff: "",
structured: [{ path: "b.ts", additions: 2, deletions: 0, status: "modified" }],
});
const { manager, getOnChange } = createManager({
getCheckoutDiffImplementation: getCheckoutDiff,
});
await manager.subscribe(
{
cwd: "/tmp/repo/packages/server",
compare: { mode: "uncommitted" },
},
vi.fn(),
);
expect(getCheckoutDiff).toHaveBeenNthCalledWith(
1,
"/tmp/repo",
expect.objectContaining({ mode: "uncommitted" }),
undefined,
);
const onChange = getOnChange();
onChange?.();
await vi.advanceTimersByTimeAsync(150);
expect(getCheckoutDiff).toHaveBeenCalledTimes(2);
const watchFiredCall = getCheckoutDiff.mock.calls[1];
expect(watchFiredCall[2]).toEqual({
force: true,
reason: expect.stringContaining("working-tree"),
});
});
test("falls back to cwd when the working tree watch returns no repo root", async () => {
const { manager, workspaceGitService } = createManager({ repoRoot: null });
@@ -171,6 +215,7 @@ describe("CheckoutDiffManager", () => {
expect(workspaceGitService.getCheckoutDiff).toHaveBeenCalledWith(
"/tmp/plain",
expect.objectContaining({ mode: "uncommitted", includeStructured: true }),
undefined,
);
});
});

View File

@@ -164,16 +164,22 @@ export class CheckoutDiffManager {
private async computeCheckoutDiffSnapshot(
cwd: string,
compare: CheckoutDiffCompareInput,
options?: { diffCwd?: string },
options?: { diffCwd?: string; force?: boolean; reason?: string },
): Promise<CheckoutDiffSnapshotPayload> {
const diffCwd = options?.diffCwd ?? cwd;
try {
const diffResult = await this.workspaceGitService.getCheckoutDiff(diffCwd, {
mode: compare.mode,
baseRef: compare.baseRef,
ignoreWhitespace: compare.ignoreWhitespace,
includeStructured: true,
});
const diffResult = await this.workspaceGitService.getCheckoutDiff(
diffCwd,
{
mode: compare.mode,
baseRef: compare.baseRef,
ignoreWhitespace: compare.ignoreWhitespace,
includeStructured: true,
},
options?.force
? { force: true, reason: options.reason ?? "checkout-diff-refresh" }
: undefined,
);
const files = [...(diffResult.structured ?? [])];
files.sort((a, b) => {
if (a.path === b.path) return 0;
@@ -204,6 +210,8 @@ export class CheckoutDiffManager {
target.refreshQueued = false;
const snapshot = await this.computeCheckoutDiffSnapshot(target.cwd, target.compare, {
diffCwd: target.diffCwd,
force: true,
reason: "working-tree-watch",
});
target.latestPayload = snapshot;
const fingerprint = JSON.stringify(snapshot);