/** * Spawn-based integration suite: the runner as the provisioner (story 06) * will actually run it — a real child process, config via env only, port * discovered from stdout, real WS from the outside. * * The full pipeline test is the story's M0-recast acceptance seam: a * `git daemon`-served vite-kitchen, cloned and npm-installed by the * spawned runner (real registry traffic — minutes on a cold cache), with * the credential-leak grep running over the child process's ENTIRE * stdout+stderr and the resulting clone's `.git/config`. */ import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; import { serveFixtureRepo, type ServedFixtureRepo, } from "@repo/core-testing/git"; import { spawnRunner, type SpawnedRunner } from "./spawn-runner"; import { connectProtocolClient, expectNamedError, handshake, sendAndAwaitReady, type ProtocolClient, } from "./protocol-client"; const TOKEN = "spawned-runner-token"; const PAT = "ghp_spawned-pipeline-pat-51c2"; /** repo-root/fixtures/vite-kitchen, resolved from this file's location. */ const FIXTURE_DIR = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "../../../fixtures/vite-kitchen", ); let runner: SpawnedRunner | undefined; let clients: ProtocolClient[] = []; let served: ServedFixtureRepo | undefined; afterEach(async () => { for (const client of clients) client.close(); clients = []; await runner?.stop(); runner = undefined; await served?.stop(); served = undefined; }); describe("spawned runner process", () => { it("boots from env config, announces its port, and completes the handshake over real WS", async () => { runner = await spawnRunner({ token: TOKEN }); expect(runner.port).toBeGreaterThan(0); const client = await connectProtocolClient(runner.port, TOKEN); clients.push(client); const ready = await handshake(client); expect(ready).toEqual({ type: "ready" }); }); it("rejects a bad workspace token from a real client and closes the socket", async () => { runner = await spawnRunner({ token: TOKEN }); const client = await connectProtocolClient(runner.port, "not-the-token"); clients.push(client); client.send({ type: "hello" }); await expectNamedError(client, "unauthorized"); await expect(client.closed).resolves.toBe(1008); // The runner never writes the workspace token to its logs. expect(runner.output()).not.toContain(TOKEN); }); it("runs the clone → install pipeline on vite-kitchen without leaking the credential", async () => { served = await serveFixtureRepo(FIXTURE_DIR); runner = await spawnRunner({ token: TOKEN }); const client = await connectProtocolClient(runner.port, TOKEN); clients.push(client); await handshake(client); await sendAndAwaitReady( client, { type: "clone", gitUrl: served.cloneUrl, pat: PAT }, "spawned clone completion", ); // Real npm install of vite-kitchen (react, vite, tailwind, …): // minutes-scale on a cold cache — this is the honest cost of the // story's acceptance seam. await sendAndAwaitReady( client, { type: "install" }, "spawned install completion", 220_000, ); // Staged progress arrived for both stages, elapsed nondecreasing per stage. for (const stage of ["cloning", "installing"] as const) { const statuses = client .received() .flatMap((m) => (m.type === "status" && m.stage === stage ? [m] : [])); expect(statuses.length).toBeGreaterThanOrEqual(2); expect(statuses[0]?.elapsedMs).toBe(0); const elapsed = statuses.map((s) => s.elapsedMs); expect([...elapsed].sort((a, b) => a - b)).toEqual(elapsed); } // The install really happened: the fixture's dependencies exist. const repoDir = path.join(runner.workspaceDir, "repo"); expect(existsSync(path.join(repoDir, "node_modules", "react"))).toBe(true); expect(existsSync(path.join(repoDir, "node_modules", "vite"))).toBe(true); // Leak assertions over the REAL process boundary (spec §15): the // PAT and the workspace token appear nowhere in the child's whole // stdout+stderr (which includes every logged git argv), nor in the // clone's .git/config. expect(runner.output()).not.toContain(PAT); expect(runner.output()).not.toContain(TOKEN); const gitConfig = await readFile( path.join(repoDir, ".git", "config"), "utf8", ); expect(gitConfig).not.toContain(PAT); expect(gitConfig).toContain(served.cloneUrl); }, 300_000); });