import assert from "node:assert/strict"; import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import path from "node:path"; import type { SelfTestCase } from "../harness.js"; import { imageRefSourceIdentity, imageRefSourceSummary, isDigestPinnedImage, validateAipodImageRef } from "../../common/env-image-ref.js"; import { smokeBundledWorkReadyCapabilities, smokeImageWorkReadyCapabilities, staticWorkReadyCapabilitySummary } from "../../common/work-ready.js"; import { loadArtificerImageRef } from "../harness.js"; const requiredRunnerPackages = Object.freeze(["ca-certificates", "curl", "git", "github-cli", "kubectl", "nodejs", "npm", "openssh-client", "ripgrep"]); const execFileAsync = promisify(execFile); const selfTest: SelfTestCase = async (context) => { const containerfile = await readFile(path.join(context.root, "deploy/container/Containerfile"), "utf8"); const apkPackages = installedApkPackages(containerfile); const tran = await readFile(path.join(context.root, "tools/tran"), "utf8"); const trans = await readFile(path.join(context.root, "tools/trans"), "utf8"); const applyPatch = await readFile(path.join(context.root, "tools/apply_patch"), "utf8"); for (const packageName of requiredRunnerPackages) { assert.equal(apkPackages.has(packageName), true, `runner image must install ${packageName}`); } assert.equal(containerfile.includes("bun install --production"), true, "runner image must install AgentRun npm dependencies at image build time"); assert.equal(containerfile.includes("npm --version"), true, "runner image build smoke must verify npm"); assert.equal(containerfile.includes("gh --version"), true, "runner image build smoke must verify GitHub CLI"); assert.equal(tran.startsWith("#!/usr/bin/env bun\n"), true, "tools/tran must be a shebang executable discovered by gitbundle tools"); assert.equal(trans.startsWith("#!/bin/sh\n"), true, "tools/trans must be a shebang executable discovered by gitbundle tools"); assert.equal(applyPatch.startsWith("#!/bin/sh\n"), true, "tools/apply_patch must be a shebang helper copied with runner tools"); assert.equal(tran.includes("UNIDESK_SSH_CLIENT_TOKEN"), true, "tools/tran must require the scoped UniDesk SSH client token"); assert.equal(tran.includes("/ws/ssh"), true, "tools/tran must use the frontend SSH WebSocket path"); assert.equal(tran.includes("apply-patch < patch.diff"), true, "tools/tran help must advertise runner-side apply-patch"); const help = await execFileAsync(path.join(context.root, "tools/tran"), ["--help"], { cwd: context.root, timeout: 10_000 }); const parsed = JSON.parse(help.stdout) as { ok?: boolean; supported?: string[]; unsupported?: string[]; valuesPrinted?: boolean }; assert.equal(parsed.ok, true); assert.equal(parsed.valuesPrinted, false); assert.equal(parsed.supported?.some((line) => line.includes("script")), true); assert.equal(parsed.supported?.some((line) => line.includes("apply-patch")), true); assert.equal(parsed.unsupported?.includes("apply-patch"), false); const patchHelp = await execFileAsync(path.join(context.root, "tools/apply_patch"), ["--help"], { cwd: context.root, timeout: 10_000 }); assert.equal(patchHelp.stdout.includes("reads *** Begin Patch format"), true); const summary = staticWorkReadyCapabilitySummary(); assert.equal(summary.valuesPrinted, false); assert.ok((summary.requiredImageTools as string[]).includes("npm"), "work-ready capability must include npm"); assert.ok((summary.requiredImageTools as string[]).includes("gh"), "work-ready capability must include gh"); assert.equal(((summary.dependencyStrategy as { projectDependencies?: unknown }).projectDependencies), "not-installed-by-default"); const fakeBin = await createFakeToolBin(path.join(context.tmp, "work-ready-bin")); const smokeEnv = { PATH: fakeBin, AGENTRUN_RESOURCE_BIN_PATH: path.join(context.root, "tools") }; const imageSmoke = await smokeImageWorkReadyCapabilities(smokeEnv); assert.equal(((imageSmoke.smoke as { ok?: unknown }).ok), true); assert.equal(imageSmoke.valuesPrinted, false); const bundleSmoke = await smokeBundledWorkReadyCapabilities(smokeEnv); assert.equal(((bundleSmoke.smoke as { ok?: unknown }).ok), true); assert.equal(bundleSmoke.valuesPrinted, false); assert.equal(JSON.stringify({ imageSmoke, bundleSmoke }).includes("GH_TOKEN"), false); const artificerImageRef = await loadArtificerImageRef(context.root); const imageRef = validateAipodImageRef({ kind: "env-image-dockerfile", repoUrl: "git@github.com:pikasTech/agentrun.git", commitId: artificerImageRef.commitId, dockerfilePath: "deploy/container/Containerfile" }); assert.equal(imageRefSourceIdentity(imageRef).length, 20); assert.equal((imageRefSourceSummary(imageRef).valuesPrinted), false); assert.equal(isDigestPinnedImage("127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111"), true); assert.equal(isDigestPinnedImage("127.0.0.1:5000/agentrun/agentrun-mgr:self-test"), false); return { name: "90-runner-image-tools", tests: ["runner image installs required CLI tools", "runner image build verifies work-ready tools", "gitbundle tran tools are executable and documented", "runner apply-patch helper is bundled", "work-ready smoke runs without printing secrets", "aipod imageRef validates env image source identity"] }; }; async function createFakeToolBin(dir: string): Promise { await mkdir(dir, { recursive: true }); for (const tool of ["bun", "node", "npm", "git", "ssh", "gh", "rg", "curl", "kubectl"]) { const file = path.join(dir, tool); await writeFile(file, `#!/bin/sh\necho ${tool}-selftest-version\n`, "utf8"); await chmod(file, 0o755); } return dir; } function installedApkPackages(containerfile: string): Set { const packages = new Set(); const normalized = containerfile.replace(/\\\s*\r?\n\s*/gu, " "); for (const match of normalized.matchAll(/\bapk\s+add\s+--no-cache\s+([^\n]+)/gu)) { const rawPackages = match[1] ?? ""; for (const item of rawPackages.split(/\s+/u)) { const packageName = item.trim(); if (packageName) packages.add(packageName); } } return packages; } export default selfTest;