@kodelyth/diffs 2026.5.39 → 2026.5.42
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.
- package/README.md +217 -0
- package/api.ts +10 -0
- package/dist/api.js +3 -0
- package/dist/assets/viewer-runtime.js +21390 -0
- package/dist/index.js +2104 -0
- package/dist/runtime-api.js +2 -0
- package/index.ts +11 -0
- package/klaw.plugin.json +11 -47
- package/package.json +2 -2
- package/runtime-api.ts +1 -0
- package/src/browser.test.ts +686 -0
- package/src/browser.ts +564 -0
- package/src/config.test.ts +573 -0
- package/src/config.ts +443 -0
- package/src/http.ts +324 -0
- package/src/language-hints.test.ts +156 -0
- package/src/language-hints.ts +117 -0
- package/src/manifest.test.ts +16 -0
- package/src/pierre-themes.ts +59 -0
- package/src/plugin.ts +67 -0
- package/src/prompt-guidance.ts +7 -0
- package/src/render-target.test.ts +132 -0
- package/src/render.test.ts +219 -0
- package/src/render.ts +557 -0
- package/src/store.test.ts +462 -0
- package/src/store.ts +387 -0
- package/src/test-helpers.ts +30 -0
- package/src/tool-render-output.test.ts +107 -0
- package/src/tool.test.ts +646 -0
- package/src/tool.ts +547 -0
- package/src/types.ts +127 -0
- package/src/url.ts +60 -0
- package/src/viewer-assets.ts +103 -0
- package/src/viewer-client.ts +353 -0
- package/src/viewer-payload.ts +94 -0
- package/tsconfig.json +16 -0
- package/api.js +0 -7
- package/index.js +0 -7
- package/runtime-api.js +0 -7
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createTestPluginApi } from "klaw/plugin-sdk/plugin-test-api";
|
|
5
|
+
import { createMockServerResponse } from "klaw/plugin-sdk/test-env";
|
|
6
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import type { KlawConfig } from "../api.js";
|
|
8
|
+
import type { KlawPluginApi, KlawPluginToolContext } from "../api.js";
|
|
9
|
+
import { registerDiffsPlugin } from "./plugin.js";
|
|
10
|
+
import { createTempDiffRoot } from "./test-helpers.js";
|
|
11
|
+
|
|
12
|
+
const { launchMock } = vi.hoisted(() => ({
|
|
13
|
+
launchMock: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
let PlaywrightDiffScreenshotter: typeof import("./browser.js").PlaywrightDiffScreenshotter;
|
|
17
|
+
let resetSharedBrowserStateForTests: typeof import("./browser.js").resetSharedBrowserStateForTests;
|
|
18
|
+
|
|
19
|
+
vi.mock("playwright-core", () => ({
|
|
20
|
+
chromium: {
|
|
21
|
+
launch: launchMock,
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
function firstMockCall(
|
|
26
|
+
mock: { mock: { calls: Array<readonly unknown[]> } },
|
|
27
|
+
label: string,
|
|
28
|
+
): readonly unknown[] {
|
|
29
|
+
const call = mock.mock.calls[0];
|
|
30
|
+
if (!call) {
|
|
31
|
+
throw new Error(`expected ${label} call`);
|
|
32
|
+
}
|
|
33
|
+
return call;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
vi.doUnmock("playwright-core");
|
|
38
|
+
vi.resetModules();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("PlaywrightDiffScreenshotter", () => {
|
|
42
|
+
let rootDir: string;
|
|
43
|
+
let outputPath: string;
|
|
44
|
+
let cleanupRootDir: () => Promise<void>;
|
|
45
|
+
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
({ PlaywrightDiffScreenshotter, resetSharedBrowserStateForTests } =
|
|
48
|
+
await import("./browser.js"));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
vi.useFakeTimers();
|
|
53
|
+
({ rootDir, cleanup: cleanupRootDir } = await createTempDiffRoot("klaw-diffs-browser-"));
|
|
54
|
+
outputPath = path.join(rootDir, "preview.png");
|
|
55
|
+
launchMock.mockReset();
|
|
56
|
+
await resetSharedBrowserStateForTests();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(async () => {
|
|
60
|
+
await resetSharedBrowserStateForTests();
|
|
61
|
+
vi.useRealTimers();
|
|
62
|
+
await cleanupRootDir();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("reuses the same browser across renders and closes it after the idle window", async () => {
|
|
66
|
+
const { pages, browser, screenshotter } = await createScreenshotterHarness();
|
|
67
|
+
|
|
68
|
+
await screenshotter.screenshotHtml({
|
|
69
|
+
html: '<html><head></head><body><main class="oc-frame"></main></body></html>',
|
|
70
|
+
outputPath,
|
|
71
|
+
theme: "dark",
|
|
72
|
+
image: {
|
|
73
|
+
format: "png",
|
|
74
|
+
qualityPreset: "standard",
|
|
75
|
+
scale: 2,
|
|
76
|
+
maxWidth: 960,
|
|
77
|
+
maxPixels: 8_000_000,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
await screenshotter.screenshotHtml({
|
|
81
|
+
html: '<html><head></head><body><main class="oc-frame"></main></body></html>',
|
|
82
|
+
outputPath,
|
|
83
|
+
theme: "dark",
|
|
84
|
+
image: {
|
|
85
|
+
format: "png",
|
|
86
|
+
qualityPreset: "standard",
|
|
87
|
+
scale: 2,
|
|
88
|
+
maxWidth: 960,
|
|
89
|
+
maxPixels: 8_000_000,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(launchMock).toHaveBeenCalledTimes(1);
|
|
94
|
+
expect(browser.newPage).toHaveBeenCalledTimes(2);
|
|
95
|
+
const firstPageParams = (
|
|
96
|
+
browser.newPage.mock.calls as Array<[{ deviceScaleFactor?: number }?]>
|
|
97
|
+
)[0]?.[0];
|
|
98
|
+
expect(firstPageParams?.deviceScaleFactor).toBe(2);
|
|
99
|
+
expect(pages).toHaveLength(2);
|
|
100
|
+
expect(pages[0]?.close).toHaveBeenCalledTimes(1);
|
|
101
|
+
expect(pages[1]?.close).toHaveBeenCalledTimes(1);
|
|
102
|
+
|
|
103
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
104
|
+
expect(browser.close).toHaveBeenCalledTimes(1);
|
|
105
|
+
|
|
106
|
+
await screenshotter.screenshotHtml({
|
|
107
|
+
html: '<html><head></head><body><main class="oc-frame"></main></body></html>',
|
|
108
|
+
outputPath,
|
|
109
|
+
theme: "light",
|
|
110
|
+
image: {
|
|
111
|
+
format: "png",
|
|
112
|
+
qualityPreset: "standard",
|
|
113
|
+
scale: 2,
|
|
114
|
+
maxWidth: 960,
|
|
115
|
+
maxPixels: 8_000_000,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(launchMock).toHaveBeenCalledTimes(2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("renders PDF output when format is pdf", async () => {
|
|
123
|
+
const { pages, screenshotter } = await createScreenshotterHarness();
|
|
124
|
+
const pdfPath = path.join(rootDir, "preview.pdf");
|
|
125
|
+
|
|
126
|
+
await screenshotter.screenshotHtml({
|
|
127
|
+
html: '<html><head></head><body><main class="oc-frame"></main></body></html>',
|
|
128
|
+
outputPath: pdfPath,
|
|
129
|
+
theme: "light",
|
|
130
|
+
image: {
|
|
131
|
+
format: "pdf",
|
|
132
|
+
qualityPreset: "standard",
|
|
133
|
+
scale: 2,
|
|
134
|
+
maxWidth: 960,
|
|
135
|
+
maxPixels: 8_000_000,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(launchMock).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(pages).toHaveLength(1);
|
|
141
|
+
expect(pages[0]?.pdf).toHaveBeenCalledTimes(1);
|
|
142
|
+
const pdfCall = firstMockCall(pages[0]?.pdf, "PDF render")[0] as
|
|
143
|
+
| Record<string, unknown>
|
|
144
|
+
| undefined;
|
|
145
|
+
if (!pdfCall) {
|
|
146
|
+
throw new Error("expected PDF render call");
|
|
147
|
+
}
|
|
148
|
+
expect(pdfCall).not.toHaveProperty("pageRanges");
|
|
149
|
+
expect(pages[0]?.screenshot).toHaveBeenCalledTimes(0);
|
|
150
|
+
await expect(fs.readFile(pdfPath, "utf8")).resolves.toContain("%PDF-1.7");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("fails fast when PDF render exceeds size limits", async () => {
|
|
154
|
+
const pages: Array<{
|
|
155
|
+
close: ReturnType<typeof vi.fn>;
|
|
156
|
+
screenshot: ReturnType<typeof vi.fn>;
|
|
157
|
+
pdf: ReturnType<typeof vi.fn>;
|
|
158
|
+
}> = [];
|
|
159
|
+
const browser = createMockBrowser(pages, {
|
|
160
|
+
boundingBox: { x: 40, y: 40, width: 960, height: 60_000 },
|
|
161
|
+
});
|
|
162
|
+
launchMock.mockResolvedValue(browser);
|
|
163
|
+
const screenshotter = new PlaywrightDiffScreenshotter({
|
|
164
|
+
config: createConfig(),
|
|
165
|
+
browserIdleMs: 1_000,
|
|
166
|
+
});
|
|
167
|
+
const pdfPath = path.join(rootDir, "oversized.pdf");
|
|
168
|
+
|
|
169
|
+
await expect(
|
|
170
|
+
screenshotter.screenshotHtml({
|
|
171
|
+
html: '<html><head></head><body><main class="oc-frame"></main></body></html>',
|
|
172
|
+
outputPath: pdfPath,
|
|
173
|
+
theme: "light",
|
|
174
|
+
image: {
|
|
175
|
+
format: "pdf",
|
|
176
|
+
qualityPreset: "standard",
|
|
177
|
+
scale: 2,
|
|
178
|
+
maxWidth: 960,
|
|
179
|
+
maxPixels: 8_000_000,
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
).rejects.toThrow("Diff frame did not render within image size limits.");
|
|
183
|
+
|
|
184
|
+
expect(launchMock).toHaveBeenCalledTimes(1);
|
|
185
|
+
expect(pages).toHaveLength(1);
|
|
186
|
+
expect(pages[0]?.pdf).toHaveBeenCalledTimes(0);
|
|
187
|
+
expect(pages[0]?.screenshot).toHaveBeenCalledTimes(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("fails fast when maxPixels is still exceeded at scale 1", async () => {
|
|
191
|
+
const { pages, screenshotter } = await createScreenshotterHarness();
|
|
192
|
+
|
|
193
|
+
await expect(
|
|
194
|
+
screenshotter.screenshotHtml({
|
|
195
|
+
html: '<html><head></head><body><main class="oc-frame"></main></body></html>',
|
|
196
|
+
outputPath,
|
|
197
|
+
theme: "dark",
|
|
198
|
+
image: {
|
|
199
|
+
format: "png",
|
|
200
|
+
qualityPreset: "standard",
|
|
201
|
+
scale: 1,
|
|
202
|
+
maxWidth: 960,
|
|
203
|
+
maxPixels: 10,
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
).rejects.toThrow("Diff frame did not render within image size limits.");
|
|
207
|
+
expect(pages).toHaveLength(1);
|
|
208
|
+
expect(pages[0]?.screenshot).toHaveBeenCalledTimes(0);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("diffs plugin registration", () => {
|
|
213
|
+
it("uses live runtime tool config through the registered tool factory", async () => {
|
|
214
|
+
type RegisteredTool = {
|
|
215
|
+
execute?: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
216
|
+
};
|
|
217
|
+
type HttpRouteHandler = (
|
|
218
|
+
req: IncomingMessage,
|
|
219
|
+
res: ServerResponse,
|
|
220
|
+
) => boolean | Promise<boolean>;
|
|
221
|
+
type RegisteredHttpRouteParams = Parameters<KlawPluginApi["registerHttpRoute"]>[0];
|
|
222
|
+
|
|
223
|
+
let registeredToolFactory:
|
|
224
|
+
| ((ctx: KlawPluginToolContext) => RegisteredTool | RegisteredTool[] | null | undefined)
|
|
225
|
+
| undefined;
|
|
226
|
+
let registeredHttpRouteHandler: HttpRouteHandler | undefined;
|
|
227
|
+
let configFile: KlawConfig = {
|
|
228
|
+
gateway: {
|
|
229
|
+
port: 18789,
|
|
230
|
+
bind: "loopback",
|
|
231
|
+
},
|
|
232
|
+
plugins: {
|
|
233
|
+
entries: {
|
|
234
|
+
diffs: {
|
|
235
|
+
config: {
|
|
236
|
+
viewerBaseUrl: "https://startup.example.com/klaw",
|
|
237
|
+
defaults: {
|
|
238
|
+
mode: "view",
|
|
239
|
+
theme: "light",
|
|
240
|
+
background: false,
|
|
241
|
+
layout: "split",
|
|
242
|
+
showLineNumbers: false,
|
|
243
|
+
diffIndicators: "classic",
|
|
244
|
+
lineSpacing: 2,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
} as KlawConfig;
|
|
251
|
+
|
|
252
|
+
const api = createTestPluginApi({
|
|
253
|
+
id: "diffs",
|
|
254
|
+
name: "Diffs",
|
|
255
|
+
description: "Diffs",
|
|
256
|
+
source: "test",
|
|
257
|
+
config: {
|
|
258
|
+
gateway: {
|
|
259
|
+
port: 18789,
|
|
260
|
+
bind: "loopback",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
pluginConfig: {
|
|
264
|
+
viewerBaseUrl: "https://startup.example.com/klaw",
|
|
265
|
+
defaults: {
|
|
266
|
+
mode: "view",
|
|
267
|
+
theme: "light",
|
|
268
|
+
background: false,
|
|
269
|
+
layout: "split",
|
|
270
|
+
showLineNumbers: false,
|
|
271
|
+
diffIndicators: "classic",
|
|
272
|
+
lineSpacing: 2,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
runtime: {
|
|
276
|
+
config: {
|
|
277
|
+
current: () => configFile,
|
|
278
|
+
},
|
|
279
|
+
} as never,
|
|
280
|
+
registerTool(tool: Parameters<KlawPluginApi["registerTool"]>[0]) {
|
|
281
|
+
registeredToolFactory = typeof tool === "function" ? tool : () => tool;
|
|
282
|
+
},
|
|
283
|
+
registerHttpRoute(params: RegisteredHttpRouteParams) {
|
|
284
|
+
registeredHttpRouteHandler = params.handler as HttpRouteHandler;
|
|
285
|
+
},
|
|
286
|
+
on: vi.fn(),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
registerDiffsPlugin(api as unknown as KlawPluginApi);
|
|
290
|
+
|
|
291
|
+
configFile = {
|
|
292
|
+
...configFile,
|
|
293
|
+
plugins: {
|
|
294
|
+
entries: {
|
|
295
|
+
diffs: {
|
|
296
|
+
config: {
|
|
297
|
+
viewerBaseUrl: "https://live.example.com/gateway",
|
|
298
|
+
defaults: {
|
|
299
|
+
mode: "view",
|
|
300
|
+
theme: "dark",
|
|
301
|
+
background: true,
|
|
302
|
+
layout: "unified",
|
|
303
|
+
showLineNumbers: true,
|
|
304
|
+
diffIndicators: "bars",
|
|
305
|
+
lineSpacing: 1.6,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
} as KlawConfig;
|
|
312
|
+
|
|
313
|
+
const registeredTool = registeredToolFactory?.({
|
|
314
|
+
agentId: "main",
|
|
315
|
+
sessionId: "session-456",
|
|
316
|
+
messageChannel: "discord",
|
|
317
|
+
agentAccountId: "default",
|
|
318
|
+
}) as RegisteredTool | undefined;
|
|
319
|
+
const result = await registeredTool?.execute?.("tool-1", {
|
|
320
|
+
before: "one\n",
|
|
321
|
+
after: "two\n",
|
|
322
|
+
});
|
|
323
|
+
const details = (result as { details?: Record<string, unknown> } | undefined)?.details;
|
|
324
|
+
const viewerPath = String(details?.viewerPath);
|
|
325
|
+
const res = createMockServerResponse();
|
|
326
|
+
const handled = await registeredHttpRouteHandler?.(
|
|
327
|
+
localReq({
|
|
328
|
+
method: "GET",
|
|
329
|
+
url: viewerPath,
|
|
330
|
+
}),
|
|
331
|
+
res,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
expect(handled).toBe(true);
|
|
335
|
+
expect(String(details?.viewerUrl)).toContain("https://live.example.com/gateway");
|
|
336
|
+
expect(res.statusCode).toBe(200);
|
|
337
|
+
expect(String(res.body)).toContain('body data-theme="dark"');
|
|
338
|
+
expect(String(res.body)).toContain('"backgroundEnabled":true');
|
|
339
|
+
expect(String(res.body)).toContain('"diffStyle":"unified"');
|
|
340
|
+
expect(String(res.body)).toContain('"disableLineNumbers":false');
|
|
341
|
+
expect(String(res.body)).toContain('"diffIndicators":"bars"');
|
|
342
|
+
expect(String(res.body)).toContain("--diffs-line-height: 24px;");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("uses live runtime viewer-access config through the registered HTTP handler", async () => {
|
|
346
|
+
type RegisteredTool = {
|
|
347
|
+
execute?: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
348
|
+
};
|
|
349
|
+
type HttpRouteHandler = (
|
|
350
|
+
req: IncomingMessage,
|
|
351
|
+
res: ServerResponse,
|
|
352
|
+
) => boolean | Promise<boolean>;
|
|
353
|
+
type RegisteredHttpRouteParams = Parameters<KlawPluginApi["registerHttpRoute"]>[0];
|
|
354
|
+
|
|
355
|
+
let registeredToolFactory:
|
|
356
|
+
| ((ctx: KlawPluginToolContext) => RegisteredTool | RegisteredTool[] | null | undefined)
|
|
357
|
+
| undefined;
|
|
358
|
+
let registeredHttpRouteHandler: HttpRouteHandler | undefined;
|
|
359
|
+
const on = vi.fn();
|
|
360
|
+
let configFile: KlawConfig = {
|
|
361
|
+
gateway: {
|
|
362
|
+
port: 18789,
|
|
363
|
+
bind: "loopback",
|
|
364
|
+
},
|
|
365
|
+
plugins: {
|
|
366
|
+
entries: {
|
|
367
|
+
diffs: {
|
|
368
|
+
config: {
|
|
369
|
+
security: {
|
|
370
|
+
allowRemoteViewer: true,
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
} as KlawConfig;
|
|
377
|
+
|
|
378
|
+
const api = createTestPluginApi({
|
|
379
|
+
id: "diffs",
|
|
380
|
+
name: "Diffs",
|
|
381
|
+
description: "Diffs",
|
|
382
|
+
source: "test",
|
|
383
|
+
config: {
|
|
384
|
+
gateway: {
|
|
385
|
+
port: 18789,
|
|
386
|
+
bind: "loopback",
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
pluginConfig: {
|
|
390
|
+
defaults: {
|
|
391
|
+
mode: "view",
|
|
392
|
+
theme: "light",
|
|
393
|
+
background: false,
|
|
394
|
+
layout: "split",
|
|
395
|
+
showLineNumbers: false,
|
|
396
|
+
diffIndicators: "classic",
|
|
397
|
+
lineSpacing: 2,
|
|
398
|
+
},
|
|
399
|
+
security: {
|
|
400
|
+
allowRemoteViewer: true,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
runtime: {
|
|
404
|
+
config: {
|
|
405
|
+
current: () => configFile,
|
|
406
|
+
},
|
|
407
|
+
} as never,
|
|
408
|
+
registerTool(tool: Parameters<KlawPluginApi["registerTool"]>[0]) {
|
|
409
|
+
registeredToolFactory = typeof tool === "function" ? tool : () => tool;
|
|
410
|
+
},
|
|
411
|
+
registerHttpRoute(params: RegisteredHttpRouteParams) {
|
|
412
|
+
registeredHttpRouteHandler = params.handler as HttpRouteHandler;
|
|
413
|
+
},
|
|
414
|
+
on,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
registerDiffsPlugin(api as unknown as KlawPluginApi);
|
|
418
|
+
|
|
419
|
+
expect(on).toHaveBeenCalledTimes(1);
|
|
420
|
+
const [hookName, beforePromptBuild] = firstMockCall(on, "plugin hook registration");
|
|
421
|
+
expect(hookName).toBe("before_prompt_build");
|
|
422
|
+
if (typeof beforePromptBuild !== "function") {
|
|
423
|
+
throw new Error("expected before_prompt_build callback");
|
|
424
|
+
}
|
|
425
|
+
const promptResult = await beforePromptBuild({}, {});
|
|
426
|
+
expect(promptResult?.prependSystemContext).toBe(
|
|
427
|
+
[
|
|
428
|
+
"When you need to show edits as a real diff, prefer the `diffs` tool instead of writing a manual summary.",
|
|
429
|
+
"It accepts either `before` + `after` text or a unified `patch`.",
|
|
430
|
+
"`mode=view` returns `details.viewerUrl` for canvas use; `mode=file` returns `details.filePath`; `mode=both` returns both.",
|
|
431
|
+
"If you need to send the rendered file, use the `message` tool with `path` or `filePath`.",
|
|
432
|
+
"Include `path` when you know the filename, and omit presentation overrides unless needed.",
|
|
433
|
+
].join("\n"),
|
|
434
|
+
);
|
|
435
|
+
expect(promptResult?.prependContext).toBeUndefined();
|
|
436
|
+
|
|
437
|
+
const registeredTool = registeredToolFactory?.({
|
|
438
|
+
agentId: "main",
|
|
439
|
+
sessionId: "session-123",
|
|
440
|
+
messageChannel: "discord",
|
|
441
|
+
agentAccountId: "default",
|
|
442
|
+
}) as RegisteredTool | undefined;
|
|
443
|
+
const result = await registeredTool?.execute?.("tool-1", {
|
|
444
|
+
before: "one\n",
|
|
445
|
+
after: "two\n",
|
|
446
|
+
});
|
|
447
|
+
const viewerPath = String(
|
|
448
|
+
(result as { details?: Record<string, unknown> } | undefined)?.details?.viewerPath,
|
|
449
|
+
);
|
|
450
|
+
const res = createMockServerResponse();
|
|
451
|
+
const handled = await registeredHttpRouteHandler?.(
|
|
452
|
+
localReq({
|
|
453
|
+
method: "GET",
|
|
454
|
+
url: viewerPath,
|
|
455
|
+
}),
|
|
456
|
+
res,
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
expect(handled).toBe(true);
|
|
460
|
+
expect(res.statusCode).toBe(200);
|
|
461
|
+
expect((result as { details?: Record<string, unknown> } | undefined)?.details?.context).toEqual(
|
|
462
|
+
{
|
|
463
|
+
agentId: "main",
|
|
464
|
+
sessionId: "session-123",
|
|
465
|
+
messageChannel: "discord",
|
|
466
|
+
agentAccountId: "default",
|
|
467
|
+
},
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
configFile = {
|
|
471
|
+
...configFile,
|
|
472
|
+
plugins: {
|
|
473
|
+
entries: {
|
|
474
|
+
diffs: {
|
|
475
|
+
config: {
|
|
476
|
+
security: {
|
|
477
|
+
allowRemoteViewer: false,
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
} as KlawConfig;
|
|
484
|
+
|
|
485
|
+
const proxiedRes = createMockServerResponse();
|
|
486
|
+
const proxiedHandled = await registeredHttpRouteHandler?.(
|
|
487
|
+
localReq({
|
|
488
|
+
method: "GET",
|
|
489
|
+
url: viewerPath,
|
|
490
|
+
headers: {
|
|
491
|
+
"x-forwarded-for": "203.0.113.10",
|
|
492
|
+
},
|
|
493
|
+
}),
|
|
494
|
+
proxiedRes,
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
expect(proxiedHandled).toBe(true);
|
|
498
|
+
expect(proxiedRes.statusCode).toBe(404);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("fails closed for remote viewer access when the live diffs plugin entry is removed", async () => {
|
|
502
|
+
type RegisteredTool = {
|
|
503
|
+
execute?: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
504
|
+
};
|
|
505
|
+
type HttpRouteHandler = (
|
|
506
|
+
req: IncomingMessage,
|
|
507
|
+
res: ServerResponse,
|
|
508
|
+
) => boolean | Promise<boolean>;
|
|
509
|
+
type RegisteredHttpRouteParams = Parameters<KlawPluginApi["registerHttpRoute"]>[0];
|
|
510
|
+
|
|
511
|
+
let registeredToolFactory:
|
|
512
|
+
| ((ctx: KlawPluginToolContext) => RegisteredTool | RegisteredTool[] | null | undefined)
|
|
513
|
+
| undefined;
|
|
514
|
+
let registeredHttpRouteHandler: HttpRouteHandler | undefined;
|
|
515
|
+
let configFile: KlawConfig = {
|
|
516
|
+
gateway: {
|
|
517
|
+
port: 18789,
|
|
518
|
+
bind: "loopback",
|
|
519
|
+
},
|
|
520
|
+
plugins: {
|
|
521
|
+
entries: {
|
|
522
|
+
diffs: {
|
|
523
|
+
config: {
|
|
524
|
+
security: {
|
|
525
|
+
allowRemoteViewer: true,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
} as KlawConfig;
|
|
532
|
+
|
|
533
|
+
const api = createTestPluginApi({
|
|
534
|
+
id: "diffs",
|
|
535
|
+
name: "Diffs",
|
|
536
|
+
description: "Diffs",
|
|
537
|
+
source: "test",
|
|
538
|
+
config: {
|
|
539
|
+
gateway: {
|
|
540
|
+
port: 18789,
|
|
541
|
+
bind: "loopback",
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
pluginConfig: {
|
|
545
|
+
security: {
|
|
546
|
+
allowRemoteViewer: true,
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
runtime: {
|
|
550
|
+
config: {
|
|
551
|
+
current: () => configFile,
|
|
552
|
+
},
|
|
553
|
+
} as never,
|
|
554
|
+
registerTool(tool: Parameters<KlawPluginApi["registerTool"]>[0]) {
|
|
555
|
+
registeredToolFactory = typeof tool === "function" ? tool : () => tool;
|
|
556
|
+
},
|
|
557
|
+
registerHttpRoute(params: RegisteredHttpRouteParams) {
|
|
558
|
+
registeredHttpRouteHandler = params.handler as HttpRouteHandler;
|
|
559
|
+
},
|
|
560
|
+
on: vi.fn(),
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
registerDiffsPlugin(api as unknown as KlawPluginApi);
|
|
564
|
+
|
|
565
|
+
const registeredTool = registeredToolFactory?.({
|
|
566
|
+
agentId: "main",
|
|
567
|
+
sessionId: "session-789",
|
|
568
|
+
messageChannel: "discord",
|
|
569
|
+
agentAccountId: "default",
|
|
570
|
+
}) as RegisteredTool | undefined;
|
|
571
|
+
const result = await registeredTool?.execute?.("tool-1", {
|
|
572
|
+
before: "one\n",
|
|
573
|
+
after: "two\n",
|
|
574
|
+
});
|
|
575
|
+
const viewerPath = String(
|
|
576
|
+
(result as { details?: Record<string, unknown> } | undefined)?.details?.viewerPath,
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
configFile = {
|
|
580
|
+
...configFile,
|
|
581
|
+
plugins: {
|
|
582
|
+
entries: {},
|
|
583
|
+
},
|
|
584
|
+
} as KlawConfig;
|
|
585
|
+
|
|
586
|
+
const proxiedRes = createMockServerResponse();
|
|
587
|
+
const proxiedHandled = await registeredHttpRouteHandler?.(
|
|
588
|
+
localReq({
|
|
589
|
+
method: "GET",
|
|
590
|
+
url: viewerPath,
|
|
591
|
+
headers: {
|
|
592
|
+
"x-forwarded-for": "203.0.113.10",
|
|
593
|
+
},
|
|
594
|
+
}),
|
|
595
|
+
proxiedRes,
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
expect(proxiedHandled).toBe(true);
|
|
599
|
+
expect(proxiedRes.statusCode).toBe(404);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
function createConfig(): KlawConfig {
|
|
604
|
+
return {
|
|
605
|
+
browser: {
|
|
606
|
+
executablePath: process.execPath,
|
|
607
|
+
},
|
|
608
|
+
} as KlawConfig;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function localReq(input: {
|
|
612
|
+
method: string;
|
|
613
|
+
url: string;
|
|
614
|
+
headers?: IncomingMessage["headers"];
|
|
615
|
+
}): IncomingMessage {
|
|
616
|
+
return {
|
|
617
|
+
...input,
|
|
618
|
+
headers: input.headers ?? {},
|
|
619
|
+
socket: { remoteAddress: "127.0.0.1" },
|
|
620
|
+
} as unknown as IncomingMessage;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function createScreenshotterHarness(options?: {
|
|
624
|
+
boundingBox?: { x: number; y: number; width: number; height: number };
|
|
625
|
+
}) {
|
|
626
|
+
const pages: Array<{
|
|
627
|
+
close: ReturnType<typeof vi.fn>;
|
|
628
|
+
screenshot: ReturnType<typeof vi.fn>;
|
|
629
|
+
pdf: ReturnType<typeof vi.fn>;
|
|
630
|
+
}> = [];
|
|
631
|
+
const browser = createMockBrowser(pages, options);
|
|
632
|
+
launchMock.mockResolvedValue(browser);
|
|
633
|
+
const screenshotter = new PlaywrightDiffScreenshotter({
|
|
634
|
+
config: createConfig(),
|
|
635
|
+
browserIdleMs: 1_000,
|
|
636
|
+
});
|
|
637
|
+
return { pages, browser, screenshotter };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function createMockBrowser(
|
|
641
|
+
pages: Array<{
|
|
642
|
+
close: ReturnType<typeof vi.fn>;
|
|
643
|
+
screenshot: ReturnType<typeof vi.fn>;
|
|
644
|
+
pdf: ReturnType<typeof vi.fn>;
|
|
645
|
+
}>,
|
|
646
|
+
options?: { boundingBox?: { x: number; y: number; width: number; height: number } },
|
|
647
|
+
) {
|
|
648
|
+
const browser = {
|
|
649
|
+
newPage: vi.fn(async (_options?: unknown) => {
|
|
650
|
+
const page = createMockPage(options);
|
|
651
|
+
pages.push(page);
|
|
652
|
+
return page;
|
|
653
|
+
}),
|
|
654
|
+
close: vi.fn(async () => {}),
|
|
655
|
+
on: vi.fn(),
|
|
656
|
+
};
|
|
657
|
+
return browser;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function createMockPage(options?: {
|
|
661
|
+
boundingBox?: { x: number; y: number; width: number; height: number };
|
|
662
|
+
}) {
|
|
663
|
+
const box = options?.boundingBox ?? { x: 40, y: 40, width: 640, height: 240 };
|
|
664
|
+
const screenshot = vi.fn(async ({ path: screenshotPath }: { path: string }) => {
|
|
665
|
+
await fs.writeFile(screenshotPath, Buffer.from("png"));
|
|
666
|
+
});
|
|
667
|
+
const pdf = vi.fn(async ({ path: pdfPath }: { path: string }) => {
|
|
668
|
+
await fs.writeFile(pdfPath, "%PDF-1.7 mock");
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
route: vi.fn(async () => {}),
|
|
673
|
+
setContent: vi.fn(async () => {}),
|
|
674
|
+
waitForFunction: vi.fn(async () => {}),
|
|
675
|
+
evaluate: vi.fn(async () => 1),
|
|
676
|
+
emulateMedia: vi.fn(async () => {}),
|
|
677
|
+
locator: vi.fn(() => ({
|
|
678
|
+
waitFor: vi.fn(async () => {}),
|
|
679
|
+
boundingBox: vi.fn(async () => box),
|
|
680
|
+
})),
|
|
681
|
+
setViewportSize: vi.fn(async () => {}),
|
|
682
|
+
screenshot,
|
|
683
|
+
pdf,
|
|
684
|
+
close: vi.fn(async () => {}),
|
|
685
|
+
};
|
|
686
|
+
}
|