From 4cd23a8f17b0bbdea09edca245c5ec9ded2f7a0a Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 21:30:59 +0000 Subject: [PATCH] test(server): wait for worktree observation without racing filesystem setup (#8700) Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../src/server/workspace-git-service.test.ts | 109 ++++++++++-------- 1 file changed, 64 insertions(+), 45 deletions(-) diff --git a/packages/server/src/server/workspace-git-service.test.ts b/packages/server/src/server/workspace-git-service.test.ts index a7a3b49f06..744cce2930 100644 --- a/packages/server/src/server/workspace-git-service.test.ts +++ b/packages/server/src/server/workspace-git-service.test.ts @@ -1,6 +1,8 @@ import { createWorkspaceRuntimeStore } from "./workspace-runtime-store/index.js"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { existsSync } from "node:fs"; +import { performance } from "node:perf_hooks"; +import { setTimeout as delay } from "node:timers/promises"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path, { join } from "node:path"; @@ -421,20 +423,20 @@ function linkedWorktreeFacts(gitCommonDir: string) { }); } -const SETTLE_ATTEMPTS = 1_000; - async function settle(assertion: () => void): Promise { - let lastFailure: unknown = null; - for (let attempt = 0; attempt < SETTLE_ATTEMPTS; attempt += 1) { + // Observation setup resolves real filesystem paths even while the polling clock is frozen. + // Give that I/O wall-clock time without advancing the service's polling deadlines. + const deadline = performance.now() + 5_000; + while (true) { try { assertion(); return; } catch (failure) { - lastFailure = failure; + if (performance.now() >= deadline) throw failure; } + await delay(1); await vi.advanceTimersByTimeAsync(0); } - throw lastFailure; } async function waitForRefreshesToSettle(service: WorkspaceGitServiceImpl): Promise { @@ -1743,48 +1745,65 @@ describe("WorkspaceGitServiceImpl with workspace folders that no longer exist", await service.dispose(); }); - test("a deleted worktree leaves its shared repository polling and fetch to the surviving worktree", async () => { - const gitCommonDir = join(await createTemporaryFolder(), ".git"); - const deletedWorktree = await createTemporaryFolder(); - const survivingWorktree = await createTemporaryFolder(); - const git = createFolderBackedGit({ facts: linkedWorktreeFacts(gitCommonDir) }); - let survivingWorktreeEdits = 0; - git.deps.getCheckoutShortstat.mockImplementation(async (cwd: string) => ({ - additions: cwd === survivingWorktree ? ++survivingWorktreeEdits : 1, - deletions: 0, - })); - const service = createService(git.deps); - const deletedSubscription = service.registerWorkspace({ cwd: deletedWorktree }, vi.fn()); - await waitForObservationToSettle(service, 1); - const survivingSubscription = service.registerWorkspace({ cwd: survivingWorktree }, vi.fn()); - await waitForObservationToSettle(service, 2); - - await rm(deletedWorktree, { recursive: true, force: true }); - await vi.advanceTimersByTimeAsync(60_000); - await waitForRefreshesToSettle(service); - const deletedWorktreeCommands = git.gitCommandsIn(deletedWorktree); - - for (let tick = 0; tick < 3; tick += 1) { - const survivingWorktreePolls = git.pollsOf(survivingWorktree); - await vi.advanceTimersByTimeAsync(5_000); - await settle(() => { - expect(git.pollsOf(survivingWorktree)).toBe(survivingWorktreePolls + 1); + test.each([0, 250])( + "a deleted worktree leaves its shared repository polling and fetch to the surviving worktree (watch delay: %ims)", + async (watchDelayMs) => { + const gitCommonDir = join(await createTemporaryFolder(), ".git"); + const deletedWorktree = await createTemporaryFolder(); + const survivingWorktree = await createTemporaryFolder(); + const watchReady = createDeferred(); + const git = createFolderBackedGit({ + facts: linkedWorktreeFacts(gitCommonDir), + heldWatch: { directory: survivingWorktree, release: watchReady.promise }, }); - } - expect(git.gitCommandsIn(deletedWorktree)).toBe(deletedWorktreeCommands); + let survivingWorktreeEdits = 0; + git.deps.getCheckoutShortstat.mockImplementation(async (cwd: string) => ({ + additions: cwd === survivingWorktree ? ++survivingWorktreeEdits : 1, + deletions: 0, + })); + const service = createService(git.deps); + try { + const deletedSubscription = service.registerWorkspace({ cwd: deletedWorktree }, vi.fn()); + await waitForObservationToSettle(service, 1); + const survivingSubscription = service.registerWorkspace( + { cwd: survivingWorktree }, + vi.fn(), + ); + const pollingTime = Date.now(); + void delay(watchDelayMs).then(() => watchReady.resolve()); + await waitForObservationToSettle(service, 2); + expect(Date.now()).toBe(pollingTime); - const fetchesBefore = git.deps.runGitFetch.mock.calls.length; - await vi.advanceTimersByTimeAsync(2 * BACKGROUND_GIT_FETCH_INTERVAL_MS); - expect(git.deps.runGitFetch.mock.calls.slice(fetchesBefore).map(([cwd]) => cwd)).toEqual([ - survivingWorktree, - survivingWorktree, - ]); - expect(git.gitCommandsIn(deletedWorktree)).toBe(deletedWorktreeCommands); + await rm(deletedWorktree, { recursive: true, force: true }); + await vi.advanceTimersByTimeAsync(60_000); + await waitForRefreshesToSettle(service); + const deletedWorktreeCommands = git.gitCommandsIn(deletedWorktree); - deletedSubscription.unsubscribe(); - survivingSubscription.unsubscribe(); - await service.dispose(); - }); + for (let tick = 0; tick < 3; tick += 1) { + const survivingWorktreePolls = git.pollsOf(survivingWorktree); + await vi.advanceTimersByTimeAsync(5_000); + await settle(() => { + expect(git.pollsOf(survivingWorktree)).toBe(survivingWorktreePolls + 1); + }); + } + expect(git.gitCommandsIn(deletedWorktree)).toBe(deletedWorktreeCommands); + + const fetchesBefore = git.deps.runGitFetch.mock.calls.length; + await vi.advanceTimersByTimeAsync(2 * BACKGROUND_GIT_FETCH_INTERVAL_MS); + expect(git.deps.runGitFetch.mock.calls.slice(fetchesBefore).map(([cwd]) => cwd)).toEqual([ + survivingWorktree, + survivingWorktree, + ]); + expect(git.gitCommandsIn(deletedWorktree)).toBe(deletedWorktreeCommands); + + deletedSubscription.unsubscribe(); + survivingSubscription.unsubscribe(); + } finally { + watchReady.resolve(); + await service.dispose(); + } + }, + ); test("deleting every worktree of a repository stops its polling and background fetch", async () => { const gitCommonDir = join(await createTemporaryFolder(), ".git");