import { chownSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { signGithubBrokerRunToken } from "@rakazo/core/node/github-broker-token"; import { describe, expect, it } from "vitest"; import { explainGitFailure, type GithubBrokerSession, githubBrokerSessionsForTests, githubHttpsRemote, gitProcessIdentity, hostGitInvocation, hostPathFor, } from "./github-broker.js"; import { demuxDockerLogs, helperContainerOptions, readMountPoints, visibleThroughMounts, } from "./github-broker-helper.js"; import { hasValidBearerToken } from "./supervisor-logic.js"; const session: GithubBrokerSession = { runId: "run-a", workspacePath: "/srv/homes/chief", allowedRemoteUrl: null, defaultBranch: "main", githubToken: "ghp_test", containerRoot: "/home/rakazo", pushApproved: false, }; describe("github broker sessions", () => { it("stores sessions for supervisor-authenticated registration", () => { const token = "supervisor-test-token"; expect(hasValidBearerToken(`Bearer ${token}`, token)).toBe(true); githubBrokerSessionsForTests().set("run-a", { ...session, workspacePath: "/tmp/project", allowedRemoteUrl: "https://github.com/example/demo.git", }); const runToken = signGithubBrokerRunToken("run-a", token); expect(runToken.startsWith("rgb1.")).toBe(true); githubBrokerSessionsForTests().delete("run-a"); }); }); describe("github broker host git", () => { it("maps computer paths onto the host workspace", () => { expect(hostPathFor(session, "/home/rakazo")).toBe("/srv/homes/chief"); expect(hostPathFor(session, "/home/rakazo/repos/demo")).toBe("/srv/homes/chief/repos/demo"); expect(hostPathFor(session, "/home/rakazo/../etc")).toBeNull(); expect(hostPathFor(session, "/tmp")).toBeNull(); expect(hostPathFor(session, "/home/rakazoX")).toBeNull(); }); it("clones where the agent stands, with the token supplied as a git credential", () => { const invocation = hostGitInvocation( session, ["git", "clone", "https://github.com/example/demo.git", "/home/rakazo/repos/demo"], "/home/rakazo/repos", ); if ("error" in invocation) throw new Error(invocation.error); expect(invocation.cwd).toBe("/srv/homes/chief/repos"); expect(invocation.argv[0]).toBe("/usr/bin/git"); expect(invocation.argv).toContain("credential.helper="); expect(invocation.argv.slice(-3)).toEqual([ "clone", "https://github.com/example/demo.git", "/srv/homes/chief/repos/demo", ]); expect(invocation.argv.join(" ")).not.toContain("ghp_test"); }); it("refuses paths outside the mounted workspace", () => { expect(hostGitInvocation(session, ["git", "clone", "https://x/y.git"], "/tmp")).toEqual({ error: "Run git inside /home/rakazo.", }); expect( hostGitInvocation(session, ["git", "clone", "https://x/y.git", "/etc/y"], "/home/rakazo"), ).toEqual({ error: "Use a path inside /home/rakazo." }); }); }); describe("github broker remotes", () => { it("sends GitHub remotes over HTTPS without pasted credentials", () => { expect(githubHttpsRemote("git@github.com:example/demo.git")).toBe( "https://github.com/example/demo.git", ); expect(githubHttpsRemote("ssh://git@github.com/example/demo")).toBe( "https://github.com/example/demo", ); expect(githubHttpsRemote("https://x-access-token:rgb1.abc@github.com/example/demo.git")).toBe( "https://github.com/example/demo.git", ); expect(githubHttpsRemote("https://gitlab.com/example/demo.git")).toBe( "https://gitlab.com/example/demo.git", ); expect(githubHttpsRemote("--depth=1")).toBe("--depth=1"); }); it("points a rejected token at reconnecting GitHub", () => { expect(explainGitFailure("remote: Invalid username or token.\n")).toContain( "reconnect GitHub under Settings, Integrations", ); expect(explainGitFailure("fatal: repository not found\n")).toBe( "fatal: repository not found\n", ); }); }); describe("github broker host tools", () => { it("ships git and gh in the supervisor image", () => { const dockerfile = readFileSync(path.join(import.meta.dirname, "../Dockerfile"), "utf8"); expect(dockerfile).toMatch(/apt-get install[^\n]* gh git/); }); it("runs git as the workspace owner only from a root supervisor", () => { const workspace = mkdtempSync(path.join(os.tmpdir(), "broker-owner-")); try { expect(gitProcessIdentity(workspace, 1000)).toBeUndefined(); expect(gitProcessIdentity(path.join(workspace, "missing"), 0)).toBeUndefined(); if (process.getuid?.() === 0) { chownSync(workspace, 1000, 1000); expect(gitProcessIdentity(workspace, 0)).toEqual({ uid: 1000, gid: 1000 }); } } finally { rmSync(workspace, { recursive: true, force: true }); } }); }); describe("github broker helper container", () => { const mounts = readMountPoints( [ "1 0 0:1 / / rw - overlay overlay rw", "2 1 0:2 /srv/data /srv/data rw - ext4 /dev/sda1 rw", "3 1 0:3 / /var/run/docker.sock rw - tmpfs tmpfs rw", ].join("\n"), ); it("sees only paths below the supervisor's own mounts", () => { expect(mounts).toEqual(["/", "/srv/data", "/var/run/docker.sock"]); expect(visibleThroughMounts("/srv/data/homes/chief", mounts)).toBe(true); // The parent of a bind mount exists in the container but is empty. expect(visibleThroughMounts("/srv", mounts)).toBe(false); expect(visibleThroughMounts("/home/owner/Projects/app", mounts)).toBe(false); }); it("binds the workspace and runs the computer image's real binary", () => { const options = helperContainerOptions( { argv: ["/usr/bin/git", "push", "origin", "fix"], cwd: "/home/owner/Projects/app", env: { GITHUB_TOKEN: "ghp_test" }, workspacePath: "/home/owner/Projects/app", }, "rakazo/computer:local", "1000:1000", ); expect(options).toMatchObject({ Image: "rakazo/computer:local", Entrypoint: ["/usr/local/libexec/rakazo/git"], Cmd: ["push", "origin", "fix"], WorkingDir: "/home/owner/Projects/app", User: "1000:1000", Env: ["GITHUB_TOKEN=ghp_test"], HostConfig: { Binds: ["/home/owner/Projects/app:/home/owner/Projects/app"] }, }); }); it("splits Docker's multiplexed output into stdout and stderr", () => { const frame = (stream: number, text: string) => { const header = Buffer.alloc(8); header[0] = stream; header.writeUInt32BE(Buffer.byteLength(text), 4); return Buffer.concat([header, Buffer.from(text)]); }; expect( demuxDockerLogs(Buffer.concat([frame(1, "out"), frame(2, "err"), frame(1, "!")])), ).toEqual({ stdout: "out!", stderr: "err" }); }); });