Skip to content
Source
Sign inSign up

use-thread-pr.test.ts

177 lines5.6 KB

1import {
2 ProjectId,
3 type PullRequestSummary,
4 SourceControlProviderKind,
5 type ThreadPullRequestLink,
6} from "@t3tools/contracts";
7import { describe, expect, it } from "vite-plus/test";
8​
9import {
10 knownPullRequestSummary,
11 presentThreadLinkedPullRequests,
12 rememberBounded,
13 resolveThreadPrSource,
14} from "./thread-pr-presentation";
15​
16function linkedPr(
17 number: number,
18 overrides: Partial<ThreadPullRequestLink> = {},
19): ThreadPullRequestLink {
20 return {
21 host: "github.com",
22 repository: "t3tools/t3code",
23 number,
24 url: `https://github.com/t3tools/t3code/pull/${number}`,
25 source: "manual",
26 linkedAt: "2026-09-08T00:00:00.000Z",
27 stack: null,
28 snapshot: {
29 state: "open",
30 title: `Change ${number}`,
31 headBranch: `change-${number}`,
32 baseBranch: "main",
33 isDraft: false,
34 updatedAt: null,
35 syncedAt: "2026-09-08T00:00:00.000Z",
36 },
37 ...overrides,
38 };
40​
41describe("presentThreadLinkedPullRequests", () => {
42 it("uses the top of a derived stack even when its bottom was linked later", () => {
43 const bottom = linkedPr(1, { linkedAt: "2026-09-09T00:00:00.000Z" });
44 const top = linkedPr(2);
45 expect(
46 presentThreadLinkedPullRequests([
47 bottom,
48 {
49 ...top,
50 snapshot: { ...top.snapshot!, baseBranch: "change-1" },
51 },
52 ]),
53 ).toMatchObject({ kind: "stack", label: "2", number: 2, url: top.url });
54 });
55​
56 it("names a single link with its host's term", () => {
57 expect(presentThreadLinkedPullRequests([linkedPr(7)])?.accessibilityLabel).toBe(
58 "#7 pull request open",
59 );
60 expect(
61 presentThreadLinkedPullRequests([
62 linkedPr(7, {
63 host: "gitlab.com",
64 repository: "t3tools/t3code",
65 url: "https://gitlab.com/t3tools/t3code/-/merge_requests/7",
66 }),
67 ])?.accessibilityLabel,
68 ).toBe("#7 merge request open");
69 });
70});
71​
72describe("resolveThreadPrSource compatibility", () => {
73 const legacyRef = {
74 projectId: ProjectId.make("project"),
75 repository: "t3tools/t3code",
76 number: 1,
77 url: "https://github.com/t3tools/t3code/pull/1",
78 };
79 const branchRef = { ...legacyRef, number: 2, url: "https://github.com/t3tools/t3code/pull/2" };
80​
81 it("polls the stored reference before the server config loads", () => {
82 expect(
83 resolveThreadPrSource(
84 { pullRequests: [], linkedPullRequest: legacyRef, branchPullRequest: branchRef },
85 false,
86 ),
87 ).toEqual({ linkedPresentation: null, pullRequestRef: legacyRef });
88 });
89​
90 it("uses snapshots without polling once the server config loads", () => {
91 expect(
92 resolveThreadPrSource(
93 { pullRequests: [linkedPr(3)], linkedPullRequest: legacyRef, branchPullRequest: branchRef },
94 true,
95 ),
96 ).toMatchObject({ linkedPresentation: { number: 3 }, pullRequestRef: null });
97 });
98​
99 it("does not revive a removed modern link from the compatibility reference", () => {
100 expect(
101 resolveThreadPrSource(
102 { pullRequests: [], linkedPullRequest: legacyRef, branchPullRequest: branchRef },
103 true,
104 ),
105 ).toEqual({ linkedPresentation: null, pullRequestRef: branchRef });
106 });
107​
108 it("does not poll when a modern link is waiting for its first snapshot", () => {
109 expect(
110 resolveThreadPrSource(
111 {
112 pullRequests: [linkedPr(3, { snapshot: null })],
113 linkedPullRequest: legacyRef,
114 branchPullRequest: branchRef,
115 },
116 true,
117 ),
118 ).toMatchObject({ linkedPresentation: { number: 3, state: null }, pullRequestRef: null });
119 });
120});
121​
122describe("knownPullRequestSummary", () => {
123 it("seeds from the newest summary of the same pull request, never another's", () => {
124 const projectId = ProjectId.make("project");
125 const link = linkedPr(7);
126 const threads = [
127 {
128 projectId,
129 pullRequests: [
130 { ...link, snapshot: { ...link.snapshot!, updatedAt: "2026-09-08T00:00:00.000Z" } },
131 ],
132 },
133 ];
134 const listed: PullRequestSummary = {
135 provider: SourceControlProviderKind.make("github"),
136 projectId,
137 repository: "t3tools/t3code",
138 number: 7,
139 title: "Listed title",
140 url: "https://github.com/t3tools/t3code/pull/7",
141 state: "open",
142 headBranch: "change-7",
143 baseBranch: "main",
144 updatedAt: "2026-09-09T00:00:00.000Z",
145 };
146 const lookup = (host: string, number: number, summaries = [listed]) =>
147 knownPullRequestSummary({ host, repository: "t3tools/t3code", number }, threads, summaries);
148​
149 expect(lookup("github.com", 7, [])?.title).toBe("Change 7");
150 expect(lookup("github.com", 7)?.title).toBe("Listed title");
151 expect(lookup("github.com", 8)).toBeUndefined();
152 expect(lookup("gitlab.com", 7)).toBeUndefined();
153​
154 const sameVersion = { ...listed, updatedAt: "2026-09-08T00:00:00.000Z" };
155 const syncedAt = Date.parse(link.snapshot!.syncedAt);
156 expect(lookup("github.com", 7, [{ ...sameVersion, observedAt: syncedAt - 1 }])?.title).toBe(
157 "Change 7",
158 );
159 expect(lookup("github.com", 7, [{ ...sameVersion, observedAt: syncedAt + 1 }])?.title).toBe(
160 "Listed title",
161 );
162 });
163});
164​
165describe("rememberBounded", () => {
166 it("moves a re-answered entry to the newest slot and evicts the oldest past the limit", () => {
167 let snapshots: ReadonlyMap<string, number> = new Map();
168 snapshots = rememberBounded(snapshots, "a", 1, 2);
169 snapshots = rememberBounded(snapshots, "b", 2, 2);
170 snapshots = rememberBounded(snapshots, "a", 3, 2);
171 snapshots = rememberBounded(snapshots, "c", 4, 2);
172 expect([...snapshots]).toEqual([
173 ["a", 3],
174 ["c", 4],
175 ]);
176 });
177});