/**
 * Live suite: spawns the compiled binary and drives it with a real `git` CLI.
 *
 * This is where the wire protocol and the REST contract are actually proven — the
 * inline Rust tests cover units, but only a real clone and push prove M1. CI sets
 * ZUKA_LIVE_TEST; without it these skip so a plain `deno test` stays fast.
 */
import {
  assert,
  assertEquals,
  assertRejects,
  assertStringIncludes,
} from "jsr:@std/assert@^1";

const ENABLED = Deno.env.get("ZUKA_LIVE_TEST") === "1";
const ROOT = new URL("../../", import.meta.url).pathname;

/** Crate name, read from Cargo.toml so a rename does not break the suite. */
const NAME = (Deno.readTextFileSync(`${ROOT}Cargo.toml`)
  .match(/^name\s*=\s*"([^"]+)"/m)?.[1]) ?? "zuka";
/**
 * The binary under test.
 *
 * Overridable because CI builds a static musl binary and deploys that exact file —
 * so that is the one the live suite has to exercise. Testing a second, differently
 * linked build and shipping the first proves nothing about what ships.
 */
const BINARY = Deno.env.get("ZUKA_TEST_BINARY") ??
  `${ROOT}target/release/${NAME}`;

interface Server {
  port: number;
  sshPort: number;
  dataDir: string;
  workDir: string;
  token: string;
  /// Private key registered with the `alice` account.
  keyPath: string;
  stop: () => Promise<void>;
}

async function freePort(): Promise<number> {
  const listener = Deno.listen({ port: 0 });
  const { port } = listener.addr as Deno.NetAddr;
  listener.close();
  return port;
}

/** Mint a token by invoking the binary's own command, as an operator would. */
async function mintToken(
  dataDir: string,
  account: string,
  extra: string[] = ["--scopes", "admin"],
): Promise<string> {
  const command = new Deno.Command(BINARY, {
    args: ["token", account, ...extra],
    env: { ZUKA_DATA_DIR: dataDir },
    stdout: "piped",
    stderr: "null",
  });
  const { success, stdout } = await command.output();
  assert(success, `minting a token for ${account} failed`);
  return new TextDecoder().decode(stdout).trim();
}

/** Generate a keypair and register the public half with an account. */
async function registerKey(
  dataDir: string,
  account: string,
  path: string,
  extra: string[] = [],
): Promise<void> {
  const keygen = await new Deno.Command("ssh-keygen", {
    args: ["-t", "ed25519", "-N", "", "-f", path, "-q"],
    stdout: "null",
    stderr: "null",
  }).output();
  assert(keygen.success, "ssh-keygen failed");

  const add = await new Deno.Command(BINARY, {
    args: ["key", account, `${path}.pub`, ...extra],
    env: { ZUKA_DATA_DIR: dataDir },
    stdout: "null",
    stderr: "null",
  }).output();
  assert(add.success, `registering a key for ${account} failed`);
}

/** Run git over SSH with a specific identity, pinned to this server. */
function sshEnv(
  server: Server,
  keyPath = server.keyPath,
): Record<string, string> {
  return {
    GIT_SSH_COMMAND:
      `ssh -i ${keyPath} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null` +
      ` -o LogLevel=ERROR -o IdentitiesOnly=yes -o BatchMode=yes`,
  };
}

function sshUrl(server: Server, account: string, repo: string) {
  return `ssh://git@127.0.0.1:${server.sshPort}/${account}/${repo}.git`;
}

/** Raw SSH exec, for proving what a session may NOT do. */
async function sshExec(server: Server, command: string[]): Promise<string> {
  const { stdout, stderr } = await new Deno.Command("ssh", {
    args: [
      "-i",
      server.keyPath,
      "-o",
      "StrictHostKeyChecking=no",
      "-o",
      "UserKnownHostsFile=/dev/null",
      "-o",
      "LogLevel=ERROR",
      "-o",
      "IdentitiesOnly=yes",
      "-o",
      "BatchMode=yes",
      "-p",
      String(server.sshPort),
      "git@127.0.0.1",
      ...command,
    ],
    stdout: "piped",
    stderr: "piped",
  }).output();
  const decode = new TextDecoder();
  return decode.decode(stdout) + decode.decode(stderr);
}

async function start(extra: Record<string, string> = {}): Promise<Server> {
  const workDir = await Deno.makeTempDir({ prefix: "zuka-live-" });
  const dataDir = `${workDir}/data`;
  await Deno.mkdir(dataDir, { recursive: true });

  const port = await freePort();
  const sshPort = await freePort();
  const token = await mintToken(dataDir, "alice");
  const keyPath = `${workDir}/id_alice`;
  await registerKey(dataDir, "alice", keyPath);

  const child = new Deno.Command(BINARY, {
    env: {
      ZUKA_DATA_DIR: dataDir,
      ZUKA_BIND: `127.0.0.1:${port}`,
      ZUKA_SSH_BIND: `127.0.0.1:${sshPort}`,
      PATH: Deno.env.get("PATH") ?? "/usr/bin:/bin",
      ...extra,
    },
    stdout: "null",
    // The server logs two lines per request. Left undrained this fills the 64 KiB
    // pipe and hangs it, so it is discarded rather than piped.
    stderr: "null",
  }).spawn();

  // Poll readiness rather than sleeping — a fixed sleep is either slow or flaky.
  const deadline = Date.now() + 10_000;
  for (;;) {
    try {
      const probe = await fetch(`http://127.0.0.1:${port}/healthz`);
      await drain(probe);
      if (probe.ok) break;
    } catch {
      // not listening yet
    }
    if (Date.now() > deadline) throw new Error("server did not become ready");
    await new Promise((r) => setTimeout(r, 50));
  }

  return {
    port,
    sshPort,
    dataDir,
    workDir,
    token,
    keyPath,
    stop: async () => {
      try {
        child.kill("SIGKILL");
      } catch {
        // already gone
      }
      await child.status;
      await Deno.remove(workDir, { recursive: true }).catch(() => {});
    },
  };
}

async function git(
  args: string[],
  cwd: string,
  env: Record<string, string> = {},
): Promise<{ code: number; stdout: string; stderr: string }> {
  const command = new Deno.Command("git", {
    args: [
      "-c",
      "user.email=agent@example.com",
      "-c",
      "user.name=Agent",
      "-c",
      "protocol.version=2",
      ...args,
    ],
    cwd,
    env: { ...Deno.env.toObject(), GIT_TERMINAL_PROMPT: "0", ...env },
    stdout: "piped",
    stderr: "piped",
  });
  const { code, stdout, stderr } = await command.output();
  return {
    code,
    stdout: new TextDecoder().decode(stdout),
    stderr: new TextDecoder().decode(stderr),
  };
}

function cloneUrl(
  server: Server,
  account: string,
  repo: string,
  token = server.token,
) {
  return `http://x-access-token:${token}@127.0.0.1:${server.port}/${account}/${repo}.git`;
}

async function api(
  server: Server,
  method: string,
  path: string,
  options: { token?: string; body?: unknown } = {},
): Promise<Response> {
  const headers: Record<string, string> = {};
  const token = options.token ?? server.token;
  if (token) headers["authorization"] = `Bearer ${token}`;
  if (options.body !== undefined) headers["content-type"] = "application/json";

  return await fetch(`http://127.0.0.1:${server.port}${path}`, {
    method,
    headers,
    body: options.body === undefined ? undefined : JSON.stringify(options.body),
  });
}

/** Read a response fully, so no test leaves a locked or dangling stream. */
async function drain(response: Response): Promise<string> {
  return await response.text();
}

async function json(response: Response): Promise<
  // deno-lint-ignore no-explicit-any
  any
> {
  return JSON.parse(await drain(response));
}

async function createRepo(server: Server, name: string) {
  const response = await api(server, "POST", "/v1/repos", { body: { name } });
  const body = await drain(response);
  assertEquals(response.status, 201, body);
}

/** A second server with extra configuration, for tests that need a tighter limit. */
async function startWith(extra: Record<string, string>): Promise<Server> {
  return await start(extra);
}

/**
 * Run the binary as a one-shot command against a running server's data directory.
 *
 * This is how an operator or a systemd timer invokes maintenance, so the tests
 * exercise the same entry point rather than an internal function.
 */
async function runCli(
  server: Server,
  args: string[],
  extra: Record<string, string>,
): Promise<{ code: number; stdout: string; stderr: string }> {
  const { code, stdout, stderr } = await new Deno.Command(BINARY, {
    args,
    env: {
      ZUKA_DATA_DIR: server.dataDir,
      PATH: Deno.env.get("PATH") ?? "/usr/bin:/bin",
      ...extra,
    },
    stdout: "piped",
    stderr: "piped",
  }).output();
  const decode = new TextDecoder();
  return { code, stdout: decode.decode(stdout), stderr: decode.decode(stderr) };
}

/** Loose objects in a bare repository — what accumulates without gc. */
async function countLoose(gitDir: string): Promise<number> {
  let count = 0;
  for await (const bucket of Deno.readDir(`${gitDir}/objects`)) {
    if (
      !bucket.isDirectory || bucket.name === "pack" || bucket.name === "info"
    ) continue;
    for await (
      const object of Deno.readDir(`${gitDir}/objects/${bucket.name}`)
    ) {
      if (object.isFile) count++;
    }
  }
  return count;
}

