Files
fusion/scripts/__tests__/dockerfile-workspace-manifests.test.mjs
gsxdsm 189087adf8 feat(docker): ship gh, tailscale and cloudflared in the image
Operator asked for cloudflared, tailscale, rg, git and gh available by default
in the container. git/ca-certificates/ripgrep already landed; this adds the
remaining three.

Each comes from its vendor's own signed apt repository rather than a
curl-to-shell installer, so signature checking and upgrades follow the normal
apt path:
  gh          https://cli.github.com/packages
  tailscale   https://pkgs.tailscale.com/stable/debian
  cloudflared https://pkg.cloudflare.com/cloudflared

Why each belongs in the image: gh backs Fusion's gh-cli GitHub auth mode (the
auth route instructs operators to run `gh auth login`, impossible without the
binary), cloudflared backs the dashboard's remote-access feature whose in-app
installer cannot bootstrap itself reliably in a slim container, and tailscale is
the private-network option for the same box.

Installing tailscale does NOT make tailscaled runnable by itself: the daemon
also needs --cap-add NET_ADMIN --device /dev/net/tun at docker run. Shipping the
binary is the image's part; granting kernel capabilities stays an explicit
operator decision.

Commands were validated live in a running container before being written here;
the guard test asserts both the repo wiring and the package names so half a
change cannot silently ship.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:48:49 -07:00

