test: collapse utility plugin suites

This commit is contained in:
Peter Steinberger
2026-03-25 04:22:51 +00:00
parent 410c2dba65
commit ed9646516d
14 changed files with 875 additions and 897 deletions

View File

@@ -1,7 +1,12 @@
import fs from "node:fs/promises";
import type { IncomingMessage } from "node:http";
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createMockServerResponse } from "../../../test/helpers/extensions/mock-http-response.js";
import { createTestPluginApi } from "../../../test/helpers/extensions/plugin-api.js";
import type { OpenClawConfig } from "../api.js";
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "../api.js";
import plugin from "../index.js";
import { createTempDiffRoot } from "./test-helpers.js";
const { launchMock } = vi.hoisted(() => ({
@@ -187,6 +192,128 @@ describe("PlaywrightDiffScreenshotter", () => {
});
});
describe("diffs plugin registration", () => {
it("registers the tool, http route, and system-prompt guidance hook", async () => {
const registerTool = vi.fn();
const registerHttpRoute = vi.fn();
const on = vi.fn();
plugin.register?.(
createTestPluginApi({
id: "diffs",
name: "Diffs",
description: "Diffs",
source: "test",
config: {},
runtime: {} as never,
registerTool,
registerHttpRoute,
on,
}),
);
expect(registerTool).toHaveBeenCalledTimes(1);
expect(registerHttpRoute).toHaveBeenCalledTimes(1);
expect(registerHttpRoute.mock.calls[0]?.[0]).toMatchObject({
path: "/plugins/diffs",
auth: "plugin",
match: "prefix",
});
expect(on).toHaveBeenCalledTimes(1);
expect(on.mock.calls[0]?.[0]).toBe("before_prompt_build");
const beforePromptBuild = on.mock.calls[0]?.[1];
const result = await beforePromptBuild?.({}, {});
expect(result).toMatchObject({
prependSystemContext: expect.stringContaining("prefer the `diffs` tool"),
});
expect(result?.prependContext).toBeUndefined();
});
it("applies plugin-config defaults through registered tool and viewer handler", async () => {
type RegisteredTool = {
execute?: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
};
type RegisteredHttpRouteParams = Parameters<OpenClawPluginApi["registerHttpRoute"]>[0];
let registeredToolFactory:
| ((ctx: OpenClawPluginToolContext) => RegisteredTool | RegisteredTool[] | null | undefined)
| undefined;
let registeredHttpRouteHandler: RegisteredHttpRouteParams["handler"] | undefined;
const api = createTestPluginApi({
id: "diffs",
name: "Diffs",
description: "Diffs",
source: "test",
config: {
gateway: {
port: 18789,
bind: "loopback",
},
},
pluginConfig: {
defaults: {
mode: "view",
theme: "light",
background: false,
layout: "split",
showLineNumbers: false,
diffIndicators: "classic",
lineSpacing: 2,
},
},
runtime: {} as never,
registerTool(tool: Parameters<OpenClawPluginApi["registerTool"]>[0]) {
registeredToolFactory = typeof tool === "function" ? tool : () => tool;
},
registerHttpRoute(params: RegisteredHttpRouteParams) {
registeredHttpRouteHandler = params.handler;
},
});
plugin.register?.(api as unknown as OpenClawPluginApi);
const registeredTool = registeredToolFactory?.({
agentId: "main",
sessionId: "session-123",
messageChannel: "discord",
agentAccountId: "default",
}) as RegisteredTool | undefined;
const result = await registeredTool?.execute?.("tool-1", {
before: "one\n",
after: "two\n",
});
const viewerPath = String(
(result as { details?: Record<string, unknown> } | undefined)?.details?.viewerPath,
);
const res = createMockServerResponse();
const handled = await registeredHttpRouteHandler?.(
localReq({
method: "GET",
url: viewerPath,
}),
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(String(res.body)).toContain('body data-theme="light"');
expect(String(res.body)).toContain('"backgroundEnabled":false');
expect(String(res.body)).toContain('"diffStyle":"split"');
expect(String(res.body)).toContain('"disableLineNumbers":true');
expect(String(res.body)).toContain('"diffIndicators":"classic"');
expect(String(res.body)).toContain("--diffs-line-height: 30px;");
expect((result as { details?: Record<string, unknown> } | undefined)?.details?.context).toEqual(
{
agentId: "main",
sessionId: "session-123",
messageChannel: "discord",
agentAccountId: "default",
},
);
});
});
function createConfig(): OpenClawConfig {
return {
browser: {
@@ -195,6 +322,18 @@ function createConfig(): OpenClawConfig {
} as OpenClawConfig;
}
function localReq(input: {
method: string;
url: string;
headers?: IncomingMessage["headers"];
}): IncomingMessage {
return {
...input,
headers: input.headers ?? {},
socket: { remoteAddress: "127.0.0.1" },
} as unknown as IncomingMessage;
}
async function createScreenshotterHarness(options?: {
boundingBox?: { x: number; y: number; width: number; height: number };
}) {

View File

@@ -8,6 +8,10 @@ import {
resolveDiffsPluginDefaults,
resolveDiffsPluginSecurity,
} from "./config.js";
import { renderDiffDocument } from "./render.js";
import { buildViewerUrl, normalizeViewerBaseUrl } from "./url.js";
import { getServedViewerAsset, VIEWER_LOADER_PATH, VIEWER_RUNTIME_PATH } from "./viewer-assets.js";
import { parseViewerPayloadJson } from "./viewer-payload.js";
const FULL_DEFAULTS = {
fontFamily: "JetBrains Mono",
@@ -177,3 +181,232 @@ describe("diffs plugin schema surfaces", () => {
expect(diffsPluginConfigSchema.jsonSchema).toEqual(manifest.configSchema);
});
});
describe("diffs viewer URL helpers", () => {
it("defaults to loopback for lan/tailnet bind modes", () => {
expect(
buildViewerUrl({
config: { gateway: { bind: "lan", port: 18789 } },
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("http://127.0.0.1:18789/plugins/diffs/view/id/token");
expect(
buildViewerUrl({
config: { gateway: { bind: "tailnet", port: 24444 } },
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("http://127.0.0.1:24444/plugins/diffs/view/id/token");
});
it("uses custom bind host when provided", () => {
expect(
buildViewerUrl({
config: {
gateway: {
bind: "custom",
customBindHost: "gateway.example.com",
port: 443,
tls: { enabled: true },
},
},
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("https://gateway.example.com/plugins/diffs/view/id/token");
});
it("joins viewer path under baseUrl pathname", () => {
expect(
buildViewerUrl({
config: {},
baseUrl: "https://example.com/openclaw",
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("https://example.com/openclaw/plugins/diffs/view/id/token");
});
it("rejects base URLs with query/hash", () => {
expect(() => normalizeViewerBaseUrl("https://example.com?a=1")).toThrow(
"baseUrl must not include query/hash",
);
expect(() => normalizeViewerBaseUrl("https://example.com#frag")).toThrow(
"baseUrl must not include query/hash",
);
});
});
describe("renderDiffDocument", () => {
it("renders before/after input into a complete viewer document", async () => {
const rendered = await renderDiffDocument(
{
kind: "before_after",
before: "const value = 1;\n",
after: "const value = 2;\n",
path: "src/example.ts",
},
{
presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
expandUnchanged: false,
},
);
expect(rendered.title).toBe("src/example.ts");
expect(rendered.fileCount).toBe(1);
expect(rendered.html).toContain("data-openclaw-diff-root");
expect(rendered.html).toContain("src/example.ts");
expect(rendered.html).toContain("/plugins/diffs/assets/viewer.js");
expect(rendered.imageHtml).toContain("/plugins/diffs/assets/viewer.js");
expect(rendered.imageHtml).toContain("max-width: 960px;");
expect(rendered.imageHtml).toContain("--diffs-font-size: 16px;");
expect(rendered.html).toContain("min-height: 100vh;");
expect(rendered.html).toContain('"diffIndicators":"bars"');
expect(rendered.html).toContain('"disableLineNumbers":false');
expect(rendered.html).toContain("--diffs-line-height: 24px;");
expect(rendered.html).toContain("--diffs-font-size: 15px;");
expect(rendered.html).not.toContain("fonts.googleapis.com");
});
it("renders multi-file patch input", async () => {
const patch = [
"diff --git a/a.ts b/a.ts",
"--- a/a.ts",
"+++ b/a.ts",
"@@ -1 +1 @@",
"-const a = 1;",
"+const a = 2;",
"diff --git a/b.ts b/b.ts",
"--- a/b.ts",
"+++ b/b.ts",
"@@ -1 +1 @@",
"-const b = 1;",
"+const b = 2;",
].join("\n");
const rendered = await renderDiffDocument(
{
kind: "patch",
patch,
title: "Workspace patch",
},
{
presentation: {
...DEFAULT_DIFFS_TOOL_DEFAULTS,
layout: "split",
theme: "dark",
},
image: resolveDiffImageRenderOptions({
defaults: DEFAULT_DIFFS_TOOL_DEFAULTS,
fileQuality: "hq",
fileMaxWidth: 1180,
}),
expandUnchanged: true,
},
);
expect(rendered.title).toBe("Workspace patch");
expect(rendered.fileCount).toBe(2);
expect(rendered.html).toContain("Workspace patch");
expect(rendered.imageHtml).toContain("max-width: 1180px;");
});
it("rejects patches that exceed file-count limits", async () => {
const patch = Array.from({ length: 129 }, (_, i) => {
return [
`diff --git a/f${i}.ts b/f${i}.ts`,
`--- a/f${i}.ts`,
`+++ b/f${i}.ts`,
"@@ -1 +1 @@",
"-const x = 1;",
"+const x = 2;",
].join("\n");
}).join("\n");
await expect(
renderDiffDocument(
{
kind: "patch",
patch,
},
{
presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
expandUnchanged: false,
},
),
).rejects.toThrow("too many files");
});
});
describe("viewer assets", () => {
it("serves a stable loader that points at the current runtime bundle", async () => {
const loader = await getServedViewerAsset(VIEWER_LOADER_PATH);
expect(loader?.contentType).toBe("text/javascript; charset=utf-8");
expect(String(loader?.body)).toContain(`${VIEWER_RUNTIME_PATH}?v=`);
});
it("serves the runtime bundle body", async () => {
const runtime = await getServedViewerAsset(VIEWER_RUNTIME_PATH);
expect(runtime?.contentType).toBe("text/javascript; charset=utf-8");
expect(String(runtime?.body)).toContain("openclawDiffsReady");
});
it("returns null for unknown asset paths", async () => {
await expect(getServedViewerAsset("/plugins/diffs/assets/not-real.js")).resolves.toBeNull();
});
});
describe("parseViewerPayloadJson", () => {
function buildValidPayload(): Record<string, unknown> {
return {
prerenderedHTML: "<div>ok</div>",
langs: ["text"],
oldFile: {
name: "README.md",
contents: "before",
},
newFile: {
name: "README.md",
contents: "after",
},
options: {
theme: {
light: "pierre-light",
dark: "pierre-dark",
},
diffStyle: "unified",
diffIndicators: "bars",
disableLineNumbers: false,
expandUnchanged: false,
themeType: "dark",
backgroundEnabled: true,
overflow: "wrap",
unsafeCSS: ":host{}",
},
};
}
it("accepts valid payload JSON", () => {
const parsed = parseViewerPayloadJson(JSON.stringify(buildValidPayload()));
expect(parsed.options.diffStyle).toBe("unified");
expect(parsed.options.diffIndicators).toBe("bars");
});
it("rejects payloads with invalid shape", () => {
const broken = buildValidPayload();
broken.options = {
...(broken.options as Record<string, unknown>),
diffIndicators: "invalid",
};
expect(() => parseViewerPayloadJson(JSON.stringify(broken))).toThrow(
"Diff payload has invalid shape.",
);
});
it("rejects invalid JSON", () => {
expect(() => parseViewerPayloadJson("{not-json")).toThrow("Diff payload is not valid JSON.");
});
});

View File

@@ -1,206 +0,0 @@
import type { IncomingMessage } from "node:http";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createMockServerResponse } from "../../../test/helpers/extensions/mock-http-response.js";
import { createDiffsHttpHandler } from "./http.js";
import { DiffArtifactStore } from "./store.js";
import { createDiffStoreHarness } from "./test-helpers.js";
describe("createDiffsHttpHandler", () => {
let store: DiffArtifactStore;
let cleanupRootDir: () => Promise<void>;
async function handleLocalGet(url: string) {
const handler = createDiffsHttpHandler({ store });
const res = createMockServerResponse();
const handled = await handler(
localReq({
method: "GET",
url,
}),
res,
);
return { handled, res };
}
beforeEach(async () => {
({ store, cleanup: cleanupRootDir } = await createDiffStoreHarness("openclaw-diffs-http-"));
});
afterEach(async () => {
await cleanupRootDir();
});
it("serves a stored diff document", async () => {
const artifact = await createViewerArtifact(store);
const { handled, res } = await handleLocalGet(artifact.viewerPath);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(res.body).toBe("<html>viewer</html>");
expect(res.getHeader("content-security-policy")).toContain("default-src 'none'");
});
it("rejects invalid tokens", async () => {
const artifact = await createViewerArtifact(store);
const { handled, res } = await handleLocalGet(
artifact.viewerPath.replace(artifact.token, "bad-token"),
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(404);
});
it("rejects malformed artifact ids before reading from disk", async () => {
const handler = createDiffsHttpHandler({ store });
const res = createMockServerResponse();
const handled = await handler(
localReq({
method: "GET",
url: "/plugins/diffs/view/not-a-real-id/not-a-real-token",
}),
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(404);
});
it("serves the shared viewer asset", async () => {
const handler = createDiffsHttpHandler({ store });
const res = createMockServerResponse();
const handled = await handler(
localReq({
method: "GET",
url: "/plugins/diffs/assets/viewer.js",
}),
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(String(res.body)).toContain("/plugins/diffs/assets/viewer-runtime.js?v=");
});
it("serves the shared viewer runtime asset", async () => {
const handler = createDiffsHttpHandler({ store });
const res = createMockServerResponse();
const handled = await handler(
localReq({
method: "GET",
url: "/plugins/diffs/assets/viewer-runtime.js",
}),
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(String(res.body)).toContain("openclawDiffsReady");
});
it.each([
{
name: "blocks non-loopback viewer access by default",
request: remoteReq,
allowRemoteViewer: false,
expectedStatusCode: 404,
},
{
name: "blocks loopback requests that carry proxy forwarding headers by default",
request: localReq,
headers: { "x-forwarded-for": "203.0.113.10" },
allowRemoteViewer: false,
expectedStatusCode: 404,
},
{
name: "allows remote access when allowRemoteViewer is enabled",
request: remoteReq,
allowRemoteViewer: true,
expectedStatusCode: 200,
},
{
name: "allows proxied loopback requests when allowRemoteViewer is enabled",
request: localReq,
headers: { "x-forwarded-for": "203.0.113.10" },
allowRemoteViewer: true,
expectedStatusCode: 200,
},
])("$name", async ({ request, headers, allowRemoteViewer, expectedStatusCode }) => {
const artifact = await createViewerArtifact(store);
const handler = createDiffsHttpHandler({ store, allowRemoteViewer });
const res = createMockServerResponse();
const handled = await handler(
request({
method: "GET",
url: artifact.viewerPath,
headers,
}),
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(expectedStatusCode);
if (expectedStatusCode === 200) {
expect(res.body).toBe("<html>viewer</html>");
}
});
it("rate-limits repeated remote misses", async () => {
const handler = createDiffsHttpHandler({ store, allowRemoteViewer: true });
for (let i = 0; i < 40; i++) {
const miss = createMockServerResponse();
await handler(
remoteReq({
method: "GET",
url: "/plugins/diffs/view/aaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}),
miss,
);
expect(miss.statusCode).toBe(404);
}
const limited = createMockServerResponse();
await handler(
remoteReq({
method: "GET",
url: "/plugins/diffs/view/aaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}),
limited,
);
expect(limited.statusCode).toBe(429);
});
});
async function createViewerArtifact(store: DiffArtifactStore) {
return await store.createArtifact({
html: "<html>viewer</html>",
title: "Demo",
inputKind: "before_after",
fileCount: 1,
});
}
function localReq(input: {
method: string;
url: string;
headers?: Record<string, string>;
}): IncomingMessage {
return {
...input,
headers: input.headers ?? {},
socket: { remoteAddress: "127.0.0.1" },
} as unknown as IncomingMessage;
}
function remoteReq(input: {
method: string;
url: string;
headers?: Record<string, string>;
}): IncomingMessage {
return {
...input,
headers: input.headers ?? {},
socket: { remoteAddress: "203.0.113.10" },
} as unknown as IncomingMessage;
}

View File

@@ -1,106 +0,0 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_DIFFS_TOOL_DEFAULTS, resolveDiffImageRenderOptions } from "./config.js";
import { renderDiffDocument } from "./render.js";
describe("renderDiffDocument", () => {
it("renders before/after input into a complete viewer document", async () => {
const rendered = await renderDiffDocument(
{
kind: "before_after",
before: "const value = 1;\n",
after: "const value = 2;\n",
path: "src/example.ts",
},
{
presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
expandUnchanged: false,
},
);
expect(rendered.title).toBe("src/example.ts");
expect(rendered.fileCount).toBe(1);
expect(rendered.html).toContain("data-openclaw-diff-root");
expect(rendered.html).toContain("src/example.ts");
expect(rendered.html).toContain("/plugins/diffs/assets/viewer.js");
expect(rendered.imageHtml).toContain("/plugins/diffs/assets/viewer.js");
expect(rendered.imageHtml).toContain("max-width: 960px;");
expect(rendered.imageHtml).toContain("--diffs-font-size: 16px;");
expect(rendered.html).toContain("min-height: 100vh;");
expect(rendered.html).toContain('"diffIndicators":"bars"');
expect(rendered.html).toContain('"disableLineNumbers":false');
expect(rendered.html).toContain("--diffs-line-height: 24px;");
expect(rendered.html).toContain("--diffs-font-size: 15px;");
expect(rendered.html).not.toContain("fonts.googleapis.com");
});
it("renders multi-file patch input", async () => {
const patch = [
"diff --git a/a.ts b/a.ts",
"--- a/a.ts",
"+++ b/a.ts",
"@@ -1 +1 @@",
"-const a = 1;",
"+const a = 2;",
"diff --git a/b.ts b/b.ts",
"--- a/b.ts",
"+++ b/b.ts",
"@@ -1 +1 @@",
"-const b = 1;",
"+const b = 2;",
].join("\n");
const rendered = await renderDiffDocument(
{
kind: "patch",
patch,
title: "Workspace patch",
},
{
presentation: {
...DEFAULT_DIFFS_TOOL_DEFAULTS,
layout: "split",
theme: "dark",
},
image: resolveDiffImageRenderOptions({
defaults: DEFAULT_DIFFS_TOOL_DEFAULTS,
fileQuality: "hq",
fileMaxWidth: 1180,
}),
expandUnchanged: true,
},
);
expect(rendered.title).toBe("Workspace patch");
expect(rendered.fileCount).toBe(2);
expect(rendered.html).toContain("Workspace patch");
expect(rendered.imageHtml).toContain("max-width: 1180px;");
});
it("rejects patches that exceed file-count limits", async () => {
const patch = Array.from({ length: 129 }, (_, i) => {
return [
`diff --git a/f${i}.ts b/f${i}.ts`,
`--- a/f${i}.ts`,
`+++ b/f${i}.ts`,
"@@ -1 +1 @@",
"-const x = 1;",
"+const x = 2;",
].join("\n");
}).join("\n");
await expect(
renderDiffDocument(
{
kind: "patch",
patch,
},
{
presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
expandUnchanged: false,
},
),
).rejects.toThrow("too many files");
});
});

View File

@@ -1,6 +1,9 @@
import fs from "node:fs/promises";
import type { IncomingMessage } from "node:http";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createMockServerResponse } from "../../../test/helpers/extensions/mock-http-response.js";
import { createDiffsHttpHandler } from "./http.js";
import { DiffArtifactStore } from "./store.js";
import { createDiffStoreHarness } from "./test-helpers.js";
@@ -211,3 +214,203 @@ describe("DiffArtifactStore", () => {
expect(cleanupSpy).toHaveBeenCalledTimes(2);
});
});
describe("createDiffsHttpHandler", () => {
let store: DiffArtifactStore;
let cleanupRootDir: () => Promise<void>;
async function handleLocalGet(url: string) {
const handler = createDiffsHttpHandler({ store });
const res = createMockServerResponse();
const handled = await handler(
localReq({
method: "GET",
url,
}),
res,
);
return { handled, res };
}
beforeEach(async () => {
({ store, cleanup: cleanupRootDir } = await createDiffStoreHarness("openclaw-diffs-http-"));
});
afterEach(async () => {
await cleanupRootDir();
});
it("serves a stored diff document", async () => {
const artifact = await createViewerArtifact(store);
const { handled, res } = await handleLocalGet(artifact.viewerPath);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(res.body).toBe("<html>viewer</html>");
expect(res.getHeader("content-security-policy")).toContain("default-src 'none'");
});
it("rejects invalid tokens", async () => {
const artifact = await createViewerArtifact(store);
const { handled, res } = await handleLocalGet(
artifact.viewerPath.replace(artifact.token, "bad-token"),
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(404);
});
it("rejects malformed artifact ids before reading from disk", async () => {
const handler = createDiffsHttpHandler({ store });
const res = createMockServerResponse();
const handled = await handler(
localReq({
method: "GET",
url: "/plugins/diffs/view/not-a-real-id/not-a-real-token",
}),
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(404);
});
it("serves the shared viewer asset", async () => {
const handler = createDiffsHttpHandler({ store });
const res = createMockServerResponse();
const handled = await handler(
localReq({
method: "GET",
url: "/plugins/diffs/assets/viewer.js",
}),
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(String(res.body)).toContain("/plugins/diffs/assets/viewer-runtime.js?v=");
});
it("serves the shared viewer runtime asset", async () => {
const handler = createDiffsHttpHandler({ store });
const res = createMockServerResponse();
const handled = await handler(
localReq({
method: "GET",
url: "/plugins/diffs/assets/viewer-runtime.js",
}),
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(String(res.body)).toContain("openclawDiffsReady");
});
it.each([
{
name: "blocks non-loopback viewer access by default",
request: remoteReq,
allowRemoteViewer: false,
expectedStatusCode: 404,
},
{
name: "blocks loopback requests that carry proxy forwarding headers by default",
request: localReq,
headers: { "x-forwarded-for": "203.0.113.10" },
allowRemoteViewer: false,
expectedStatusCode: 404,
},
{
name: "allows remote access when allowRemoteViewer is enabled",
request: remoteReq,
allowRemoteViewer: true,
expectedStatusCode: 200,
},
{
name: "allows proxied loopback requests when allowRemoteViewer is enabled",
request: localReq,
headers: { "x-forwarded-for": "203.0.113.10" },
allowRemoteViewer: true,
expectedStatusCode: 200,
},
])("$name", async ({ request, headers, allowRemoteViewer, expectedStatusCode }) => {
const artifact = await createViewerArtifact(store);
const handler = createDiffsHttpHandler({ store, allowRemoteViewer });
const res = createMockServerResponse();
const handled = await handler(
request({
method: "GET",
url: artifact.viewerPath,
headers,
}),
res,
);
expect(handled).toBe(true);
expect(res.statusCode).toBe(expectedStatusCode);
if (expectedStatusCode === 200) {
expect(res.body).toBe("<html>viewer</html>");
}
});
it("rate-limits repeated remote misses", async () => {
const handler = createDiffsHttpHandler({ store, allowRemoteViewer: true });
for (let i = 0; i < 40; i++) {
const miss = createMockServerResponse();
await handler(
remoteReq({
method: "GET",
url: "/plugins/diffs/view/aaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}),
miss,
);
expect(miss.statusCode).toBe(404);
}
const limited = createMockServerResponse();
await handler(
remoteReq({
method: "GET",
url: "/plugins/diffs/view/aaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}),
limited,
);
expect(limited.statusCode).toBe(429);
});
});
async function createViewerArtifact(store: DiffArtifactStore) {
return await store.createArtifact({
html: "<html>viewer</html>",
title: "Demo",
inputKind: "before_after",
fileCount: 1,
});
}
function localReq(input: {
method: string;
url: string;
headers?: Record<string, string>;
}): IncomingMessage {
return {
...input,
headers: input.headers ?? {},
socket: { remoteAddress: "127.0.0.1" },
} as unknown as IncomingMessage;
}
function remoteReq(input: {
method: string;
url: string;
headers?: Record<string, string>;
}): IncomingMessage {
return {
...input,
headers: input.headers ?? {},
socket: { remoteAddress: "203.0.113.10" },
} as unknown as IncomingMessage;
}

View File

@@ -1,55 +0,0 @@
import { describe, expect, it } from "vitest";
import { buildViewerUrl, normalizeViewerBaseUrl } from "./url.js";
describe("diffs viewer URL helpers", () => {
it("defaults to loopback for lan/tailnet bind modes", () => {
expect(
buildViewerUrl({
config: { gateway: { bind: "lan", port: 18789 } },
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("http://127.0.0.1:18789/plugins/diffs/view/id/token");
expect(
buildViewerUrl({
config: { gateway: { bind: "tailnet", port: 24444 } },
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("http://127.0.0.1:24444/plugins/diffs/view/id/token");
});
it("uses custom bind host when provided", () => {
expect(
buildViewerUrl({
config: {
gateway: {
bind: "custom",
customBindHost: "gateway.example.com",
port: 443,
tls: { enabled: true },
},
},
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("https://gateway.example.com/plugins/diffs/view/id/token");
});
it("joins viewer path under baseUrl pathname", () => {
expect(
buildViewerUrl({
config: {},
baseUrl: "https://example.com/openclaw",
viewerPath: "/plugins/diffs/view/id/token",
}),
).toBe("https://example.com/openclaw/plugins/diffs/view/id/token");
});
it("rejects base URLs with query/hash", () => {
expect(() => normalizeViewerBaseUrl("https://example.com?a=1")).toThrow(
"baseUrl must not include query/hash",
);
expect(() => normalizeViewerBaseUrl("https://example.com#frag")).toThrow(
"baseUrl must not include query/hash",
);
});
});

View File

@@ -1,22 +0,0 @@
import { describe, expect, it } from "vitest";
import { getServedViewerAsset, VIEWER_LOADER_PATH, VIEWER_RUNTIME_PATH } from "./viewer-assets.js";
describe("viewer assets", () => {
it("serves a stable loader that points at the current runtime bundle", async () => {
const loader = await getServedViewerAsset(VIEWER_LOADER_PATH);
expect(loader?.contentType).toBe("text/javascript; charset=utf-8");
expect(String(loader?.body)).toContain(`${VIEWER_RUNTIME_PATH}?v=`);
});
it("serves the runtime bundle body", async () => {
const runtime = await getServedViewerAsset(VIEWER_RUNTIME_PATH);
expect(runtime?.contentType).toBe("text/javascript; charset=utf-8");
expect(String(runtime?.body)).toContain("openclawDiffsReady");
});
it("returns null for unknown asset paths", async () => {
await expect(getServedViewerAsset("/plugins/diffs/assets/not-real.js")).resolves.toBeNull();
});
});

View File

@@ -1,55 +0,0 @@
import { describe, expect, it } from "vitest";
import { parseViewerPayloadJson } from "./viewer-payload.js";
function buildValidPayload(): Record<string, unknown> {
return {
prerenderedHTML: "<div>ok</div>",
langs: ["text"],
oldFile: {
name: "README.md",
contents: "before",
},
newFile: {
name: "README.md",
contents: "after",
},
options: {
theme: {
light: "pierre-light",
dark: "pierre-dark",
},
diffStyle: "unified",
diffIndicators: "bars",
disableLineNumbers: false,
expandUnchanged: false,
themeType: "dark",
backgroundEnabled: true,
overflow: "wrap",
unsafeCSS: ":host{}",
},
};
}
describe("parseViewerPayloadJson", () => {
it("accepts valid payload JSON", () => {
const parsed = parseViewerPayloadJson(JSON.stringify(buildValidPayload()));
expect(parsed.options.diffStyle).toBe("unified");
expect(parsed.options.diffIndicators).toBe("bars");
});
it("rejects payloads with invalid shape", () => {
const broken = buildValidPayload();
broken.options = {
...(broken.options as Record<string, unknown>),
diffIndicators: "invalid",
};
expect(() => parseViewerPayloadJson(JSON.stringify(broken))).toThrow(
"Diff payload has invalid shape.",
);
});
it("rejects invalid JSON", () => {
expect(() => parseViewerPayloadJson("{not-json")).toThrow("Diff payload is not valid JSON.");
});
});