@actagent/diffs 2026.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,300 @@
1
+ // Diffs tests cover render plugin behavior.
2
+ import {
3
+ disposeHighlighter,
4
+ RegisteredCustomThemes,
5
+ ResolvedThemes,
6
+ ResolvingThemes,
7
+ } from "@pierre/diffs";
8
+ import { afterEach, describe, expect, it } from "vitest";
9
+ import { DEFAULT_DIFFS_TOOL_DEFAULTS, resolveDiffImageRenderOptions } from "./config.js";
10
+ import { renderDiffDocument } from "./render.js";
11
+ import { parseViewerPayloadJson } from "./viewer-payload.js";
12
+
13
+ describe("renderDiffDocument", () => {
14
+ afterEach(async () => {
15
+ await disposeHighlighter();
16
+ });
17
+
18
+ it("renders before/after input into a complete viewer document", async () => {
19
+ const rendered = await renderDiffDocument(
20
+ {
21
+ kind: "before_after",
22
+ before: "const value = 1;\n",
23
+ after: "const value = 2;\n",
24
+ path: "src/example.ts",
25
+ },
26
+ {
27
+ presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
28
+ image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
29
+ expandUnchanged: false,
30
+ },
31
+ );
32
+
33
+ expect(rendered.title).toBe("src/example.ts");
34
+ expect(rendered.fileCount).toBe(1);
35
+ expect(rendered.viewerRuntime).toBe("base");
36
+ expect(rendered.html).toContain("data-actagent-diff-root");
37
+ expect(rendered.html).toContain("src/example.ts");
38
+ expect(rendered.html).toContain("../../assets/viewer.js");
39
+ expect(rendered.imageHtml).toContain("../../assets/viewer.js");
40
+ expect(rendered.imageHtml).toContain("max-width: 960px;");
41
+ expect(rendered.imageHtml).toContain("--diffs-font-size: 16px;");
42
+ expect(rendered.html).toContain("min-height: 100vh;");
43
+ expect(rendered.html).toContain('"diffIndicators":"bars"');
44
+ expect(rendered.html).toContain('"disableLineNumbers":false');
45
+ expect(rendered.html).toContain("--diffs-line-height: 24px;");
46
+ expect(rendered.html).toContain("--diffs-font-size: 15px;");
47
+ expect(rendered.html).not.toContain("fonts.googleapis.com");
48
+ });
49
+
50
+ it("normalizes non-finite presentation numbers before rendering CSS", async () => {
51
+ const rendered = await renderDiffDocument(
52
+ {
53
+ kind: "before_after",
54
+ before: "old\n",
55
+ after: "new\n",
56
+ },
57
+ {
58
+ presentation: {
59
+ ...DEFAULT_DIFFS_TOOL_DEFAULTS,
60
+ fontSize: Number.NaN,
61
+ lineSpacing: Number.POSITIVE_INFINITY,
62
+ },
63
+ image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
64
+ expandUnchanged: false,
65
+ },
66
+ );
67
+
68
+ expect(rendered.html).toContain("--diffs-font-size: 15px;");
69
+ expect(rendered.html).toContain("--diffs-line-height: 24px;");
70
+ expect(rendered.imageHtml).toContain("--diffs-font-size: 16px;");
71
+ expect(rendered.html).not.toContain("NaNpx");
72
+ expect(rendered.imageHtml).not.toContain("NaNpx");
73
+ });
74
+
75
+ it("resolves viewer assets under an optional base path", async () => {
76
+ const rendered = await renderDiffDocument(
77
+ {
78
+ kind: "before_after",
79
+ before: "const value = 1;\n",
80
+ after: "const value = 2;\n",
81
+ },
82
+ {
83
+ presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
84
+ image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
85
+ expandUnchanged: false,
86
+ },
87
+ );
88
+
89
+ const html = rendered.html ?? "";
90
+ const loaderSrc = html.match(/<script type="module" src="([^"]+)"><\/script>/)?.[1];
91
+ expect(loaderSrc).toBe("../../assets/viewer.js");
92
+ expect(
93
+ new URL(loaderSrc ?? "", "https://example.com/actagent/plugins/diffs/view/id/token").pathname,
94
+ ).toBe("/actagent/plugins/diffs/assets/viewer.js");
95
+ });
96
+
97
+ it("downgrades invalid language hints to plain text", async () => {
98
+ const rendered = await renderDiffDocument(
99
+ {
100
+ kind: "before_after",
101
+ before: "const value = 1;\n",
102
+ after: "const value = 2;\n",
103
+ lang: "not-a-real-language",
104
+ },
105
+ {
106
+ presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
107
+ image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
108
+ expandUnchanged: false,
109
+ },
110
+ );
111
+
112
+ const html = rendered.html ?? "";
113
+
114
+ expect(rendered.title).toBe("Text diff");
115
+ expect(html).toContain("diff.txt");
116
+ expect(html).not.toContain("not-a-real-language");
117
+
118
+ const payloads = [...html.matchAll(/data-actagent-diff-payload>(.*?)<\/script>/g)].map(
119
+ (match) => parseViewerPayloadJson(match[1] ?? ""),
120
+ );
121
+ expect(payloads).toHaveLength(1);
122
+ expect(payloads[0]?.langs).toEqual(["text"]);
123
+ expect(payloads[0]?.oldFile?.lang).toBeUndefined();
124
+ expect(payloads[0]?.newFile?.lang).toBeUndefined();
125
+ });
126
+
127
+ it("keeps uncommon language diffs readable without the language pack", async () => {
128
+ const rendered = await renderDiffDocument(
129
+ {
130
+ kind: "before_after",
131
+ before: "REPORT z_demo.\n",
132
+ after: "REPORT z_demo2.\n",
133
+ lang: "abap",
134
+ },
135
+ {
136
+ presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
137
+ image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
138
+ expandUnchanged: false,
139
+ },
140
+ "viewer",
141
+ );
142
+
143
+ const html = rendered.html ?? "";
144
+ const payload = parseViewerPayloadJson(
145
+ html.match(/data-actagent-diff-payload>(.*?)<\/script>/)?.[1] ?? "",
146
+ );
147
+
148
+ expect(rendered.viewerRuntime).toBe("base");
149
+ expect(html).toContain("../../assets/viewer.js");
150
+ expect(html).not.toContain("diffs-language-pack");
151
+ expect(payload.langs).toEqual(["text"]);
152
+ });
153
+
154
+ it("uses the language-pack viewer runtime for uncommon languages when available", async () => {
155
+ const rendered = await renderDiffDocument(
156
+ {
157
+ kind: "before_after",
158
+ before: "REPORT z_demo.\n",
159
+ after: "REPORT z_demo2.\n",
160
+ lang: "abap",
161
+ },
162
+ {
163
+ presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
164
+ image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
165
+ expandUnchanged: false,
166
+ languagePackAvailable: true,
167
+ },
168
+ "viewer",
169
+ );
170
+
171
+ const html = rendered.html ?? "";
172
+ const payload = parseViewerPayloadJson(
173
+ html.match(/data-actagent-diff-payload>(.*?)<\/script>/)?.[1] ?? "",
174
+ );
175
+
176
+ expect(rendered.viewerRuntime).toBe("language-pack");
177
+ expect(html).toContain("../../../diffs-language-pack/assets/viewer.js");
178
+ expect(payload.langs).toEqual(["abap"]);
179
+ });
180
+
181
+ it("renders multi-file patch input", async () => {
182
+ const patch = [
183
+ "diff --git a/a.ts b/a.ts",
184
+ "--- a/a.ts",
185
+ "+++ b/a.ts",
186
+ "@@ -1 +1 @@",
187
+ "-const a = 1;",
188
+ "+const a = 2;",
189
+ "diff --git a/b.ts b/b.ts",
190
+ "--- a/b.ts",
191
+ "+++ b/b.ts",
192
+ "@@ -1 +1 @@",
193
+ "-const b = 1;",
194
+ "+const b = 2;",
195
+ ].join("\n");
196
+
197
+ const rendered = await renderDiffDocument(
198
+ {
199
+ kind: "patch",
200
+ patch,
201
+ title: "Workspace patch",
202
+ },
203
+ {
204
+ presentation: {
205
+ ...DEFAULT_DIFFS_TOOL_DEFAULTS,
206
+ layout: "split",
207
+ theme: "dark",
208
+ },
209
+ image: resolveDiffImageRenderOptions({
210
+ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS,
211
+ fileQuality: "hq",
212
+ fileMaxWidth: 1180,
213
+ }),
214
+ expandUnchanged: true,
215
+ },
216
+ );
217
+
218
+ expect(rendered.title).toBe("Workspace patch");
219
+ expect(rendered.fileCount).toBe(2);
220
+ expect(rendered.html).toContain("Workspace patch");
221
+ expect(rendered.imageHtml).toContain("max-width: 1180px;");
222
+ });
223
+
224
+ it("re-registers pierre theme loaders before rendering", async () => {
225
+ await disposeHighlighter();
226
+
227
+ const originalLightLoader = RegisteredCustomThemes.get("pierre-light");
228
+ const originalDarkLoader = RegisteredCustomThemes.get("pierre-dark");
229
+ const brokenLoader = async () => {
230
+ throw new Error("broken pierre theme loader");
231
+ };
232
+
233
+ RegisteredCustomThemes.set("pierre-light", brokenLoader);
234
+ RegisteredCustomThemes.set("pierre-dark", brokenLoader);
235
+ ResolvedThemes.delete("pierre-light");
236
+ ResolvedThemes.delete("pierre-dark");
237
+ ResolvingThemes.delete("pierre-light");
238
+ ResolvingThemes.delete("pierre-dark");
239
+
240
+ try {
241
+ const rendered = await renderDiffDocument(
242
+ {
243
+ kind: "before_after",
244
+ before: "const value = 1;\n",
245
+ after: "const value = 2;\n",
246
+ path: "src/example.ts",
247
+ },
248
+ {
249
+ presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
250
+ image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
251
+ expandUnchanged: false,
252
+ },
253
+ );
254
+
255
+ expect(rendered.fileCount).toBe(1);
256
+ expect(rendered.html).toContain("src/example.ts");
257
+ expect(RegisteredCustomThemes.get("pierre-light")).not.toBe(brokenLoader);
258
+ expect(RegisteredCustomThemes.get("pierre-dark")).not.toBe(brokenLoader);
259
+ } finally {
260
+ if (originalLightLoader) {
261
+ RegisteredCustomThemes.set("pierre-light", originalLightLoader);
262
+ } else {
263
+ RegisteredCustomThemes.delete("pierre-light");
264
+ }
265
+ if (originalDarkLoader) {
266
+ RegisteredCustomThemes.set("pierre-dark", originalDarkLoader);
267
+ } else {
268
+ RegisteredCustomThemes.delete("pierre-dark");
269
+ }
270
+ await disposeHighlighter();
271
+ }
272
+ });
273
+
274
+ it("rejects patches that exceed file-count limits", async () => {
275
+ const patch = Array.from({ length: 129 }, (_, i) => {
276
+ return [
277
+ `diff --git a/f${i}.ts b/f${i}.ts`,
278
+ `--- a/f${i}.ts`,
279
+ `+++ b/f${i}.ts`,
280
+ "@@ -1 +1 @@",
281
+ "-const x = 1;",
282
+ "+const x = 2;",
283
+ ].join("\n");
284
+ }).join("\n");
285
+
286
+ await expect(
287
+ renderDiffDocument(
288
+ {
289
+ kind: "patch",
290
+ patch,
291
+ },
292
+ {
293
+ presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
294
+ image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
295
+ expandUnchanged: false,
296
+ },
297
+ ),
298
+ ).rejects.toThrow("too many files");
299
+ });
300
+ });