From 9972f3029f07a12ff28fda8398de3c22e7c714df Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 30 Mar 2026 15:58:04 -0400 Subject: [PATCH] fix: accept empty rendered diff output --- CHANGELOG.md | 1 + .../diffs/src/tool-render-output.test.ts | 99 +++++++++++++++++++ extensions/diffs/src/tool.ts | 2 +- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 extensions/diffs/src/tool-render-output.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ec1fbbe638..fe50f59e83f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai - Flows/tasks: add a minimal SQLite-backed flow registry plus task-to-flow linkage scaffolding, so orchestrated work can start gaining a first-class parent record without changing current task delivery behavior. - Flows/tasks: route one-task ACP and subagent updates through a parent flow owner context, so detached work can emerge back through the intended parent thread/session instead of speaking only as a raw child task. - Matrix/history: add optional room history context for Matrix group triggers via `channels.matrix.historyLimit`, with per-agent watermarks and retry-safe snapshots so failed trigger retries do not drift into newer room messages. (#57022) thanks @chain710. +- Diffs: skip unused viewer-versus-file SSR preload work so `diffs` view-only and file-only runs do less render work while keeping mode outputs aligned. (#57909) thanks @gumadeiras. ### Fixes diff --git a/extensions/diffs/src/tool-render-output.test.ts b/extensions/diffs/src/tool-render-output.test.ts new file mode 100644 index 00000000000..97d838b1054 --- /dev/null +++ b/extensions/diffs/src/tool-render-output.test.ts @@ -0,0 +1,99 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTestPluginApi } from "../../../test/helpers/plugins/plugin-api.js"; +import type { OpenClawPluginApi } from "../api.js"; +import type { DiffScreenshotter } from "./browser.js"; +import { DEFAULT_DIFFS_TOOL_DEFAULTS } from "./config.js"; +import { createDiffStoreHarness } from "./test-helpers.js"; + +const { renderDiffDocumentMock } = vi.hoisted(() => ({ + renderDiffDocumentMock: vi.fn(), +})); + +vi.mock("./render.js", () => ({ + renderDiffDocument: renderDiffDocumentMock, +})); + +describe("diffs tool rendered output guards", () => { + let cleanupRootDir: () => Promise; + let store: Awaited>["store"]; + + beforeEach(async () => { + vi.resetModules(); + renderDiffDocumentMock.mockReset(); + ({ store, cleanup: cleanupRootDir } = await createDiffStoreHarness( + "openclaw-diffs-tool-render-output-", + )); + }); + + afterEach(async () => { + await cleanupRootDir(); + }); + + it("accepts empty string image html for file output", async () => { + renderDiffDocumentMock.mockResolvedValue({ + title: "Text diff", + fileCount: 1, + inputKind: "before_after", + imageHtml: "", + }); + + const { createDiffsTool } = await import("./tool.js"); + const screenshotter = createPngScreenshotter({ + assertHtml: (html) => { + expect(html).toBe(""); + }, + }); + + const tool = createDiffsTool({ + api: createApi(), + store, + defaults: DEFAULT_DIFFS_TOOL_DEFAULTS, + screenshotter, + }); + + const result = await tool.execute?.("tool-empty-image-html", { + before: "one\n", + after: "two\n", + mode: "file", + }); + + expect(screenshotter.screenshotHtml).toHaveBeenCalledTimes(1); + expect((result?.details as Record).filePath).toEqual(expect.any(String)); + }); +}); + +function createApi(): OpenClawPluginApi { + return createTestPluginApi({ + id: "diffs", + name: "Diffs", + description: "Diffs", + source: "test", + config: { + gateway: { + port: 18789, + bind: "loopback", + }, + }, + runtime: {} as OpenClawPluginApi["runtime"], + }) as OpenClawPluginApi; +} + +function createPngScreenshotter( + params: { + assertHtml?: (html: string) => void; + } = {}, +): DiffScreenshotter { + const screenshotHtml: DiffScreenshotter["screenshotHtml"] = vi.fn( + async ({ html, outputPath }: { html: string; outputPath: string }) => { + params.assertHtml?.(html); + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.writeFile(outputPath, Buffer.from("png")); + return outputPath; + }, + ); + return { + screenshotHtml, + }; +} diff --git a/extensions/diffs/src/tool.ts b/extensions/diffs/src/tool.ts index 92adad8f351..9a990c10141 100644 --- a/extensions/diffs/src/tool.ts +++ b/extensions/diffs/src/tool.ts @@ -341,7 +341,7 @@ function resolveRenderTarget(mode: DiffMode): DiffRenderTarget { } function requireRenderedHtml(html: string | undefined, target: DiffRenderTarget): string { - if (html) { + if (html !== undefined) { return html; } throw new Error(`Missing ${target} render output.`);