fix: CI test failures — git default branch, clone dirs, spawn timeout

Git tests: use `git init -b main` instead of `gitInit()` to ensure
consistent branch name on CI where default may be master. Use bare
init with `-b main` for remote repos. Don't pre-create clone target
dirs (git clone needs to create them).

Shell timeout tests: replace spawn's `timeout` option (unreliable in
Bun) with manual SIGKILL timer + guarded resolve to prevent hangs.
This commit is contained in:
Pouiiro committed 2026-03-08 11:44:30 +01:00
1 parent 5728602e3a
commit c7aa28c70a
2 files changed
+54 -16

No files matched your search

+39 -7
View File
@@ -11,7 +11,6 @@ import {
getGitStatus,
getGitLog,
getGitDiff,
gitInit,
gitCommit,
gitAdd,
gitStash,
@@ -41,7 +40,7 @@ function makeTempDir(name: string): string {
async function initRepo(name: string): Promise<string> {
const dir = makeTempDir(name);
await gitInit(dir);
await run(["init", "-b", "main"], dir);
await run(["config", "user.email", "[email protected]"], dir);
await run(["config", "user.name", "Test"], dir);
return dir;
@@ -151,6 +150,26 @@ describe("unquoteGitPath", () => {
expect(unquoteGitPath("")).toBe("");
});
it("unescapes octal byte sequences (non-ASCII filenames)", () => {
// "café.txt" → git quotes as "caf\303\251.txt" (UTF-8 bytes for é = 0xC3 0xA9)
expect(unquoteGitPath('"caf\\303\\251.txt"')).toBe("café.txt");
});
it("unescapes multi-byte CJK octal sequences", () => {
// "日" = UTF-8 bytes E6 97 A5 = octal 346 227 245
expect(unquoteGitPath('"\\346\\227\\245.txt"')).toBe("日.txt");
});
it("handles mixed ASCII and octal escapes", () => {
// "src/café/naïve.ts"
expect(unquoteGitPath('"src/caf\\303\\251/na\\303\\257ve.ts"')).toBe("src/café/naïve.ts");
});
it("handles single octal digit", () => {
// \0 = null byte (octal 0)
expect(unquoteGitPath('"a\\0b"')).toBe("a\0b");
});
it("does not strip single leading quote without trailing", () => {
expect(unquoteGitPath('"no-close')).toBe('"no-close');
});
@@ -883,11 +902,10 @@ describe("buildGitContext — upstream tracking", () => {
});
it("upstream arrow shown when tracking remote", async () => {
// Create a bare repo as "remote" and clone it
const bareDir = makeTempDir("ctx-upstream-bare");
await run(["init", "--bare"], bareDir);
await run(["init", "--bare", "-b", "main"], bareDir);
const cloneDir = makeTempDir("ctx-upstream-clone");
const cloneDir = join(TMP, "ctx-upstream-clone");
await run(["clone", bareDir, cloneDir], TMP);
await run(["config", "user.email", "[email protected]"], cloneDir);
await run(["config", "user.name", "Test"], cloneDir);
@@ -905,9 +923,9 @@ describe("buildGitContext — upstream tracking", () => {
it("ahead/behind counts with remote", async () => {
const bareDir = makeTempDir("ctx-ahead-bare");
await run(["init", "--bare"], bareDir);
await run(["init", "--bare", "-b", "main"], bareDir);
const cloneDir = makeTempDir("ctx-ahead-clone");
const cloneDir = join(TMP, "ctx-ahead-clone");
await run(["clone", bareDir, cloneDir], TMP);
await run(["config", "user.email", "[email protected]"], cloneDir);
await run(["config", "user.name", "Test"], cloneDir);
@@ -982,6 +1000,20 @@ describe("git — special file names", () => {
const s = await getGitStatus(dir);
expect(s.untracked).toContain("empty.txt");
});
it("non-ASCII filename unquoted correctly through getGitStatus", async () => {
const dir = await initRepoWithCommit("special-unicode");
writeFileSync(join(dir, "café.txt"), "latte");
const s = await getGitStatus(dir);
expect(s.untracked).toContain("café.txt");
});
it("CJK filename unquoted correctly through getGitStatus", async () => {
const dir = await initRepoWithCommit("special-cjk");
writeFileSync(join(dir, "日本語.txt"), "hello");
const s = await getGitStatus(dir);
expect(s.untracked).toContain("日本語.txt");
});
});
// ─── Commit message edge cases ───
+15 -9
View File
@@ -15,13 +15,19 @@ function runShell(
const maxOutput = opts.maxOutput ?? 16_384;
return new Promise((resolve) => {
const proc = spawn("sh", ["-c", command], { timeout });
const proc = spawn("sh", ["-c", command]);
const stdoutChunks: Buffer[] = [];
const stderrChunks: Buffer[] = [];
let stdoutLen = 0;
let stderrLen = 0;
let timedOut = false;
let resolved = false;
const timer = setTimeout(() => {
timedOut = true;
proc.kill("SIGKILL");
}, timeout);
proc.stdout.on("data", (data: Buffer) => {
if (stdoutLen < maxOutput) {
@@ -37,20 +43,20 @@ function runShell(
}
});
proc.on("error", (err: Error & { code?: string }) => {
if (err.code === "ERR_CHILD_PROCESS_TIMEOUT" || err.message.includes("timed out")) {
timedOut = true;
}
});
proc.on("close", (code) => {
const finish = (code: number | null) => {
if (resolved) return;
resolved = true;
clearTimeout(timer);
resolve({
stdout: Buffer.concat(stdoutChunks).toString("utf-8").slice(0, maxOutput),
stderr: Buffer.concat(stderrChunks).toString("utf-8").slice(0, maxOutput),
code,
timedOut,
});
});
};
proc.on("error", () => finish(null));
proc.on("close", (code) => finish(code));
});
}