gsd-pi 2.7.1 → 2.8.0

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.
Files changed (53) hide show
  1. package/README.md +12 -5
  2. package/dist/loader.js +0 -0
  3. package/dist/modes/interactive/theme/dark.json +85 -0
  4. package/dist/modes/interactive/theme/light.json +84 -0
  5. package/dist/modes/interactive/theme/theme-schema.json +335 -0
  6. package/dist/modes/interactive/theme/theme.d.ts +78 -0
  7. package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  8. package/dist/modes/interactive/theme/theme.js +949 -0
  9. package/dist/modes/interactive/theme/theme.js.map +1 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  11. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  12. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  13. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  14. package/node_modules/cliui/CHANGELOG.md +121 -0
  15. package/node_modules/color-convert/CHANGELOG.md +54 -0
  16. package/node_modules/esprima/ChangeLog +235 -0
  17. package/node_modules/mz/HISTORY.md +66 -0
  18. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  19. package/node_modules/source-map/CHANGELOG.md +301 -0
  20. package/node_modules/thenify/History.md +11 -0
  21. package/node_modules/thenify-all/History.md +11 -0
  22. package/node_modules/y18n/CHANGELOG.md +100 -0
  23. package/node_modules/yargs/CHANGELOG.md +88 -0
  24. package/node_modules/yargs-parser/CHANGELOG.md +263 -0
  25. package/package.json +5 -2
  26. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  28. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  29. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  30. package/src/resources/extensions/browser-tools/capture.ts +165 -0
  31. package/src/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
  32. package/src/resources/extensions/browser-tools/index.ts +47 -4985
  33. package/src/resources/extensions/browser-tools/lifecycle.ts +265 -0
  34. package/src/resources/extensions/browser-tools/package.json +5 -1
  35. package/src/resources/extensions/browser-tools/refs.ts +264 -0
  36. package/src/resources/extensions/browser-tools/settle.ts +197 -0
  37. package/src/resources/extensions/browser-tools/state.ts +408 -0
  38. package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
  39. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
  40. package/src/resources/extensions/browser-tools/tools/assertions.ts +342 -0
  41. package/src/resources/extensions/browser-tools/tools/forms.ts +801 -0
  42. package/src/resources/extensions/browser-tools/tools/inspection.ts +492 -0
  43. package/src/resources/extensions/browser-tools/tools/intent.ts +614 -0
  44. package/src/resources/extensions/browser-tools/tools/interaction.ts +865 -0
  45. package/src/resources/extensions/browser-tools/tools/navigation.ts +232 -0
  46. package/src/resources/extensions/browser-tools/tools/pages.ts +303 -0
  47. package/src/resources/extensions/browser-tools/tools/refs.ts +541 -0
  48. package/src/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
  49. package/src/resources/extensions/browser-tools/tools/session.ts +400 -0
  50. package/src/resources/extensions/browser-tools/tools/wait.ts +247 -0
  51. package/src/resources/extensions/browser-tools/utils.ts +660 -0
  52. package/src/resources/extensions/gsd/git-service.ts +3 -0
  53. package/src/resources/extensions/shared/interview-ui.ts +1 -1