function live(name: string, fn: (server: Server) => Promise<void>) {
  Deno.test({
    name,
    ignore: !ENABLED,
    async fn() {
      const server = await start();
      try {
        await fn(server);
      } finally {
        await server.stop();
      }
    },
  });
}

live(
  "a repository round-trips through clone, commit, push and clone",
  async (server) => {
    await createRepo(server, "site");

    const cloned = await git([
      "clone",
      cloneUrl(server, "alice", "site"),
      "work",
    ], server.workDir);
    assertEquals(cloned.code, 0, cloned.stderr);

    const repo = `${server.workDir}/work`;
    await Deno.writeTextFile(`${repo}/README.md`, "hello from an agent\n");
    assertEquals((await git(["add", "README.md"], repo)).code, 0);
    assertEquals((await git(["commit", "-m", "add readme"], repo)).code, 0);

    const pushed = await git(["push", "origin", "HEAD:refs/heads/main"], repo);
    assertEquals(pushed.code, 0, pushed.stderr);

    const verify = await git([
      "clone",
      cloneUrl(server, "alice", "site"),
      "verify",
    ], server.workDir);
    assertEquals(verify.code, 0, verify.stderr);

    const content = await Deno.readTextFile(
      `${server.workDir}/verify/README.md`,
    );
    assertEquals(content, "hello from an agent\n");
  },
);

live(
  "the pushed commit is visible over the REST refs endpoint",
  async (server) => {
    await createRepo(server, "site");
    const repo = `${server.workDir}/work`;
    assertEquals(
      (await git(
        ["clone", cloneUrl(server, "alice", "site"), "work"],
        server.workDir,
      )).code,
      0,
    );
    await Deno.writeTextFile(`${repo}/a.txt`, "a");
    await git(["add", "."], repo);
    await git(["commit", "-m", "a"], repo);
    await git(["push", "origin", "HEAD:refs/heads/main"], repo);

    const response = await api(server, "GET", "/v1/repos/alice/site/refs");
    assertEquals(response.status, 200);
    const body = await json(response);
    assertEquals(body.items.length, 1);
    assertEquals(body.items[0].name, "refs/heads/main");
    assertEquals(body.items[0].type, "branch");
    assert(
      /^[0-9a-f]{40}$/.test(body.items[0].sha),
      "sha must be a full object id",
    );
  },
);

live(
  "an unauthenticated git request challenges so the credential helper runs",
  async (server) => {
    await createRepo(server, "site");
    const response = await fetch(
      `http://127.0.0.1:${server.port}/alice/site.git/info/refs?service=git-upload-pack`,
    );
    await drain(response);

    assertEquals(response.status, 401);
    const challenge = response.headers.get("www-authenticate") ?? "";
    assertStringIncludes(challenge, "Basic realm=");
    // git renders a JSON error body as noise at the user.
    assert(
      !(response.headers.get("content-type") ?? "").includes("problem+json"),
      "the git paths must not answer with problem+json",
    );
  },
);

live(
  "another account cannot read, clone or enumerate a repository",
  async (server) => {
    await createRepo(server, "site");
    const bob = await mintToken(server.dataDir, "bob");

    const read = await api(server, "GET", "/v1/repos/alice/site", {
      token: bob,
    });
    await drain(read);
    assertEquals(read.status, 404, "403 would confirm the repository exists");

    const cloned = await git(
      ["clone", cloneUrl(server, "alice", "site", bob), "steal"],
      server.workDir,
    );
    assert(cloned.code !== 0, "a foreign account must not clone");

    const list = await api(server, "GET", "/v1/repos", { token: bob });
    assertEquals((await json(list)).items.length, 0);
  },
);

live(
  "names that would escape the data directory are refused",
  async (server) => {
    for (const name of ["../../etc", ".git", "a/b", "", "-leading", "x.lock"]) {
      const response = await api(server, "POST", "/v1/repos", {
        body: { name },
      });
      const body = await json(response);
      assertEquals(response.status, 400, `${name} must be refused`);
      assertStringIncludes(body.type, "/problems/invalid-name");
    }
  },
);

live(
  "only the three smart-HTTP endpoints are routable under .git",
  async (server) => {
    await createRepo(server, "site");
    for (
      const path of [
        "/alice/site.git/config",
        "/alice/site.git/HEAD",
        "/alice/site.git/objects/info/packs",
        "/alice/site.git/info/refs",
      ]
    ) {
      const response = await api(server, "GET", path);
      await drain(response);
      assertEquals(response.status, 404, `${path} must not be served`);
    }
  },
);

live(
  "creating the same repository twice conflicts, including by case",
  async (server) => {
    await createRepo(server, "site");

    const same = await api(server, "POST", "/v1/repos", {
      body: { name: "site" },
    });
    assertEquals(same.status, 409);
    assertStringIncludes((await json(same)).type, "/problems/repo-exists");

    const cased = await api(server, "POST", "/v1/repos", {
      body: { name: "Site" },
    });
    assertEquals(
      cased.status,
      409,
      "a case variant would collide on a case-insensitive disk",
    );
    await drain(cased);
  },
);

live(
  "a deleted repository stops serving and is recoverable on disk",
  async (server) => {
    await createRepo(server, "site");

    const deleted = await api(server, "DELETE", "/v1/repos/alice/site");
    await drain(deleted);
    assertEquals(deleted.status, 204);

    const gone = await api(server, "GET", "/v1/repos/alice/site");
    await drain(gone);
    assertEquals(gone.status, 404);

    const again = await api(server, "DELETE", "/v1/repos/alice/site");
    await drain(again);
    assertEquals(
      again.status,
      404,
      "deleting an absent repo must not report success",
    );

    const graves = [...Deno.readDirSync(`${server.dataDir}/tmp/deleted`)];
    assertEquals(
      graves.length,
      1,
      "delete must move the repo aside, not remove it",
    );
  },
);

live("health reports mode and disk headroom", async (server) => {
  const response = await api(server, "GET", "/healthz");
  const body = await json(response);
  assertEquals(response.status, 200);
  assertEquals(body.status, "ok");
  assertEquals(body.mode, "standalone");
  assert(typeof body.disk_free_bytes === "number");
});

// ── SSH transport ──────────────────────────────────────────────────────────

live("a repository round-trips over SSH", async (server) => {
  await createRepo(server, "site");
  const env = sshEnv(server);

  const cloned = await git(
    ["clone", sshUrl(server, "alice", "site"), "work"],
    server.workDir,
    env,
  );
  assertEquals(cloned.code, 0, cloned.stderr);

  const repo = `${server.workDir}/work`;
  await Deno.writeTextFile(`${repo}/README.md`, "over ssh\n");
  await git(["add", "."], repo, env);
  await git(["commit", "-m", "add readme"], repo, env);

  const pushed = await git(
    ["push", "origin", "HEAD:refs/heads/main"],
    repo,
    env,
  );
  assertEquals(pushed.code, 0, pushed.stderr);

  // Fetching a repository that HAS content is the case that deadlocks if the
  // client-to-stdin copy is joined on rather than pumped independently.
  const verify = await git(
    ["clone", sshUrl(server, "alice", "site"), "verify"],
    server.workDir,
    env,
  );
  assertEquals(verify.code, 0, verify.stderr);
  assertEquals(
    await Deno.readTextFile(`${server.workDir}/verify/README.md`),
    "over ssh\n",
  );
});

live(
  "an SSH session cannot open a shell or run anything but git",
  async (server) => {
    await createRepo(server, "site");

    assertStringIncludes(await sshExec(server, []), "not a shell");

    for (
      const command of [
        ["cat /etc/passwd"],
        ["scp -t /tmp"],
        ["git-daemon"],
      ]
    ) {
      const output = await sshExec(server, command);
      assertStringIncludes(
        output,
        `${NAME}:`,
        `${command} must be refused by us`,
      );
      assert(
        !output.includes("root:"),
        "a refused command must not have executed",
      );
    }
  },
);

live(
  "SSH refuses command injection and traversal in the repository path",
  async (server) => {
    await createRepo(server, "site");

    for (
      const command of [
        "git-upload-pack '/alice/site.git'; id",
        "git-upload-pack '/alice/site.git' && id",
        "git-upload-pack '/alice/$(id).git'",
        "git-upload-pack '/alice/../../etc/passwd'",
        "git-upload-pack '//etc/shadow'",
        "git-upload-pack '/a/b/c.git'",
      ]
    ) {
      const output = await sshExec(server, [command]);
      assertStringIncludes(output, `${NAME}:`, `${command} must be refused`);
      assert(!output.includes("uid="), "injected command must not have run");
    }
  },
);

live("an unregistered SSH key cannot authenticate", async (server) => {
  await createRepo(server, "site");

  const rogue = `${server.workDir}/rogue`;
  await new Deno.Command("ssh-keygen", {
    args: ["-t", "ed25519", "-N", "", "-f", rogue, "-q"],
    stdout: "null",
    stderr: "null",
  }).output();

  const cloned = await git(
    ["clone", sshUrl(server, "alice", "site"), "stolen"],
    server.workDir,
    sshEnv(server, rogue),
  );
  assert(cloned.code !== 0, "an unregistered key must not authenticate");
});

