import { afterAll, beforeAll, expect, test } from "vitest"; import { execFileSync } from "node:child_process"; import { mkdtempSync, mkdirSync, rmSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { ChildProcessGitClient } from "./git"; let root: string, bare: string, base: string; beforeAll(() => { root = mkdtempSync(join(tmpdir(), "jp-git-")); bare = join(root, "remote.git"); base = join(root, "work"); mkdirSync(bare); mkdirSync(base); execFileSync("git", ["init", "--bare", "-b", "main", bare]); // seed an initial commit so the branch exists const seed = join(root, "seed"); mkdirSync(seed); execFileSync("git", ["init", "-b", "main", seed]); execFileSync("git", ["-C", seed, "config", "user.email", "t@t"]); execFileSync("git", ["-C", seed, "config", "user.name", "t"]); execFileSync("git", ["-C", seed, "commit", "--allow-empty", "-m", "init"]); execFileSync("git", ["-C", seed, "remote", "add", "origin", bare]); execFileSync("git", ["-C", seed, "push", "origin", "main"]); }); afterAll(() => rmSync(root, { recursive: true, force: true })); test("clone, write, commit, push lands files in the remote", async () => { const client = new ChildProcessGitClient({ baseDir: base }); await client.syncClone({ url: bare, branch: "main" }); await client.writeFiles([ { repoPath: "_posts/2026-06-19-hi.md", data: "---\nlayout: post\n---\n\nHi\n" }, { repoPath: "assets/img/hi.png", data: Buffer.from([1, 2, 3]) }, ]); await client.commitAndPush({ message: "Publish: Hi", branch: "main", authorName: "t", authorEmail: "t@t" }); const verify = join(root, "verify"); execFileSync("git", ["clone", bare, verify]); expect(readFileSync(join(verify, "_posts/2026-06-19-hi.md"), "utf8")).toContain("Hi"); expect(Array.from(readFileSync(join(verify, "assets/img/hi.png")))).toEqual([1, 2, 3]); }); test("second sync resets cleanly (fetch+reset path)", async () => { const client = new ChildProcessGitClient({ baseDir: base }); await client.syncClone({ url: bare, branch: "main" }); await client.writeFiles([{ repoPath: "_posts/second.md", data: "x" }]); // Pass an explicit author so the test does not depend on an ambient git // identity (a bare CI runner has none and `git commit` would fail). await client.commitAndPush({ message: "second", branch: "main", authorName: "t", authorEmail: "t@t" }); const verify = join(root, "verify2"); execFileSync("git", ["clone", bare, verify]); expect(readFileSync(join(verify, "_posts/second.md"), "utf8")).toBe("x"); }); test("commitAndPush rejects with diagnostic error when nothing is staged", async () => { // Use a fresh base dir so we get a clean clone const base2 = join(root, "work2"); mkdirSync(base2, { recursive: true }); const client = new ChildProcessGitClient({ baseDir: base2 }); // Clone and write a file, then commit it await client.syncClone({ url: bare, branch: "main" }); await client.writeFiles([{ repoPath: "_posts/no-change.md", data: "same content" }]); await client.commitAndPush({ message: "initial no-change", branch: "main", authorName: "t", authorEmail: "t@t" }); // Now sync again — tree now matches origin, re-writing the same content stages nothing const client2 = new ChildProcessGitClient({ baseDir: base2 }); await client2.syncClone({ url: bare, branch: "main" }); await client2.writeFiles([{ repoPath: "_posts/no-change.md", data: "same content" }]); await expect( client2.commitAndPush({ message: "noop", branch: "main", authorName: "t", authorEmail: "t@t" }) ).rejects.toThrow(/no changes to publish/i); });