188 lines · 8.6 KB
#!/usr/bin/env bun
/**
 * End-to-end check for AI merge fixes against a running dev server started with MERGEFIX_PROVIDER=mock.
 *
 *   bun scripts/e2e-mergefix.ts                 # seeds a user + token in the local D1, uses BASE_URL
 *   BASE_URL=http://localhost:3015 GITFOR_TOKEN=gitfor_pat_... bun scripts/e2e-mergefix.ts
 *
 * Scenario 1: a cosmetic conflict is fixed automatically; the merge commit lands on the head branch, is
 * marker-free, and a CI run is scheduled for it. Scenario 2: a security-looking conflict is only suggested.
 */
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const BASE_URL = (process.env.BASE_URL ?? "http://localhost:3015").replace(/\/$/, "");
const POLL_MS = 1000;
const FIX_TIMEOUT_MS = 120_000;
const CI_TIMEOUT_MS = 60_000;
/** A merge commit lists itself plus two parents in `rev-list --parents`. */
const MERGE_PARENT_FIELDS = 3;
/** Two generations and one Clef decision for the single hunk. */
const EXPECTED_MIN_CALLS = 3;

function sh(cmd: string[], cwd?: string): string {
  const result = Bun.spawnSync(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
  if (result.exitCode !== 0) throw new Error(`${cmd.join(" ")} failed: ${result.stderr.toString()}`);
  return result.stdout.toString();
}

function d1(sql: string): Array<Record<string, unknown>> {
  const out = sh(["bunx", "wrangler", "d1", "execute", "gitfor", "--local", "--json", "--command", sql]);
  return (JSON.parse(out) as Array<{ results: Array<Record<string, unknown>> }>)[0]?.results ?? [];
}

async function sha256Hex(value: string): Promise<string> {
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
}

/** Local-only bootstrap: a throwaway user with a classic token, written straight into the dev D1. */