live("a read-only SSH key may clone but not push", async (server) => {
  await createRepo(server, "site");

  const readOnly = `${server.workDir}/id_readonly`;
  await registerKey(server.dataDir, "alice", readOnly, ["--read-only"]);
  const env = sshEnv(server, readOnly);

  const cloned = await git(
    ["clone", sshUrl(server, "alice", "site"), "ro"],
    server.workDir,
    env,
  );
  assertEquals(cloned.code, 0, cloned.stderr);

  const repo = `${server.workDir}/ro`;
  await Deno.writeTextFile(`${repo}/a.txt`, "a");
  await git(["add", "."], repo, env);
  await git(["commit", "-m", "a"], repo, env);

  const pushed = await git(
    ["push", "origin", "HEAD:refs/heads/main"],
    repo,
    env,
  );
  assert(pushed.code !== 0, "a read-only key must not push");
});

live(
  "branch protection is enforced over SSH by the same rule",
  async (server) => {
    await createRepo(server, "site");
    const env = sshEnv(server);
    const repo = `${server.workDir}/work`;

    await git(
      ["clone", sshUrl(server, "alice", "site"), "work"],
      server.workDir,
      env,
    );
    await Deno.writeTextFile(`${repo}/a.txt`, "a");
    await git(["add", "."], repo, env);
    await git(["commit", "-m", "first"], repo, env);
    await git(["push", "origin", "HEAD:refs/heads/main"], repo, env);

    // Unprotected: the rewrite goes through over SSH just as it does over HTTP.
    await git(["commit", "--amend", "-m", "rewritten"], repo, env);
    assertEquals(
      (await git(
        ["push", "--force", "origin", "HEAD:refs/heads/main"],
        repo,
        env,
      )).code,
      0,
    );

    await drain(
      await api(server, "PATCH", "/v1/repos/alice/site", {
        body: { protected_refs: ["refs/heads/main"] },
      }),
    );

    const before = await json(
      await api(server, "GET", "/v1/repos/alice/site/refs"),
    );
    await git(["commit", "--amend", "-m", "again"], repo, env);
    const forced = await git(
      ["push", "--force", "origin", "HEAD:refs/heads/main"],
      repo,
      env,
    );

    assert(forced.code !== 0, "protection must hold over SSH");
    assertStringIncludes(forced.stderr, "protected");

    const after = await json(
      await api(server, "GET", "/v1/repos/alice/site/refs"),
    );
    assertEquals(after.items[0].sha, before.items[0].sha);
  },
);

// ── Regressions ────────────────────────────────────────────────────────────

live(
  "a case-variant account cannot address another account's repository",
  async (server) => {
    await createRepo(server, "site");
    const bob = await mintToken(server.dataDir, "bob");

    // `Alice` folds to `alice`; bob must not reach it under either spelling.
    for (const account of ["alice", "Alice", "ALICE"]) {
      const response = await api(server, "GET", `/v1/repos/${account}/site`, {
        token: bob,
      });
      await drain(response);
      assertEquals(
        response.status,
        404,
        `${account} must not be readable by bob`,
      );
    }

    // The owner reaches the same single repository under any spelling.
    const canonical = await json(
      await api(server, "GET", "/v1/repos/Alice/SITE"),
    );
    assertEquals(canonical.account, "alice");
    assertEquals(canonical.name, "site");
  },
);

live(
  "a repo-scoped admin token cannot delete a repository it cannot read",
  async (server) => {
    await createRepo(server, "site");
    await createRepo(server, "secret");

    const confined = await mintToken(server.dataDir, "alice", [
      "--scopes",
      "admin",
      "--repos",
      "site",
    ]);

    const read = await api(server, "GET", "/v1/repos/alice/secret", {
      token: confined,
    });
    await drain(read);
    assertEquals(read.status, 404);

    const deleted = await api(server, "DELETE", "/v1/repos/alice/secret", {
      token: confined,
    });
    await drain(deleted);
    assertEquals(
      deleted.status,
      404,
      "a token denied read must not be granted destroy",
    );

    const still = await api(server, "GET", "/v1/repos/alice/secret");
    await drain(still);
    assertEquals(still.status, 200, "the repository must still exist");
  },
);

live(
  "a duplicated service parameter is refused rather than resolved differently",
  async (server) => {
    await createRepo(server, "site");
    const readOnly = await mintToken(server.dataDir, "alice", [
      "--scopes",
      "repo:read",
    ]);

    const single = await api(
      server,
      "GET",
      "/alice/site.git/info/refs?service=git-receive-pack",
      { token: readOnly },
    );
    await drain(single);
    assertEquals(
      single.status,
      403,
      "a reader must not get the push advertisement",
    );

    // This server takes the first value and the CGI takes the last, so a duplicate
    // would let the authorization decision and the running service disagree.
    const duplicated = await api(
      server,
      "GET",
      "/alice/site.git/info/refs?service=git-upload-pack&service=git-receive-pack",
      { token: readOnly },
    );
    const body = await drain(duplicated);
    assertEquals(
      duplicated.status,
      404,
      "a duplicated service parameter must not route",
    );
    assert(
      !body.includes("receive-pack"),
      "the write advertisement must not be reachable this way",
    );
  },
);

live("every git error is plain text, not problem+json", async (server) => {
  await createRepo(server, "site");
  const readOnly = await mintToken(server.dataDir, "alice", [
    "--scopes",
    "repo:read",
  ]);

  const cases: Array<[string, Record<string, unknown>]> = [
    ["/alice/site.git/info/refs?service=git-receive-pack", { token: readOnly }],
    ["/alice/absent.git/info/refs?service=git-upload-pack", {}],
  ];

  for (const [path, options] of cases) {
    const response = await api(server, "GET", path, options);
    const body = await drain(response);
    assert(response.status >= 400, `${path} should have failed`);
    assert(
      !(response.headers.get("content-type") ?? "").includes("problem+json"),
      `${path} answered problem+json, which git prints at the user`,
    );
    assert(!body.trimStart().startsWith("{"), `${path} answered JSON`);
  }
});

live("a token expires and stops working", async (server) => {
  await createRepo(server, "site");

  // `--days 0` expires at the moment it is minted.
  const expired = await mintToken(server.dataDir, "alice", [
    "--scopes",
    "admin",
    "--days",
    "0",
  ]);

  const response = await api(server, "GET", "/v1/repos", { token: expired });
  await drain(response);
  assertEquals(response.status, 401, "an expired token must not authenticate");
});

live("credential files are not world-readable", async (server) => {
  await createRepo(server, "site");

  for (const file of ["tokens.json", "ssh_keys.json", "ssh_host_ed25519_key"]) {
    const info = await Deno.stat(`${server.dataDir}/${file}`);
    const mode = (info.mode ?? 0) & 0o077;
    assertEquals(mode, 0, `${file} is readable by group or other`);
  }
});

// ── Read API ───────────────────────────────────────────────────────────────

live("repository content is readable over REST", async (server) => {
  await createRepo(server, "site");
  const env = sshEnv(server);
  const repo = `${server.workDir}/work`;

  await git(
    ["clone", sshUrl(server, "alice", "site"), "work"],
    server.workDir,
    env,
  );
  await Deno.mkdir(`${repo}/src`, { recursive: true });
  await Deno.writeTextFile(`${repo}/README.md`, "# hello\n");
  await Deno.writeTextFile(`${repo}/src/main.rs`, "fn main() {}\n");
  await git(["add", "."], repo, env);
  await git(["commit", "-m", "initial"], repo, env);
  await git(["push", "origin", "HEAD:refs/heads/main"], repo, env);

  const tree = await json(
    await api(server, "GET", "/v1/repos/alice/site/tree"),
  );
  const names = tree.items.map((e: { name: string }) => e.name).sort();
  assertEquals(names, ["README.md", "src"]);

  const nested = await json(
    await api(server, "GET", "/v1/repos/alice/site/tree/src"),
  );
  assertEquals(nested.items[0].path, "src/main.rs");
  assertEquals(nested.items[0].type, "file");

  const raw = await api(server, "GET", "/v1/repos/alice/site/raw/README.md");
  const body = await drain(raw);
  assertEquals(body, "# hello\n");
  assertEquals(raw.headers.get("content-type"), "application/octet-stream");
  assertEquals(raw.headers.get("x-content-type-options"), "nosniff");

  const log = await json(
    await api(server, "GET", "/v1/repos/alice/site/commits"),
  );
  assertEquals(log.items.length, 1);
  assertEquals(log.items[0].message, "initial");
  assert(/^[0-9a-f]{40}$/.test(log.items[0].sha));
});

live(
  "read endpoints refuse traversal and smuggled git arguments",
  async (server) => {
    await createRepo(server, "site");

    for (
      const path of [
        "/v1/repos/alice/site/raw/../../../etc/passwd",
        "/v1/repos/alice/site/tree/.git/config",
        "/v1/repos/alice/site/commits?ref=--upload-pack%3Devil",
        "/v1/repos/alice/site/commits/--help",
        "/v1/repos/alice/site/commits?limit=999999",
      ]
    ) {
      const response = await api(server, "GET", path);
      await drain(response);
      assert(
        response.status >= 400,
        `${path} must be refused, got ${response.status}`,
      );
    }
  },
);

live(
  "the OpenAPI document is served and matches the crate version",
  async (server) => {
    const response = await api(server, "GET", "/openapi.json");
    const doc = await json(response);
    assertEquals(response.status, 200);
    assertEquals(doc.openapi, "3.1.0");
    assert(
      doc.paths["/v1/repos"],
      "the document must describe the repo collection",
    );
  },
);

