1import type { JjChange } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import {
5 clampSelection,
6 newestPullRequestIndex,
7 pullRequestFor,
8 pullRequestText,
9 selectChange,
10 selectionRange,
11 visibleChanges,
12} from "./jjReview.logic";
13
14const change = (id: string, description: string, overrides: Partial<JjChange> = {}): JjChange => ({
15 changeId: id,
16 shortId: id.slice(0, 4),
17 commitId: `${id}-commit`,
18 parentCommitIds: [`${id}-parent`],
19 description,
20 trailers: [...description.matchAll(/^Assisted-by: (.+)$/gm)].map((m) => ({
21 key: "Assisted-by",
22 value: m[1]!,
23 })),
24 bookmarks: [],
25 immutable: false,
26 conflict: false,
27 empty: false,
28 workingCopy: false,
29 additions: 1,
30 deletions: 0,
31 authoredAt: "2026-10-15T12:00:00-07:00",
32 ...overrides,
33});
34
35// Newest first, like the stack.
36const followUp = change("follow", "chore: follow-up changes\n\nAssisted-by: gpt-6.1-sol");
37const feature = change(
38 "feature",
39 "feat(reader): keep scroll\n\nRestore offsets per tab.\n\nAssisted-by: claude-opus-5.5",
40);
41const fix = change("fixit", "fix(api): retry uploads");
42
43describe("selection", () => {
44 it("selects one change, or extends a contiguous range from the anchor", () => {
45 expect(selectChange(1, 2, false)).toEqual([2, 2]);
46 expect(selectChange(2, 0, true)).toEqual([0, 2]);
47 });
48
49 it("clamps a selection when the stack shrinks", () => {
50 expect(clampSelection([2, 3], 2)).toEqual([1, 1]);
51 expect(clampSelection([0, 0], 0)).toEqual([0, 0]);
52 });
53
54 it("diffs a range from the oldest change's parent to the newest change", () => {
55 expect(selectionRange([followUp, feature], [0, 1])).toEqual({
56 fromCommitId: "feature-parent",
57 toCommitId: "follow-commit",
58 });
59 expect(
60 selectionRange([change("root", "x", { parentCommitIds: [] })], [0, 0])?.fromCommitId,
61 ).toBe("0".repeat(40));
62 });
63
64 it("hides an empty working copy but keeps one with changes", () => {
65 const stack = { workspaceRoot: "/w", repository: null, trunk: null };
66 expect(
67 visibleChanges({
68 ...stack,
69 changes: [change("wc", "", { workingCopy: true, empty: true }), feature],
70 }),
71 ).toEqual([feature]);
72 expect(
73 visibleChanges({ ...stack, changes: [change("wc", "", { workingCopy: true }), feature] }),
74 ).toHaveLength(2);
75 });
76});
77
78describe("pull requests", () => {
79 const prs = new Map([["feature-commit", 80]]);
80
81 it("puts a change in the PR of the nearest head at or above it", () => {
82 const changes = [followUp, feature, fix];
83 expect(pullRequestFor(changes, prs, 2)).toBe(80);
84 expect(pullRequestFor(changes, prs, 1)).toBe(80);
85 expect(pullRequestFor(changes, prs, 0)).toBeNull();
86 expect(newestPullRequestIndex(changes, prs)).toBe(1);
87 });
88
89 it("titles a PR after its oldest non-follow-up change and credits every model", () => {
90 expect(pullRequestText([followUp, feature])).toEqual({
91 title: "feat(reader): keep scroll",
92 body: "Restore offsets per tab.\n\nAssisted-by: gpt-6.1-sol\nAssisted-by: claude-opus-5.5",
93 baseChangeId: "feature",
94 });
95 });
96});