From 4e84152c390ed04416bfe7ba02a8fda65dd78f3e Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 2 Apr 2026 14:18:05 -0700 Subject: [PATCH] Add integration tests for server daemon management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cmd/server_start: help snapshot, start-already-running error - cmd/server_stop: help snapshot, stop-when-not-running error - cmd/server_status: help snapshot, status-when-not-running error - scenario/server_lifecycle: full start → status → status --json → stop cycle - Remove stale server.rs help test (replaced by per-command files) Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/crates/fabro-cli/tests/it/cmd/mod.rs | 3 + lib/crates/fabro-cli/tests/it/cmd/server.rs | 49 ---------- .../fabro-cli/tests/it/cmd/server_start.rs | 90 +++++++++++++++++++ .../fabro-cli/tests/it/cmd/server_status.rs | 43 +++++++++ .../fabro-cli/tests/it/cmd/server_stop.rs | 44 +++++++++ lib/crates/fabro-cli/tests/it/scenario/mod.rs | 1 + .../tests/it/scenario/server_lifecycle.rs | 90 +++++++++++++++++++ 7 files changed, 271 insertions(+), 49 deletions(-) create mode 100644 lib/crates/fabro-cli/tests/it/cmd/server_start.rs create mode 100644 lib/crates/fabro-cli/tests/it/cmd/server_status.rs create mode 100644 lib/crates/fabro-cli/tests/it/cmd/server_stop.rs create mode 100644 lib/crates/fabro-cli/tests/it/scenario/server_lifecycle.rs diff --git a/lib/crates/fabro-cli/tests/it/cmd/mod.rs b/lib/crates/fabro-cli/tests/it/cmd/mod.rs index 127308e22..0cf72823c 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/mod.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/mod.rs @@ -52,6 +52,9 @@ mod secret_set; mod send_analytics; mod send_panic; mod server; +mod server_start; +mod server_status; +mod server_stop; mod start; mod store; mod store_dump; diff --git a/lib/crates/fabro-cli/tests/it/cmd/server.rs b/lib/crates/fabro-cli/tests/it/cmd/server.rs index aebf605b2..8b1378917 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/server.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/server.rs @@ -1,50 +1 @@ -#[test] -#[cfg(feature = "server")] -fn help() { - use fabro_test::{fabro_snapshot, test_context}; - let context = test_context!(); - let mut cmd = context.command(); - cmd.args(["server", "start", "--help"]); - fabro_snapshot!(context.filters(), cmd, @" - success: true - exit_code: 0 - ----- stdout ----- - Start the HTTP API server - - Usage: fabro server start [OPTIONS] - - Options: - --debug - Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] - --port - Port to listen on [default: 3000] - --host - Host address to bind to [default: 127.0.0.1] - --no-upgrade-check - Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] - --model - Override default LLM model - --quiet - Suppress non-essential output [env: FABRO_QUIET=] - --provider - Override default LLM provider - --verbose - Enable verbose output [env: FABRO_VERBOSE=] - --dry-run - Execute with simulated LLM backend - --storage-dir - Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]] - --sandbox - Sandbox for agent tools - --server-url - Server URL (overrides server.base_url from user.toml) [env: FABRO_SERVER_URL=] - --max-concurrent-runs - Maximum number of concurrent run executions - --config - Path to server config file (default: ~/.fabro/server.toml) - -h, --help - Print help - ----- stderr ----- - "); -} diff --git a/lib/crates/fabro-cli/tests/it/cmd/server_start.rs b/lib/crates/fabro-cli/tests/it/cmd/server_start.rs new file mode 100644 index 000000000..15bfbc24f --- /dev/null +++ b/lib/crates/fabro-cli/tests/it/cmd/server_start.rs @@ -0,0 +1,90 @@ +use fabro_test::{fabro_snapshot, test_context}; + +#[test] +#[cfg(feature = "server")] +fn help() { + let context = test_context!(); + let mut cmd = context.command(); + cmd.args(["server", "start", "--help"]); + fabro_snapshot!(context.filters(), cmd, @" + success: true + exit_code: 0 + ----- stdout ----- + Start the HTTP API server + + Usage: fabro server start [OPTIONS] + + Options: + --foreground + Run in the foreground instead of daemonizing + --json + Output as JSON [env: FABRO_JSON=] + --bind + Address to bind to (host:port for TCP, or path containing / for Unix socket) + --debug + Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] + --model + Override default LLM model + --no-upgrade-check + Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] + --provider + Override default LLM provider + --quiet + Suppress non-essential output [env: FABRO_QUIET=] + --dry-run + Execute with simulated LLM backend + --verbose + Enable verbose output [env: FABRO_VERBOSE=] + --sandbox + Sandbox for agent tools + --storage-dir + Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]] + --max-concurrent-runs + Maximum number of concurrent run executions + --server-url + Server URL (overrides server.base_url from user.toml) [env: FABRO_SERVER_URL=] + --config + Path to server config file (default: ~/.fabro/server.toml) + -h, --help + Print help + ----- stderr ----- + "); +} + +#[test] +#[cfg(feature = "server")] +fn start_already_running_exits_with_error() { + let context = test_context!(); + + let sock_dir = tempfile::tempdir_in("/tmp").unwrap(); + let bind_addr = sock_dir.path().join("test.sock"); + let bind_str = bind_addr.to_string_lossy().to_string(); + + // Start the daemon + context + .command() + .args(["server", "start", "--dry-run", "--bind", &bind_str]) + .assert() + .success(); + + // Try to start again -- should fail with "already running" + let mut filters = context.filters(); + filters.push((r"pid \d+".to_string(), "pid [PID]".to_string())); + filters.push((regex::escape(&bind_str), "[SOCKET_PATH]".to_string())); + let mut cmd = context.command(); + cmd.args(["server", "start", "--dry-run", "--bind", &bind_str]); + fabro_snapshot!(filters, cmd, @" + success: false + exit_code: 1 + ----- stdout ----- + ----- stderr ----- + error: Server already running (pid [PID]) on [SOCKET_PATH] + "); + + // Clean up: stop the server + context + .command() + .args(["server", "stop"]) + .assert() + .success(); +} diff --git a/lib/crates/fabro-cli/tests/it/cmd/server_status.rs b/lib/crates/fabro-cli/tests/it/cmd/server_status.rs new file mode 100644 index 000000000..4dd5c7182 --- /dev/null +++ b/lib/crates/fabro-cli/tests/it/cmd/server_status.rs @@ -0,0 +1,43 @@ +use fabro_test::{fabro_snapshot, test_context}; + +#[test] +#[cfg(feature = "server")] +fn help() { + let context = test_context!(); + let mut cmd = context.command(); + cmd.args(["server", "status", "--help"]); + fabro_snapshot!(context.filters(), cmd, @" + success: true + exit_code: 0 + ----- stdout ----- + Show server status + + Usage: fabro server status [OPTIONS] + + Options: + --json Output as JSON + --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] + --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] + --quiet Suppress non-essential output [env: FABRO_QUIET=] + --verbose Enable verbose output [env: FABRO_VERBOSE=] + --storage-dir Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]] + --server-url Server URL (overrides server.base_url from user.toml) [env: FABRO_SERVER_URL=] + -h, --help Print help + ----- stderr ----- + "); +} + +#[test] +#[cfg(feature = "server")] +fn status_when_not_running() { + let context = test_context!(); + let mut cmd = context.command(); + cmd.args(["server", "status"]); + fabro_snapshot!(context.filters(), cmd, @" + success: false + exit_code: 1 + ----- stdout ----- + ----- stderr ----- + Server is not running + "); +} diff --git a/lib/crates/fabro-cli/tests/it/cmd/server_stop.rs b/lib/crates/fabro-cli/tests/it/cmd/server_stop.rs new file mode 100644 index 000000000..70fef0005 --- /dev/null +++ b/lib/crates/fabro-cli/tests/it/cmd/server_stop.rs @@ -0,0 +1,44 @@ +use fabro_test::{fabro_snapshot, test_context}; + +#[test] +#[cfg(feature = "server")] +fn help() { + let context = test_context!(); + let mut cmd = context.command(); + cmd.args(["server", "stop", "--help"]); + fabro_snapshot!(context.filters(), cmd, @" + success: true + exit_code: 0 + ----- stdout ----- + Stop the HTTP API server + + Usage: fabro server stop [OPTIONS] + + Options: + --json Output as JSON [env: FABRO_JSON=] + --timeout Seconds to wait for graceful shutdown before SIGKILL [default: 10] + --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] + --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] + --quiet Suppress non-essential output [env: FABRO_QUIET=] + --verbose Enable verbose output [env: FABRO_VERBOSE=] + --storage-dir Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]] + --server-url Server URL (overrides server.base_url from user.toml) [env: FABRO_SERVER_URL=] + -h, --help Print help + ----- stderr ----- + "); +} + +#[test] +#[cfg(feature = "server")] +fn stop_when_not_running() { + let context = test_context!(); + let mut cmd = context.command(); + cmd.args(["server", "stop"]); + fabro_snapshot!(context.filters(), cmd, @" + success: false + exit_code: 1 + ----- stdout ----- + ----- stderr ----- + Server is not running + "); +} diff --git a/lib/crates/fabro-cli/tests/it/scenario/mod.rs b/lib/crates/fabro-cli/tests/it/scenario/mod.rs index 0b4d5e379..d68034f3e 100644 --- a/lib/crates/fabro-cli/tests/it/scenario/mod.rs +++ b/lib/crates/fabro-cli/tests/it/scenario/mod.rs @@ -1,6 +1,7 @@ mod exec; mod lifecycle; mod recovery; +mod server_lifecycle; use std::path::{Path, PathBuf}; use std::sync::Arc; diff --git a/lib/crates/fabro-cli/tests/it/scenario/server_lifecycle.rs b/lib/crates/fabro-cli/tests/it/scenario/server_lifecycle.rs new file mode 100644 index 000000000..e2a262fc8 --- /dev/null +++ b/lib/crates/fabro-cli/tests/it/scenario/server_lifecycle.rs @@ -0,0 +1,90 @@ +use fabro_test::{fabro_snapshot, test_context}; + +#[test] +#[cfg(feature = "server")] +fn start_status_stop_lifecycle() { + let context = test_context!(); + + let sock_dir = tempfile::tempdir_in("/tmp").unwrap(); + let bind_addr = sock_dir.path().join("test.sock"); + let bind_str = bind_addr.to_string_lossy().to_string(); + + let mut filters = context.filters(); + filters.push((r"pid \d+".to_string(), "pid [PID]".to_string())); + filters.push((regex::escape(&bind_str), "[SOCKET_PATH]".to_string())); + filters.push(( + r"started \d+[hms] (?:\d+[hms] )*ago".to_string(), + "started [UPTIME] ago".to_string(), + )); + + // Start the server as a daemon with a Unix socket in a short path + let mut cmd = context.command(); + cmd.args(["server", "start", "--dry-run", "--bind", &bind_str]); + fabro_snapshot!(filters.clone(), cmd, @" + success: true + exit_code: 0 + ----- stdout ----- + ----- stderr ----- + Server started (pid [PID]) on [SOCKET_PATH] + "); + + // Verify server.json is written in storage_dir + assert!( + context.storage_dir.join("server.json").exists(), + "server.json should exist after start" + ); + + // Status should report running + let mut cmd = context.command(); + cmd.args(["server", "status"]); + fabro_snapshot!(filters.clone(), cmd, @" + success: true + exit_code: 0 + ----- stdout ----- + ----- stderr ----- + Server running (pid [PID]) on [SOCKET_PATH], started [UPTIME] ago + "); + + // Status --json should produce valid JSON with "running" status + let status_output = context + .command() + .args(["server", "status", "--json"]) + .assert() + .success(); + let stdout = String::from_utf8(status_output.get_output().stdout.clone()).unwrap(); + let json: serde_json::Value = + serde_json::from_str(&stdout).expect("status --json should be valid JSON"); + assert_eq!( + json["status"].as_str(), + Some("running"), + "status should be running" + ); + + // Stop the server + let mut cmd = context.command(); + cmd.args(["server", "stop"]); + fabro_snapshot!(filters.clone(), cmd, @" + success: true + exit_code: 0 + ----- stdout ----- + ----- stderr ----- + Server stopped + "); + + // Status should report not running + let mut cmd = context.command(); + cmd.args(["server", "status"]); + fabro_snapshot!(filters.clone(), cmd, @" + success: false + exit_code: 1 + ----- stdout ----- + ----- stderr ----- + Server is not running + "); + + // Verify server.json is cleaned up + assert!( + !context.storage_dir.join("server.json").exists(), + "server.json should be removed after stop" + ); +}