mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-29 10:50:58 +00:00
413 lines
12 KiB
TypeScript
413 lines
12 KiB
TypeScript
import fs from "node:fs";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
DEFAULT_DIFFS_PLUGIN_SECURITY,
|
|
DEFAULT_DIFFS_TOOL_DEFAULTS,
|
|
diffsPluginConfigSchema,
|
|
resolveDiffImageRenderOptions,
|
|
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",
|
|
fontSize: 17,
|
|
lineSpacing: 1.8,
|
|
layout: "split",
|
|
showLineNumbers: false,
|
|
diffIndicators: "classic",
|
|
wordWrap: false,
|
|
background: false,
|
|
theme: "light",
|
|
fileFormat: "pdf",
|
|
fileQuality: "hq",
|
|
fileScale: 2.6,
|
|
fileMaxWidth: 1280,
|
|
mode: "file",
|
|
} as const;
|
|
|
|
describe("resolveDiffsPluginDefaults", () => {
|
|
it("returns built-in defaults when config is missing", () => {
|
|
expect(resolveDiffsPluginDefaults(undefined)).toEqual(DEFAULT_DIFFS_TOOL_DEFAULTS);
|
|
});
|
|
|
|
it("applies configured defaults from plugin config", () => {
|
|
expect(
|
|
resolveDiffsPluginDefaults({
|
|
defaults: FULL_DEFAULTS,
|
|
}),
|
|
).toEqual(FULL_DEFAULTS);
|
|
});
|
|
|
|
it("clamps and falls back for invalid line spacing and indicators", () => {
|
|
expect(
|
|
resolveDiffsPluginDefaults({
|
|
defaults: {
|
|
lineSpacing: -5,
|
|
diffIndicators: "unknown",
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
lineSpacing: 1,
|
|
diffIndicators: "bars",
|
|
});
|
|
|
|
expect(
|
|
resolveDiffsPluginDefaults({
|
|
defaults: {
|
|
lineSpacing: 9,
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
lineSpacing: 3,
|
|
});
|
|
|
|
expect(
|
|
resolveDiffsPluginDefaults({
|
|
defaults: {
|
|
lineSpacing: Number.NaN,
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
lineSpacing: DEFAULT_DIFFS_TOOL_DEFAULTS.lineSpacing,
|
|
});
|
|
});
|
|
|
|
it("derives file defaults from quality preset and clamps explicit overrides", () => {
|
|
expect(
|
|
resolveDiffsPluginDefaults({
|
|
defaults: {
|
|
fileQuality: "print",
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
fileQuality: "print",
|
|
fileScale: 3,
|
|
fileMaxWidth: 1400,
|
|
});
|
|
|
|
expect(
|
|
resolveDiffsPluginDefaults({
|
|
defaults: {
|
|
fileQuality: "hq",
|
|
fileScale: 99,
|
|
fileMaxWidth: 99999,
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
fileQuality: "hq",
|
|
fileScale: 4,
|
|
fileMaxWidth: 2400,
|
|
});
|
|
});
|
|
|
|
it("falls back to png for invalid file format defaults", () => {
|
|
expect(
|
|
resolveDiffsPluginDefaults({
|
|
defaults: {
|
|
fileFormat: "invalid" as "png",
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
fileFormat: "png",
|
|
});
|
|
});
|
|
|
|
it("resolves file render format from defaults and explicit overrides", () => {
|
|
const defaults = resolveDiffsPluginDefaults({
|
|
defaults: {
|
|
fileFormat: "pdf",
|
|
},
|
|
});
|
|
|
|
expect(resolveDiffImageRenderOptions({ defaults }).format).toBe("pdf");
|
|
expect(resolveDiffImageRenderOptions({ defaults, fileFormat: "png" }).format).toBe("png");
|
|
expect(resolveDiffImageRenderOptions({ defaults, format: "png" }).format).toBe("png");
|
|
});
|
|
|
|
it("accepts format as a config alias for fileFormat", () => {
|
|
expect(
|
|
resolveDiffsPluginDefaults({
|
|
defaults: {
|
|
format: "pdf",
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
fileFormat: "pdf",
|
|
});
|
|
});
|
|
|
|
it("accepts image* config aliases for backward compatibility", () => {
|
|
expect(
|
|
resolveDiffsPluginDefaults({
|
|
defaults: {
|
|
imageFormat: "pdf",
|
|
imageQuality: "hq",
|
|
imageScale: 2.2,
|
|
imageMaxWidth: 1024,
|
|
},
|
|
}),
|
|
).toMatchObject({
|
|
fileFormat: "pdf",
|
|
fileQuality: "hq",
|
|
fileScale: 2.2,
|
|
fileMaxWidth: 1024,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("resolveDiffsPluginSecurity", () => {
|
|
it("defaults to local-only viewer access", () => {
|
|
expect(resolveDiffsPluginSecurity(undefined)).toEqual(DEFAULT_DIFFS_PLUGIN_SECURITY);
|
|
});
|
|
|
|
it("allows opt-in remote viewer access", () => {
|
|
expect(resolveDiffsPluginSecurity({ security: { allowRemoteViewer: true } })).toEqual({
|
|
allowRemoteViewer: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("diffs plugin schema surfaces", () => {
|
|
it("keeps the runtime json schema in sync with the manifest config schema", () => {
|
|
const manifest = JSON.parse(
|
|
fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"),
|
|
) as { configSchema?: unknown };
|
|
|
|
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.");
|
|
});
|
|
});
|