1import type { ThreadPullRequestLink } from "@t3tools/contracts";
2import { ProjectId } from "@t3tools/contracts";
3import { describe, expect, it } from "vite-plus/test";
4import { savedPullRequestStack, pullRequestStackView } from "./pullRequestStackSnapshot";
5
6const reference = {
7 projectId: ProjectId.make("project"),
8 host: "github.com",
9 repository: "acme/web",
10 number: 2,
11};
12const link: ThreadPullRequestLink = {
13 host: "github.com",
14 repository: "acme/web",
15 number: 2,
16 url: "https://github.com/acme/web/pull/2",
17 source: "manual",
18 linkedAt: "2026-09-09T10:00:00Z",
19 snapshot: {
20 title: "Top layer",
21 headBranch: "top",
22 baseBranch: "bottom",
23 state: "open",
24 isDraft: false,
25 updatedAt: null,
26 syncedAt: "2026-09-09T10:00:00Z",
27 },
28 stack: {
29 kind: "native",
30 id: "stack",
31 number: 3,
32 url: "https://github.com/acme/web/pull/3",
33 base: "main",
34 layers: [
35 { number: 1, headBranch: "bottom", state: "open" },
36 { number: 2, headBranch: "top", state: "open" },
37 ],
38 },
39};
40
41describe("saved stack navigation", () => {
42 it("preserves all native layers even when only one has a linked snapshot, without action SHAs", () => {
43 const saved = savedPullRequestStack([link], reference);
44 expect(saved?.layers).toEqual([
45 { number: 1, headBranch: "bottom", state: "open" },
46 { number: 2, headBranch: "top", state: "open", title: "Top layer", isDraft: false },
47 ]);
48 expect(savedPullRequestStack([link], { ...reference, number: 1 })?.number).toBe(3);
49 });
50 it("does not borrow stacks across hosts or repositories", () => {
51 expect(savedPullRequestStack([link], { ...reference, host: "enterprise.example" })).toBeNull();
52 expect(savedPullRequestStack([link], { ...reference, repository: "other/web" })).toBeNull();
53 expect(savedPullRequestStack([link], { ...reference, host: undefined })).toBeNull();
54 });
55 it("honors a newer saved removal across linked threads", () => {
56 expect(
57 savedPullRequestStack(
58 [
59 link,
60 {
61 ...link,
62 stack: null,
63 snapshot: { ...link.snapshot!, syncedAt: "2026-09-09T11:00:00Z" },
64 },
65 ],
66 reference,
67 ),
68 ).toBeNull();
69 });
70 it("shows saved data during loading and marks a failed refresh stale", () => {
71 const saved = savedPullRequestStack([link], reference);
72 const query = { data: null, isSuccess: false, isPending: true, error: null };
73 expect(pullRequestStackView(query, saved)).toMatchObject({
74 data: saved,
75 isFresh: false,
76 notice: expect.stringContaining("Refreshing"),
77 });
78 expect(
79 pullRequestStackView({ ...query, isPending: false, error: "Rate limited" }, saved),
80 ).toMatchObject({ data: saved, isFresh: false, notice: expect.stringContaining("stale") });
81 });
82 it("prefers refreshed data and honors a successful absence", () => {
83 const saved = savedPullRequestStack([link], reference);
84 const query = { data: saved, isSuccess: true, isPending: false, error: null };
85 expect(pullRequestStackView(query, null)).toEqual({ data: saved, isFresh: true, notice: null });
86 expect(pullRequestStackView({ ...query, data: null }, saved)).toEqual({
87 data: null,
88 isFresh: true,
89 notice: null,
90 });
91 expect(pullRequestStackView({ ...query, isPending: true }, saved).isFresh).toBe(true);
92 expect(pullRequestStackView({ ...query, data: null, isPending: true }, saved)).toMatchObject({
93 data: null,
94 isFresh: true,
95 });
96 });
97});