From 5b5a5bcc2619951cdee7901b0443f05fa99a03a6 Mon Sep 17 00:00:00 2001 From: wings1029 Date: Sun, 12 Jul 2026 05:39:06 +0800 Subject: [PATCH] fix(oc-path): report UTF-8 byte counts in set command output (#104496) * fix(oc-path): report UTF-8 byte counts in set command output Use Buffer.byteLength(newBytes, 'utf8') instead of string.length for bytesWritten and dry-run byte reports. JavaScript's string.length counts UTF-16 code units which undercounts multi-byte characters when writing UTF-8 files. Co-Authored-By: Claude * test(oc-path): cover human UTF-8 dry-run count --------- Co-authored-by: Claude Co-authored-by: Peter Steinberger --- extensions/oc-path/src/cli.test.ts | 43 ++++++++++++++++++++++++++++++ extensions/oc-path/src/cli.ts | 8 +++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/extensions/oc-path/src/cli.test.ts b/extensions/oc-path/src/cli.test.ts index d80af3240298..1dbe9853dbd2 100644 --- a/extensions/oc-path/src/cli.test.ts +++ b/extensions/oc-path/src/cli.test.ts @@ -169,6 +169,26 @@ describe("openclaw path CLI", () => { expect(readFileSync(filePath, "utf-8")).toBe(before); }); + it("CLI-S02b --dry-run human output reports the rendered UTF-8 byte count", async () => { + const filePath = join(workspaceDir, "gateway.jsonc"); + const before = '{ "version": "1.0" }'; + writeFileSync(filePath, before, "utf-8"); + const rt = createTestRuntime(); + await pathSetCommand( + "oc://gateway.jsonc/version", + "中文", + { cwd: workspaceDir, human: true, dryRun: true }, + rt, + ); + + const [header, ...bodyLines] = stdoutText(rt).split("\n"); + const body = bodyLines.join("\n"); + expect(header).toBe( + `--dry-run: would write ${Buffer.byteLength(body, "utf8")} bytes to ${filePath}`, + ); + expect(readFileSync(filePath, "utf-8")).toBe(before); + }); + it("CLI-S05 --dry-run --diff prints a unified diff", async () => { const filePath = join(workspaceDir, "gateway.jsonc"); const before = '{\n "version": "1.0",\n "enabled": true\n}\n'; @@ -414,6 +434,29 @@ describe("openclaw path CLI", () => { expect(out.bytes).toBe(before); }); + it("CLI-S07b reports accurate UTF-8 byte counts for multibyte set output", async () => { + const filePath = join(workspaceDir, "gateway.jsonc"); + const before = '{\n "version": "1.0"\n}\n'; + writeFileSync(filePath, before, "utf-8"); + // Replace the whole file with CJK content via the version key. + // CJK chars are 1 UTF-16 unit but 3 UTF-8 bytes. + const cjkValue = "中".repeat(30); + const rt = createTestRuntime(); + await pathSetCommand( + "oc://gateway.jsonc/version", + cjkValue, + { cwd: workspaceDir, json: true }, + rt, + ); + expect(rt.exitCode).toBe(0); + const out = JSON.parse(stdoutText(rt)); + // bytesWritten must match the file's actual UTF-8 byte size on disk + const onDisk = readFileSync(filePath, "utf-8"); + expect(out.bytesWritten).toBe(Buffer.byteLength(onDisk, "utf8")); + // bytesWritten exceeds JS string length (50 UTF-16 units < ~110 UTF-8 bytes) + expect(out.bytesWritten).toBeGreaterThan(onDisk.length); + }); + it("CLI-E03 emit --cwd resolves against the supplied directory", async () => { // Closes round-10 finding F2: emit advertises --cwd / --file in // the docs but the handler resolved against process.cwd() diff --git a/extensions/oc-path/src/cli.ts b/extensions/oc-path/src/cli.ts index c47caebd8530..e0d36f046789 100644 --- a/extensions/oc-path/src/cli.ts +++ b/extensions/oc-path/src/cli.ts @@ -358,6 +358,8 @@ export async function pathSetCommand( return; } + const byteLength = Buffer.byteLength(newBytes, "utf8"); + if (options.dryRun === true) { const diff = options.diff === true ? formatUnifiedDiff(oldBytes, newBytes, fsPath) : undefined; emit( @@ -367,7 +369,7 @@ export async function pathSetCommand( () => diff !== undefined ? diff || `--dry-run: no byte changes for ${fsPath}` - : `--dry-run: would write ${newBytes.length} bytes to ${fsPath}\n${newBytes}`, + : `--dry-run: would write ${byteLength} bytes to ${fsPath}\n${newBytes}`, ); return; } @@ -375,8 +377,8 @@ export async function pathSetCommand( emit( runtime, mode, - { ok: true, dryRun: false, bytesWritten: newBytes.length, fsPath }, - () => `wrote ${newBytes.length} bytes to ${fsPath}`, + { ok: true, dryRun: false, bytesWritten: byteLength, fsPath }, + () => `wrote ${byteLength} bytes to ${fsPath}`, ); }