live(
  "the product name is not baked into behaviour a rename would break",
  async (server) => {
    // Problem type URIs are built from one place and are configurable, so a rename
    // or a self-hoster's own docs origin does not require touching call sites.
    const response = await api(server, "POST", "/v1/repos", {
      body: { name: "../escape" },
    });
    const body = await json(response);
    assertEquals(response.status, 400);
    assertStringIncludes(body.type, "/problems/invalid-name");

    // The auth realm is derived too.
    const challenge = await fetch(`http://127.0.0.1:${server.port}/v1/repos`);
    await challenge.text();
    assertStringIncludes(
      challenge.headers.get("www-authenticate") ?? "",
      "realm=",
    );
  },
);

live("repository settings are patchable", async (server) => {
  await createRepo(server, "site");

  const described = await json(
    await api(server, "PATCH", "/v1/repos/alice/site", {
      body: { description: "the shop" },
    }),
  );
  assertEquals(described.description, "the shop");

  // merge-patch: null clears, absent leaves alone.
  const cleared = await json(
    await api(server, "PATCH", "/v1/repos/alice/site", {
      body: { description: null },
    }),
  );
  assertEquals(cleared.description, undefined);
  assertEquals(cleared.name, "site", "an absent field must be untouched");

  // A default branch that does not exist would leave clones detached.
  const bad = await api(server, "PATCH", "/v1/repos/alice/site", {
    body: { default_branch: "refs/heads/nope" },
  });
  await drain(bad);
  assertEquals(bad.status, 409);
});

// ── git, at full power ─────────────────────────────────────────────────────

live("git archive --remote works over SSH", async (server) => {
  await createRepo(server, "site");
  const env = sshEnv(server);
  const repo = `${server.workDir}/work`;

  await git(
    ["clone", sshUrl(server, "alice", "site"), "work"],
    server.workDir,
    env,
  );
  await Deno.writeTextFile(`${repo}/README.md`, "archived\n");
  await git(["add", "."], repo, env);
  await git(["commit", "-m", "initial"], repo, env);
  await git(["push", "origin", "HEAD:refs/heads/main"], repo, env);

  const archived = await git(
    [
      "archive",
      "--remote",
      sshUrl(server, "alice", "site"),
      "--format=tar",
      "-o",
      `${server.workDir}/out.tar`,
      "refs/heads/main",
    ],
    server.workDir,
    env,
  );
  assertEquals(archived.code, 0, archived.stderr);

  const info = await Deno.stat(`${server.workDir}/out.tar`);
  assert(info.size > 0, "the archive must contain something");
});

live("a read-only key may archive but still may not push", async (server) => {
  await createRepo(server, "site");
  const env = sshEnv(server);
  const repo = `${server.workDir}/work`;
  await git(
    ["clone", sshUrl(server, "alice", "site"), "work"],
    server.workDir,
    env,
  );
  await Deno.writeTextFile(`${repo}/a.txt`, "a");
  await git(["add", "."], repo, env);
  await git(["commit", "-m", "a"], repo, env);
  await git(["push", "origin", "HEAD:refs/heads/main"], repo, env);

  const readOnly = `${server.workDir}/id_ro_archive`;
  await registerKey(server.dataDir, "alice", readOnly, ["--read-only"]);
  const roEnv = sshEnv(server, readOnly);

  const archived = await git(
    [
      "archive",
      "--remote",
      sshUrl(server, "alice", "site"),
      "--format=tar",
      "-o",
      `${server.workDir}/ro.tar`,
      "refs/heads/main",
    ],
    server.workDir,
    roEnv,
  );
  assertEquals(archived.code, 0, "archiving is a read and must be allowed");
});

// ── discovery ──────────────────────────────────────────────────────────────

/** Seed a repository with two commits and a small tree. */
async function seed(server: Server, name = "site") {
  await createRepo(server, name);
  const env = sshEnv(server);
  const repo = `${server.workDir}/${name}-work`;
  await git(
    ["clone", sshUrl(server, "alice", name), `${name}-work`],
    server.workDir,
    env,
  );

  await Deno.mkdir(`${repo}/src`, { recursive: true });
  await Deno.writeTextFile(`${repo}/README.md`, "# project\nneedle-one\n");
  await Deno.writeTextFile(`${repo}/src/main.rs`, "fn main() {}\n");
  await git(["add", "."], repo, env);
  await git(["commit", "-m", "initial"], repo, env);
  await git(["push", "origin", "HEAD:refs/heads/main"], repo, env);

  await Deno.writeTextFile(
    `${repo}/src/main.rs`,
    "fn main() {\n  // needle-two\n}\n",
  );
  await git(["add", "."], repo, env);
  await git(["commit", "-m", "second"], repo, env);
  await git(["push", "origin", "HEAD:refs/heads/main"], repo, env);

  return { repo, env };
}

live("compare reports what changed between two points", async (server) => {
  await seed(server);
  const log = await json(
    await api(server, "GET", "/v1/repos/alice/site/commits"),
  );
  const [head, base] = log.items.map((c: { sha: string }) => c.sha);

  const compared = await json(
    await api(
      server,
      "GET",
      `/v1/repos/alice/site/compare?base=${base}&head=${head}`,
    ),
  );
  assertEquals(compared.merge_base, base);
  assertEquals(compared.commits.length, 1);
  assertEquals(compared.changed.length, 1);
  assertEquals(compared.changed[0].path, "src/main.rs");
  assert(compared.additions > 0, "an edit should report additions");
});

live(
  "search finds content and refuses to be used as a flag",
  async (server) => {
    await seed(server);

    const found = await json(
      await api(server, "GET", "/v1/repos/alice/site/search?q=needle-two"),
    );
    assertEquals(found.items.length, 1);
    assertEquals(found.items[0].path, "src/main.rs");

    const scoped = await json(
      await api(server, "GET", "/v1/repos/alice/site/search?q=needle&path=src"),
    );
    assert(
      scoped.items.every((m: { path: string }) => m.path.startsWith("src/")),
    );

    const missing = await json(
      await api(server, "GET", "/v1/repos/alice/site/search?q=nothinghere"),
    );
    assertEquals(
      missing.items.length,
      0,
      "no matches is an answer, not an error",
    );

    const noQuery = await api(server, "GET", "/v1/repos/alice/site/search");
    await drain(noQuery);
    assertEquals(noQuery.status, 400);
  },
);

live("blame attributes each line to a commit", async (server) => {
  await seed(server);
  const blamed = await json(
    await api(server, "GET", "/v1/repos/alice/site/blame/src/main.rs"),
  );
  assertEquals(blamed.path, "src/main.rs");
  assert(blamed.items.length >= 3, "every line should be attributed");
  assert(/^[0-9a-f]{40}$/.test(blamed.items[0].sha));
  assertEquals(blamed.items[0].author, "Agent");
  assert(
    blamed.items.some((l: { content: string }) =>
      l.content.includes("needle-two")
    ),
  );
});

live("a path-filtered log reports whether it was truncated", async (server) => {
  await seed(server);
  const filtered = await json(
    await api(server, "GET", "/v1/repos/alice/site/commits?path=src/main.rs"),
  );
  assertEquals(filtered.items.length, 2);
  assertEquals(filtered.truncated, false);
});

// ── MCP ────────────────────────────────────────────────────────────────────

let rpcId = 0;
async function rpc(
  server: Server,
  method: string,
  params: unknown = {},
  token = server.token,
  // deno-lint-ignore no-explicit-any
): Promise<any> {
  const response = await fetch(`http://127.0.0.1:${server.port}/mcp`, {
    method: "POST",
    headers: {
      authorization: `Bearer ${token}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({ jsonrpc: "2.0", id: ++rpcId, method, params }),
  });
  return JSON.parse(await response.text());
}

async function callTool(
  server: Server,
  name: string,
  args: Record<string, unknown> = {},
  token = server.token,
  // deno-lint-ignore no-explicit-any
): Promise<any> {
  return await rpc(server, "tools/call", { name, arguments: args }, token);
}

live(
  "MCP handshake advertises tools and identifies the server",
  async (server) => {
    const init = await rpc(server, "initialize", {});
    assertEquals(init.jsonrpc, "2.0");
    assert(init.result.protocolVersion, "a protocol version is required");
    assert(
      init.result.capabilities.tools,
      "tools capability must be advertised",
    );
    assert(init.result.serverInfo.name);

    // An agent must be told that writes go through git, not through a tool.
    assertStringIncludes(init.result.instructions, "git");

    const listed = await rpc(server, "tools/list");
    const names = listed.result.tools.map((t: { name: string }) => t.name);
    for (
      const expected of [
        "repo_create",
        "key_add",
        "tree_list",
        "search",
        "blame",
        "compare",
      ]
    ) {
      assert(names.includes(expected), `${expected} must be offered`);
    }
    for (const banned of ["file_write", "commit_create", "push"]) {
      assert(!names.includes(banned), `${banned} would be a second write path`);
    }
  },
);

live(
  "MCP handles unknown methods and notifications correctly",
  async (server) => {
    const unknown = await rpc(server, "nonsense/method");
    assertEquals(unknown.error.code, -32601);

    // A notification carries no id and takes no reply.
    const notified = await fetch(`http://127.0.0.1:${server.port}/mcp`, {
      method: "POST",
      headers: {
        authorization: `Bearer ${server.token}`,
        "content-type": "application/json",
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        method: "notifications/initialized",
      }),
    });
    await notified.text();
    assertEquals(notified.status, 202);

    const unauthenticated = await fetch(`http://127.0.0.1:${server.port}/mcp`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });
    await unauthenticated.text();
    assertEquals(unauthenticated.status, 401);
  },
);

