Skip to content

Commit 4cbfb83

Browse files
authored
test: add focused coverage for review metrics (#133)
1 parent 9e1aead commit 4cbfb83

5 files changed

Lines changed: 308 additions & 1 deletion

File tree

src/core/loaders.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ function buildUntrackedDiffFile(
242242
}
243243

244244
/** Reorder files to follow agent-context narrative order when a sidecar provides one. */
245-
function orderDiffFiles(files: DiffFile[], agentContext: AgentContext | null) {
245+
export function orderDiffFiles(files: DiffFile[], agentContext: AgentContext | null) {
246246
if (!agentContext || agentContext.files.length === 0) {
247247
return files;
248248
}

test/fixtures/diff-helpers.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { parseDiffFromFile } from "@pierre/diffs";
2+
import type { DiffFile } from "../../src/core/types";
3+
4+
export function lines(...values: string[]) {
5+
return `${values.join("\n")}\n`;
6+
}
7+
8+
export function createDiffFile({
9+
after = "const alpha = 10;\nconst beta = 2;\nconst gamma = 30;\nconst stable = true;\n",
10+
before = "const alpha = 1;\nconst beta = 2;\nconst gamma = 3;\nconst stable = true;\n",
11+
id = "example",
12+
language = "typescript",
13+
path = "example.ts",
14+
previousPath,
15+
}: {
16+
after?: string;
17+
before?: string;
18+
id?: string;
19+
language?: string;
20+
path?: string;
21+
previousPath?: string;
22+
} = {}): DiffFile {
23+
const metadata = parseDiffFromFile(
24+
{ cacheKey: `${id}:before`, contents: before, name: path },
25+
{ cacheKey: `${id}:after`, contents: after, name: path },
26+
{ context: 0 },
27+
true,
28+
);
29+
30+
return {
31+
agent: null,
32+
id,
33+
language,
34+
metadata,
35+
patch: "",
36+
path,
37+
previousPath,
38+
stats: { additions: 1, deletions: 1 },
39+
};
40+
}
41+
42+
export function createHeaderOnlyDiffFile(): DiffFile {
43+
const file = createDiffFile({
44+
before: "const alpha = 1;\n",
45+
after: "const alpha = 2;\n",
46+
id: "header-only",
47+
path: "header-only.ts",
48+
});
49+
50+
return {
51+
...file,
52+
metadata: {
53+
...file.metadata,
54+
isPartial: true,
55+
hunks: file.metadata.hunks.map((hunk) => ({
56+
...hunk,
57+
additionLines: 0,
58+
deletionLines: 0,
59+
hunkContent: [],
60+
})),
61+
},
62+
};
63+
}

test/hunk-scroll.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { computeHunkRevealScrollTop } from "../src/ui/lib/hunkScroll";
3+
4+
describe("computeHunkRevealScrollTop", () => {
5+
test("keeps a fitting hunk fully visible when the preferred padding would clip the end", () => {
6+
expect(
7+
computeHunkRevealScrollTop({
8+
hunkTop: 20,
9+
hunkHeight: 10,
10+
preferredTopPadding: 4,
11+
viewportHeight: 12,
12+
}),
13+
).toBe(18);
14+
});
15+
16+
test("preserves the preferred top padding when the full hunk still fits", () => {
17+
expect(
18+
computeHunkRevealScrollTop({
19+
hunkTop: 20,
20+
hunkHeight: 10,
21+
preferredTopPadding: 4,
22+
viewportHeight: 16,
23+
}),
24+
).toBe(16);
25+
});
26+
27+
test("biases toward the hunk top when the hunk is taller than the viewport", () => {
28+
expect(
29+
computeHunkRevealScrollTop({
30+
hunkTop: 40,
31+
hunkHeight: 18,
32+
preferredTopPadding: 5,
33+
viewportHeight: 10,
34+
}),
35+
).toBe(35);
36+
});
37+
38+
test("clamps negative tops and padding at the start of the content", () => {
39+
expect(
40+
computeHunkRevealScrollTop({
41+
hunkTop: -3,
42+
hunkHeight: 6,
43+
preferredTopPadding: 4,
44+
viewportHeight: 12,
45+
}),
46+
).toBe(0);
47+
});
48+
49+
test("falls back to the desired top when the viewport height is zero", () => {
50+
expect(
51+
computeHunkRevealScrollTop({
52+
hunkTop: 25,
53+
hunkHeight: 8,
54+
preferredTopPadding: 6,
55+
viewportHeight: 0,
56+
}),
57+
).toBe(19);
58+
});
59+
});

test/loaders-ordering.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { describe, expect, test } from "bun:test";
2+
import type { AgentContext } from "../src/core/types";
3+
import { orderDiffFiles } from "../src/core/loaders";
4+
import { createDiffFile } from "./fixtures/diff-helpers";
5+
6+
function agentContext(...paths: string[]): AgentContext {
7+
return {
8+
files: paths.map((path) => ({ annotations: [], path })),
9+
version: 1,
10+
};
11+
}
12+
13+
describe("orderDiffFiles", () => {
14+
test("orders files by agent narrative order", () => {
15+
const files = [
16+
createDiffFile({ id: "beta", path: "beta.ts" }),
17+
createDiffFile({ id: "gamma", path: "gamma.ts" }),
18+
createDiffFile({ id: "alpha", path: "alpha.ts" }),
19+
];
20+
21+
const ordered = orderDiffFiles(files, agentContext("alpha.ts", "gamma.ts"));
22+
23+
expect(ordered.map((file) => file.path)).toEqual(["alpha.ts", "gamma.ts", "beta.ts"]);
24+
});
25+
26+
test("keeps files not mentioned in context in their original relative order at the end", () => {
27+
const files = [
28+
createDiffFile({ id: "beta", path: "beta.ts" }),
29+
createDiffFile({ id: "delta", path: "delta.ts" }),
30+
createDiffFile({ id: "alpha", path: "alpha.ts" }),
31+
createDiffFile({ id: "gamma", path: "gamma.ts" }),
32+
];
33+
34+
const ordered = orderDiffFiles(files, agentContext("gamma.ts"));
35+
36+
expect(ordered.map((file) => file.path)).toEqual([
37+
"gamma.ts",
38+
"beta.ts",
39+
"delta.ts",
40+
"alpha.ts",
41+
]);
42+
});
43+
44+
test("returns files unchanged when the agent context is empty or missing", () => {
45+
const files = [
46+
createDiffFile({ id: "alpha", path: "alpha.ts" }),
47+
createDiffFile({ id: "beta", path: "beta.ts" }),
48+
];
49+
50+
expect(orderDiffFiles(files, null).map((file) => file.path)).toEqual(["alpha.ts", "beta.ts"]);
51+
expect(orderDiffFiles(files, agentContext()).map((file) => file.path)).toEqual([
52+
"alpha.ts",
53+
"beta.ts",
54+
]);
55+
});
56+
57+
test("matches context entries by previousPath when a file was renamed", () => {
58+
const files = [
59+
createDiffFile({ id: "renamed", path: "new-name.ts", previousPath: "old-name.ts" }),
60+
createDiffFile({ id: "beta", path: "beta.ts" }),
61+
createDiffFile({ id: "alpha", path: "alpha.ts" }),
62+
];
63+
64+
const ordered = orderDiffFiles(files, agentContext("alpha.ts", "old-name.ts"));
65+
66+
expect(ordered.map((file) => file.path)).toEqual(["alpha.ts", "new-name.ts", "beta.ts"]);
67+
});
68+
});

test/section-heights.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { describe, expect, test } from "bun:test";
2+
import type { VisibleAgentNote } from "../src/ui/lib/agentAnnotations";
3+
import { measureDiffSectionMetrics } from "../src/ui/lib/sectionHeights";
4+
import { resolveTheme } from "../src/ui/themes";
5+
import { createDiffFile, createHeaderOnlyDiffFile, lines } from "./fixtures/diff-helpers";
6+
7+
describe("measureDiffSectionMetrics", () => {
8+
const theme = resolveTheme("midnight", null);
9+
10+
test("measures split and stack layouts from the render plan", () => {
11+
const file = createDiffFile();
12+
13+
const split = measureDiffSectionMetrics(file, "split", true, theme);
14+
const stack = measureDiffSectionMetrics(file, "stack", true, theme);
15+
16+
expect(split.bodyHeight).toBeGreaterThan(0);
17+
expect(stack.bodyHeight).toBeGreaterThan(split.bodyHeight);
18+
expect(split.hunkBounds.get(0)?.height).toBeGreaterThan(0);
19+
expect(stack.hunkBounds.get(0)?.height).toBeGreaterThan(split.hunkBounds.get(0)?.height ?? 0);
20+
});
21+
22+
test("accounts for visible inline notes without moving the hunk anchor", () => {
23+
const file = createDiffFile();
24+
const visibleAgentNotes: VisibleAgentNote[] = [
25+
{
26+
id: "annotation:example:0",
27+
annotation: {
28+
newRange: [1, 1],
29+
rationale: "Keep note height in section metrics.",
30+
summary: "Explain the change",
31+
},
32+
},
33+
];
34+
35+
const baseMetrics = measureDiffSectionMetrics(file, "split", true, theme, [], 120);
36+
const noteMetrics = measureDiffSectionMetrics(
37+
file,
38+
"split",
39+
true,
40+
theme,
41+
visibleAgentNotes,
42+
120,
43+
);
44+
45+
expect(noteMetrics.bodyHeight).toBeGreaterThan(baseMetrics.bodyHeight);
46+
expect(noteMetrics.hunkAnchorRows.get(0)).toBe(baseMetrics.hunkAnchorRows.get(0));
47+
expect(noteMetrics.rowMetrics.some((row) => row.key.startsWith("inline-note:"))).toBe(true);
48+
});
49+
50+
test("wraps long rows into taller section metrics when wrapping is enabled", () => {
51+
const file = createDiffFile({
52+
before: lines("const alpha = 1;", "const beta = 2;"),
53+
after: lines(
54+
"const alpha = 1;",
55+
"const beta = 'this is a deliberately long line that should wrap in a narrow viewport';",
56+
),
57+
id: "wrapped",
58+
path: "wrapped.ts",
59+
});
60+
61+
const nowrapMetrics = measureDiffSectionMetrics(
62+
file,
63+
"stack",
64+
true,
65+
theme,
66+
[],
67+
32,
68+
true,
69+
false,
70+
);
71+
const wrappedMetrics = measureDiffSectionMetrics(
72+
file,
73+
"stack",
74+
true,
75+
theme,
76+
[],
77+
32,
78+
true,
79+
true,
80+
);
81+
82+
expect(wrappedMetrics.bodyHeight).toBeGreaterThan(nowrapMetrics.bodyHeight);
83+
expect(wrappedMetrics.hunkBounds.get(0)?.height).toBeGreaterThan(
84+
nowrapMetrics.hunkBounds.get(0)?.height ?? 0,
85+
);
86+
});
87+
88+
test("returns a one-row placeholder for files with no visible hunks", () => {
89+
const file = createDiffFile({
90+
after: "const stable = true;\n",
91+
before: "const stable = true;\n",
92+
id: "empty",
93+
path: "empty.ts",
94+
});
95+
96+
const metrics = measureDiffSectionMetrics(file, "split", true, theme);
97+
98+
expect(file.metadata.hunks).toHaveLength(0);
99+
expect(metrics.bodyHeight).toBe(1);
100+
expect(metrics.hunkBounds.size).toBe(0);
101+
expect(metrics.rowMetrics).toEqual([]);
102+
});
103+
104+
test("can measure a header-only hunk stream without line rows", () => {
105+
const file = createHeaderOnlyDiffFile();
106+
107+
const metrics = measureDiffSectionMetrics(file, "split", true, theme);
108+
109+
expect(file.metadata.hunks).toHaveLength(1);
110+
expect(metrics.bodyHeight).toBe(1);
111+
expect(metrics.hunkAnchorRows.size).toBe(1);
112+
expect(metrics.hunkAnchorRows.get(0)).toBe(0);
113+
expect(metrics.hunkBounds.get(0)).toMatchObject({ height: 1, top: 0 });
114+
expect(metrics.rowMetrics).toHaveLength(1);
115+
expect(metrics.rowMetrics[0]?.key).toContain(":header:");
116+
});
117+
});

0 commit comments

Comments
 (0)