Fix workspace diff stat watch refresh

This commit is contained in:
Mohamed Boudra
2026-04-20 00:36:54 +07:00
parent 4a5dd986e1
commit 86b83c3366
2 changed files with 65 additions and 10 deletions
@@ -731,9 +731,54 @@ describe("WorkspaceGitServiceImpl", () => {
watchCallbacks[0]?.(); watchCallbacks[0]?.();
expect(listener).toHaveBeenCalledTimes(1); expect(listener).toHaveBeenCalledTimes(1);
expect(refreshSpy).toHaveBeenCalledWith("/tmp/repo"); expect(refreshSpy).toHaveBeenCalledWith("/tmp/repo", {
force: true,
reason: "working-tree-watch",
});
subscription.unsubscribe(); subscription.unsubscribe();
service.dispose(); service.dispose();
}); });
test("working tree changes force a fresh diff stat for workspace subscribers", async () => {
const watchCallbacks: Array<{ path: string; callback: () => void }> = [];
const watch = vi.fn(
(watchPath: string, _options: { recursive: boolean }, callback: () => void) => {
watchCallbacks.push({ path: watchPath, callback });
return createWatcher() as any;
},
);
const getCheckoutShortstat = vi
.fn()
.mockResolvedValueOnce({ additions: 1, deletions: 0 })
.mockResolvedValueOnce({ additions: 8, deletions: 3 });
const service = createService({ getCheckoutShortstat, watch });
const workspaceListener = vi.fn();
const workspaceSubscription = await service.subscribe({ cwd: "/tmp/repo" }, workspaceListener);
const diffSubscription = await service.requestWorkingTreeWatch("/tmp/repo", vi.fn());
expect(workspaceSubscription.initial.git.diffStat).toEqual({ additions: 1, deletions: 0 });
const repoRootWatch = watchCallbacks.find((entry) => entry.path === "/tmp/repo");
expect(repoRootWatch).toBeDefined();
repoRootWatch?.callback();
await vi.advanceTimersByTimeAsync(500);
await flushPromises();
expect(getCheckoutShortstat).toHaveBeenLastCalledWith(
"/tmp/repo",
{ paseoHome: "/tmp/paseo-test" },
{ force: true },
);
expect(workspaceListener).toHaveBeenCalledWith(
createSnapshot("/tmp/repo", {
git: { diffStat: { additions: 8, deletions: 3 } },
}),
);
diffSubscription.unsubscribe();
workspaceSubscription.unsubscribe();
service.dispose();
});
}); });
@@ -784,7 +784,10 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
const missingRepoCoverage = repoRoot === null || !hasRecursiveRepoCoverage; const missingRepoCoverage = repoRoot === null || !hasRecursiveRepoCoverage;
if (target.watchers.length === 0 || missingRepoCoverage) { if (target.watchers.length === 0 || missingRepoCoverage) {
target.fallbackRefreshInterval = setInterval(() => { target.fallbackRefreshInterval = setInterval(() => {
this.scheduleWorkspaceRefresh(cwd); this.scheduleWorkspaceRefresh(cwd, {
force: true,
reason: "working-tree-watch-fallback",
});
for (const listener of target.listeners) { for (const listener of target.listeners) {
listener(); listener();
} }
@@ -894,7 +897,10 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
void this.runRepoFetch(repoTarget); void this.runRepoFetch(repoTarget);
} }
private scheduleWorkspaceRefresh(targetOrCwd: WorkspaceGitTarget | string): void { private scheduleWorkspaceRefresh(
targetOrCwd: WorkspaceGitTarget | string,
options?: { force?: boolean; reason?: string },
): void {
const target = const target =
typeof targetOrCwd === "string" typeof targetOrCwd === "string"
? this.workspaceTargets.get(normalizeWorkspaceId(targetOrCwd)) ? this.workspaceTargets.get(normalizeWorkspaceId(targetOrCwd))
@@ -910,9 +916,9 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
target.debounceTimer = setTimeout(() => { target.debounceTimer = setTimeout(() => {
target.debounceTimer = null; target.debounceTimer = null;
void this.refreshWorkspaceTarget(target, { void this.refreshWorkspaceTarget(target, {
force: false, force: options?.force === true,
includeGitHub: false, includeGitHub: false,
reason: "watch", reason: options?.reason ?? "watch",
notify: true, notify: true,
}); });
}, WORKSPACE_GIT_WATCH_DEBOUNCE_MS); }, WORKSPACE_GIT_WATCH_DEBOUNCE_MS);
@@ -990,7 +996,10 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
if (process.platform === "linux" && target.repoWatchPath) { if (process.platform === "linux" && target.repoWatchPath) {
void this.refreshLinuxRepoTreeWatchers(target); void this.refreshLinuxRepoTreeWatchers(target);
} }
this.scheduleWorkspaceRefresh(cwd); this.scheduleWorkspaceRefresh(cwd, {
force: true,
reason: "working-tree-watch",
});
for (const listener of target.listeners) { for (const listener of target.listeners) {
listener(); listener();
} }
@@ -1264,7 +1273,8 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
): Promise<WorkspaceGitRuntimeSnapshot> { ): Promise<WorkspaceGitRuntimeSnapshot> {
const now = this.deps.now(); const now = this.deps.now();
target.lastShellOutAtMs = now.getTime(); target.lastShellOutAtMs = now.getTime();
if (request.force) { const forceGitHub = request.force && request.includeGitHub;
if (forceGitHub) {
this.deps.github.invalidate({ cwd: target.cwd }); this.deps.github.invalidate({ cwd: target.cwd });
} }
const snapshot = await loadWorkspaceGitRuntimeSnapshot( const snapshot = await loadWorkspaceGitRuntimeSnapshot(
@@ -1272,7 +1282,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
{ paseoHome: this.paseoHome }, { paseoHome: this.paseoHome },
now, now,
this.deps, this.deps,
{ force: request.force, reason: request.reason }, { force: request.force, forceGitHub, reason: request.reason },
); );
target.latestSnapshotLoadedAtMs = now.getTime(); target.latestSnapshotLoadedAtMs = now.getTime();
return snapshot; return snapshot;
@@ -1430,7 +1440,7 @@ async function loadWorkspaceGitRuntimeSnapshot(
WorkspaceGitServiceDependencies, WorkspaceGitServiceDependencies,
"getCheckoutStatus" | "getCheckoutShortstat" | "getPullRequestStatus" | "github" "getCheckoutStatus" | "getCheckoutShortstat" | "getPullRequestStatus" | "github"
>, >,
options?: { force?: boolean; reason?: string }, options?: { force?: boolean; forceGitHub?: boolean; reason?: string },
): Promise<WorkspaceGitRuntimeSnapshot> { ): Promise<WorkspaceGitRuntimeSnapshot> {
const checkoutStatus = await deps.getCheckoutStatus(cwd, context); const checkoutStatus = await deps.getCheckoutStatus(cwd, context);
if (!checkoutStatus.isGit) { if (!checkoutStatus.isGit) {
@@ -1444,7 +1454,7 @@ async function loadWorkspaceGitRuntimeSnapshot(
remoteUrl: checkoutStatus.remoteUrl, remoteUrl: checkoutStatus.remoteUrl,
now, now,
deps, deps,
force: options?.force, force: options?.forceGitHub,
reason: options?.reason, reason: options?.reason,
}), }),
]); ]);