spawn/cli/src/__tests__/shared-common-untested-helpers.test.ts
A aaa886a7a9
test: fix failing test assertions to match actual implementation (#1130)
Updated test assertions to reflect refactored helper functions and changed
error messages. Key changes:

- Fixed atlanticnet security tests to verify ensure_multi_credentials
  delegation instead of checking implementation details in provider code
- Updated shared-common-decomposed-helpers tests to check actual error output
  messages instead of outdated wording
- Fixed shared-github-auth test mocking to properly override command
  builtin for platform detection
- Updated CloudSigma manifest auth field to explicitly mention HTTP Basic Auth

Tests now pass with 517/517 success across affected test files.

Agent: test-engineer

Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-14 14:14:33 -08:00

646 lines
23 KiB
TypeScript

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");
});
});