|
| 1 | +import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; |
| 2 | +import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts"; |
1 | 3 | import { SourceControlProviderKind } from "@t3tools/contracts"; |
2 | 4 | // @effect-diagnostics nodeBuiltinImport:off |
3 | 5 | import * as NodeFS from "node:fs"; |
@@ -69,6 +71,10 @@ type FakeChangeRequest = Omit<ChangeRequest, "provider" | "state" | "updatedAt"> |
69 | 71 | }; |
70 | 72 |
|
71 | 73 | interface FakeProviderScenario { |
| 74 | + prListGate?: { |
| 75 | + readonly started: Deferred.Deferred<void>; |
| 76 | + readonly release: Deferred.Deferred<void>; |
| 77 | + }; |
72 | 78 | /** The kind the fake host reports itself as. */ |
73 | 79 | kind?: string; |
74 | 80 | /** Answers to successive change request listings, whatever their head selector. */ |
@@ -452,6 +458,13 @@ function createFakeSourceControlProvider(scenario: FakeProviderScenario = {}): { |
452 | 458 | [] |
453 | 459 | ).map(toChangeRequest), |
454 | 460 | ), |
| 461 | + Effect.tap(() => |
| 462 | + scenario.prListGate |
| 463 | + ? Deferred.succeed(scenario.prListGate.started, undefined).pipe( |
| 464 | + Effect.andThen(Deferred.await(scenario.prListGate.release)), |
| 465 | + ) |
| 466 | + : Effect.void, |
| 467 | + ), |
455 | 468 | ), |
456 | 469 | createChangeRequest: (input) => |
457 | 470 | record(input.cwd, { |
@@ -723,6 +736,108 @@ const layerGitManagerTest = GitVcsDriver.layer.pipe( |
723 | 736 | ); |
724 | 737 |
|
725 | 738 | it.layer(layerGitManagerTest)("GitManager", (it) => { |
| 739 | + it.effect( |
| 740 | + "publishes local Git changes during a held PR lookup without caching its old branch", |
| 741 | + () => |
| 742 | + Effect.gen(function* () { |
| 743 | + const repoDir = yield* makeTempDir("t3code-refresh-status-gh-gate-"); |
| 744 | + yield* initRepo(repoDir); |
| 745 | + yield* runGit(repoDir, ["checkout", "-b", "release"]); |
| 746 | + const remoteDir = yield* createBareRemote(); |
| 747 | + yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]); |
| 748 | + yield* runGit(repoDir, ["push", "-u", "origin", "release"]); |
| 749 | + yield* runGit(repoDir, ["tag", "release"]); |
| 750 | + const oldPr = { |
| 751 | + number: 613, |
| 752 | + title: "Release PR", |
| 753 | + url: "https://github.com/pingdotgg/codething-mvp/pull/613", |
| 754 | + baseRefName: "main", |
| 755 | + headRefName: "release", |
| 756 | + }; |
| 757 | + const newPr = { ...oldPr, number: 614, headRefName: "heads/release" }; |
| 758 | + const providerScenario: FakeProviderScenario = { |
| 759 | + prListSequence: [oldPr, oldPr, newPr].map((pr) => [pr]), |
| 760 | + }; |
| 761 | + const { manager } = yield* makeManager({ providerScenario }); |
| 762 | + const context = yield* Layer.build( |
| 763 | + VcsStatusBroadcaster.layer.pipe( |
| 764 | + Layer.provide( |
| 765 | + GitWorkflowService.layer.pipe( |
| 766 | + Layer.provide(Layer.succeed(GitManager.GitManager, manager)), |
| 767 | + Layer.provide( |
| 768 | + VcsDriverRegistry.layer.pipe( |
| 769 | + Layer.provide(VcsProjectConfig.layer), |
| 770 | + Layer.provide(VcsProcess.layer), |
| 771 | + ), |
| 772 | + ), |
| 773 | + ), |
| 774 | + ), |
| 775 | + Layer.provide( |
| 776 | + Layer.mock(BackgroundPolicy.BackgroundPolicy)({ |
| 777 | + shouldRunScopeWork: () => Effect.succeed(true), |
| 778 | + }), |
| 779 | + ), |
| 780 | + ), |
| 781 | + ); |
| 782 | + const broadcaster = Context.get(context, VcsStatusBroadcaster.VcsStatusBroadcaster); |
| 783 | + const initial = yield* broadcaster.getStatus({ cwd: repoDir }); |
| 784 | + expect(initial.refName).toBe("release"); |
| 785 | + expect(initial.pr?.number).toBe(613); |
| 786 | + |
| 787 | + const subscribed = yield* Deferred.make<void>(); |
| 788 | + const published = yield* Deferred.make<void>(); |
| 789 | + yield* broadcaster.streamStatus({ cwd: repoDir, includeRemote: false }).pipe( |
| 790 | + Stream.runForEach((event) => { |
| 791 | + if (event._tag === "snapshot") return Deferred.succeed(subscribed, undefined); |
| 792 | + if (event._tag === "localUpdated" && event.local.hasWorkingTreeChanges) { |
| 793 | + return Deferred.succeed(published, undefined); |
| 794 | + } |
| 795 | + return Effect.void; |
| 796 | + }), |
| 797 | + Effect.forkScoped, |
| 798 | + ); |
| 799 | + yield* Deferred.await(subscribed); |
| 800 | + const gate = { |
| 801 | + started: yield* Deferred.make<void>(), |
| 802 | + release: yield* Deferred.make<void>(), |
| 803 | + }; |
| 804 | + providerScenario.prListGate = gate; |
| 805 | + const fs = yield* FileSystem.FileSystem; |
| 806 | + yield* fs.writeFileString(NodePath.join(repoDir, "README.md"), "changed before refresh\n"); |
| 807 | + const completed = yield* Deferred.make<void>(); |
| 808 | + const refresh = yield* broadcaster |
| 809 | + .refreshStatus(repoDir) |
| 810 | + .pipe(Effect.ensuring(Deferred.succeed(completed, undefined)), Effect.forkScoped); |
| 811 | + yield* Deferred.await(gate.started); |
| 812 | + const pending = Option.getOrThrow( |
| 813 | + yield* Stream.runHead(broadcaster.streamStatus({ cwd: repoDir, includeRemote: false })), |
| 814 | + ); |
| 815 | + expect(pending._tag).toBe("snapshot"); |
| 816 | + if (pending._tag === "snapshot") expect(pending.local.hasWorkingTreeChanges).toBe(true); |
| 817 | + yield* Deferred.await(published); |
| 818 | + expect(yield* Deferred.isDone(completed)).toBe(false); |
| 819 | + |
| 820 | + yield* runGit(repoDir, ["checkout", "-b", "heads/release"]); |
| 821 | + yield* runGit(repoDir, ["push", "-u", "origin", "refs/heads/heads/release"]); |
| 822 | + yield* Deferred.succeed(gate.release, undefined); |
| 823 | + const refreshed = yield* Fiber.join(refresh); |
| 824 | + expect(refreshed.refName).toBe("heads/release"); |
| 825 | + expect(refreshed.hasWorkingTreeChanges).toBe(true); |
| 826 | + expect(refreshed.pr).toBeNull(); |
| 827 | + const snapshot = Option.getOrThrow( |
| 828 | + yield* Stream.runHead(broadcaster.streamStatus({ cwd: repoDir, includeRemote: false })), |
| 829 | + ); |
| 830 | + expect(snapshot._tag).toBe("snapshot"); |
| 831 | + if (snapshot._tag === "snapshot") expect(snapshot.remote).toBeNull(); |
| 832 | + |
| 833 | + delete providerScenario.prListGate; |
| 834 | + const recovered = yield* broadcaster.getStatus({ cwd: repoDir }); |
| 835 | + expect(recovered.refName).toBe("heads/release"); |
| 836 | + expect(recovered.pr?.number).toBe(614); |
| 837 | + expect((yield* broadcaster.getStatus({ cwd: repoDir })).pr?.number).toBe(614); |
| 838 | + }), |
| 839 | + ); |
| 840 | + |
726 | 841 | it.effect("status reports the listed PR whose head is the branch", () => |
727 | 842 | Effect.gen(function* () { |
728 | 843 | const repoDir = yield* makeTempDir("t3code-git-manager-"); |
|
0 commit comments