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 <noreply@anthropic.com>

* test(oc-path): cover human UTF-8 dry-run count

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
wings1029
2026-07-12 05:39:06 +08:00
committed by GitHub
parent 26e1b18a9b
commit 5b5a5bcc26
2 changed files with 48 additions and 3 deletions
+43
View File
@@ -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 <file> against the supplied directory", async () => {
// Closes round-10 finding F2: emit advertises --cwd / --file in
// the docs but the handler resolved <file> against process.cwd()
+5 -3
View File
@@ -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}`,
);
}