mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 11:59:29 +00:00
test: add 67 tests for untested shared/common.sh helpers (#978)
Cover 8 functions that had zero test coverage: - log_install_failed: actionable guidance for agent install failures (added across 126 scripts) - ensure_jq: cross-platform jq installation - get_cloud_init_userdata: cloud-init template structure and content - _multi_creds_validate: credential validation with env var cleanup on failure - check_python_available: dependency check with install guidance - calculate_retry_backoff: exponential backoff with jitter bounds - verify_agent_installed: agent command verification with diagnostic output - _log_diagnostic: structured error reporting with causes and fixes - _generate_csrf_state: CSRF token generation with multiple backends - register_cleanup_trap / track_temp_file / cleanup_temp_files: temp file lifecycle Agent: test-engineer Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
50b2e98d7d
commit
8c8511f9a2
1 changed files with 645 additions and 0 deletions
645
cli/src/__tests__/shared-common-untested-helpers.test.ts
Normal file
645
cli/src/__tests__/shared-common-untested-helpers.test.ts
Normal file
|
|
@ -0,0 +1,645 @@
|
|||
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 verification failed");
|
||||
});
|
||||
|
||||
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("Re-run the install 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("Re-run the install manually");
|
||||
});
|
||||
|
||||
it("should always show possible causes section", () => {
|
||||
const result = runBash(`log_install_failed "Test" 2>&1`);
|
||||
expect(result.stdout).toContain("Possible causes");
|
||||
expect(result.stdout).toContain("Package manager timeout");
|
||||
expect(result.stdout).toContain("Insufficient disk space");
|
||||
});
|
||||
|
||||
it("should always suggest re-running the command", () => {
|
||||
const result = runBash(`log_install_failed "Test" 2>&1`);
|
||||
expect(result.stdout).toContain("Re-run this spawn command");
|
||||
});
|
||||
|
||||
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 verification failed");
|
||||
});
|
||||
|
||||
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 verification failed");
|
||||
expect(result.stdout).not.toContain("ssh root@");
|
||||
expect(result.stdout).not.toContain("Re-run the install 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 verify jq is in PATH after installation attempt", () => {
|
||||
// The function checks command -v jq after installation
|
||||
const result = runBash("type ensure_jq");
|
||||
expect(result.stdout).toContain("jq not found in PATH after installation");
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 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");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue