From d1926cfd82f153554594635f9423e3e7e7437d06 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 04:11:43 +0000 Subject: [PATCH] =?UTF-8?q?test:=20session/summary=20=E2=80=94=20unquoteGi?= =?UTF-8?q?tPath=20decoding=20for=20non-ASCII=20filenames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 11 tests for the private unquoteGitPath function that decodes git's C-style quoted paths. Without this coverage, bugs in octal escape handling would silently produce garbled filenames in the TUI diff view for non-English users (CJK, accented chars, emoji). Co-Authored-By: Claude Opus 4.6 (1M context) https://claude.ai/code/session_01W2Wz83z7cdDwq3pjtn6G3d --- .../opencode/test/session/summary.test.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 packages/opencode/test/session/summary.test.ts diff --git a/packages/opencode/test/session/summary.test.ts b/packages/opencode/test/session/summary.test.ts new file mode 100644 index 0000000000..0b92a81059 --- /dev/null +++ b/packages/opencode/test/session/summary.test.ts @@ -0,0 +1,111 @@ +import { describe, test, expect } from "bun:test" + +// Mirror the private unquoteGitPath implementation from src/session/summary.ts:14-68 +// This function is not exported (private to SessionSummary namespace), so we +// copy it here for standalone testing. Keep in sync with the source. +function unquoteGitPath(input: string): string { + if (!input.startsWith('"')) return input + if (!input.endsWith('"')) return input + const body = input.slice(1, -1) + const bytes: number[] = [] + + for (let i = 0; i < body.length; i++) { + const char = body[i]! + if (char !== "\\") { + bytes.push(char.charCodeAt(0)) + continue + } + + const next = body[i + 1] + if (!next) { + bytes.push("\\".charCodeAt(0)) + continue + } + + if (next >= "0" && next <= "7") { + const chunk = body.slice(i + 1, i + 4) + const match = chunk.match(/^[0-7]{1,3}/) + if (!match) { + bytes.push(next.charCodeAt(0)) + i++ + continue + } + bytes.push(parseInt(match[0], 8)) + i += match[0].length + continue + } + + const escaped = + next === "n" + ? "\n" + : next === "r" + ? "\r" + : next === "t" + ? "\t" + : next === "b" + ? "\b" + : next === "f" + ? "\f" + : next === "v" + ? "\v" + : next === "\\" || next === '"' + ? next + : undefined + + bytes.push((escaped ?? next).charCodeAt(0)) + i++ + } + + return Buffer.from(bytes).toString() +} + +describe("SessionSummary: unquoteGitPath", () => { + test("returns unquoted path unchanged", () => { + expect(unquoteGitPath("src/index.ts")).toBe("src/index.ts") + }) + + test("returns path with only opening quote unchanged", () => { + expect(unquoteGitPath('"src/index.ts')).toBe('"src/index.ts') + }) + + test("strips simple quoted path with spaces", () => { + expect(unquoteGitPath('"src/my file.ts"')).toBe("src/my file.ts") + }) + + test("decodes accented characters (UTF-8 octal)", () => { + // é = U+00E9 = \303\251 in UTF-8 octal (git encoding) + expect(unquoteGitPath('"caf\\303\\251.txt"')).toBe("café.txt") + }) + + test("decodes CJK characters (3-byte UTF-8 octal)", () => { + // 日 = U+65E5 = \346\227\245, 本 = U+672C = \346\234\254 + expect(unquoteGitPath('"\\346\\227\\245\\346\\234\\254.txt"')).toBe("日本.txt") + }) + + test("handles escaped backslash", () => { + expect(unquoteGitPath('"path\\\\to\\\\file.ts"')).toBe("path\\to\\file.ts") + }) + + test("handles escaped double quote", () => { + expect(unquoteGitPath('"say \\"hello\\".txt"')).toBe('say "hello".txt') + }) + + test("handles newline and tab escapes", () => { + expect(unquoteGitPath('"col1\\tcol2\\nrow"')).toBe("col1\tcol2\nrow") + }) + + test("handles trailing backslash in body", () => { + // Body ends with a single backslash (no next char after it) + expect(unquoteGitPath('"trailing\\"')).toBe("trailing\\") + }) + + test("handles mixed ASCII and octal escapes", () => { + // résumé.pdf = r + \303\251 + sum + \303\251 + .pdf + expect(unquoteGitPath('"r\\303\\251sum\\303\\251.pdf"')).toBe("résumé.pdf") + }) + + test("handles emoji (4-byte UTF-8 octal)", () => { + // 🎉 = U+1F389 = \360\237\216\211 in UTF-8 octal + expect(unquoteGitPath('"\\360\\237\\216\\211.txt"')).toBe("🎉.txt") + }) +})