
























@@ -4,11 +4,17 @@ import { Buffer } from "node:buffer";
44import fs from "node:fs/promises";
55import os from "node:os";
66import path from "node:path";
7-import { describe, expect, it } from "vitest";
7+import { beforeEach, describe, expect, it, vi } from "vitest";
88import { withEnvAsync } from "../../../test-utils/env.js";
99import { createReadToolDefinition } from "./read.js";
1010import { DEFAULT_MAX_BYTES } from "./truncate.js";
111112+const decodeWindowsTextFileBufferMock = vi.hoisted(() => vi.fn(() => ""));
13+14+vi.mock("../../../infra/windows-encoding.js", () => ({
15+decodeWindowsTextFileBuffer: decodeWindowsTextFileBufferMock,
16+}));
17+1218const ONE_PIXEL_PNG_BASE64 =
1319"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=";
1420@@ -20,6 +26,10 @@ function textContent(
2026}
21272228describe("read tool", () => {
29+beforeEach(() => {
30+decodeWindowsTextFileBufferMock.mockReset();
31+});
32+2333it("reads managed inbound media refs as image files", async () => {
2434const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-read-media-"));
2535const mediaId = `read-tool-${Date.now()}-${Math.random().toString(36).slice(2)}.png`;
@@ -100,4 +110,71 @@ describe("read tool", () => {
100110101111expect(textContent(result)).toBe("alpha\n\n[2 more lines in file. Use offset=2 to continue.]");
102112});
113+114+it("uses the shared Windows decoder for local filesystem reads", async () => {
115+const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-read-encoding-"));
116+const filePath = path.join(tempDir, "legacy.txt");
117+const legacyBytes = Buffer.from([0xc4, 0xe3, 0xba, 0xc3]);
118+decodeWindowsTextFileBufferMock.mockReturnValueOnce("decoded legacy text");
119+120+try {
121+await fs.writeFile(filePath, legacyBytes);
122+const tool = createReadToolDefinition(tempDir);
123+const result = await tool.execute(
124+"call-1",
125+{ path: "legacy.txt" },
126+undefined,
127+undefined,
128+{} as never,
129+);
130+131+expect(decodeWindowsTextFileBufferMock).toHaveBeenCalledWith({ buffer: legacyBytes });
132+expect(textContent(result)).toBe("decoded legacy text");
133+} finally {
134+await fs.rm(tempDir, { recursive: true, force: true });
135+}
136+});
137+138+it("leaves injected read operation decoding owner-controlled", async () => {
139+const bytes = Buffer.from([0xc4, 0xe3, 0xba, 0xc3]);
140+const tool = createReadToolDefinition("/workspace", {
141+operations: {
142+access: async () => {},
143+detectImageMimeType: async () => null,
144+readFile: async () => bytes,
145+},
146+});
147+const result = await tool.execute(
148+"call-1",
149+{ path: "legacy.txt" },
150+undefined,
151+undefined,
152+{} as never,
153+);
154+155+expect(decodeWindowsTextFileBufferMock).not.toHaveBeenCalled();
156+expect(textContent(result)).toBe(bytes.toString("utf8"));
157+});
158+159+it("uses an injected backend decoder when declared", async () => {
160+const bytes = Buffer.from([0xc4, 0xe3, 0xba, 0xc3]);
161+const tool = createReadToolDefinition("/workspace", {
162+operations: {
163+decodeText: ({ buffer, absolutePath }) => `${absolutePath}:${buffer.toString("hex")}`,
164+access: async () => {},
165+detectImageMimeType: async () => null,
166+readFile: async () => bytes,
167+},
168+});
169+const result = await tool.execute(
170+"call-1",
171+{ path: "legacy.txt" },
172+undefined,
173+undefined,
174+{} as never,
175+);
176+177+expect(decodeWindowsTextFileBufferMock).not.toHaveBeenCalled();
178+expect(textContent(result)).toBe("/workspace/legacy.txt:c4e3bac3");
179+});
103180});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。