diff --git a/apps/server/src/git.test.ts b/apps/server/src/git.test.ts index 323b385edd3c757dc5c268419944e5df12e73e0c..515a763cf727dc19df843e4986c7dc8cdc969608 100644 --- a/apps/server/src/git.test.ts +++ b/apps/server/src/git.test.ts @@ -272,11 +272,12 @@ describe("git integration", () => { await checkoutGitBranch({ cwd: source.path, branch: featureBranch }); const core = new GitCoreService(); - const details = await core.statusDetails(source.path); - - expect(details.branch).toBe(featureBranch); - expect(details.aheadCount).toBe(0); - expect(details.behindCount).toBe(1); + await vi.waitFor(async () => { + const details = await core.statusDetails(source.path); + expect(details.branch).toBe(featureBranch); + expect(details.aheadCount).toBe(0); + expect(details.behindCount).toBe(1); + }); }); it("keeps checkout successful when upstream refresh fails", async () => { @@ -320,7 +321,9 @@ describe("git integration", () => { await expect(core.checkoutBranch({ cwd: source.path, branch: featureBranch })).resolves.toBe( undefined, ); - expect(core.refreshFetchAttempts).toBe(1); + await vi.waitFor(() => { + expect(core.refreshFetchAttempts).toBe(1); + }); expect(await git(source.path, "branch --show-current")).toBe(featureBranch); }); @@ -362,6 +365,9 @@ describe("git integration", () => { const core = new FetchRecordingGitCoreService(); await core.checkoutBranch({ cwd: source.path, branch: featureBranch }); + await vi.waitFor(() => { + expect(core.fetchArgs).not.toBeNull(); + }); expect(await git(source.path, "branch --show-current")).toBe(featureBranch); expect(core.fetchArgs).toEqual([ @@ -373,6 +379,67 @@ describe("git integration", () => { ]); }); + it("returns checkout result before background upstream refresh completes", async () => { + await using remote = await makeTmpDir(); + await using source = await makeTmpDir(); + await git(remote.path, "init --bare"); + + await initRepoWithCommit(source.path); + const defaultBranch = (await listGitBranches({ cwd: source.path })).branches.find( + (branch) => branch.current, + )!.name; + await git(source.path, `remote add origin ${JSON.stringify(remote.path)}`); + await git(source.path, `push -u origin ${defaultBranch}`); + + const featureBranch = "feature/background-refresh"; + await git(source.path, `checkout -b ${featureBranch}`); + await writeFile(path.join(source.path, "feature.txt"), "feature base\n"); + await git(source.path, "add feature.txt"); + await git(source.path, "commit -m 'feature base'"); + await git(source.path, `push -u origin ${featureBranch}`); + await git(source.path, `checkout ${defaultBranch}`); + + class SlowRefreshGitCoreService extends GitCoreService { + fetchStarted = false; + private readonly waitForReleasePromise: Promise; + private releaseWait: () => void = () => undefined; + + constructor() { + super(); + this.waitForReleasePromise = new Promise((resolve) => { + this.releaseWait = resolve; + }); + } + + releaseFetch(): void { + this.releaseWait(); + } + + override async git( + cwd: string, + args: readonly string[], + allowNonZeroExit = false, + ): Promise { + if (args[0] === "fetch") { + this.fetchStarted = true; + await this.waitForReleasePromise; + return; + } + await super.git(cwd, args, allowNonZeroExit); + } + } + + const core = new SlowRefreshGitCoreService(); + await expect(core.checkoutBranch({ cwd: source.path, branch: featureBranch })).resolves.toBe( + undefined, + ); + await vi.waitFor(() => { + expect(core.fetchStarted).toBe(true); + }); + expect(await git(source.path, "branch --show-current")).toBe(featureBranch); + core.releaseFetch(); + }); + it("throws when branch does not exist", async () => { await using tmp = await makeTmpDir(); await initRepoWithCommit(tmp.path); diff --git a/apps/server/src/git.ts b/apps/server/src/git.ts index ea035d9889a15577877f1aa12f2f96dfd3a99b19..1fbd86c7bee1a07cdeea77b3101fa332ac136a91 100644 --- a/apps/server/src/git.ts +++ b/apps/server/src/git.ts @@ -587,11 +587,8 @@ export class GitCoreService { timeoutMs: 10_000, fallbackErrorMessage: "git checkout failed", }); - try { - await this.refreshCheckedOutBranchUpstream(input.cwd); - } catch { - // Best effort: checkout already succeeded, so avoid surfacing refresh failures. - } + // Refresh upstream refs in the background so checkout remains responsive. + void this.refreshCheckedOutBranchUpstream(input.cwd).catch(() => undefined); } async initRepo(input: GitInitInput): Promise {