add per-agent smoke tests (#100)

This commit is contained in:
David Blass
2026-01-16 08:00:16 +00:00
committed by pullfrog[bot]
parent 9e019d89d2
commit f34379415e
24 changed files with 413 additions and 48 deletions
+83 -28
View File
@@ -1,5 +1,7 @@
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { existsSync, readFileSync, rmSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { platform, tmpdir } from "node:os";
import { dirname, extname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { fromHere } from "@ark/fs";
@@ -19,11 +21,14 @@ config();
config({ path: join(__dirname, "..", ".env") });
export async function run(inputsOrPrompt: Inputs | string): Promise<AgentResult> {
try {
const tempDir = join(__dirname, ".temp");
setupTestRepo({ tempDir });
// create unique temp directory path in OS temp location for parallel execution
// use a parent dir from mkdtemp, then clone into a 'repo' subdirectory
const tempParent = await mkdtemp(join(tmpdir(), "pullfrog-play-"));
const tempDir = join(tempParent, "repo");
const originalCwd = process.cwd();
const originalCwd = process.cwd();
try {
setupTestRepo({ tempDir });
process.chdir(tempDir);
// allow passing full Inputs object or just a prompt string
@@ -59,6 +64,10 @@ export async function run(inputsOrPrompt: Inputs | string): Promise<AgentResult>
const errorMessage = (err as Error).message;
log.error(`Error: ${errorMessage}`);
return { success: false, error: errorMessage, output: undefined };
} finally {
// cleanup temp directory
process.chdir(originalCwd);
rmSync(tempParent, { recursive: true, force: true });
}
}
@@ -82,7 +91,7 @@ Arguments:
Options:
--raw [prompt] Use raw string as prompt instead of loading from file
--local, -l Run locally on macOS (default: runs in Docker)
--local, -l Run locally (default: runs in Docker)
-h, --help Show this help message
Environment:
@@ -115,26 +124,72 @@ Examples:
value !== undefined ? ["-e", `${key}=${value}`] : []
);
// SSH for git - use SSH agent forwarding (no passphrase prompts)
// SSH for git - platform-specific handling
const sshFlags: string[] = [];
let sshSetupCmd = "";
const plat = platform();
const home = process.env.HOME;
if (home) {
const sshDir = join(home, ".ssh");
// mount known_hosts for host key verification
const knownHostsPath = join(sshDir, "known_hosts");
if (existsSync(knownHostsPath)) {
sshFlags.push("-v", `${knownHostsPath}:/root/.ssh/known_hosts:ro`);
if (plat === "win32") {
throw new Error(
"Docker mode is not supported on native Windows. Use WSL2 or set PLAY_LOCAL=1."
);
} else if (plat === "darwin") {
// macOS: Docker Desktop SSH agent forwarding
if (home) {
const knownHostsPath = join(home, ".ssh", "known_hosts");
if (existsSync(knownHostsPath)) {
sshFlags.push("-v", `${knownHostsPath}:/root/.ssh/known_hosts:ro`);
}
}
sshFlags.push(
"-v",
"/run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock",
"-e",
"SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock"
);
} else {
// Linux/WSL: copy .ssh files into container with correct permissions
if (home) {
const sshDir = join(home, ".ssh");
if (existsSync(sshDir)) {
sshFlags.push("-v", `${sshDir}:/tmp/.ssh-host:ro`);
// copy ssh keys, add github.com to known_hosts, set GIT_SSH_COMMAND to use them
sshSetupCmd =
"mkdir -p /tmp/home/.ssh && cp /tmp/.ssh-host/id_* /tmp/home/.ssh/ 2>/dev/null; chmod 600 /tmp/home/.ssh/id_* 2>/dev/null; " +
"ssh-keyscan -t ed25519,rsa github.com >> /tmp/home/.ssh/known_hosts 2>/dev/null; chmod 644 /tmp/home/.ssh/known_hosts; " +
"export GIT_SSH_COMMAND='ssh -i /tmp/home/.ssh/id_rsa -o UserKnownHostsFile=/tmp/home/.ssh/known_hosts -o StrictHostKeyChecking=no'; ";
}
}
}
// forward SSH agent from Docker Desktop on macOS
sshFlags.push(
"-v",
"/run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock",
"-e",
"SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock"
);
const ttyFlags = process.stdin.isTTY ? ["-it"] : [];
// always allocate a pseudo-TTY - Claude Code may require it
const ttyFlags = ["-t"];
// run as current user to avoid Claude CLI's root user restriction
const uid = process.getuid?.() ?? 1000;
const gid = process.getgid?.() ?? 1000;
// use agent-specific volume to avoid conflicts when running in parallel
const agentOverride = process.env.AGENT_OVERRIDE ?? "default";
const volumeName = `pullfrog-action-node-modules-${agentOverride}`;
// initialize volume with correct ownership (runs as root briefly)
spawnSync(
"docker",
[
"run",
"--rm",
"-v",
`${volumeName}:/app/action/node_modules`,
"node:24",
"chown",
"-R",
`${uid}:${gid}`,
"/app/action/node_modules",
],
{ stdio: "ignore", cwd: __dirname }
);
const result = spawnSync(
"docker",
@@ -142,24 +197,24 @@ Examples:
"run",
"--rm",
...ttyFlags,
"--user",
`${uid}:${gid}`,
"-v",
`${__dirname}:/app/action:cached`,
"-v",
"pullfrog-action-node-modules:/app/action/node_modules",
`${volumeName}:/app/action/node_modules`,
"-w",
"/app/action",
...envFlags,
...sshFlags,
"-e",
"COREPACK_ENABLE_DOWNLOAD_PROMPT=0",
"--cap-add",
"SYS_ADMIN",
"--security-opt",
"seccomp:unconfined",
"-e",
`HOME=/tmp/home`,
"node:24",
"bash",
"-c",
`corepack enable pnpm >/dev/null 2>&1 && pnpm install --frozen-lockfile && ${nodeCmd}`,
`${sshSetupCmd}mkdir -p /tmp/home/.config /tmp/home/.cache && corepack pnpm install --frozen-lockfile --ignore-scripts && ${nodeCmd}`,
],
{ stdio: "inherit", cwd: __dirname }
);
@@ -177,7 +232,7 @@ Examples:
const ext = extname(filePath).toLowerCase();
let resolvedPath: string;
const fixturesPath = fromHere("fixtures", filePath);
const fixturesPath = fromHere("test", "fixtures", filePath);
if (existsSync(fixturesPath)) {
resolvedPath = fixturesPath;
} else if (existsSync(filePath)) {