Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions docs/plans/2026-03-09-add-restart-command-test-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Test Plan: `devproxy restart` command

## Strategy reconciliation

The implementation plan describes a thin wrapper around `platform::restart_daemon()` with three touchpoints: CLI parsing (`cli.rs`), command dispatch (`main.rs`), and the command module (`commands/restart.rs`). The agreed medium-fidelity strategy assumed exactly this shape and holds without modification. No external dependencies, no new harnesses needed.

## Test plan

### 1. `test_restart_no_daemon` — restart reports error when no platform daemon exists

- **Name**: Restart without a platform-managed daemon reports error and exits non-zero
- **Type**: scenario
- **Harness**: Direct binary invocation with `DEVPROXY_NO_SOCKET_ACTIVATION=1`
- **Preconditions**: No platform-managed daemon (launchd/systemd) is running. Socket activation is disabled via env var. A valid config dir with `config.json` exists.
- **Actions**: Run `devproxy restart` with isolated config dir and `DEVPROXY_NO_SOCKET_ACTIVATION=1`.
- **Expected outcome**: Exit code is non-zero. Stderr contains "no platform-managed daemon found". Source of truth: `commands/restart.rs` prints this message when `restart_daemon()` returns `Ok(false)`, and the implementation plan specifies this behavior.
- **Interactions**: `platform::restart_daemon()` checks env var and returns `Ok(false)` without touching launchd/systemd.

### 2. `test_restart_running_daemon` — restart reports error when daemon is running but not platform-managed

- **Name**: Restart with a running non-platform daemon still reports no platform daemon
- **Type**: scenario
- **Harness**: Test daemon harness (existing `start_test_daemon`) + direct binary invocation
- **Preconditions**: A daemon is running on an ephemeral port via direct spawn (not launchd/systemd). `DEVPROXY_NO_SOCKET_ACTIVATION=1` is set.
- **Actions**: Start a test daemon. Run `devproxy restart` against the same config dir.
- **Expected outcome**: Exit code is non-zero. Stderr contains "no platform-managed daemon found". The running daemon is unaffected. Source of truth: implementation plan states "run restart, verify it reports 'no platform-managed daemon'" for this scenario.
- **Interactions**: Tests that `restart` does not accidentally kill a non-platform-managed daemon process.

### 3. `test_parse_restart_command` — CLI parses "restart" subcommand

- **Name**: `devproxy restart` parses as the Restart command variant
- **Type**: unit
- **Harness**: In-process clap parsing via `Cli::try_parse_from`
- **Preconditions**: None.
- **Actions**: Call `Cli::try_parse_from(["devproxy", "restart"])`.
- **Expected outcome**: Parsing succeeds. Returned `Commands` variant matches `Commands::Restart`. Source of truth: implementation plan specifies `Restart` variant with no arguments.
- **Interactions**: None.

### 4. `test_cli_help` — help output includes restart (existing test, extended)

- **Name**: Help output lists the restart command
- **Type**: boundary
- **Harness**: Direct binary invocation
- **Preconditions**: Binary is built.
- **Actions**: Run `devproxy --help`.
- **Expected outcome**: Stdout contains "restart". Source of truth: the `Restart` variant in `Commands` enum has no `#[command(hide = true)]` attribute, so clap will list it.
- **Interactions**: None beyond clap help generation.

## Coverage summary

**Covered:**
- User-visible restart command (both success-path messaging and error-path messaging)
- CLI parsing of the `restart` subcommand
- Behavior when daemon is running but not platform-managed
- Behavior when no daemon exists at all
- Discoverability via `--help`

**Explicitly excluded per agreed strategy:**
- Actual launchd/systemd restart (would require platform-managed daemon installation, which is impractical in CI and covered by the existing `platform::restart_daemon()` unit-level logic)
- Testing on Linux (systemd path) -- covered by existing `tests/linux-docker/` infrastructure for the `update` command which exercises the same `restart_daemon()` codepath

