The repo keeps binary assets as Git LFS objects, but the runner stage installed plain git. Without git-lfs, checkout writes ~130-byte pointer files in place of the real content and reports a clean tree, so an agent reads a text stub where an image should be and any `git lfs` subcommand fails. That is silent corruption of a working checkout, not a visibly missing tool, which is why it goes in beside git rather than with the optional tooling. Confirmed in the running container: screenshots/fn-061-medieval-desktop.png was a `version https://git-lfs.github.com/spec/v1` stub — 129 tracked files in that state — and became a valid 753KB PNG (signature and IEND intact) after installing git-lfs and running `git lfs pull`. The Dockerfile manifest guard now asserts the package so it cannot be dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
167 lines
7.4 KiB
JavaScript
167 lines
7.4 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-20-04:30:
|
|
Without git-lfs, git checks out 130-byte pointer files in place of LFS-tracked binaries and still
|
|
reports a clean tree — silent corruption of a working checkout rather than a visibly missing tool.
|
|
*/
|
|
assert.match(
|
|
aptInstall,
|
|
/\bgit-lfs\b/,
|
|
"runner stage must install git-lfs — LFS-tracked assets otherwise check out as pointer stubs and git reports the tree clean",
|
|
);
|
|
});
|
|
|
|
/*
|
|
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}`);
|
|
}
|
|
});
|