Add integration tests for server daemon management

- 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) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-02 14:18:05 -07:00
parent e0ac11f1d5
commit 4e84152c39
7 changed files with 271 additions and 49 deletions

View file

@ -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;

View file

@ -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>
Port to listen on [default: 3000]
--host <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 <MODEL>
Override default LLM model
--quiet
Suppress non-essential output [env: FABRO_QUIET=]
--provider <PROVIDER>
Override default LLM provider
--verbose
Enable verbose output [env: FABRO_VERBOSE=]
--dry-run
Execute with simulated LLM backend
--storage-dir <STORAGE_DIR>
Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
--sandbox <SANDBOX>
Sandbox for agent tools
--server-url <SERVER_URL>
Server URL (overrides server.base_url from user.toml) [env: FABRO_SERVER_URL=]
--max-concurrent-runs <MAX_CONCURRENT_RUNS>
Maximum number of concurrent run executions
--config <CONFIG>
Path to server config file (default: ~/.fabro/server.toml)
-h, --help
Print help
----- stderr -----
");
}

View file

@ -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 <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 <MODEL>
Override default LLM model
--no-upgrade-check
Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--provider <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>
Sandbox for agent tools
--storage-dir <STORAGE_DIR>
Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
--max-concurrent-runs <MAX_CONCURRENT_RUNS>
Maximum number of concurrent run executions
--server-url <SERVER_URL>
Server URL (overrides server.base_url from user.toml) [env: FABRO_SERVER_URL=]
--config <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();
}

View file

@ -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_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
--server-url <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
");
}

View file

@ -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 <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_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
--server-url <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
");
}

View file

@ -1,6 +1,7 @@
mod exec;
mod lifecycle;
mod recovery;
mod server_lifecycle;
use std::path::{Path, PathBuf};
use std::sync::Arc;

View file

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