import fs from "node:fs"; import { disposeHighlighter, RegisteredCustomThemes, ResolvedThemes, ResolvingThemes, } from "@pierre/diffs"; import AjvPkg from "ajv"; 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, }); }); it("keeps loader-applied schema defaults from shadowing aliases and quality-derived defaults", () => { const manifest = JSON.parse( fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"), ) as { configSchema: Record }; const Ajv = AjvPkg as unknown as new (opts?: object) => import("ajv").default; const ajv = new Ajv({ allErrors: true, strict: false, useDefaults: true }); const validate = ajv.compile(manifest.configSchema); const aliasOnly = { defaults: { format: "pdf", imageQuality: "hq", }, }; expect(validate(aliasOnly)).toBe(true); expect(resolveDiffsPluginDefaults(aliasOnly)).toMatchObject({ fileFormat: "pdf", fileQuality: "hq", fileScale: 2.5, fileMaxWidth: 1200, }); const qualityOnly = { defaults: { fileQuality: "hq", }, }; expect(validate(qualityOnly)).toBe(true); expect(resolveDiffsPluginDefaults(qualityOnly)).toMatchObject({ fileQuality: "hq", fileScale: 2.5, fileMaxWidth: 1200, }); }); }); 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("preserves defaults and security for direct safeParse callers", () => { expect( diffsPluginConfigSchema.safeParse?.({ defaults: { theme: "light", }, security: { allowRemoteViewer: true, }, }), ).toMatchObject({ success: true, data: { defaults: { fontFamily: "Fira Code", fontSize: 15, lineSpacing: 1.6, layout: "unified", showLineNumbers: true, diffIndicators: "bars", wordWrap: true, background: true, theme: "light", fileFormat: "png", fileQuality: "standard", fileScale: 2, fileMaxWidth: 960, mode: "both", }, security: { allowRemoteViewer: true, }, }, }); }); it("canonicalizes alias-driven defaults for direct safeParse callers", () => { expect( diffsPluginConfigSchema.safeParse?.({ defaults: { format: "pdf", imageQuality: "hq", }, }), ).toMatchObject({ success: true, data: { defaults: { fileFormat: "pdf", fileQuality: "hq", fileScale: 2.5, fileMaxWidth: 1200, }, }, }); }); 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("../../assets/viewer.js"); expect(rendered.imageHtml).toContain("../../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("resolves viewer assets under an optional base path", async () => { const rendered = await renderDiffDocument( { kind: "before_after", before: "const value = 1;\n", after: "const value = 2;\n", }, { presentation: DEFAULT_DIFFS_TOOL_DEFAULTS, image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }), expandUnchanged: false, }, ); const html = rendered.html ?? ""; const loaderSrc = html.match(/