Vectrum
Sign In
117 lines · 6.0 KB · TypeScript
// The git operations pull requests and agents rely on, against real repositories. Run with `bun test`.
import assert from "node:assert/strict";
import { execFile, execFileSync } from "node:child_process";
import { promisify } from "node:util";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { after, before, test } from "node:test";

const tmp = mkdtempSync(path.join(os.tmpdir(), "vectrum-agent-test-"));
process.env.DATA_ROOT = path.join(tmp, "data");
const sh = (cwd: string, ...args: string[]) => execFileSync("git", ["-c", "user.name=a", "-c", "user.email=a@x", ...args], { cwd, encoding: "utf8", stdio: "pipe" });

let git: typeof import("./git");
let write: typeof import("./git-write");
let repo: string;
let work: string;

before(async () => {
  git = await import("./git");
  write = await import("./git-write");
  repo = path.join(tmp, "srv.git");
  await git.initBareRepo(repo);
  work = path.join(tmp, "work");
  sh(tmp, "init", "-q", "-b", "main", work);
  writeFileSync(path.join(work, "a.txt"), "one\ntwo\nthree\n");
  writeFileSync(path.join(work, "b.txt"), "hello world\n");
  sh(work, "add", ".");
  sh(work, "commit", "-qm", "init");
  sh(work, "checkout", "-qb", "feature");
  writeFileSync(path.join(work, "a.txt"), "one\nTWO\nthree\n");
  sh(work, "commit", "-qam", "feature");
  sh(work, "checkout", "-qb", "conflict", "main");
  writeFileSync(path.join(work, "a.txt"), "one\n2\nthree\n");
  sh(work, "commit", "-qam", "conflict");
  sh(work, "push", "-q", repo, "main", "feature", "conflict");
});

after(() => rmSync(tmp, { recursive: true, force: true }));

test("merging in memory: a clean tree, or the files that conflict", async () => {
  const clean = await git.mergeTrees(repo, "main", "feature");
  assert.equal(clean.clean, true);
  const conflict = await git.mergeTrees(repo, "feature", "conflict");
  assert.deepEqual(conflict, { clean: false, conflicts: ["a.txt"] });
  assert.equal(await git.isAncestor(repo, "main", "feature"), true);
  assert.equal(await git.isAncestor(repo, "feature", "main"), false);
  assert.equal(await git.countBetween(repo, "main", "feature"), 1);
  assert.equal((await git.commitsBetween(repo, "main", "feature"))[0].subject, "feature");
});

test("grep: plain text and patterns, case-insensitive by default", async () => {
  const sha = (await git.revParse(repo, "feature"))!;
  assert.deepEqual((await git.grep(repo, sha, "two")).matches, [{ path: "a.txt", line: 2, text: "TWO" }]);
  assert.equal((await git.grep(repo, sha, "two", { caseSensitive: true })).matches.length, 0);
  assert.equal((await git.grep(repo, sha, "^h.*d$", { regex: true })).matches[0].path, "b.txt");
  assert.equal((await git.grep(repo, sha, "error")).matches.length, 0);
  assert.deepEqual((await git.grep(repo, sha, "o", { limit: 1 })).truncated, true);
});

test("patches apply to a tree without a working tree, and undo", async () => {
  const head = (await git.revParse(repo, "main"))!;
  const patch = "--- a/b.txt\n+++ b/b.txt\n@@ -1,1 +1,2 @@\n hello world\n+again\n";
  const tree = await write.applyPatchToTree(repo, head, patch);
  const commit = await write.writeCommit(repo, { tree, parents: [head], message: "patch\n", author: { name: "A", email: "a@x" }, committer: { name: "Agent", email: "agent+tok_abcdefghijklmno@noreply.vectrum" } });
  await write.updateBranch(repo, "main", commit, head);
  assert.equal((await git.readBlob(repo, "main", "b.txt")).content.toString(), "hello world\nagain\n");
  const info = (await git.getCommit(repo, commit))!;
  assert.equal(info.committerName, "Agent");
  assert.equal(info.authorName, "A");
  await assert.rejects(write.applyPatchToTree(repo, head, "--- a/b.txt\n+++ b/b.txt\n@@ -1 +1 @@\n-nope\n+x\n"), write.PatchError);
  // Undoing the change on top of the branch as it is now.
  const reverse = await git.diffRangeText(repo, head, commit, [], { binary: true });
  const undone = await write.applyPatchToTree(repo, commit, reverse, { reverse: true });
  assert.equal(undone, await write.treeOf(repo, head));
  await assert.rejects(write.updateBranch(repo, "main", commit, head), /updated by someone else/);
});

test("the pre-receive hook asks the server, and refuses what it refuses", { timeout: 30_000 }, async () => {
  let asked: { token: string; updates: { ref: string; ff: boolean }[] } | null = null;
  const socket = path.join(tmp, "hook.sock");
  const server = net.createServer((conn) => {
    let input = "";
    conn.on("data", (d) => {
      input += d;
      if (!input.includes("\n")) return;
      asked = JSON.parse(input);
      const refused = asked!.updates.some((u) => u.ref === "refs/heads/main");
      conn.end(`${JSON.stringify({ ok: !refused, messages: refused ? ["main is protected"] : [] })}\n`);
    });
  });
  await new Promise<void>((resolve) => server.listen(socket, resolve));
  const wrapper = path.join(tmp, "receive-pack.ts");
  writeFileSync(
    wrapper,
    `const { spawnService } = await import(${JSON.stringify(path.join(import.meta.dirname, "git.ts"))});
const child = spawnService("git-receive-pack", process.argv[2], { stateless: false, env: { VECTRUM_HOOK_SOCKET: ${JSON.stringify(socket)}, VECTRUM_PUSH_TOKEN: "t0ken" } });
process.stdin.pipe(child.stdin);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
child.on("close", (code) => process.exit(code ?? 1));`,
  );
  // Async, so this process's socket server can answer the hook while git pushes.
  const push = (refspec: string) => promisify(execFile)("git", ["push", "--porcelain", "--force", `--receive-pack=${process.execPath} ${wrapper}`, repo, refspec], { cwd: work });
  try {
    sh(work, "checkout", "-q", "feature");
    await assert.rejects(push("HEAD:refs/heads/main"), (e: { stderr?: string; stdout?: string }) => /main is protected/.test(`${e.stderr}${e.stdout}`));
    await push("HEAD:refs/heads/other");
    assert.equal(asked!.token, "t0ken");
    // A force push is reported as not fast-forward.
    await push("conflict:refs/heads/other");
    assert.equal(asked!.updates[0].ff, false);
  } finally {
    server.close();
  }
});