| ... | @@ -272,11 +272,12 @@ describe("git integration", () => { | ... | @@ -272,11 +272,12 @@ describe("git integration", () => { |
| 272 | | 272 | |
| 273 | await checkoutGitBranch({ cwd: source.path, branch: featureBranch }); | 273 | await checkoutGitBranch({ cwd: source.path, branch: featureBranch }); |
| 274 | const core = new GitCoreService(); | 274 | const core = new GitCoreService(); |
| 275 | const details = await core.statusDetails(source.path); | 275 | await vi.waitFor(async () => { |
| 276 | | 276 | const details = await core.statusDetails(source.path); |
| 277 | expect(details.branch).toBe(featureBranch); | 277 | expect(details.branch).toBe(featureBranch); |
| 278 | expect(details.aheadCount).toBe(0); | 278 | expect(details.aheadCount).toBe(0); |
| 279 | expect(details.behindCount).toBe(1); | 279 | expect(details.behindCount).toBe(1); |
| | 280 | }); |
| 280 | }); | 281 | }); |
| 281 | | 282 | |
| 282 | it("keeps checkout successful when upstream refresh fails", async () => { | 283 | it("keeps checkout successful when upstream refresh fails", async () => { |
| ... | @@ -320,7 +321,9 @@ describe("git integration", () => { | ... | @@ -320,7 +321,9 @@ describe("git integration", () => { |
| 320 | await expect(core.checkoutBranch({ cwd: source.path, branch: featureBranch })).resolves.toBe( | 321 | await expect(core.checkoutBranch({ cwd: source.path, branch: featureBranch })).resolves.toBe( |
| 321 | undefined, | 322 | undefined, |
| 322 | ); | 323 | ); |
| 323 | expect(core.refreshFetchAttempts).toBe(1); | 324 | await vi.waitFor(() => { |
| | 325 | expect(core.refreshFetchAttempts).toBe(1); |
| | 326 | }); |
| 324 | expect(await git(source.path, "branch --show-current")).toBe(featureBranch); | 327 | expect(await git(source.path, "branch --show-current")).toBe(featureBranch); |
| 325 | }); | 328 | }); |
| 326 | | 329 | |
| ... | @@ -362,6 +365,9 @@ describe("git integration", () => { | ... | @@ -362,6 +365,9 @@ describe("git integration", () => { |
| 362 | | 365 | |
| 363 | const core = new FetchRecordingGitCoreService(); | 366 | const core = new FetchRecordingGitCoreService(); |
| 364 | await core.checkoutBranch({ cwd: source.path, branch: featureBranch }); | 367 | await core.checkoutBranch({ cwd: source.path, branch: featureBranch }); |
| | 368 | await vi.waitFor(() => { |
| | 369 | expect(core.fetchArgs).not.toBeNull(); |
| | 370 | }); |
| 365 | | 371 | |
| 366 | expect(await git(source.path, "branch --show-current")).toBe(featureBranch); | 372 | expect(await git(source.path, "branch --show-current")).toBe(featureBranch); |
| 367 | expect(core.fetchArgs).toEqual([ | 373 | expect(core.fetchArgs).toEqual([ |
| ... | @@ -373,6 +379,67 @@ describe("git integration", () => { | ... | @@ -373,6 +379,67 @@ describe("git integration", () => { |
| 373 | ]); | 379 | ]); |
| 374 | }); | 380 | }); |
| 375 | | 381 | |
| | 382 | it("returns checkout result before background upstream refresh completes", async () => { |
| | 383 | await using remote = await makeTmpDir(); |
| | 384 | await using source = await makeTmpDir(); |
| | 385 | await git(remote.path, "init --bare"); |
| | 386 | |
| | 387 | await initRepoWithCommit(source.path); |
| | 388 | const defaultBranch = (await listGitBranches({ cwd: source.path })).branches.find( |
| | 389 | (branch) => branch.current, |
| | 390 | )!.name; |
| | 391 | await git(source.path, `remote add origin ${JSON.stringify(remote.path)}`); |
| | 392 | await git(source.path, `push -u origin ${defaultBranch}`); |
| | 393 | |
| | 394 | const featureBranch = "feature/background-refresh"; |
| | 395 | await git(source.path, `checkout -b ${featureBranch}`); |
| | 396 | await writeFile(path.join(source.path, "feature.txt"), "feature base\n"); |
| | 397 | await git(source.path, "add feature.txt"); |
| | 398 | await git(source.path, "commit -m 'feature base'"); |
| | 399 | await git(source.path, `push -u origin ${featureBranch}`); |
| | 400 | await git(source.path, `checkout ${defaultBranch}`); |
| | 401 | |
| | 402 | class SlowRefreshGitCoreService extends GitCoreService { |
| | 403 | fetchStarted = false; |
| | 404 | private readonly waitForReleasePromise: Promise<void>; |
| | 405 | private releaseWait: () => void = () => undefined; |
| | 406 | |
| | 407 | constructor() { |
| | 408 | super(); |
| | 409 | this.waitForReleasePromise = new Promise<void>((resolve) => { |
| | 410 | this.releaseWait = resolve; |
| | 411 | }); |
| | 412 | } |
| | 413 | |
| | 414 | releaseFetch(): void { |
| | 415 | this.releaseWait(); |
| | 416 | } |
| | 417 | |
| | 418 | override async git( |
| | 419 | cwd: string, |
| | 420 | args: readonly string[], |
| | 421 | allowNonZeroExit = false, |
| | 422 | ): Promise<void> { |
| | 423 | if (args[0] === "fetch") { |
| | 424 | this.fetchStarted = true; |
| | 425 | await this.waitForReleasePromise; |
| | 426 | return; |
| | 427 | } |
| | 428 | await super.git(cwd, args, allowNonZeroExit); |
| | 429 | } |
| | 430 | } |
| | 431 | |
| | 432 | const core = new SlowRefreshGitCoreService(); |
| | 433 | await expect(core.checkoutBranch({ cwd: source.path, branch: featureBranch })).resolves.toBe( |
| | 434 | undefined, |
| | 435 | ); |
| | 436 | await vi.waitFor(() => { |
| | 437 | expect(core.fetchStarted).toBe(true); |
| | 438 | }); |
| | 439 | expect(await git(source.path, "branch --show-current")).toBe(featureBranch); |
| | 440 | core.releaseFetch(); |
| | 441 | }); |
| | 442 | |
| 376 | it("throws when branch does not exist", async () => { | 443 | it("throws when branch does not exist", async () => { |
| 377 | await using tmp = await makeTmpDir(); | 444 | await using tmp = await makeTmpDir(); |
| 378 | await initRepoWithCommit(tmp.path); | 445 | await initRepoWithCommit(tmp.path); |