157 lines
7.0 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { globSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import YAML from "yaml";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "../..");
function normalizeDockerSource(source) {
return source.replace(/^\.\//, "").replace(/\/$/, "");
}
function readWorkspacePackageManifestPaths(root = repoRoot) {
const workspacePath = path.join(root, "pnpm-workspace.yaml");
const workspace = YAML.parse(readFileSync(workspacePath, "utf8"));
const entries = Array.isArray(workspace?.packages) ? workspace.packages : [];
const manifestPaths = new Set();
for (const entry of entries) {
if (typeof entry !== "string" || entry.startsWith("!")) {
continue;
}
for (const manifest of globSync(`${entry.replace(/\/$/, "")}/package.json`, {
cwd: root,
nodir: true,
})) {
manifestPaths.add(manifest.split(path.sep).join("/"));
}
}
return manifestPaths;
}
function readBuilderPreInstallCopySources(dockerfile) {
const builderStart = dockerfile.match(/^FROM\s+.*\s+AS\s+builder\s*$/im);
assert.ok(builderStart?.index !== undefined, "Dockerfile must define a builder stage");
const afterBuilder = dockerfile.slice(builderStart.index + builderStart[0].length);
const nextStage = afterBuilder.search(/^FROM\s+/im);
const builderStage = nextStage === -1 ? afterBuilder : afterBuilder.slice(0, nextStage);
const install = builderStage.match(/RUN\s+pnpm\s+install\s+--frozen-lockfile\b/);
assert.ok(install?.index !== undefined, "builder stage must run pnpm install --frozen-lockfile");
const copied = [];
for (const match of builderStage.slice(0, install.index).matchAll(/^COPY\s+(?:--\S+\s+)*(.*?)\s+\S+\s*$/gm)) {
const sources = match[1].trim().split(/\s+/).map(normalizeDockerSource);
copied.push(...sources);
}
return copied;
}
function findMissingWorkspaceManifests(manifests, copySources) {
return [...manifests].filter((manifest) => !copySources.some((source) => (
source === manifest || source === "." || manifest.startsWith(`${source}/`)
))).sort();
}
function readDockerfileCopiedManifestPaths() {
const dockerfile = readFileSync(path.join(repoRoot, "Dockerfile"), "utf8");
const copied = readBuilderPreInstallCopySources(dockerfile);
return { copied, dockerfile };
}
test("Dockerfile builder pre-install copies cover every current workspace manifest", () => {
const expected = readWorkspacePackageManifestPaths();
const { copied } = readDockerfileCopiedManifestPaths();
assert.deepEqual(findMissingWorkspaceManifests(expected, copied), []);
assert.equal(new Set(copied).size, copied.length, "builder pre-install COPY sources must not be duplicated");
});
test("coverage rejects a selected plugin omitted before frozen install", () => {
const expected = readWorkspacePackageManifestPaths();
const omitted = [...expected].sort().find((manifest) => manifest.startsWith("plugins/"));
assert.ok(omitted, "workspace fixture must include a plugin manifest");
const completeSources = [...expected];
const incompleteSources = completeSources.filter((source) => source !== omitted);
assert.deepEqual(findMissingWorkspaceManifests(expected, incompleteSources), [omitted]);
});
test("coverage ignores post-install and runner copies while tolerating removed paths", () => {
const expected = readWorkspacePackageManifestPaths();
const omitted = [...expected].sort().find((manifest) => manifest.startsWith("plugins/"));
assert.ok(omitted, "workspace fixture must include a plugin manifest");
const builderCopies = [...expected]
.filter((manifest) => manifest !== omitted)
.map((manifest) => `COPY ${manifest} ./${manifest}`)
.join("\n");
const dockerfile = `FROM node:22-slim AS builder\n${builderCopies}\nRUN pnpm install --frozen-lockfile\nCOPY ${omitted} ./${omitted}\nFROM node:22-slim AS runner\nCOPY ${omitted} ./${omitted}`;
const copied = readBuilderPreInstallCopySources(dockerfile);
assert.deepEqual(findMissingWorkspaceManifests(expected, copied), [omitted]);
assert.deepEqual(
findMissingWorkspaceManifests(expected, [...expected, "plugins/not-in-workspace/package.json"]),
[],
"removed or nonexistent COPY paths must not affect selected workspace coverage",
);
});
/*
FNXC:DockerRun 2026-08-18-05:35:
The runner stage MUST install ca-certificates. The slim base ships none, and git verifies TLS
against the system store, so without it every HTTPS clone dies with "server certificate
verification failed. CAfile: none CRLfile: none" and project setup is impossible in Docker.
This regressed unnoticed because Node carries its OWN bundled CA store: the dashboard, model APIs
and OAuth token exchanges all worked, so nothing looked wrong until the first clone. Nothing else
in the image exercises the system trust store, which is exactly why it needs a guard rather than
relying on someone noticing.
*/
test("runner stage installs ca-certificates alongside git", () => {
const dockerfile = readFileSync(path.join(repoRoot, "Dockerfile"), "utf8");
const runnerStage = dockerfile.slice(dockerfile.indexOf("FROM node:22-slim AS runner"));
assert.ok(runnerStage.length > 0, "runner stage must exist");
const aptInstall = runnerStage.match(/apt-get install[^\n]*(?:\\\n[^\n]*)*/)?.[0] ?? "";
assert.match(aptInstall, /\bgit\b/, "runner stage must install git");
assert.match(
aptInstall,
/\bca-certificates\b/,
"runner stage must install ca-certificates — git cannot verify HTTPS remotes without a system CA bundle",
);
assert.match(
aptInstall,
/\bripgrep\b/,
"runner stage must install ripgrep — the coding agents Fusion drives use `rg` as their primary search tool",
);
});
/*
FNXC:DockerRun 2026-08-18-06:40:
The operator tooling the image promises must actually be in it. `gh` backs the gh-cli GitHub auth
mode, `cloudflared` backs remote access, and `tailscale` is the private-network option; each is
installed from its vendor's signed apt repository. Assert the repo wiring AND the package names, so
dropping either half (a keyring without the install, or an install whose repo line was removed) fails
here instead of at first use inside a container.
*/
test("runner stage installs gh, tailscale and cloudflared from vendor repositories", () => {
const dockerfile = readFileSync(path.join(repoRoot, "Dockerfile"), "utf8");
const runnerStage = dockerfile.slice(dockerfile.indexOf("FROM node:22-slim AS runner"));
for (const [tool, repo] of [
["gh", "https://cli.github.com/packages"],
["tailscale", "https://pkgs.tailscale.com/stable/debian"],
["cloudflared", "https://pkg.cloudflare.com/cloudflared"],
]) {
assert.ok(runnerStage.includes(repo), `runner stage must configure the ${tool} apt repository (${repo})`);
assert.match(runnerStage, new RegExp(`apt-get install[^\n]*(?:\\\n[^\n]*)*\\b${tool}\\b`), `runner stage must install ${tool}`);
}
});