live(
  "an agent provisions and inspects a repository entirely over MCP",
  async (server) => {
    // repo_create → key_add → git push → discovery. The whole product loop.
    const created = await callTool(server, "repo_create", { name: "shop" });
    const view = created.result.structuredContent;
    assertEquals(view.name, "shop");
    assert(view.clone_url_ssh, "the agent needs somewhere to push");

    const keyPath = `${server.workDir}/id_mcp`;
    await new Deno.Command("ssh-keygen", {
      args: ["-t", "ed25519", "-N", "", "-f", keyPath, "-q"],
      stdout: "null",
      stderr: "null",
    }).output();
    const pub = await Deno.readTextFile(`${keyPath}.pub`);

    const added = await callTool(server, "key_add", {
      title: "agent",
      key: pub,
    });
    assert(added.result.structuredContent.fingerprint.startsWith("SHA256:"));

    // The key just registered must actually work for git.
    const env = sshEnv(server, keyPath);
    const repo = `${server.workDir}/shop`;
    assertEquals(
      (await git(
        ["clone", sshUrl(server, "alice", "shop"), "shop"],
        server.workDir,
        env,
      )).code,
      0,
      "a key registered over MCP must authenticate over SSH",
    );
    await Deno.writeTextFile(`${repo}/price.txt`, "10\n");
    await git(["add", "."], repo, env);
    await git(["commit", "-m", "add price"], repo, env);
    assertEquals(
      (await git(["push", "origin", "HEAD:refs/heads/main"], repo, env)).code,
      0,
    );

    // Discovery sees what git pushed.
    const tree = await callTool(server, "tree_list", { repo: "shop" });
    const names = tree.result.structuredContent.items.map((
      e: { name: string },
    ) => e.name);
    assert(names.includes("price.txt"));

    const read = await callTool(server, "file_read", {
      repo: "shop",
      path: "price.txt",
    });
    assertEquals(read.result.structuredContent.content, "10\n");

    const refs = await callTool(server, "ref_list", { repo: "shop" });
    assertEquals(
      refs.result.structuredContent.items[0].name,
      "refs/heads/main",
    );

    const listed = await callTool(server, "repo_list");
    const repos = listed.result.structuredContent.items.map((
      r: { name: string },
    ) => r.name);
    assert(repos.includes("shop"));
  },
);

live(
  "MCP tool errors come back as tool results, not protocol failures",
  async (server) => {
    await createRepo(server, "site");

    // A model should see the failure and adapt; a protocol error looks like a bug.
    const missing = await callTool(server, "file_read", {
      repo: "site",
      path: "nope.txt",
    });
    assertEquals(missing.result.isError, true);
    assertEquals(missing.error, undefined);

    const badArgs = await callTool(server, "tree_list", {});
    assertEquals(badArgs.result.isError, true);

    const unknownTool = await callTool(server, "not_a_tool", {});
    assertEquals(unknownTool.result.isError, true);
  },
);

live("MCP enforces the same access rules as REST", async (server) => {
  await createRepo(server, "site");
  const bob = await mintToken(server.dataDir, "bob");

  const foreign = await callTool(server, "repo_get", { repo: "site" }, bob);
  assertEquals(
    foreign.result.isError,
    true,
    "bob must not read alice's repository",
  );

  const readOnly = await mintToken(server.dataDir, "alice", [
    "--scopes",
    "repo:read",
  ]);
  const denied = await callTool(
    server,
    "repo_create",
    { name: "nope" },
    readOnly,
  );
  assertEquals(denied.result.isError, true, "a read token must not create");

  // Key management requires an unconfined admin token, as it does over REST.
  const confined = await mintToken(server.dataDir, "alice", [
    "--scopes",
    "admin",
    "--repos",
    "site",
  ]);
  const keys = await callTool(server, "key_list", {}, confined);
  assertEquals(keys.result.isError, true);
});

live(
  "MCP discovery refuses traversal and smuggled git arguments",
  async (server) => {
    await seed(server);

    for (
      const [tool, args] of [
        ["file_read", { repo: "site", path: "../../../etc/passwd" }],
        ["tree_list", { repo: "site", path: ".git" }],
        ["commit_log", { repo: "site", ref: "--upload-pack=evil" }],
        ["commit_get", { repo: "site", sha: "--help" }],
        ["compare", { repo: "site", base: "HEAD", head: "HEAD" }],
        ["blame", { repo: "site", path: ".git/config" }],
      ] as Array<[string, Record<string, unknown>]>
    ) {
      const result = await callTool(server, tool, args);
      assertEquals(
        result.result.isError,
        true,
        `${tool} must refuse ${JSON.stringify(args)}`,
      );
    }
  },
);

// ── M4: limits, quotas and maintenance ─────────────────────────────────────

live(
  "an over-quota account cannot create more repositories",
  async (server) => {
    // The harness server runs with the default repo cap; assert against what the
    // account endpoint reports rather than a number baked into the test.
    const account = await json(await api(server, "GET", "/v1/account"));
    assert(account.limits.max_repos > 0, "a repo cap must be configured");
    assertEquals(account.usage.repos, 0);

    await createRepo(server, "one");
    const after = await json(await api(server, "GET", "/v1/account"));
    assertEquals(after.usage.repos, 1, "usage must track creation");
  },
);

live(
  "quota enforcement is reported as a problem with limit and usage",
  async (server) => {
    // A dedicated server with a cap of one repository.
    const tight = await startWith({ ZUKA_MAX_REPOS_PER_ACCOUNT: "1" });
    try {
      await createRepo(tight, "first");

      const refused = await api(tight, "POST", "/v1/repos", {
        body: { name: "second" },
      });
      const problem = await json(refused);
      assertEquals(refused.status, 413);
      assertStringIncludes(problem.type, "/problems/quota-exceeded");
      assertEquals(problem.limit, 1);
      assertEquals(problem.used, 1);

      // Reads keep working: over-quota blocks writes only.
      const readable = await api(tight, "GET", "/v1/repos/alice/first");
      await drain(readable);
      assertEquals(readable.status, 200, "over-quota must not break reads");
    } finally {
      await tight.stop();
    }
  },
);

live("a rate-limited caller is refused with Retry-After", async (_server) => {
  const limited = await startWith({ ZUKA_RATE_PER_MINUTE: "5" });
  try {
    // Set the repository up before spending the allowance.
    await createRepo(limited, "site");

    let refused: Response | undefined;
    for (let i = 0; i < 20; i++) {
      const response = await api(limited, "GET", "/v1/account");
      await drain(response);
      if (response.status === 429) {
        refused = response;
        break;
      }
    }
    assert(refused, "a caller must eventually be rate limited");
    assert(
      refused.headers.get("retry-after"),
      "a 429 must say when to come back",
    );

    // The git transport is deliberately NOT rate limited: a clone is one long
    // request, already bounded by the concurrency semaphore, and a per-minute
    // count is the wrong instrument for it.
    const cloned = await git(
      ["clone", cloneUrl(limited, "alice", "site"), "still-works"],
      limited.workDir,
    );
    assertEquals(
      cloned.code,
      0,
      `git must keep working while the API is rate limited: ${cloned.stderr}`,
    );
  } finally {
    await limited.stop();
  }
});

live(
  "gc reclaims unreachable objects and leaves the repository intact",
  async (server) => {
    const { repo, env } = await seed(server, "churn");
    const gitDir = `${server.dataDir}/git/alice/churn.git`;

    // Rewriting a branch orphans the objects the old tip referenced.
    await Deno.writeTextFile(
      `${repo}/src/main.rs`,
      "fn main() { /* rewritten */ }\n",
    );
    await git(["add", "."], repo, env);
    await git(["commit", "--amend", "-m", "rewritten"], repo, env);
    await git(["push", "--force", "origin", "HEAD:refs/heads/main"], repo, env);

    const before = await countLoose(gitDir);
    assert(before > 0, "there should be loose objects to collect");

    // Zero grace so the test does not have to wait two weeks.
    const gc = await runCli(server, ["gc"], { ZUKA_GC_PRUNE_GRACE: "now" });
    assertEquals(gc.code, 0, gc.stderr);
    assertStringIncludes(gc.stderr, "repacked");

    const intact = await new Deno.Command("git", {
      args: [
        "--git-dir",
        gitDir,
        "fsck",
        "--no-progress",
        "--connectivity-only",
      ],
      stdout: "null",
      stderr: "piped",
    }).output();
    assertEquals(intact.code, 0, "gc must not corrupt the repository");

    // The repository still serves after collection.
    const cloned = await git(
      ["clone", sshUrl(server, "alice", "churn"), "after-gc"],
      server.workDir,
      env,
    );
    assertEquals(cloned.code, 0, cloned.stderr);
  },
);