**Residual risk:**
- Low. The command is a direct delegation to `platform::restart_daemon()` which is already exercised by the `update` command's e2e tests. The only new code is the 20-line `commands/restart.rs` wrapper.
16 changes: 16 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub enum Commands {
Open,
/// Show daemon health and active route count
Status,
/// Restart the daemon
Restart,
/// Check for updates and self-update the binary
Update,
/// Run the proxy daemon (internal, hidden)
Expand All @@ -47,3 +49,17 @@ pub enum Commands {
port: u16,
},
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_restart_command() {
let cli = Cli::try_parse_from(["devproxy", "restart"]).expect("should parse restart");
assert!(
matches!(cli.command, Commands::Restart),
"should parse as Restart variant"
);
}
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod get_url;
pub mod init;
pub mod ls;
pub mod open;
pub mod restart;
pub mod status;
pub mod up;
pub mod update;
20 changes: 20 additions & 0 deletions src/commands/restart.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use anyhow::Result;
use colored::Colorize;

pub fn run() -> Result<()> {
match crate::platform::restart_daemon() {
Ok(true) => {
eprintln!("{} daemon restarted", "ok:".green());
Ok(())
}
Ok(false) => {
eprintln!(
"{} no platform-managed daemon found. Run {} to set one up",
"error:".red(),
"devproxy init".bold()
);
std::process::exit(1);
}
Err(e) => Err(e),
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ async fn main() -> anyhow::Result<()> {
Commands::Ls => commands::ls::run().await,
Commands::Open => commands::open::run().await,
Commands::Status => commands::status::run().await,
Commands::Restart => commands::restart::run(),
Commands::Update => commands::update::run().await,
Commands::Daemon { port } => commands::daemon::run(port).await,
}
Expand Down
64 changes: 64 additions & 0 deletions tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ fn test_cli_help() {
assert!(stdout.contains("down"));
assert!(stdout.contains("ls"));
assert!(stdout.contains("status"));
assert!(
stdout.contains("restart"),
"help should list the restart command"
);
assert!(
stdout.contains("update"),
"help should list the update command"
Expand Down Expand Up @@ -338,6 +342,66 @@ fn test_status_without_daemon() {
let _ = std::fs::remove_dir_all(&config_dir);
}

#[test]
fn test_restart_no_daemon() {
// Without a platform-managed daemon (DEVPROXY_NO_SOCKET_ACTIVATION=1),
// restart should report that no daemon was found and exit non-zero.
let config_dir =
std::env::temp_dir().join(format!("devproxy-restart-nodaemon-{}", std::process::id()));
std::fs::create_dir_all(&config_dir).unwrap();
std::fs::write(
config_dir.join("config.json"),
format!(r#"{{"domain":"{TEST_DOMAIN}"}}"#),
)
.unwrap();

let output = Command::new(devproxy_bin())
.args(["restart"])
.env("DEVPROXY_CONFIG_DIR", &config_dir)
.env("DEVPROXY_NO_SOCKET_ACTIVATION", "1")
.output()
.expect("failed to run restart");

assert!(
!output.status.success(),
"restart without a platform-managed daemon should exit non-zero"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("no platform-managed daemon found"),
"should report no daemon found: {stderr}"
);

let _ = std::fs::remove_dir_all(&config_dir);
}

#[test]
fn test_restart_running_daemon() {
// Start a daemon with DEVPROXY_NO_SOCKET_ACTIVATION=1 (no launchd/systemd).
// `restart` should still report "no platform-managed daemon" because the
// daemon is running directly, not via launchd/systemd.
let config_dir = create_test_config_dir("restart-running");
let port = find_free_port();
let _guard = start_test_daemon(&config_dir, port);

let output = Command::new(devproxy_bin())
.args(["restart"])
.env("DEVPROXY_CONFIG_DIR", &config_dir)
.env("DEVPROXY_NO_SOCKET_ACTIVATION", "1")
.output()
.expect("failed to run restart");

assert!(
!output.status.success(),
"restart should exit non-zero when daemon is not platform-managed"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("no platform-managed daemon found"),
"should report no platform-managed daemon: {stderr}"
);
}

#[test]
fn test_up_without_label() {
let config_dir = std::env::temp_dir().join(format!("devproxy-nolabel-{}", std::process::id()));
Expand Down
Loading