@@ -0,0 +1,614 @@
1
+ /**
2
+ * browser-tools — Node-side unit tests
3
+ *
4
+ * Uses jiti for TypeScript imports (the resolve-ts ESM hook breaks on core.js),
5
+ * node:test for the runner, and node:assert/strict for assertions.
6
+ *
7
+ * Tests pure functions from utils.ts, state.ts accessors, evaluate-helpers.ts
8
+ * syntax, and constrainScreenshot from capture.ts.
9
+ */
10
+
11
+ const { describe, it, beforeEach } = require("node:test");
12
+ const assert = require("node:assert/strict");
13
+ const jiti = require("jiti")(__filename, { interopDefault: true, debug: false });
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Module imports via jiti
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const {
20
+ parseRef,
21
+ formatVersionedRef,
22
+ staleRefGuidance,
23
+ formatCompactStateSummary,
24
+ verificationFromChecks,
25
+ verificationLine,
26
+ sanitizeArtifactName,
27
+ isCriticalResourceType,
28
+ getUrlHash,
29
+ firstErrorLine,
30
+ formatArtifactTimestamp,
31
+ } = jiti("../utils.ts");
32
+
33
+ const {
34
+ getBrowser,
35
+ setBrowser,
36
+ getContext,
37
+ setContext,
38
+ getActiveFrame,
39
+ setActiveFrame,
40
+ getSessionStartedAt,
41
+ setSessionStartedAt,
42
+ getSessionArtifactDir,
43
+ setSessionArtifactDir,
44
+ getCurrentRefMap,
45
+ setCurrentRefMap,
46
+ getRefVersion,
47
+ setRefVersion,
48
+ getRefMetadata,
49
+ setRefMetadata,
50
+ getLastActionBeforeState,
51
+ setLastActionBeforeState,
52
+ getLastActionAfterState,
53
+ setLastActionAfterState,
54
+ resetAllState,
55
+ } = jiti("../state.ts");
56
+
57
+ const { EVALUATE_HELPERS_SOURCE } = jiti("../evaluate-helpers.ts");
58
+
59
+ const { constrainScreenshot } = jiti("../capture.ts");
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // utils.ts — parseRef
63
+ // ---------------------------------------------------------------------------
64
+
65
+ describe("parseRef", () => {
66
+ it("parses a valid versioned ref", () => {
67
+ const result = parseRef("@v3:e12");
68
+ assert.deepStrictEqual(result, {
69
+ key: "e12",
70
+ version: 3,
71
+ display: "@v3:e12",
72
+ });
73
+ });
74
+
75
+ it("parses a ref without leading @", () => {
76
+ const result = parseRef("v1:e5");
77
+ assert.deepStrictEqual(result, {
78
+ key: "e5",
79
+ version: 1,
80
+ display: "@v1:e5",
81
+ });
82
+ });
83
+
84
+ it("handles legacy (unversioned) format", () => {
85
+ const result = parseRef("@e7");
86
+ assert.deepStrictEqual(result, {
87
+ key: "e7",
88
+ version: null,
89
+ display: "@e7",
90
+ });
91
+ });
92
+
93
+ it("trims whitespace", () => {
94
+ const result = parseRef(" @v2:e1 ");
95
+ assert.equal(result.key, "e1");
96
+ assert.equal(result.version, 2);
97
+ });
98
+
99
+ it("is case-insensitive", () => {
100
+ const result = parseRef("@V10:E3");
101
+ assert.equal(result.key, "e3");
102
+ assert.equal(result.version, 10);
103
+ });
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // utils.ts — formatVersionedRef
108
+ // ---------------------------------------------------------------------------
109
+
110
+ describe("formatVersionedRef", () => {
111
+ it("formats a versioned ref string", () => {
112
+ assert.equal(formatVersionedRef(5, "e3"), "@v5:e3");
113
+ });
114
+
115
+ it("formats version 0", () => {
116
+ assert.equal(formatVersionedRef(0, "e1"), "@v0:e1");
117
+ });
118
+ });
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // utils.ts — staleRefGuidance
122
+ // ---------------------------------------------------------------------------
123
+
124
+ describe("staleRefGuidance", () => {
125
+ it("includes the ref display and reason", () => {
126
+ const result = staleRefGuidance("@v2:e5", "element removed");
127
+ assert.ok(result.includes("@v2:e5"));
128
+ assert.ok(result.includes("element removed"));
129
+ assert.ok(result.includes("browser_snapshot_refs"));
130
+ });
131
+ });
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // utils.ts — formatCompactStateSummary
135
+ // ---------------------------------------------------------------------------
136
+
137
+ describe("formatCompactStateSummary", () => {
138
+ it("formats a compact page state into a readable summary", () => {
139
+ /** @type {import('../state.ts').CompactPageState} */
140
+ const mockState = {
141
+ url: "http://localhost:3000/dashboard",
142
+ title: "Dashboard",
143
+ focus: "input#search",
144
+ headings: ["Welcome", "Recent Activity"],
145
+ bodyText: "",
146
+ counts: {
147
+ landmarks: 3,
148
+ buttons: 5,
149
+ links: 12,
150
+ inputs: 2,
151
+ },
152
+ dialog: { count: 0, title: "" },
153
+ selectorStates: {},
154
+ };
155
+
156
+ const summary = formatCompactStateSummary(mockState);
157
+ assert.ok(summary.includes("Title: Dashboard"));
158
+ assert.ok(summary.includes("URL: http://localhost:3000/dashboard"));
159
+ assert.ok(summary.includes("3 landmarks"));
160
+ assert.ok(summary.includes("5 buttons"));
161
+ assert.ok(summary.includes("12 links"));
162
+ assert.ok(summary.includes("2 inputs"));
163
+ assert.ok(summary.includes("Focused: input#search"));
164
+ assert.ok(summary.includes('H1 "Welcome"'));
165
+ assert.ok(summary.includes('H2 "Recent Activity"'));
166
+ });
167
+
168
+ it("omits focus line when empty", () => {
169
+ const mockState = {
170
+ url: "http://example.com",
171
+ title: "Test",
172
+ focus: "",
173
+ headings: [],
174
+ bodyText: "",
175
+ counts: { landmarks: 0, buttons: 0, links: 0, inputs: 0 },
176
+ dialog: { count: 0, title: "" },
177
+ selectorStates: {},
178
+ };
179
+ const summary = formatCompactStateSummary(mockState);
180
+ assert.ok(!summary.includes("Focused:"));
181
+ });
182
+
183
+ it("includes dialog title when present", () => {
184
+ const mockState = {
185
+ url: "http://example.com",
186
+ title: "Test",
187
+ focus: "",
188
+ headings: [],
189
+ bodyText: "",
190
+ counts: { landmarks: 0, buttons: 0, links: 0, inputs: 0 },
191
+ dialog: { count: 1, title: "Confirm Delete" },
192
+ selectorStates: {},
193
+ };
194
+ const summary = formatCompactStateSummary(mockState);
195
+ assert.ok(summary.includes('Active dialog: "Confirm Delete"'));
196
+ });
197
+ });
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // utils.ts — verificationFromChecks
201
+ // ---------------------------------------------------------------------------
202
+
203
+ describe("verificationFromChecks", () => {
204
+ it("returns verified=true when at least one check passes", () => {
205
+ const checks = [
206
+ { name: "url_changed", passed: true },
207
+ { name: "title_changed", passed: false },
208
+ ];
209
+ const result = verificationFromChecks(checks);
210
+ assert.equal(result.verified, true);
211
+ assert.ok(result.verificationSummary.includes("PASS"));
212
+ assert.ok(result.verificationSummary.includes("url_changed"));
213
+ assert.equal(result.retryHint, undefined);
214
+ });
215
+
216
+ it("returns verified=false when no checks pass", () => {
217
+ const checks = [
218
+ { name: "url_changed", passed: false },
219
+ { name: "title_changed", passed: false },
220
+ ];
221
+ const result = verificationFromChecks(checks, "try clicking again");
222
+ assert.equal(result.verified, false);
223
+ assert.ok(result.verificationSummary.includes("SOFT-FAIL"));
224
+ assert.equal(result.retryHint, "try clicking again");
225
+ });
226
+
227
+ it("lists multiple passing checks", () => {
228
+ const checks = [
229
+ { name: "a", passed: true },
230
+ { name: "b", passed: true },
231
+ ];
232
+ const result = verificationFromChecks(checks);
233
+ assert.ok(result.verificationSummary.includes("a"));
234
+ assert.ok(result.verificationSummary.includes("b"));
235
+ });
236
+ });
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // utils.ts — verificationLine
240
+ // ---------------------------------------------------------------------------
241
+
242
+ describe("verificationLine", () => {
243
+ it("formats a verification result into a single line", () => {
244
+ const result = {
245
+ verified: true,
246
+ checks: [],
247
+ verificationSummary: "PASS (url_changed)",
248
+ };
249
+ const line = verificationLine(result);
250
+ assert.equal(line, "Verification: PASS (url_changed)");
251
+ });
252
+ });
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // utils.ts — sanitizeArtifactName
256
+ // ---------------------------------------------------------------------------
257
+
258
+ describe("sanitizeArtifactName", () => {
259
+ it("passes through valid names", () => {
260
+ assert.equal(sanitizeArtifactName("my-trace", "default"), "my-trace");
261
+ });
262
+
263
+ it("replaces special characters with hyphens", () => {
264
+ assert.equal(sanitizeArtifactName("hello world!@#", "default"), "hello-world");
265
+ });
266
+
267
+ it("strips leading/trailing hyphens", () => {
268
+ assert.equal(sanitizeArtifactName(" --foo-- ", "default"), "foo");
269
+ });
270
+
271
+ it("returns fallback for empty string", () => {
272
+ assert.equal(sanitizeArtifactName("", "fallback"), "fallback");
273
+ });
274
+
275
+ it("returns fallback for whitespace-only string", () => {
276
+ assert.equal(sanitizeArtifactName(" ", "fallback"), "fallback");
277
+ });
278
+
279
+ it("returns fallback for all-special-chars string", () => {
280
+ assert.equal(sanitizeArtifactName("@#$%", "default"), "default");
281
+ });
282
+
283
+ it("preserves dots and underscores", () => {
284
+ assert.equal(sanitizeArtifactName("file_name.ext", "default"), "file_name.ext");
285
+ });
286
+ });
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // utils.ts — isCriticalResourceType
290
+ // ---------------------------------------------------------------------------
291
+
292
+ describe("isCriticalResourceType", () => {
293
+ it("returns true for document", () => {
294
+ assert.equal(isCriticalResourceType("document"), true);
295
+ });
296
+
297
+ it("returns true for fetch", () => {
298
+ assert.equal(isCriticalResourceType("fetch"), true);
299
+ });
300
+
301
+ it("returns true for xhr", () => {
302
+ assert.equal(isCriticalResourceType("xhr"), true);
303
+ });
304
+
305
+ it("returns false for image", () => {
306
+ assert.equal(isCriticalResourceType("image"), false);
307
+ });
308
+
309
+ it("returns false for font", () => {
310
+ assert.equal(isCriticalResourceType("font"), false);
311
+ });
312
+
313
+ it("returns false for stylesheet", () => {
314
+ assert.equal(isCriticalResourceType("stylesheet"), false);
315
+ });
316
+
317
+ it("returns false for script", () => {
318
+ assert.equal(isCriticalResourceType("script"), false);
319
+ });
320
+ });
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // utils.ts — getUrlHash
324
+ // ---------------------------------------------------------------------------
325
+
326
+ describe("getUrlHash", () => {
327
+ it("returns the hash from a URL", () => {
328
+ assert.equal(getUrlHash("http://example.com/page#section"), "#section");
329
+ });
330
+
331
+ it("returns empty string when no hash", () => {
332
+ assert.equal(getUrlHash("http://example.com/page"), "");
333
+ });
334
+
335
+ it("returns empty string for invalid URL", () => {
336
+ assert.equal(getUrlHash("not-a-url"), "");
337
+ });
338
+ });
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // utils.ts — firstErrorLine
342
+ // ---------------------------------------------------------------------------
343
+
344
+ describe("firstErrorLine", () => {
345
+ it("extracts first line from an Error", () => {
346
+ const err = new Error("line1\nline2\nline3");
347
+ assert.equal(firstErrorLine(err), "line1");
348
+ });
349
+
350
+ it("handles string errors", () => {
351
+ assert.equal(firstErrorLine("something broke"), "something broke");
352
+ });
353
+
354
+ it("handles null/undefined", () => {
355
+ assert.equal(firstErrorLine(null), "unknown error");
356
+ assert.equal(firstErrorLine(undefined), "unknown error");
357
+ });
358
+
359
+ it("handles objects without message property", () => {
360
+ // {} has no .message, so falls to String({}) = "[object Object]"
361
+ assert.equal(firstErrorLine({}), "[object Object]");
362
+ });
363
+
364
+ it("handles objects with empty message", () => {
365
+ assert.equal(firstErrorLine({ message: "" }), "unknown error");
366
+ });
367
+ });
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // utils.ts — formatArtifactTimestamp
371
+ // ---------------------------------------------------------------------------
372
+
373
+ describe("formatArtifactTimestamp", () => {
374
+ it("formats a timestamp into an ISO-like string with dashes", () => {
375
+ // 2024-01-15T10:30:45.123Z
376
+ const ts = new Date("2024-01-15T10:30:45.123Z").getTime();
377
+ const result = formatArtifactTimestamp(ts);
378
+ // Should replace colons and dots with dashes
379
+ assert.ok(!result.includes(":"));
380
+ assert.ok(!result.includes("."));
381
+ assert.ok(result.includes("2024-01-15"));
382
+ });
383
+ });
384
+
385
+ // ---------------------------------------------------------------------------
386
+ // evaluate-helpers.ts — EVALUATE_HELPERS_SOURCE
387
+ // ---------------------------------------------------------------------------
388
+
389
+ describe("EVALUATE_HELPERS_SOURCE", () => {
390
+ it("is a parseable string (valid JavaScript)", () => {
391
+ assert.doesNotThrow(() => {
392
+ new Function(EVALUATE_HELPERS_SOURCE);
393
+ });
394
+ });
395
+
396
+ const expectedFunctions = [
397
+ "cssPath",
398
+ "simpleHash",
399
+ "isVisible",
400
+ "isEnabled",
401
+ "inferRole",
402
+ "accessibleName",
403
+ "isInteractiveEl",
404
+ "domPath",
405
+ "selectorHints",
406
+ ];
407
+
408
+ for (const fnName of expectedFunctions) {
409
+ it(`contains assignment for pi.${fnName}`, () => {
410
+ assert.ok(
411
+ EVALUATE_HELPERS_SOURCE.includes(`pi.${fnName} = function`),
412
+ `Expected pi.${fnName} = function assignment in source`,
413
+ );
414
+ });
415
+ }
416
+ });
417
+
418
+ // ---------------------------------------------------------------------------
419
+ // state.ts — accessor round-trips
420
+ // ---------------------------------------------------------------------------
421
+
422
+ describe("state accessors", () => {
423
+ beforeEach(() => {
424
+ resetAllState();
425
+ });
426
+
427
+ it("setBrowser/getBrowser round-trip", () => {
428
+ assert.equal(getBrowser(), null);
429
+ const fakeBrowser = { close: () => {} };
430
+ setBrowser(fakeBrowser);
431
+ assert.equal(getBrowser(), fakeBrowser);
432
+ });
433
+
434
+ it("setContext/getContext round-trip", () => {
435
+ assert.equal(getContext(), null);
436
+ const fakeContext = { newPage: () => {} };
437
+ setContext(fakeContext);
438
+ assert.equal(getContext(), fakeContext);
439
+ });
440
+
441
+ it("setActiveFrame/getActiveFrame round-trip", () => {
442
+ assert.equal(getActiveFrame(), null);
443
+ const fakeFrame = { name: () => "test" };
444
+ setActiveFrame(fakeFrame);
445
+ assert.equal(getActiveFrame(), fakeFrame);
446
+ });
447
+
448
+ it("setSessionStartedAt/getSessionStartedAt round-trip", () => {
449
+ assert.equal(getSessionStartedAt(), null);
450
+ setSessionStartedAt(1234567890);
451
+ assert.equal(getSessionStartedAt(), 1234567890);
452
+ });
453
+
454
+ it("setSessionArtifactDir/getSessionArtifactDir round-trip", () => {
455
+ assert.equal(getSessionArtifactDir(), null);
456
+ setSessionArtifactDir("/tmp/artifacts");
457
+ assert.equal(getSessionArtifactDir(), "/tmp/artifacts");
458
+ });
459
+
460
+ it("setCurrentRefMap/getCurrentRefMap round-trip", () => {
461
+ assert.deepStrictEqual(getCurrentRefMap(), {});
462
+ const refMap = { e1: { ref: "e1", tag: "button" } };
463
+ setCurrentRefMap(refMap);
464
+ assert.deepStrictEqual(getCurrentRefMap(), refMap);
465
+ });
466
+
467
+ it("setRefVersion/getRefVersion round-trip", () => {
468
+ assert.equal(getRefVersion(), 0);
469
+ setRefVersion(5);
470
+ assert.equal(getRefVersion(), 5);
471
+ });
472
+
473
+ it("setRefMetadata/getRefMetadata round-trip", () => {
474
+ assert.equal(getRefMetadata(), null);
475
+ const metadata = { url: "http://test.com", timestamp: 123, interactiveOnly: true, limit: 40, version: 1 };
476
+ setRefMetadata(metadata);
477
+ assert.deepStrictEqual(getRefMetadata(), metadata);
478
+ });
479
+
480
+ it("setLastActionBeforeState/getLastActionBeforeState round-trip", () => {
481
+ assert.equal(getLastActionBeforeState(), null);
482
+ const state = { url: "http://test.com", title: "Test", focus: "", headings: [], bodyText: "", counts: { landmarks: 0, buttons: 0, links: 0, inputs: 0 }, dialog: { count: 0, title: "" }, selectorStates: {} };
483
+ setLastActionBeforeState(state);
484
+ assert.deepStrictEqual(getLastActionBeforeState(), state);
485
+ });
486
+
487
+ it("setLastActionAfterState/getLastActionAfterState round-trip", () => {
488
+ assert.equal(getLastActionAfterState(), null);
489
+ const state = { url: "http://test.com/after", title: "After", focus: "", headings: [], bodyText: "", counts: { landmarks: 0, buttons: 0, links: 0, inputs: 0 }, dialog: { count: 0, title: "" }, selectorStates: {} };
490
+ setLastActionAfterState(state);
491
+ assert.deepStrictEqual(getLastActionAfterState(), state);
492
+ });
493
+ });
494
+
495
+ // ---------------------------------------------------------------------------
496
+ // state.ts — resetAllState
497
+ // ---------------------------------------------------------------------------
498
+
499
+ describe("resetAllState", () => {
500
+ it("clears all state back to defaults", () => {
501
+ // Set various state values
502
+ setBrowser({ close: () => {} });
503
+ setContext({ newPage: () => {} });
504
+ setActiveFrame({ name: () => "frame" });
505
+ setSessionStartedAt(9999);
506
+ setSessionArtifactDir("/tmp/test");
507
+ setCurrentRefMap({ e1: {} });
508
+ setRefVersion(10);
509
+ setRefMetadata({ url: "http://x", timestamp: 1, interactiveOnly: true, limit: 40, version: 1 });
510
+ setLastActionBeforeState({ url: "before" });
511
+ setLastActionAfterState({ url: "after" });
512
+
513
+ // Reset
514
+ resetAllState();
515
+
516
+ // Verify all cleared
517
+ assert.equal(getBrowser(), null);
518
+ assert.equal(getContext(), null);
519
+ assert.equal(getActiveFrame(), null);
520
+ assert.equal(getSessionStartedAt(), null);
521
+ assert.equal(getSessionArtifactDir(), null);
522
+ assert.deepStrictEqual(getCurrentRefMap(), {});
523
+ assert.equal(getRefVersion(), 0);
524
+ assert.equal(getRefMetadata(), null);
525
+ assert.equal(getLastActionBeforeState(), null);
526
+ assert.equal(getLastActionAfterState(), null);
527
+ });
528
+ });
529
+
530
+ // ---------------------------------------------------------------------------
531
+ // capture.ts — constrainScreenshot
532
+ // ---------------------------------------------------------------------------
533
+
534
+ describe("constrainScreenshot", () => {
535
+ // Helper: create a synthetic JPEG buffer via sharp
536
+ async function createTestJpeg(width, height) {
537
+ const sharp = require("sharp");
538
+ return sharp({
539
+ create: {
540
+ width,
541
+ height,
542
+ channels: 3,
543
+ background: { r: 128, g: 128, b: 128 },
544
+ },
545
+ })
546
+ .jpeg({ quality: 80 })
547
+ .toBuffer();
548
+ }
549
+
550
+ // Helper: create a synthetic PNG buffer via sharp
551
+ async function createTestPng(width, height) {
552
+ const sharp = require("sharp");
553
+ return sharp({
554
+ create: {
555
+ width,
556
+ height,
557
+ channels: 4,
558
+ background: { r: 128, g: 128, b: 128, alpha: 1 },
559
+ },
560
+ })
561
+ .png()
562
+ .toBuffer();
563
+ }
564
+
565
+ it("passes through a small JPEG unchanged", async () => {
566
+ const buf = await createTestJpeg(800, 600);
567
+ const result = await constrainScreenshot(null, buf, "image/jpeg", 80);
568
+ // Should return the same buffer (no resize needed)
569
+ assert.equal(Buffer.isBuffer(result), true);
570
+ const sharp = require("sharp");
571
+ const meta = await sharp(result).metadata();
572
+ assert.equal(meta.width, 800);
573
+ assert.equal(meta.height, 600);
574
+ });
575
+
576
+ it("resizes an oversized JPEG within 1568px", async () => {
577
+ const buf = await createTestJpeg(3000, 2000);
578
+ const result = await constrainScreenshot(null, buf, "image/jpeg", 80);
579
+ assert.equal(Buffer.isBuffer(result), true);
580
+
581
+ const sharp = require("sharp");
582
+ const meta = await sharp(result).metadata();
583
+ // Both dimensions should be <= 1568
584
+ assert.ok(meta.width <= 1568, `width ${meta.width} should be <= 1568`);
585
+ assert.ok(meta.height <= 1568, `height ${meta.height} should be <= 1568`);
586
+ // Aspect ratio preserved: 3000/2000 = 1.5, so width = 1568, height ~= 1045
587
+ assert.equal(meta.width, 1568);
588
+ assert.ok(meta.height > 1000 && meta.height < 1100);
589
+ assert.equal(meta.format, "jpeg");
590
+ });
591
+
592
+ it("resizes an oversized PNG and returns PNG", async () => {
593
+ const buf = await createTestPng(2500, 1800);
594
+ const result = await constrainScreenshot(null, buf, "image/png", 80);
595
+ assert.equal(Buffer.isBuffer(result), true);
596
+
597
+ const sharp = require("sharp");
598
+ const meta = await sharp(result).metadata();
599
+ assert.ok(meta.width <= 1568, `width ${meta.width} should be <= 1568`);
600
+ assert.ok(meta.height <= 1568, `height ${meta.height} should be <= 1568`);
601
+ assert.equal(meta.format, "png");
602
+ });
603
+
604
+ it("handles an image where only height exceeds the limit", async () => {
605
+ const buf = await createTestJpeg(1000, 2000);
606
+ const result = await constrainScreenshot(null, buf, "image/jpeg", 80);
607
+ const sharp = require("sharp");
608
+ const meta = await sharp(result).metadata();
609
+ assert.ok(meta.width <= 1568);
610
+ assert.ok(meta.height <= 1568);
611
+ // Height was the constraining dimension
612
+ assert.equal(meta.height, 1568);
613
+ });
614
+ });