live("gc runs safely while a clone is in flight", async (server) => {
  const { repo, env } = await seed(server, "busy");

  // Make the repository big enough that the clone is still running when gc starts.
  // getRandomValues caps at 64 KiB per call, so fill in chunks. Incompressible so
  // the pack cannot be shrunk away and the clone actually takes time.
  const big = new Uint8Array(12 * 1024 * 1024);
  for (let offset = 0; offset < big.length; offset += 65536) {
    crypto.getRandomValues(
      big.subarray(offset, Math.min(offset + 65536, big.length)),
    );
  }
  await Deno.writeFile(`${repo}/blob.bin`, big);
  await git(["add", "."], repo, env);
  await git(["commit", "-m", "big"], repo, env);
  await git(["push", "origin", "HEAD:refs/heads/main"], repo, env);

  const cloning = git(
    ["clone", sshUrl(server, "alice", "busy"), "concurrent"],
    server.workDir,
    env,
  );
  // Default grace, which is what makes this safe — not a lock.
  const collecting = runCli(server, ["gc"], {});

  const [cloned, gc] = await Promise.all([cloning, collecting]);
  assertEquals(gc.code, 0, gc.stderr);
  assertEquals(
    cloned.code,
    0,
    `a clone must survive concurrent gc: ${cloned.stderr}`,
  );

  const size = (await Deno.stat(`${server.workDir}/concurrent/blob.bin`)).size;
  assertEquals(size, big.length, "the cloned content must be complete");
});

live("sweep reclaims deleted repositories once they expire", async (server) => {
  await createRepo(server, "doomed");
  await drain(await api(server, "DELETE", "/v1/repos/alice/doomed"));

  const graves = () =>
    [...Deno.readDirSync(`${server.dataDir}/tmp/deleted`)].length;
  assertEquals(graves(), 1, "delete must keep the repository recoverable");

  // Retention still running: nothing is reclaimed.
  const kept = await runCli(server, ["sweep"], {
    ZUKA_DELETED_RETENTION_DAYS: "7",
  });
  assertEquals(kept.code, 0);
  assertEquals(graves(), 1, "a fresh delete must stay recoverable");

  const swept = await runCli(server, ["sweep"], {
    ZUKA_DELETED_RETENTION_DAYS: "0",
  });
  assertEquals(swept.code, 0, swept.stderr);
  assertStringIncludes(swept.stderr, "removed 1");
  assertEquals(graves(), 0, "an expired delete must be reclaimed");
});

live("fsck finds an orphan in both directions", async (server) => {
  await createRepo(server, "paired");
  const clean = await runCli(server, ["fsck"], {});
  assertEquals(clean.code, 0, "a consistent store must exit zero");

  // Metadata without a repository: what a crash mid-delete leaves.
  await Deno.remove(`${server.dataDir}/git/alice/paired.git`, {
    recursive: true,
  });
  const dangling = await runCli(server, ["fsck"], {});
  assertEquals(
    dangling.code,
    1,
    "an orphan must be reported as a non-zero exit",
  );
  assertStringIncludes(dangling.stdout, "record-without-repo");

  // Repair removes the record, and the result is durable.
  const repaired = await runCli(server, ["fsck", "--repair"], {});
  assertEquals(repaired.code, 0);
  assertStringIncludes(repaired.stderr, "1 repaired");
  assertEquals((await runCli(server, ["fsck"], {})).code, 0);

  // A repository without metadata: reported, never deleted.
  await createRepo(server, "lonely");
  await Deno.remove(`${server.dataDir}/meta/alice/lonely.json`);
  const lonely = await runCli(server, ["fsck", "--repair"], {});
  assertStringIncludes(lonely.stdout, "repo-without-record");
  assert(
    await Deno.stat(`${server.dataDir}/git/alice/lonely.git/HEAD`).then(() =>
      true
    ),
    "repair must never delete real repository bytes",
  );
});

live(
  "newly created repositories do not run gc inside a push",
  async (server) => {
    await createRepo(server, "quiet");
    const gitDir = `${server.dataDir}/git/alice/quiet.git`;

    for (
      const [key, expected] of [["gc.auto", "0"], ["receive.autogc", "false"]]
    ) {
      const value = await new Deno.Command("git", {
        args: ["--git-dir", gitDir, "config", "--get", key],
        stdout: "piped",
        stderr: "null",
      }).output();
      assertEquals(
        new TextDecoder().decode(value.stdout).trim(),
        expected,
        `${key} must be off so maintenance is scheduled, not opportunistic`,
      );
    }
  },
);

// ── M5: CI ─────────────────────────────────────────────────────────────────

/** Push a repository carrying a CI spec, on a server with CI enabled. */
async function seedCi(server: Server, name: string, spec: string) {
  await createRepo(server, name);
  const env = sshEnv(server);
  const repo = `${server.workDir}/${name}-ci`;
  await git(
    ["clone", sshUrl(server, "alice", name), `${name}-ci`],
    server.workDir,
    env,
  );

  await Deno.writeTextFile(`${repo}/.zuka.toml`, spec);
  await git(["add", "."], repo, env);
  await git(["commit", "-m", "add ci"], repo, env);
  const pushed = await git(
    ["push", "origin", "HEAD:refs/heads/main"],
    repo,
    env,
  );
  assertEquals(pushed.code, 0, pushed.stderr);
  return { repo, env };
}

/** Poll until a run reaches a terminal state. */
async function settle(server: Server, repo: string, timeoutMs = 30_000) {
  const deadline = Date.now() + timeoutMs;
  for (;;) {
    const body = await json(
      await api(server, "GET", `/v1/repos/alice/${repo}/runs`),
    );
    const run = body.items[0];
    if (run && !["queued", "running"].includes(run.status)) return run;
    if (Date.now() > deadline) {
      throw new Error(`run did not settle: ${JSON.stringify(body.items)}`);
    }
    await new Promise((r) => setTimeout(r, 200));
  }
}

live(
  "a push triggers a run that succeeds and captures output",
  async (_server) => {
    const ci = await startWith({ ZUKA_CI_ENABLED: "1" });
    try {
      await seedCi(
        ci,
        "green",
        '[run]\nsteps = ["echo hello-from-ci", "true"]\n',
      );

      const run = await settle(ci, "green");
      assertEquals(run.status, "succeeded");
      assertEquals(run.exit_code, 0);
      assertEquals(run.git_ref, "refs/heads/main");
      assert(/^[0-9a-f]{40}$/.test(run.sha));

      const logs = await drain(
        await api(ci, "GET", `/v1/repos/alice/green/runs/${run.id}/logs`),
      );
      assertStringIncludes(logs, "hello-from-ci");
      assertStringIncludes(logs, "all steps succeeded");
    } finally {
      await ci.stop();
    }
  },
);

live(
  "a failing step fails the run and stops the remaining steps",
  async (_server) => {
    const ci = await startWith({ ZUKA_CI_ENABLED: "1" });
    try {
      await seedCi(
        ci,
        "red",
        '[run]\nsteps = ["echo first", "exit 3", "echo never-runs"]\n',
      );

      const run = await settle(ci, "red");
      assertEquals(run.status, "failed");
      assertEquals(run.exit_code, 3);

      const logs = await drain(
        await api(ci, "GET", `/v1/repos/alice/red/runs/${run.id}/logs`),
      );
      assertStringIncludes(logs, "first");
      assert(!logs.includes("never-runs"), "a failed step must stop the run");
    } finally {
      await ci.stop();
    }
  },
);

live("a run that exceeds its timeout is killed", async (_server) => {
  const ci = await startWith({
    ZUKA_CI_ENABLED: "1",
    ZUKA_CI_TIMEOUT_SECS: "2",
  });
  try {
    await seedCi(ci, "slow", '[run]\nsteps = ["sleep 60"]\ntimeout_secs = 2\n');

    const run = await settle(ci, "slow", 30_000);
    assertEquals(run.status, "timed_out");

    // The whole process group must be gone, not just the shell.
    const survivors = await new Deno.Command("pgrep", {
      args: ["-f", "sleep 60"],
      stdout: "piped",
    })
      .output();
    assertEquals(
      new TextDecoder().decode(survivors.stdout).trim(),
      "",
      "a timeout must kill the process group, not orphan it",
    );
  } finally {
    await ci.stop();
  }
});

live("CI steps cannot read the server's environment", async (_server) => {
  const ci = await startWith({
    ZUKA_CI_ENABLED: "1",
    // A secret the service process holds. A step must not see it.
    ZUKA_TEST_SECRET: "hunter2",
  });
  try {
    await seedCi(ci, "envtest", '[run]\nsteps = ["env | sort"]\n');

    const run = await settle(ci, "envtest");
    const logs = await drain(
      await api(ci, "GET", `/v1/repos/alice/envtest/runs/${run.id}/logs`),
    );

    assert(
      !logs.includes("hunter2"),
      "the service's environment must be scrubbed",
    );
    assertStringIncludes(
      logs,
      "ZUKA_RUN_ID=",
      "a step should know its own run",
    );
    assertStringIncludes(logs, "PATH=", "a step needs to find its tools");
  } finally {
    await ci.stop();
  }
});

live("a repository with no spec is a no-op, not a failure", async (_server) => {
  const ci = await startWith({ ZUKA_CI_ENABLED: "1" });
  try {
    await createRepo(ci, "nospec");
    const env = sshEnv(ci);
    const repo = `${ci.workDir}/nospec-x`;
    await git(
      ["clone", sshUrl(ci, "alice", "nospec"), "nospec-x"],
      ci.workDir,
      env,
    );
    await Deno.writeTextFile(`${repo}/README.md`, "no ci here\n");
    await git(["add", "."], repo, env);
    await git(["commit", "-m", "no ci"], repo, env);
    await git(["push", "origin", "HEAD:refs/heads/main"], repo, env);

    const run = await settle(ci, "nospec");
    assertEquals(run.status, "succeeded");
    assertEquals(run.detail, "no spec");
  } finally {
    await ci.stop();
  }
});

