1import { SourceControlProviderKind } from "@t3tools/contracts";
2import { ProjectId, type PullRequestSummary, type VcsStatusResult } from "@t3tools/contracts";
3import { describe, expect, it } from "@effect/vitest";
4
5import {
6 resolveDisplayedThreadPr,
7 resolveDisplayedThreadPrProvider,
8} from "./ThreadStatusIndicators";
9import { newestPullRequestSummary } from "../state/pullRequests";
10
11function status(overrides: Partial<VcsStatusResult> = {}): VcsStatusResult {
12 return {
13 isRepo: true,
14 hasPrimaryRemote: true,
15 isDefaultRef: false,
16 refName: "feature/current",
17 hasWorkingTreeChanges: false,
18 workingTree: { files: [], insertions: 0, deletions: 0 },
19 hasUpstream: true,
20 aheadCount: 0,
21 behindCount: 0,
22 pr: {
23 number: 42,
24 title: "PR branch",
25 url: "https://github.com/pingdotgg/t3code/pull/42",
26 baseRef: "main",
27 headRef: "feature/current",
28 state: "open",
29 },
30 ...overrides,
31 };
32}
33
34function mergedFeaturePr(): NonNullable<VcsStatusResult["pr"]> {
35 return {
36 number: 42,
37 title: "Feature PR",
38 url: "https://github.com/pingdotgg/t3code/pull/42",
39 baseRef: "main",
40 headRef: "feature/current",
41 state: "merged",
42 };
43}
44
45function pullRequestSummary(
46 state: PullRequestSummary["state"],
47 updatedAt: string,
48): PullRequestSummary {
49 return {
50 provider: SourceControlProviderKind.make("github"),
51 projectId: ProjectId.make("project-1"),
52 repository: "pingdotgg/t3code",
53 number: 42,
54 title: "Feature PR",
55 url: "https://github.com/pingdotgg/t3code/pull/42",
56 state,
57 headBranch: "feature/current",
58 baseBranch: "main",
59 updatedAt,
60 };
61}
62
63describe("shared pull request state", () => {
64 it("never lets a stale open response regress a merged observation", () => {
65 const merged = pullRequestSummary("merged", "2026-09-03T01:01:00.000Z");
66 const staleOpen = pullRequestSummary("open", "2026-09-03T01:00:00.000Z");
67
68 expect(newestPullRequestSummary(merged, staleOpen)).toBe(merged);
69 });
70
71 it("accepts a newer open state after a closed pull request is reopened", () => {
72 const closed = pullRequestSummary("closed", "2026-09-03T01:00:00.000Z");
73 const reopened = pullRequestSummary("open", "2026-09-03T01:01:00.000Z");
74
75 expect(newestPullRequestSummary(closed, reopened)).toBe(reopened);
76 });
77});
78
79describe("resolveDisplayedThreadPr", () => {
80 const featureBranch = "feature/current";
81 const mergedPr = mergedFeaturePr();
82 const linkedPullRequest = {
83 projectId: ProjectId.make("project-1"),
84 repository: "pingdotgg/t3code",
85 number: 42,
86 url: "https://github.com/pingdotgg/t3code/pull/42",
87 };
88 const provider = {
89 kind: SourceControlProviderKind.make("github"),
90 name: "GitHub",
91 baseUrl: "https://github.com",
92 };
93
94 it("shows a linked pull request when the checkout has a different branch", () => {
95 const input = {
96 threadBranch: "feature/other",
97 gitStatus: status({ refName: "feature/other", pr: null }),
98 linkedPullRequest,
99 linkedPullRequestStatus: { pr: mergedPr, sourceControlProvider: provider },
100 };
101
102 expect(resolveDisplayedThreadPr(input)).toEqual(mergedPr);
103 expect(resolveDisplayedThreadPrProvider(input)).toEqual(provider);
104 });
105
106 it.each(["open", "merged", "closed"] as const)(
107 "only shows an unlinked branch PR while open (state=%s)",
108 (state) => {
109 const branchPr = { ...mergedPr, state };
110 const input = {
111 threadBranch: featureBranch,
112 gitStatus: status({ pr: branchPr, sourceControlProvider: provider }),
113 };
114
115 expect(resolveDisplayedThreadPr(input)).toEqual(state === "open" ? branchPr : null);
116 expect(resolveDisplayedThreadPrProvider(input)).toEqual(
117 state === "open" ? provider : undefined,
118 );
119 },
120 );
121
122 it("never attaches a PR reported by main to the feature thread", () => {
123 const mainPr = {
124 number: 99,
125 title: "Unrelated main PR",
126 url: "https://github.com/pingdotgg/t3code/pull/99",
127 baseRef: "develop",
128 headRef: "main",
129 state: "merged" as const,
130 };
131 expect(
132 resolveDisplayedThreadPr({
133 threadBranch: featureBranch,
134 gitStatus: status({ refName: "main", pr: mainPr }),
135 }),
136 ).toBeNull();
137 });
138});