import { describe, it, expect } from "bun:test"; import { execSync } from "child_process"; import { resolve, join } from "path"; import { mkdirSync, writeFileSync, rmSync, existsSync } from "fs"; import { tmpdir } from "os"; /** * Tests for shared/common.sh helper functions that had zero test coverage: * * - log_install_failed: Actionable error guidance for agent installation failures * (recently added across 126 scripts in commit 0f60a2b) * - ensure_jq: Cross-platform jq installation (used by many cloud providers) * - get_cloud_init_userdata: Cloud-init template generation * - _multi_creds_validate: Multi-credential validation with provider test function * * Each test sources shared/common.sh in a real bash subprocess to catch * actual shell behavior (quoting, variable expansion, exit codes). * * Agent: test-engineer */ const REPO_ROOT = resolve(import.meta.dir, "../../.."); const COMMON_SH = resolve(REPO_ROOT, "shared/common.sh"); /** * Run a bash snippet that sources shared/common.sh first. * Returns { exitCode, stdout, stderr }. */ function runBash(script: string): { exitCode: number; stdout: string; stderr: string } { const fullScript = `source "${COMMON_SH}"\n${script}`; try { const stdout = execSync(`bash -c '${fullScript.replace(/'/g, "'\\''")}'`, { encoding: "utf-8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"], }); return { exitCode: 0, stdout: stdout.trim(), stderr: "" }; } catch (err: any) { return { exitCode: err.status ?? 1, stdout: (err.stdout ?? "").toString().trim(), stderr: (err.stderr ?? "").toString().trim(), }; } } // ============================================================================ // log_install_failed // ============================================================================ describe("log_install_failed", () => { it("should include agent name in error output", () => { const result = runBash(`log_install_failed "Claude Code" 2>&1`); expect(result.stdout).toContain("Claude Code"); expect(result.stdout).toContain("installation failed to complete successfully"); }); it("should include all three arguments in error output", () => { const result = runBash( `log_install_failed "Claude Code" "npm install -g claude" "192.168.1.1" 2>&1` ); expect(result.stdout).toContain("Claude Code"); expect(result.stdout).toContain("npm install -g claude"); expect(result.stdout).toContain("192.168.1.1"); }); it("should show SSH hint when server IP is provided", () => { const result = runBash( `log_install_failed "Aider" "" "10.0.0.5" 2>&1` ); expect(result.stdout).toContain("ssh root@10.0.0.5"); }); it("should not show SSH hint when server IP is empty", () => { const result = runBash(`log_install_failed "Aider" "pip install aider" "" 2>&1`); expect(result.stdout).not.toContain("ssh root@"); }); it("should show install command hint when install_cmd is provided", () => { const result = runBash( `log_install_failed "Goose" "pip install goose-ai" 2>&1` ); expect(result.stdout).toContain("Try the installation manually"); expect(result.stdout).toContain("pip install goose-ai"); }); it("should not show install hint when install_cmd is empty", () => { const result = runBash(`log_install_failed "Goose" "" 2>&1`); expect(result.stdout).not.toContain("Try the installation manually"); }); it("should always show common causes section", () => { const result = runBash(`log_install_failed "Test" 2>&1`); expect(result.stdout).toContain("Common causes"); expect(result.stdout).toContain("Network timeout downloading packages"); expect(result.stdout).toContain("Insufficient disk space"); }); it("should always suggest re-running the spawn command", () => { const result = runBash(`log_install_failed "Test" 2>&1`); expect(result.stdout).toContain("Re-run spawn to try on a fresh server"); }); it("should handle agent name with spaces", () => { const result = runBash( `log_install_failed "Claude Code Extended" "curl install.sh" "1.2.3.4" 2>&1` ); expect(result.stdout).toContain("Claude Code Extended"); expect(result.stdout).toContain("installation failed to complete successfully"); }); it("should not exit with an error code (informational only)", () => { const result = runBash(`log_install_failed "Test" "cmd" "1.2.3.4"`); expect(result.exitCode).toBe(0); }); it("should handle single argument (only agent name)", () => { const result = runBash(`log_install_failed "GPTMe" 2>&1`); expect(result.stdout).toContain("GPTMe"); expect(result.stdout).toContain("installation failed to complete successfully"); expect(result.stdout).not.toContain("ssh root@"); expect(result.stdout).not.toContain("Try the installation manually"); }); }); // ============================================================================ // ensure_jq // ============================================================================ describe("ensure_jq", () => { it("should return 0 when jq is already installed", () => { // jq is available in the test environment const result = runBash("ensure_jq"); // If jq is available, should return 0 silently if (result.exitCode === 0) { expect(result.exitCode).toBe(0); } else { // If jq is not available and install fails (e.g. no sudo), that's OK for testing expect(result.exitCode).not.toBe(0); } }); it("should return 0 without printing when jq is present", () => { // When jq is already installed, should short-circuit const checkResult = runBash("command -v jq &>/dev/null && echo found || echo missing"); if (checkResult.stdout === "found") { const result = runBash("ensure_jq 2>/dev/null"); expect(result.exitCode).toBe(0); } }); it("should check for jq using command -v", () => { // Verify the function structure: it should check command -v jq const result = runBash("type ensure_jq | head -5"); expect(result.stdout).toContain("command -v jq"); }); it("should handle case where jq is in PATH", () => { // If jq is available, calling ensure_jq multiple times should be idempotent const checkResult = runBash("command -v jq &>/dev/null && echo found || echo missing"); if (checkResult.stdout === "found") { const result = runBash("ensure_jq && ensure_jq && echo ok"); expect(result.stdout).toContain("ok"); } }); it("should fail when jq cannot be installed (no package manager)", () => { // Simulate by overriding PATH to hide all package managers and jq const result = runBash( `PATH=/usr/bin:/bin # Remove jq from PATH if present hash -r command -v jq &>/dev/null && { echo "jq-present"; exit 0; } # With no jq and possibly no package manager, ensure_jq should fail ensure_jq 2>&1 || echo "FAILED"` ); // Either jq is already in the system path, or the function should fail if (!result.stdout.includes("jq-present")) { expect(result.stdout).toContain("FAILED"); } }); it("should define ensure_jq function with error handling", () => { // Verify the function exists and contains error handling logic const result = runBash("type ensure_jq"); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("ensure_jq"); }); }); // ============================================================================ // get_cloud_init_userdata // ============================================================================ describe("get_cloud_init_userdata", () => { it("should output valid cloud-config YAML", () => { const result = runBash("get_cloud_init_userdata"); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("#cloud-config"); }); it("should include package_update directive", () => { const result = runBash("get_cloud_init_userdata"); expect(result.stdout).toContain("package_update: true"); }); it("should include required packages", () => { const result = runBash("get_cloud_init_userdata"); expect(result.stdout).toContain("curl"); expect(result.stdout).toContain("unzip"); expect(result.stdout).toContain("git"); expect(result.stdout).toContain("zsh"); }); it("should include Bun installation", () => { const result = runBash("get_cloud_init_userdata"); expect(result.stdout).toContain("bun.sh/install"); }); it("should include Claude Code installation", () => { const result = runBash("get_cloud_init_userdata"); expect(result.stdout).toContain("claude.ai/install.sh"); }); it("should set IS_SANDBOX=1 in both bashrc and zshrc", () => { const result = runBash("get_cloud_init_userdata"); expect(result.stdout).toContain("IS_SANDBOX=1"); expect(result.stdout).toContain(".bashrc"); expect(result.stdout).toContain(".zshrc"); }); it("should configure PATH in both bashrc and zshrc", () => { const result = runBash("get_cloud_init_userdata"); expect(result.stdout).toContain(".claude/local/bin"); expect(result.stdout).toContain(".bun/bin"); }); it("should signal completion with touch marker file", () => { const result = runBash("get_cloud_init_userdata"); expect(result.stdout).toContain(".cloud-init-complete"); }); it("should include runcmd section", () => { const result = runBash("get_cloud_init_userdata"); expect(result.stdout).toContain("runcmd:"); }); it("should include packages section", () => { const result = runBash("get_cloud_init_userdata"); expect(result.stdout).toContain("packages:"); }); it("should not contain variable expansion (heredoc uses single quotes)", () => { // The heredoc is CLOUD_INIT_EOF (single-quoted), so no expansion const result = runBash("get_cloud_init_userdata"); // ${HOME} should appear literally, not expanded expect(result.stdout).toContain("${HOME}"); }); }); // ============================================================================ // _multi_creds_validate // ============================================================================ describe("_multi_creds_validate", () => { it("should return 0 when test function succeeds", () => { const result = runBash(` test_pass() { return 0; } _multi_creds_validate test_pass "TestProvider" `); expect(result.exitCode).toBe(0); }); it("should return 1 when test function fails", () => { const result = runBash(` test_fail() { return 1; } _multi_creds_validate test_fail "TestProvider" 2>/dev/null `); expect(result.exitCode).toBe(1); }); it("should return 0 when test function is empty (no validation)", () => { const result = runBash(`_multi_creds_validate "" "TestProvider"`); expect(result.exitCode).toBe(0); }); it("should unset env vars on validation failure", () => { const result = runBash(` export MY_VAR1="secret1" export MY_VAR2="secret2" test_fail() { return 1; } _multi_creds_validate test_fail "TestProvider" MY_VAR1 MY_VAR2 2>/dev/null echo "VAR1=\${MY_VAR1:-unset}" echo "VAR2=\${MY_VAR2:-unset}" `); expect(result.stdout).toContain("VAR1=unset"); expect(result.stdout).toContain("VAR2=unset"); }); it("should not unset env vars on validation success", () => { const result = runBash(` export MY_VAR1="secret1" export MY_VAR2="secret2" test_pass() { return 0; } _multi_creds_validate test_pass "TestProvider" MY_VAR1 MY_VAR2 echo "VAR1=\${MY_VAR1:-unset}" echo "VAR2=\${MY_VAR2:-unset}" `); expect(result.stdout).toContain("VAR1=secret1"); expect(result.stdout).toContain("VAR2=secret2"); }); it("should show error message with provider name on failure", () => { const result = runBash(` test_fail() { return 1; } _multi_creds_validate test_fail "Contabo" MY_VAR 2>&1 `); expect(result.stdout).toContain("Invalid Contabo credentials"); }); it("should show actionable guidance on failure", () => { const result = runBash(` test_fail() { return 1; } _multi_creds_validate test_fail "UpCloud" MY_VAR 2>&1 `); expect(result.stdout).toContain("expired"); expect(result.stdout).toContain("Re-run"); }); it("should show testing message during validation", () => { const result = runBash(` test_pass() { return 0; } _multi_creds_validate test_pass "Hetzner" 2>&1 `); expect(result.stdout).toContain("Testing Hetzner credentials"); }); it("should handle single env var unset on failure", () => { const result = runBash(` export SINGLE_VAR="value" test_fail() { return 1; } _multi_creds_validate test_fail "Provider" SINGLE_VAR 2>/dev/null echo "\${SINGLE_VAR:-unset}" `); expect(result.stdout).toContain("unset"); }); it("should handle three env vars unset on failure", () => { const result = runBash(` export V1="a" V2="b" V3="c" test_fail() { return 1; } _multi_creds_validate test_fail "Provider" V1 V2 V3 2>/dev/null echo "\${V1:-x}\${V2:-x}\${V3:-x}" `); expect(result.stdout).toBe("xxx"); }); }); // ============================================================================ // check_python_available // ============================================================================ describe("check_python_available", () => { it("should return 0 when python3 is available", () => { const result = runBash("check_python_available"); // python3 should be available in CI/test environment expect(result.exitCode).toBe(0); }); it("should return 1 when python3 is not in PATH", () => { const result = runBash(` PATH=/nonexistent hash -r check_python_available 2>/dev/null `); expect(result.exitCode).toBe(1); }); it("should show installation instructions when python3 is missing", () => { const result = runBash(` PATH=/nonexistent hash -r check_python_available 2>&1 `); expect(result.stdout).toContain("Python 3 is required"); expect(result.stdout).toContain("sudo apt-get"); expect(result.stdout).toContain("brew install python3"); }); }); // ============================================================================ // calculate_retry_backoff // ============================================================================ describe("calculate_retry_backoff", () => { it("should double the interval (base case)", () => { // With interval=2 and max=100, next should be doubled from base (but with jitter) // The function returns the CURRENT interval with jitter, not the doubled one const result = runBash("calculate_retry_backoff 10 100"); expect(result.exitCode).toBe(0); const value = parseInt(result.stdout, 10); // 10 * 0.8 = 8, 10 * 1.2 = 12 (jitter range) expect(value).toBeGreaterThanOrEqual(8); expect(value).toBeLessThanOrEqual(12); }); it("should cap at max interval", () => { // interval=100, max=50 => capped at 50, then jitter on 100 const result = runBash("calculate_retry_backoff 100 50"); expect(result.exitCode).toBe(0); const value = parseInt(result.stdout, 10); // Jitter on 100: 80-120 expect(value).toBeGreaterThanOrEqual(80); expect(value).toBeLessThanOrEqual(120); }); it("should handle interval of 1", () => { const result = runBash("calculate_retry_backoff 1 100"); expect(result.exitCode).toBe(0); const value = parseInt(result.stdout, 10); // 1 * 0.8 = 0.8 -> 0 or 1, 1 * 1.2 = 1.2 -> 1 expect(value).toBeGreaterThanOrEqual(0); expect(value).toBeLessThanOrEqual(2); }); it("should return numeric output", () => { const result = runBash("calculate_retry_backoff 5 60"); expect(result.exitCode).toBe(0); expect(parseInt(result.stdout, 10)).not.toBeNaN(); }); it("should apply jitter (non-deterministic but bounded)", () => { // Run multiple times and check they're all within jitter range const values: number[] = []; for (let i = 0; i < 5; i++) { const result = runBash("calculate_retry_backoff 50 200"); values.push(parseInt(result.stdout, 10)); } for (const v of values) { // 50 * 0.8 = 40, 50 * 1.2 = 60 expect(v).toBeGreaterThanOrEqual(40); expect(v).toBeLessThanOrEqual(60); } }); }); // ============================================================================ // verify_agent_installed // Signature: verify_agent_installed CMD [VERIFY_ARG] [AGENT_NAME] // ============================================================================ describe("verify_agent_installed", () => { it("should return 0 when command exists", () => { // bash is always available const result = runBash(`verify_agent_installed "bash"`); expect(result.exitCode).toBe(0); }); it("should return 0 with custom verify arg", () => { // ls --help should succeed const result = runBash(`verify_agent_installed "ls" "--help"`); expect(result.exitCode).toBe(0); }); it("should return 1 when command does not exist", () => { const result = runBash( `verify_agent_installed "nonexistent_cmd_12345" 2>/dev/null` ); expect(result.exitCode).toBe(1); }); it("should show diagnostic error on failure", () => { const result = runBash( `verify_agent_installed "nonexistent_cmd_12345" "--version" "Claude Code" 2>&1` ); expect(result.exitCode).toBe(1); expect(result.stdout).toContain("Claude Code"); expect(result.stdout).toContain("installation failed"); }); it("should show how-to-fix guidance on failure", () => { const result = runBash( `verify_agent_installed "nonexistent_cmd_12345" "--version" "Aider" 2>&1` ); expect(result.stdout).toContain("How to fix"); expect(result.stdout).toContain("Aider"); }); it("should use command name as default agent name", () => { const result = runBash( `verify_agent_installed "nonexistent_cmd_12345" 2>&1` ); expect(result.stdout).toContain("nonexistent_cmd_12345"); }); it("should show verified message on success", () => { const result = runBash(`verify_agent_installed "bash" "--version" "Bash" 2>&1`); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("verified successfully"); }); }); // ============================================================================ // _log_diagnostic // ============================================================================ describe("_log_diagnostic", () => { it("should show header, causes, and fixes", () => { const result = runBash( `_log_diagnostic "Something failed" "Cause 1" "Cause 2" --- "Fix 1" "Fix 2" 2>&1` ); expect(result.stdout).toContain("Something failed"); expect(result.stdout).toContain("Possible causes"); expect(result.stdout).toContain("Cause 1"); expect(result.stdout).toContain("Cause 2"); expect(result.stdout).toContain("How to fix"); expect(result.stdout).toContain("Fix 1"); expect(result.stdout).toContain("Fix 2"); }); it("should number fix steps", () => { const result = runBash( `_log_diagnostic "Error" "cause" --- "First step" "Second step" "Third step" 2>&1` ); expect(result.stdout).toContain("1. First step"); expect(result.stdout).toContain("2. Second step"); expect(result.stdout).toContain("3. Third step"); }); it("should handle single cause and single fix", () => { const result = runBash( `_log_diagnostic "Header" "Only cause" --- "Only fix" 2>&1` ); expect(result.stdout).toContain("Only cause"); expect(result.stdout).toContain("1. Only fix"); }); }); // ============================================================================ // _generate_csrf_state // ============================================================================ describe("_generate_csrf_state", () => { it("should generate a non-empty string", () => { const result = runBash("_generate_csrf_state"); expect(result.exitCode).toBe(0); expect(result.stdout.length).toBeGreaterThan(0); }); it("should generate hex-only output", () => { const result = runBash("_generate_csrf_state"); expect(result.stdout).toMatch(/^[0-9a-f]+$/); }); it("should generate at least 16 characters", () => { const result = runBash("_generate_csrf_state"); expect(result.stdout.length).toBeGreaterThanOrEqual(16); }); it("should generate different values on successive calls", () => { const result1 = runBash("_generate_csrf_state"); const result2 = runBash("_generate_csrf_state"); // Very unlikely to get the same value twice expect(result1.stdout).not.toBe(result2.stdout); }); it("should work with openssl if available", () => { const checkOpenssl = runBash("command -v openssl && echo found || echo missing"); if (checkOpenssl.stdout.includes("found")) { const result = runBash("_generate_csrf_state"); expect(result.exitCode).toBe(0); // openssl rand -hex 16 produces 32 hex characters expect(result.stdout.length).toBe(32); } }); it("should work with /dev/urandom fallback", () => { // Force the /dev/urandom path by temporarily renaming openssl in PATH // Use a subshell with modified PATH to hide openssl const result = runBash(` PATH=$(echo "$PATH" | tr ':' '\\n' | grep -v openssl | tr '\\n' ':') unset -f openssl 2>/dev/null # Override command -v to hide openssl command() { if [[ "$1" == "-v" && "$2" == "openssl" ]]; then return 1; fi builtin command "$@" } _generate_csrf_state `); // If /dev/urandom is available (it should be on Linux), this should work if (result.exitCode === 0) { expect(result.stdout.length).toBeGreaterThan(0); expect(result.stdout).toMatch(/^[0-9a-f]+$/); } }); }); // ============================================================================ // register_cleanup_trap and cleanup_temp_files // ============================================================================ describe("register_cleanup_trap and cleanup_temp_files", () => { it("should register EXIT trap without error", () => { const result = runBash("register_cleanup_trap"); expect(result.exitCode).toBe(0); }); it("should track temp files for cleanup", () => { const result = runBash(` TMPF=$(mktemp) track_temp_file "$TMPF" echo "$TMPF" `); expect(result.exitCode).toBe(0); expect(result.stdout.length).toBeGreaterThan(0); }); it("should clean up tracked temp files", () => { const result = runBash(` TMPF=$(mktemp) track_temp_file "$TMPF" test -f "$TMPF" && echo "before:exists" cleanup_temp_files test -f "$TMPF" && echo "after:exists" || echo "after:removed" `); expect(result.stdout).toContain("before:exists"); expect(result.stdout).toContain("after:removed"); }); it("should handle cleanup with no tracked files", () => { const result = runBash("cleanup_temp_files"); expect(result.exitCode).toBe(0); }); it("should handle cleanup when tracked file already removed", () => { const result = runBash(` TMPF=$(mktemp) track_temp_file "$TMPF" rm -f "$TMPF" cleanup_temp_files echo "ok" `); expect(result.exitCode).toBe(0); expect(result.stdout).toContain("ok"); }); });