live(
  "a spec may lower its timeout but never raise it past the host ceiling",
  async (_server) => {
    const ci = await startWith({
      ZUKA_CI_ENABLED: "1",
      ZUKA_CI_TIMEOUT_SECS: "3",
    });
    try {
      // The spec asks for an hour; the host allows three seconds.
      await seedCi(
        ci,
        "greedy",
        '[run]\nsteps = ["sleep 60"]\ntimeout_secs = 3600\n',
      );

      const started = Date.now();
      const run = await settle(ci, "greedy", 30_000);
      assertEquals(run.status, "timed_out");
      assert(
        Date.now() - started < 25_000,
        "a repository must not be able to hold a runner past the host ceiling",
      );
    } finally {
      await ci.stop();
    }
  },
);

live("runs are triggerable and cancellable over the API", async (_server) => {
  const ci = await startWith({ ZUKA_CI_ENABLED: "1" });
  try {
    await seedCi(ci, "manual", '[run]\nsteps = ["sleep 30"]\n');
    await settle(ci, "manual", 40_000).catch(() => {});

    const triggered = await api(ci, "POST", `/v1/repos/alice/manual/runs`, {
      body: { ref: "refs/heads/main" },
    });
    const queued = await json(triggered);
    assertEquals(triggered.status, 201);
    assertEquals(queued.status, "queued");

    const cancelled = await json(
      await api(ci, "PATCH", `/v1/repos/alice/manual/runs/${queued.id}`, {
        body: { status: "cancelled" },
      }),
    );
    assertEquals(cancelled.status, "cancelled");

    // Cancelling a finished run is a conflict, not a silent no-op.
    const again = await api(
      ci,
      "PATCH",
      `/v1/repos/alice/manual/runs/${queued.id}`,
      {
        body: { status: "cancelled" },
      },
    );
    await drain(again);
    assertEquals(again.status, 409);
  } finally {
    await ci.stop();
  }
});

live("CI is off by default and triggering says so", async (server) => {
  await createRepo(server, "quiet");
  const health = await json(await api(server, "GET", "/healthz"));
  assertEquals(health.ci_enabled, false);

  const refused = await api(server, "POST", "/v1/repos/alice/quiet/runs", {
    body: {},
  });
  const problem = await json(refused);
  assertEquals(refused.status, 409);
  assertStringIncludes(problem.type, "/problems/ci-disabled");
});

live("an agent sees CI results over MCP", async (_server) => {
  const ci = await startWith({ ZUKA_CI_ENABLED: "1" });
  try {
    await seedCi(ci, "mcpci", '[run]\nsteps = ["echo built-ok"]\n');
    await settle(ci, "mcpci");

    const listed = await callTool(ci, "run_list", { repo: "mcpci" });
    const runs = listed.result.structuredContent.items;
    assertEquals(runs[0].status, "succeeded");

    const logs = await callTool(ci, "run_logs", {
      repo: "mcpci",
      id: runs[0].id,
    });
    assertStringIncludes(logs.result.structuredContent.logs, "built-ok");

    const one = await callTool(ci, "run_get", {
      repo: "mcpci",
      id: runs[0].id,
    });
    assertEquals(one.result.structuredContent.exit_code, 0);
  } finally {
    await ci.stop();
  }
});

live("deleting a repository takes its runs with it", async (_server) => {
  const ci = await startWith({ ZUKA_CI_ENABLED: "1" });
  try {
    await seedCi(ci, "doomed", '[run]\nsteps = ["true"]\n');
    await settle(ci, "doomed");

    await drain(await api(ci, "DELETE", "/v1/repos/alice/doomed"));
    const runsDir = `${ci.dataDir}/runs/alice/doomed`;
    await assertRejects(() => Deno.stat(runsDir));
  } finally {
    await ci.stop();
  }
});

// ── M7: import ─────────────────────────────────────────────────────────────

live(
  "import refuses every URL that could reach the local network",
  async (server) => {
    for (
      const url of [
        `http://127.0.0.1:${server.port}/alice/x.git`, // this very server
        "http://169.254.169.254/latest/meta-data/", // cloud metadata
        "http://localhost/a.git", // a name resolving to loopback
        "http://10.0.0.1/a.git",
        "http://192.168.1.1/a.git",
        "file:///etc/passwd",
        "ssh://git@github.com/a/b.git",
        "git://github.com/a/b.git",
        "https://user:token@github.com/a/b.git", // would persist creds in config
        "--upload-pack=evil",
      ]
    ) {
      const response = await api(server, "POST", "/v1/repos", {
        body: {
          name: `imp${Math.floor(Math.random() * 1e9)}`,
          import_url: url,
        },
      });
      const problem = await json(response);
      assertEquals(response.status, 400, `${url} must be refused`);
      assertStringIncludes(problem.type, "/problems/invalid-import-url");
    }
  },
);

live(
  "a repository imports with its history and is adopted",
  async (_server) => {
    // Serve a real bare repository over plain HTTP with git's dumb protocol, so the
    // import exercises the actual fetch path rather than only the guards. Importing
    // from loopback needs the operator opt-in, which is the point of that setting.
    const target = await startWith({ ZUKA_IMPORT_ALLOW_PRIVATE: "1" });
    const serveDir = await Deno.makeTempDir({ prefix: "zuka-origin-" });
    let httpd: Deno.HttpServer | undefined;

    try {
      // Build a repository with two commits.
      const work = `${serveDir}/work`;
      await git(["init", "-q", "--initial-branch=main", work], serveDir);
      await Deno.writeTextFile(`${work}/README.md`, "imported\n");
      await git(["add", "."], work);
      await git(["commit", "-m", "first"], work);
      await Deno.writeTextFile(`${work}/second.txt`, "two\n");
      await git(["add", "."], work);
      await git(["commit", "-m", "second"], work);

      // Mirror it and generate the files the dumb protocol needs.
      const bare = `${serveDir}/origin.git`;
      await git(["clone", "--bare", "-q", work, bare], serveDir);
      await git(["update-server-info"], bare);
      await git(
        ["--git-dir", bare, "config", "http.receivepack", "false"],
        serveDir,
      );

      const port = await freePort();
      httpd = Deno.serve(
        { port, hostname: "127.0.0.1", onListen: () => {} },
        async (req) => {
          const path = new URL(req.url).pathname.replace(/^\/origin\.git/, "");
          try {
            return new Response(await Deno.readFile(`${bare}${path}`));
          } catch {
            return new Response("not found", { status: 404 });
          }
        },
      );

      const created = await api(target, "POST", "/v1/repos", {
        body: {
          name: "copy",
          import_url: `http://127.0.0.1:${port}/origin.git`,
        },
      });
      const view = await json(created);
      assertEquals(created.status, 201, JSON.stringify(view));
      assertEquals(view.name, "copy");

      // History arrived.
      const log = await json(
        await api(target, "GET", "/v1/repos/alice/copy/commits"),
      );
      assertEquals(log.items.length, 2, "the full history must be imported");
      assertEquals(log.items[0].message, "second");

      const readme = await drain(
        await api(target, "GET", "/v1/repos/alice/copy/raw/README.md"),
      );
      assertEquals(readme, "imported\n");

      // Adopted: an imported repository must be indistinguishable from one we made.
      const gitDir = `${target.dataDir}/git/alice/copy.git`;
      for (
        const [key, expected] of [["gc.auto", "0"], [
          "receive.fsckObjects",
          "true",
        ]]
      ) {
        const value = await new Deno.Command("git", {
          args: ["--git-dir", gitDir, "config", "--get", key],
          stdout: "piped",
          stderr: "null",
        }).output();
        assertEquals(
          new TextDecoder().decode(value.stdout).trim(),
          expected,
          `an imported repository must carry ${key}`,
        );
      }
      const hook = await Deno.lstat(`${gitDir}/hooks/update`);
      assert(hook.isSymlink, "an imported repository must get our hooks");

      // And it serves: clone the copy back out.
      const cloned = await git(
        ["clone", cloneUrl(target, "alice", "copy"), "roundtrip"],
        target.workDir,
      );
      assertEquals(cloned.code, 0, cloned.stderr);
      assertEquals(
        await Deno.readTextFile(`${target.workDir}/roundtrip/second.txt`),
        "two\n",
      );
    } finally {
      await httpd?.shutdown();
      await target.stop();
      await Deno.remove(serveDir, { recursive: true }).catch(() => {});
    }
  },
);

live("import can be disabled entirely", async (_server) => {
  const off = await startWith({ ZUKA_IMPORT_ENABLED: "0" });
  try {
    const response = await api(off, "POST", "/v1/repos", {
      body: {
        name: "nope",
        import_url: "https://github.com/rust-lang/rust.git",
      },
    });
    const problem = await json(response);
    assertEquals(response.status, 409);
    assertStringIncludes(problem.type, "/problems/import-disabled");
  } finally {
    await off.stop();
  }
});

// ── public repositories and the browser surface ────────────────────────────
//
// This is the only surface reachable without a credential, so its boundaries are
// proven against a real server rather than asserted in a unit test.

