diff --git a/packages/server/src/server/workspace-git-service.test.ts b/packages/server/src/server/workspace-git-service.test.ts index 26ed7f0684..a7a3b49f06 100644 --- a/packages/server/src/server/workspace-git-service.test.ts +++ b/packages/server/src/server/workspace-git-service.test.ts @@ -257,7 +257,7 @@ function createGitHubServiceStub(): ForgeService { interface CreateServiceTestOptions { subscribe?: ReturnType; getCheckoutStatus?: ReturnType; - getCheckoutSnapshotFacts?: ReturnType; + getCheckoutSnapshotFacts?: (cwd: string) => Promise; getCheckoutShortstat?: ReturnType; getCheckoutWorktreeState?: ReturnType; getPullRequestStatus?: ReturnType; @@ -2045,6 +2045,108 @@ describe("WorkspaceGitServiceImpl with workspace folders that no longer exist", await service.dispose(); }); + test("a refresh crossing suspension preserves the cached snapshot and drops queued work", async () => { + const workspace = await createTemporaryFolder(); + const git = createFolderBackedGit(); + const started = createDeferred(); + const release = createDeferred(); + let holdNextRead = false; + let factsReads = 0; + const service = createService({ + ...git.deps, + getCheckoutSnapshotFacts: async (cwd: string) => { + factsReads++; + if (holdNextRead) { + holdNextRead = false; + started.resolve(); + await release.promise; + } + return git.deps.getCheckoutSnapshotFacts(cwd); + }, + }); + const updates: WorkspaceGitRuntimeSnapshot[] = []; + const subscription = service.registerWorkspace({ cwd: workspace }, (snapshot) => + updates.push(snapshot), + ); + await waitForObservationToSettle(service, 1); + const cached = await service.getSnapshot(workspace); + holdNextRead = true; + const active = service.getSnapshot(workspace, { + force: true, + includeForge: false, + reason: "overlapping-refresh", + }); + await started.promise; + const queued = service.getSnapshot(workspace, { + force: true, + includeForge: true, + reason: "queued-refresh", + }); + await rm(workspace, { recursive: true, force: true }); + await vi.advanceTimersByTimeAsync(5_000); + await settle(() => expect(service.getMetrics().repositoryWorkspaceLinkCount).toBe(0)); + const readsAtSuspension = factsReads; + const updatesAtSuspension = updates.length; + release.resolve(); + expect(await active).toEqual(cached); + expect(await queued).toEqual(cached); + expect(await service.getSnapshot(workspace)).toEqual(cached); + expect(updates).toHaveLength(updatesAtSuspension); + expect(factsReads).toBe(readsAtSuspension); + subscription.unsubscribe(); + await service.dispose(); + }); + + test("restoration settles a suspended refresh before reloading facts", async () => { + const workspace = await createTemporaryFolder(); + const git = createFolderBackedGit(); + const staleFacts = createDeferred(); + const started = createDeferred(); + let holdNextRead = false; + const service = createService({ + ...git.deps, + getCheckoutSnapshotFacts: async (cwd: string) => { + if (holdNextRead) { + holdNextRead = false; + started.resolve(); + return staleFacts.promise; + } + return git.deps.getCheckoutSnapshotFacts(cwd); + }, + }); + const updates: WorkspaceGitRuntimeSnapshot[] = []; + const subscription = service.registerWorkspace({ cwd: workspace }, (snapshot) => + updates.push(snapshot), + ); + await waitForObservationToSettle(service, 1); + const cached = await service.getSnapshot(workspace); + holdNextRead = true; + const active = service.getSnapshot(workspace, { + force: true, + includeForge: false, + reason: "overlapping-refresh", + }); + await started.promise; + await rm(workspace, { recursive: true, force: true }); + await vi.advanceTimersByTimeAsync(5_000); + await settle(() => expect(service.getMetrics().repositoryWorkspaceLinkCount).toBe(0)); + const updatesAtSuspension = updates.length; + await mkdir(workspace); + const restored = service.getSnapshot(workspace, { + force: true, + includeForge: false, + reason: "folder-returned", + }); + await vi.advanceTimersByTimeAsync(0); + staleFacts.resolve({ isGit: false }); + expect(await active).toEqual(cached); + expect((await restored).git.isGit).toBe(true); + expect(updates.slice(updatesAtSuspension).every((snapshot) => snapshot.git.isGit)).toBe(true); + await waitForObservationToSettle(service, 1); + subscription.unsubscribe(); + await service.dispose(); + }); + test("workspaceFolderExists reports a missing folder and a path under a file as missing", async () => { const folder = await createTemporaryFolder(); const file = join(folder, "file.txt"); diff --git a/packages/server/src/server/workspace-git-service.ts b/packages/server/src/server/workspace-git-service.ts index 47a36a2b04..26a7f16419 100644 --- a/packages/server/src/server/workspace-git-service.ts +++ b/packages/server/src/server/workspace-git-service.ts @@ -1671,6 +1671,9 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { return; } target.folderMissing = true; + if (target.refreshState.status === "in-flight") { + target.refreshState.queued = null; + } this.detachWorkspaceFromRepoTarget(target); target.repoGitRoot = null; this.stopWorkspaceObservation(target); @@ -1718,6 +1721,9 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { return; } await target.observationSetupPromise; + if (target.refreshState.status === "in-flight") { + await target.refreshState.promise.catch(() => undefined); + } if (!this.isSuspendedWorkspaceTarget(target)) { return; } @@ -3328,11 +3334,19 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { try { const runRefreshGitCommand = createRunGitCommand(`workspace-refresh:${request.reason}`); const admittedSnapshot = await this.workspaceRefreshLimit(() => { - if (target.closed || this.workspaceTargets.get(target.cwd) !== target) { + if ( + target.closed || + target.folderMissing || + this.workspaceTargets.get(target.cwd) !== target + ) { return null; } return this.refreshSnapshot(target, request, runRefreshGitCommand); }); + if (target.folderMissing) { + failure = null; + break; + } if (!admittedSnapshot) { break; } @@ -3346,6 +3360,10 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { failure = { error }; } + if (target.folderMissing) { + failure = null; + break; + } if (this.disposed || target.closed || this.workspaceTargets.get(target.cwd) !== target) { break; } @@ -3641,7 +3659,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { snapshot: WorkspaceGitRuntimeSnapshot, options?: { forceEmit?: boolean }, ): void { - if (target.closed || this.workspaceTargets.get(target.cwd) !== target) { + if (target.closed || target.folderMissing || this.workspaceTargets.get(target.cwd) !== target) { return; } target.latestSnapshot = snapshot;