import { readFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; const repoRoot = resolve(fileURLToPath(new URL(".", import.meta.url)), ".."); const dockerfilePath = join(repoRoot, "Dockerfile"); describe("Dockerfile", () => { it("uses shared multi-arch base image refs for all root Node stages", async () => { const dockerfile = await readFile(dockerfilePath, "utf8"); expect(dockerfile).toContain( 'ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"', ); expect(dockerfile).toContain( 'ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"', ); expect(dockerfile).toContain("FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS ext-deps"); expect(dockerfile).toContain("FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS build"); expect(dockerfile).toContain("FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS base-default"); expect(dockerfile).toContain("FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-slim"); expect(dockerfile).toContain("current multi-arch manifest list entry"); expect(dockerfile).not.toContain("current amd64 entry"); }); it("installs optional browser dependencies after pnpm install", async () => { const dockerfile = await readFile(dockerfilePath, "utf8"); const installIndex = dockerfile.indexOf("pnpm install --frozen-lockfile"); const browserArgIndex = dockerfile.indexOf("ARG OPENCLAW_INSTALL_BROWSER"); expect(installIndex).toBeGreaterThan(-1); expect(browserArgIndex).toBeGreaterThan(-1); expect(browserArgIndex).toBeGreaterThan(installIndex); expect(dockerfile).toContain( "node /app/node_modules/playwright-core/cli.js install --with-deps chromium", ); expect(dockerfile).toContain("apt-get install -y --no-install-recommends xvfb"); }); it("prunes runtime dependencies after the build stage", async () => { const dockerfile = await readFile(dockerfilePath, "utf8"); expect(dockerfile).toContain("FROM build AS runtime-assets"); expect(dockerfile).toContain("CI=true pnpm prune --prod"); expect(dockerfile).toContain( "COPY --from=runtime-assets --chown=node:node /app/node_modules ./node_modules", ); }); it("normalizes plugin and agent paths permissions in image layers", async () => { const dockerfile = await readFile(dockerfilePath, "utf8"); expect(dockerfile).toContain("for dir in /app/extensions /app/.agent /app/.agents"); expect(dockerfile).toContain('find "$dir" -type d -exec chmod 755 {} +'); expect(dockerfile).toContain('find "$dir" -type f -exec chmod 644 {} +'); }); it("Docker GPG fingerprint awk uses correct quoting for OPENCLAW_SANDBOX=1 build", async () => { const dockerfile = await readFile(dockerfilePath, "utf8"); expect(dockerfile).toContain('== "fpr" {'); expect(dockerfile).not.toContain('\\"fpr\\"'); }); it("keeps runtime pnpm available", async () => { const dockerfile = await readFile(dockerfilePath, "utf8"); expect(dockerfile).toContain("ENV COREPACK_HOME=/usr/local/share/corepack"); expect(dockerfile).toContain( 'corepack prepare "$(node -p "require(\'./package.json\').packageManager")" --activate', ); }); });