/** Create a repository and say whether the world may read it. */
async function createRepoWithVisibility(
  server: Server,
  name: string,
  visibility: "public" | "private",
) {
  const response = await api(server, "POST", "/v1/repos", {
    body: { name, visibility },
  });
  const body = await drain(response);
  assertEquals(response.status, 201, body);
  assertEquals(JSON.parse(body).visibility, visibility);
}

/** Push one commit over HTTP so a repository has something to show. */
async function seedOverHttp(server: Server, repo: string, dir: string) {
  await Deno.mkdir(dir, { recursive: true });
  await git(["init", "-q", "-b", "main"], dir);
  await Deno.writeTextFile(`${dir}/README.md`, "# Title\n\nHello.\n");
  await git(["add", "-A"], dir);
  await git(["commit", "-qm", "first"], dir);
  const push = await git(
    [
      "push",
      "-q",
      `http://alice:${server.token}@127.0.0.1:${server.port}/alice/${repo}.git`,
      "main",
    ],
    dir,
  );
  assertEquals(push.code, 0, push.stderr);
}

live("a public repository can be cloned with no credential", async (server) => {
  await createRepoWithVisibility(server, "open", "public");
  const work = await Deno.makeTempDir();
  await seedOverHttp(server, "open", `${work}/src`);

  const cloned = await git(
    ["clone", "-q", `http://127.0.0.1:${server.port}/alice/open.git`, "copy"],
    work,
  );
  assertEquals(cloned.code, 0, cloned.stderr);
  assertStringIncludes(
    await Deno.readTextFile(`${work}/copy/README.md`),
    "Hello.",
  );
});

live("a public repository still refuses an anonymous write", async (server) => {
  await createRepoWithVisibility(server, "open", "public");
  const work = await Deno.makeTempDir();
  await seedOverHttp(server, "open", `${work}/src`);

  // Asked over fetch rather than through `git push`, because git consults
  // credential helpers — on a developer machine the keychain may still hold the
  // token used to seed the repository, and the test would then be measuring the
  // keychain rather than the service.
  //
  // Advertising refs for receive-pack is itself the write signal, so this is the
  // first point at which an anonymous push is refused.
  const advertise = await fetch(
    `http://127.0.0.1:${server.port}/alice/open.git/info/refs?service=git-receive-pack`,
  );
  await drain(advertise);
  assertEquals(advertise.status, 401, "public grants read, never write");

  const push = await fetch(
    `http://127.0.0.1:${server.port}/alice/open.git/git-receive-pack`,
    {
      method: "POST",
      headers: { "content-type": "application/x-git-receive-pack-request" },
      body: new Uint8Array([0, 0, 0, 0]),
    },
  );
  await drain(push);
  assertEquals(push.status, 401, "the pack endpoint must refuse it too");
});

live(
  "a private repository challenges rather than reporting absence",
  async (server) => {
    await createRepo(server, "closed");

    // 401, not 404: git only runs its credential helper on a challenge, so a 404
    // here would fail a legitimate clone as "not found" without ever asking for
    // the credential the user had configured.
    const response = await fetch(
      `http://127.0.0.1:${server.port}/alice/closed.git/info/refs?service=git-upload-pack`,
    );
    await drain(response);
    assertEquals(response.status, 401);

    // And a repository that does not exist answers identically, so the status
    // code cannot be used to enumerate private names.
    const absent = await fetch(
      `http://127.0.0.1:${server.port}/alice/no-such-thing.git/info/refs?service=git-upload-pack`,
    );
    await drain(absent);
    assertEquals(absent.status, 401);
  },
);

live("the browser shows a public repository and hides a private one", async (
  server,
) => {
  await createRepoWithVisibility(server, "open", "public");
  await createRepo(server, "closed");
  const work = await Deno.makeTempDir();
  await seedOverHttp(server, "open", `${work}/src`);

  const shown = await fetch(`http://127.0.0.1:${server.port}/alice/open`);
  const page = await drain(shown);
  assertEquals(shown.status, 200);
  assertStringIncludes(shown.headers.get("content-type") ?? "", "text/html");
  assertStringIncludes(page, "README.md");
  assertStringIncludes(page, "Hello.");

  // A page whose content depends on a credential must never be cached and
  // handed to someone else.
  assertStringIncludes(shown.headers.get("cache-control") ?? "", "no-store");
  // Nothing on this surface runs script, so an injection has nowhere to execute.
  assertStringIncludes(
    shown.headers.get("content-security-policy") ?? "",
    "default-src 'none'",
  );

  const hidden = await fetch(`http://127.0.0.1:${server.port}/alice/closed`);
  await drain(hidden);
  assertEquals(hidden.status, 404);
});

live("an owner's credential still opens their private repository", async (
  server,
) => {
  await createRepo(server, "closed");
  const work = await Deno.makeTempDir();
  await seedOverHttp(server, "closed", `${work}/src`);

  const response = await fetch(
    `http://127.0.0.1:${server.port}/alice/closed`,
    { headers: { authorization: `Bearer ${server.token}` } },
  );
  const page = await drain(response);
  assertEquals(response.status, 200, page);
  assertStringIncludes(page, "private");
});

live("a rejected credential does not become an anonymous visitor", async (
  server,
) => {
  await createRepoWithVisibility(server, "open", "public");
  const work = await Deno.makeTempDir();
  await seedOverHttp(server, "open", `${work}/src`);

  // Falling back would turn "your token expired" into a success on public
  // repositories and a bare 404 on private ones — the least debuggable failure a
  // credential can have.
  const response = await fetch(`http://127.0.0.1:${server.port}/alice/open`, {
    headers: { authorization: "Bearer sk_definitely_not_valid" },
  });
  await drain(response);
  assertEquals(response.status, 401);
});

live("visibility can be changed after creation and takes effect", async (
  server,
) => {
  await createRepo(server, "later");
  const work = await Deno.makeTempDir();
  await seedOverHttp(server, "later", `${work}/src`);

  const before = await fetch(`http://127.0.0.1:${server.port}/alice/later`);
  await drain(before);
  assertEquals(before.status, 404);

  const patched = await api(server, "PATCH", "/v1/repos/alice/later", {
    body: { visibility: "public" },
  });
  assertEquals(patched.status, 200, await drain(patched));

  const after = await fetch(`http://127.0.0.1:${server.port}/alice/later`);
  await drain(after);
  assertEquals(after.status, 200);

  // And back again, because a mistake has to be reversible.
  const closed = await api(server, "PATCH", "/v1/repos/alice/later", {
    body: { visibility: "private" },
  });
  assertEquals(closed.status, 200, await drain(closed));
  const gone = await fetch(`http://127.0.0.1:${server.port}/alice/later`);
  await drain(gone);
  assertEquals(gone.status, 404);
});

live("a file can be downloaded from the browser with no credential", async (
  server,
) => {
  await createRepoWithVisibility(server, "open", "public");
  const work = await Deno.makeTempDir();
  await seedOverHttp(server, "open", `${work}/src`);

  // The REST raw endpoint requires a credential; the viewer's Download link and a
  // README's relative images depend on this anonymous route instead.
  const response = await fetch(
    `http://127.0.0.1:${server.port}/alice/open/raw/main/README.md`,
  );
  const bytes = await response.text();
  assertEquals(response.status, 200);
  assertStringIncludes(bytes, "Hello.");
  assertStringIncludes(
    response.headers.get("content-disposition") ?? "",
    "attachment",
  );
  // Raw bytes are attacker-controlled; they must never render on this origin.
  assertStringIncludes(
    response.headers.get("content-security-policy") ?? "",
    "sandbox",
  );
});

live("a missing page is a readable page, not machine json", async (server) => {
  // A person tapping a dead link in a chat gets a sentence and a way home; the
  // status still says 404 so nothing can probe private names through the copy.
  const response = await fetch(
    `http://127.0.0.1:${server.port}/alice/no-such-project`,
  );
  const page = await drain(response);
  assertEquals(response.status, 404);
  assertStringIncludes(response.headers.get("content-type") ?? "", "text/html");
  assertStringIncludes(page, "nothing at this address");
});

live("a README cannot smuggle script into the page", async (server) => {
  await createRepoWithVisibility(server, "hostile", "public");
  const work = await Deno.makeTempDir();
  const dir = `${work}/src`;
  await Deno.mkdir(dir, { recursive: true });
  await git(["init", "-q", "-b", "main"], dir);
  await Deno.writeTextFile(
    `${dir}/README.md`,
    "# T\n\n<script>alert(1)</script>\n\n[x](javascript:alert(2))\n\n<img src=x onerror=alert(3)>\n",
  );
  await git(["add", "-A"], dir);
  await git(["commit", "-qm", "hostile"], dir);
  await git(
    [
      "push",
      "-q",
      `http://alice:${server.token}@127.0.0.1:${server.port}/alice/hostile.git`,
      "main",
    ],
    dir,
  );

  const page = await drain(
    await fetch(`http://127.0.0.1:${server.port}/alice/hostile`),
  );
  // Anyone who can push writes this file, and on a public repository that is a
  // stranger.
  assert(!page.includes("<script"), "raw script survived into the page");
  assert(!page.includes("onerror="), "an event handler survived into the page");
  assert(
    !page.toLowerCase().includes("href=\"javascript:"),
    "an executable href survived into the page",
  );
});
