diff --git a/test/forge.test.ts b/test/forge.test.ts new file mode 100644 index 0000000..e8e3862 --- /dev/null +++ b/test/forge.test.ts @@ -0,0 +1,50 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { carry } from "./integration/forge.ts"; + +/** + * A bed's forge takes the lab's checkouts as they are: shallow (novox/hq issue 689). The lab module clones + * every repository with `--depth`, and the genesis bed's forge refused the push ("shallow update not + * allowed"), so the bed never reached its installer. Held here with a real git and no lab. + */ + +const git = (cwd: string, ...args: string[]) => + execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, GIT_AUTHOR_NAME: "lab", GIT_AUTHOR_EMAIL: "lab@example.org", + GIT_COMMITTER_NAME: "lab", GIT_COMMITTER_EMAIL: "lab@example.org" } }).trim(); + +test("a shallow checkout, as the lab clones it, is carried into the bed's forge and served from it", () => { + const dir = mkdtempSync(join(tmpdir(), "mesh-lab-689-")); + try { + const origin = join(dir, "origin"); + execFileSync("git", ["init", "-q", "-b", "main", origin]); + for (let i = 0; i < 5; i++) { + writeFileSync(join(origin, "file"), `${i}\n`); + git(origin, "add", "file"); + git(origin, "commit", "-q", "-m", `commit ${i}`); + } + // As the lab module clones: shallow, over a transport that honours the depth. + const checkout = join(dir, "checkout"); + execFileSync("git", ["clone", "-q", "--depth", "2", "--branch", "main", `file://${origin}`, checkout]); + assert.equal(git(checkout, "rev-parse", "--is-shallow-repository"), "true"); + + const bare = join(dir, "forge", "repo.git"); + const head = carry(checkout, bare, ["main", "lab"]); + assert.equal(head, git(origin, "rev-parse", "HEAD")); + assert.equal(git(bare, "rev-parse", "refs/heads/main"), head); + assert.equal(git(bare, "rev-parse", "refs/heads/lab"), head); + + // What the bed's machines do with it: clone over git's protocol, and find the commit under test. + const served = join(dir, "served"); + execFileSync("git", ["clone", "-q", "--branch", "main", `file://${bare}`, served]); + assert.equal(git(served, "rev-parse", "HEAD"), head); + assert.equal(git(served, "show", "HEAD:file"), "4"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/test/integration/adoption.test.ts b/test/integration/adoption.test.ts index 33ba582..685c4e2 100644 --- a/test/integration/adoption.test.ts +++ b/test/integration/adoption.test.ts @@ -55,6 +55,7 @@ import { catalogueRoot } from "../../src/repos.ts"; import { ready, returnTo, keep } from "../../src/warm.ts"; import { labIsUsable, destroyAll, foundationBundle, catalogueModule, catalogueManifest } from "./harness.ts"; import { genesis, type GenesisOptions } from "./genesis.ts"; +import { carry } from "./forge.ts"; const SCENARIO = "adoption"; const CONTROL = "anchor"; @@ -246,11 +247,7 @@ async function raiseForge(): Promise> { const heads: Record = {}; try { for (const [name, checkout] of Object.entries(REPOS)) { - const bare = join(dir, `${name}.git`); - execFileSync("git", ["init", "-q", "--bare", bare]); - execFileSync("git", ["-C", checkout, "push", "-q", "--force", bare, - "HEAD:refs/heads/main", "HEAD:refs/heads/lab"], { stdio: "pipe" }); - heads[name] = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); + heads[name] = carry(checkout, join(dir, `${name}.git`), ["main", "lab"]); } const tar = join(tmpdir(), `mesh-lab-forge-${process.pid}.tar`); execFileSync("tar", ["-cf", tar, "-C", dir, "."]); diff --git a/test/integration/forge.ts b/test/integration/forge.ts new file mode 100644 index 0000000..df2529b --- /dev/null +++ b/test/integration/forge.ts @@ -0,0 +1,31 @@ +/** + * A bed's forge: a checkout's HEAD carried into a fresh bare repository, which a git daemon then serves. + * + * **A shallow checkout is carried as it is** (novox/hq issue 689). The lab module checks every repository + * out shallow, and a fresh bare repository refuses a push whose history ends in a cut ("shallow update not + * allowed"), so the genesis bed raised its anchor and forge and then skipped. The bare repository accepts + * the cut (`receive.shallowUpdate`) and serves the history the checkout holds: the bed serves the commit + * under test, and nothing it raises reads further back than the run's checkout does. Unshallowing first is + * not an option in a lab run: the forge credential is gone once the run's clones are done, so a private + * repository could not be fetched again. + */ + +import { execFileSync } from "node:child_process"; + +/** + * Initialise `bare` and push `checkout`'s HEAD into it under each of `branches`. Returns the commit served. + */ +export function carry(checkout: string, bare: string, branches: string[] = ["main"]): string { + execFileSync("git", ["init", "-q", "--bare", bare]); + // No collection after the push: one run in the background rewrites the objects while they are carried. + execFileSync("git", ["-C", bare, "config", "receive.autogc", "false"]); + // The lab's checkouts are shallow, and a bare repository refuses a shallow push without this. + execFileSync("git", ["-C", bare, "config", "receive.shallowUpdate", "true"]); + const [first, ...rest] = branches; + execFileSync("git", ["-C", checkout, "push", "-q", "--force", bare, `HEAD:refs/heads/${first}`], { stdio: "pipe" }); + const head = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); + // The other branches are set in the bare repository itself: a shallow push of several refs at once has + // receive-pack fail every ref after the first ("remote failed to report status"). + for (const branch of rest) execFileSync("git", ["-C", bare, "update-ref", `refs/heads/${branch}`, head]); + return head; +} diff --git a/test/integration/genesis-keeps-its-bus.test.ts b/test/integration/genesis-keeps-its-bus.test.ts index 2fbb13e..612f2f1 100644 --- a/test/integration/genesis-keeps-its-bus.test.ts +++ b/test/integration/genesis-keeps-its-bus.test.ts @@ -44,6 +44,7 @@ import { destroy, exec, push } from "../../src/lifecycle/operate.ts"; import { bootstrapBinaryPath, hostBinaryPath } from "../../src/lifecycle/place.ts"; import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts"; import { genesis, type GenesisResult } from "./genesis.ts"; +import { carry } from "./forge.ts"; import { BUS, busContainers, busNotKept, runningFrom, streamsFrom, written, type Seen } from "./bus-kept.ts"; const SCENARIO = "genesis-keeps-its-bus"; @@ -107,13 +108,7 @@ async function raiseForge(): Promise { const dir = mkdtempSync(join(tmpdir(), "mesh-lab-590-forge-")); try { for (const repo of REPOS) { - const checkout = resolve(repos, repo); - heads[repo] = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); - const bare = join(dir, `${repo}.git`); - execFileSync("git", ["init", "-q", "--bare", bare]); - // No collection after the push: one run in the background rewrites the objects while they are carried. - execFileSync("git", ["-C", bare, "config", "receive.autogc", "false"]); - execFileSync("git", ["-C", checkout, "push", "-q", "--force", bare, "HEAD:refs/heads/main"], { stdio: "pipe" }); + heads[repo] = carry(resolve(repos, repo), join(dir, `${repo}.git`)); } const tar = join(tmpdir(), `mesh-lab-590-forge-${process.pid}.tar`); execFileSync("tar", ["-cf", tar, "-C", dir, "."]); diff --git a/test/integration/replay92.test.ts b/test/integration/replay92.test.ts index 23870d6..4ade955 100644 --- a/test/integration/replay92.test.ts +++ b/test/integration/replay92.test.ts @@ -43,6 +43,7 @@ import { destroy, exec, push } from "../../src/lifecycle/operate.ts"; import { bootstrapBinaryPath, hostBinaryPath } from "../../src/lifecycle/place.ts"; import { labIsUsable, destroyAll, foundationBundle } from "./harness.ts"; import { genesis, type GenesisResult } from "./genesis.ts"; +import { carry } from "./forge.ts"; const SCENARIO = "genesis-untrusted"; const ANCHOR = "anchor"; @@ -98,13 +99,9 @@ async function must(machine: string, command: string, timeoutMs?: number): Promi /** The control plane's repository at the checkout's HEAD, served from the forge machine over git's protocol. */ async function raiseForge(): Promise { const dir = mkdtempSync(join(tmpdir(), "mesh-lab-r92-forge-")); - const head = execFileSync("git", ["-C", controller, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); + let head: string; try { - const bare = join(dir, "mesh-controller.git"); - execFileSync("git", ["init", "-q", "--bare", bare]); - // No collection after the push: one run in the background rewrites the objects while they are carried. - execFileSync("git", ["-C", bare, "config", "receive.autogc", "false"]); - execFileSync("git", ["-C", controller, "push", "-q", "--force", bare, "HEAD:refs/heads/main"], { stdio: "pipe" }); + head = carry(controller, join(dir, "mesh-controller.git")); const tar = join(tmpdir(), `mesh-lab-r92-forge-${process.pid}.tar`); execFileSync("tar", ["-cf", tar, "-C", dir, "."]); await push(instanceId, FORGE, tar, "/tmp/forge.tar");