#!/usr/bin/env bun /** * End-to-end check for AI merge fixes against a running dev server started with MERGEFIX_PROVIDER=mock. * * GITFOR_TOKEN=... bun scripts/e2e-mergefix.ts * GITFOR_TOKEN=... BASE_URL=http://localhost:3000 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 { api, assert, CI_TIMEOUT_MS, EXPECTED_MIN_CALLS, MERGE_PARENT_FIELDS, poll, scenario, } from "./mergefix-harness"; const line = (middle: string) => `header\none\n${middle}\nthree\nfooter\n`; const appFile = (lines: { base: string; head: string; main: string }) => [ { path: "app.txt", base: line(lines.base), head: line(lines.head), main: line(lines.main) }, ]; const user = await api<{ login: string }>("GET", "/user"); const stamp = Date.now().toString(36); const cosmetic = await scenario( user.login, `mergefix-e2e-${stamp}`, appFile({ base: "two", head: "two (head wording)", main: "two (main wording)" }), ); try { assert(cosmetic.fix.status === "applied", `expected applied, got ${cosmetic.fix.status}: ${cosmetic.fix.error}`); const sha = cosmetic.fix.commit_sha; assert(sha, "fix commit sha"); assert(cosmetic.remoteHead() === sha, "head branch moved to the fix commit"); cosmetic.git("fetch", "-q", cosmetic.url, `+refs/heads/feature:refs/remotes/check/feature`); const parents = cosmetic.git("rev-list", "--parents", "-n", "1", sha).trim().split(" "); assert( parents.length === MERGE_PARENT_FIELDS && parents[1] === cosmetic.before, "merge commit has head and base as parents", ); const content = cosmetic.git("show", `${sha}:app.txt`); assert(!/^(<{7}|={7}|>{7})/m.test(content), "no conflict markers"); assert(content.includes("two (head wording)") && content.includes("two (main wording)"), "both sides kept"); const run = await poll("a CI run for the fix commit", CI_TIMEOUT_MS, async () => { const runs = await api>( "GET", `/repos/${user.login}/${cosmetic.repo}/actions/runs`, ); return runs.find((candidate) => candidate.sha === sha) ?? null; }); console.log(`CI run ${run.id} (${run.event}, ${run.status}) scheduled for ${sha.slice(0, 7)}`); const usage = await api<{ meters: Array<{ meter: string; quantity: number }> }>("GET", "/user/usage"); const calls = usage.meters.find((meter) => meter.meter === "ai.calls")?.quantity ?? 0; assert(calls >= EXPECTED_MIN_CALLS, "model usage was recorded"); console.log(`ok: cosmetic conflict auto-fixed in ${sha.slice(0, 7)}`); } finally { cosmetic.cleanup(); } const sensitive = await scenario( user.login, `mergefix-e2e-sec-${stamp}`, appFile({ base: "checkAccess()", head: "checkAuth(user)", main: "checkPermission(user, 'admin')" }), ); try { assert( sensitive.fix.status === "suggested", `expected suggested, got ${sensitive.fix.status}: ${sensitive.fix.error}`, ); const file = sensitive.fix.files[0]; assert(file?.risk === "security-sensitive" || file?.auto_apply === false, "security conflict was not auto-applied"); assert(sensitive.remoteHead() === sensitive.before, "head branch untouched"); console.log(`ok: security-sensitive conflict left as suggestions (${JSON.stringify(file?.reasons)})`); } finally { sensitive.cleanup(); }