From e4133f7f2f4cdd3239e83ac5489eac95b630d6e6 Mon Sep 17 00:00:00 2001 From: Chris Fenton Date: Mon, 9 Mar 2026 20:47:04 -0700 Subject: [PATCH 1/3] feat: add `devproxy restart` command Adds a user-facing `restart` subcommand that wraps the existing `platform::restart_daemon()` function, giving users a direct way to restart the daemon without going through `init` or `update`. Co-Authored-By: Claude Opus 4.6 --- .plan/add-restart-command.md | 36 ++++++++++++++++++++ src/cli.rs | 16 +++++++++ src/commands/mod.rs | 1 + src/commands/restart.rs | 20 +++++++++++ src/main.rs | 1 + tests/e2e.rs | 64 ++++++++++++++++++++++++++++++++++++ 6 files changed, 138 insertions(+) create mode 100644 .plan/add-restart-command.md create mode 100644 src/commands/restart.rs diff --git a/.plan/add-restart-command.md b/.plan/add-restart-command.md new file mode 100644 index 0000000..10202b1 --- /dev/null +++ b/.plan/add-restart-command.md @@ -0,0 +1,36 @@ +# Plan: Add `devproxy restart` command + +## Summary + +Add a user-facing `devproxy restart` command that wraps the existing `platform::restart_daemon()` function. This provides users a direct way to restart the daemon without going through `devproxy init` or `devproxy update`. + +## Changes + +### 1. `src/cli.rs` — Add `Restart` variant to `Commands` enum +- Add `Restart` variant with doc comment `/// Restart the daemon` +- Add unit test verifying clap parses `["devproxy", "restart"]` + +### 2. `src/commands/restart.rs` — New command module +- Call `platform::restart_daemon()` +- On `Ok(true)`: print success message +- On `Ok(false)`: print "no platform-managed daemon found" and exit 1 +- On `Err(e)`: propagate error + +### 3. `src/commands/mod.rs` — Register module +- Add `pub mod restart;` + +### 4. `src/main.rs` — Dispatch command +- Add `Commands::Restart => commands::restart::run()` match arm + +### 5. `tests/e2e.rs` — E2e tests +- `test_restart_no_daemon`: run restart with `DEVPROXY_NO_SOCKET_ACTIVATION=1`, verify non-zero exit and error message +- `test_restart_running_daemon`: start a daemon via the test harness, run restart, verify it reports "no platform-managed daemon" (because the test daemon is not platform-managed) +- Update `test_cli_help` to assert "restart" appears in help output + +## Testing strategy + +Medium fidelity. The command is a thin wrapper around `platform::restart_daemon()` which is already well-tested via `devproxy update`. + +- **Unit**: clap parsing test for `Restart` variant +- **E2e**: two tests covering the no-daemon and running-but-not-platform-managed scenarios +- **E2e**: help output verification diff --git a/src/cli.rs b/src/cli.rs index d0ec02b..0bdcdcb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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) @@ -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" + ); + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f8bdd6a..99093be 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -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; diff --git a/src/commands/restart.rs b/src/commands/restart.rs new file mode 100644 index 0000000..32e8c3d --- /dev/null +++ b/src/commands/restart.rs @@ -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), + } +} diff --git a/src/main.rs b/src/main.rs index 280b328..302be11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, } diff --git a/tests/e2e.rs b/tests/e2e.rs index 24f9a4b..6f3586c 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -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" @@ -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())); From 5f8037e2e97caab1e79e77ebd708576eeeac36f3 Mon Sep 17 00:00:00 2001 From: Chris Fenton Date: Mon, 9 Mar 2026 20:49:40 -0700 Subject: [PATCH 2/3] docs: add test plan for devproxy restart command Co-Authored-By: Claude Opus 4.6 --- ...026-03-09-add-restart-command-test-plan.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/plans/2026-03-09-add-restart-command-test-plan.md diff --git a/docs/plans/2026-03-09-add-restart-command-test-plan.md b/docs/plans/2026-03-09-add-restart-command-test-plan.md new file mode 100644 index 0000000..c47a936 --- /dev/null +++ b/docs/plans/2026-03-09-add-restart-command-test-plan.md @@ -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. From fd65b08c84623d5dcf3a3a3edeeccc9b79dc7806 Mon Sep 17 00:00:00 2001 From: Chris Fenton Date: Mon, 9 Mar 2026 20:52:29 -0700 Subject: [PATCH 3/3] chore: remove planning artifacts Co-Authored-By: Claude Opus 4.6 --- .plan/add-restart-command.md | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 .plan/add-restart-command.md diff --git a/.plan/add-restart-command.md b/.plan/add-restart-command.md deleted file mode 100644 index 10202b1..0000000 --- a/.plan/add-restart-command.md +++ /dev/null @@ -1,36 +0,0 @@ -# Plan: Add `devproxy restart` command - -## Summary - -Add a user-facing `devproxy restart` command that wraps the existing `platform::restart_daemon()` function. This provides users a direct way to restart the daemon without going through `devproxy init` or `devproxy update`. - -## Changes - -### 1. `src/cli.rs` — Add `Restart` variant to `Commands` enum -- Add `Restart` variant with doc comment `/// Restart the daemon` -- Add unit test verifying clap parses `["devproxy", "restart"]` - -### 2. `src/commands/restart.rs` — New command module -- Call `platform::restart_daemon()` -- On `Ok(true)`: print success message -- On `Ok(false)`: print "no platform-managed daemon found" and exit 1 -- On `Err(e)`: propagate error - -### 3. `src/commands/mod.rs` — Register module -- Add `pub mod restart;` - -### 4. `src/main.rs` — Dispatch command -- Add `Commands::Restart => commands::restart::run()` match arm - -### 5. `tests/e2e.rs` — E2e tests -- `test_restart_no_daemon`: run restart with `DEVPROXY_NO_SOCKET_ACTIVATION=1`, verify non-zero exit and error message -- `test_restart_running_daemon`: start a daemon via the test harness, run restart, verify it reports "no platform-managed daemon" (because the test daemon is not platform-managed) -- Update `test_cli_help` to assert "restart" appears in help output - -## Testing strategy - -Medium fidelity. The command is a thin wrapper around `platform::restart_daemon()` which is already well-tested via `devproxy update`. - -- **Unit**: clap parsing test for `Restart` variant -- **E2e**: two tests covering the no-daemon and running-but-not-platform-managed scenarios -- **E2e**: help output verification