From 9d9a50c2bc8675684138dd6f3f45861e950199ce Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 28 Jun 2026 22:11:48 -0500 Subject: [PATCH] test(cli): pin the `hermes serve` decoupling contract Add a focused contract test for the headless `serve` command (routes to the shared dashboard handler, headless by default while `dashboard` is not, accepts the legacy --no-open, shares the same runtime/lifecycle flag surface). Also refresh the dashboard.py module docstring to cover both commands. --- hermes_cli/subcommands/dashboard.py | 9 ++-- tests/hermes_cli/test_serve_command.py | 60 ++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 tests/hermes_cli/test_serve_command.py diff --git a/hermes_cli/subcommands/dashboard.py b/hermes_cli/subcommands/dashboard.py index bd605ab1561..bea3c2244de 100644 --- a/hermes_cli/subcommands/dashboard.py +++ b/hermes_cli/subcommands/dashboard.py @@ -1,7 +1,10 @@ -"""``hermes dashboard`` subcommand parser. +"""``hermes dashboard`` / ``hermes serve`` subcommand parsers. -Extracted verbatim from ``hermes_cli/main.py:main()`` (god-file Phase 2). -Handler injected to avoid importing ``main``. +``dashboard`` is the browser web UI; ``serve`` is the same gateway, headless — +what the desktop app and remote backends run. Both share one handler +(``cmd_dashboard`` → ``start_server``). Extracted from +``hermes_cli/main.py:main()`` (god-file Phase 2); handler injected to avoid +importing ``main``. """ from __future__ import annotations diff --git a/tests/hermes_cli/test_serve_command.py b/tests/hermes_cli/test_serve_command.py new file mode 100644 index 00000000000..6b1f566cfcd --- /dev/null +++ b/tests/hermes_cli/test_serve_command.py @@ -0,0 +1,60 @@ +"""Contract for the headless ``hermes serve`` backend command. + +``serve`` is what the desktop app and remote backends launch — the same gateway +as ``dashboard`` (shared handler) but always headless, and decoupled in name so +the desktop never invokes ``dashboard``. These tests pin that contract: + +- ``serve`` routes to the same handler as ``dashboard``; +- ``serve`` is headless by default, ``dashboard`` is not; +- both expose the identical server-runtime flag surface. +""" + +from __future__ import annotations + +import argparse + +import pytest + +from hermes_cli.subcommands.dashboard import build_dashboard_parser + +_DASH = object() +_REGISTER = object() + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser() + build_dashboard_parser( + parser.add_subparsers(dest="command"), + cmd_dashboard=_DASH, + cmd_dashboard_register=_REGISTER, + ) + return parser + + +def test_serve_routes_to_the_shared_dashboard_handler(): + args = _parser().parse_args(["serve"]) + assert args.func is _DASH + + +def test_serve_is_headless_by_default_but_dashboard_is_not(): + assert _parser().parse_args(["serve"]).no_open is True + assert _parser().parse_args(["dashboard"]).no_open is False + + +def test_serve_accepts_the_legacy_no_open_flag_as_a_noop(): + # The desktop backend spawn (and old shells) may still pass --no-open; + # serve must tolerate it rather than erroring on an unknown argument. + assert _parser().parse_args(["serve", "--no-open"]).no_open is True + + +def test_serve_takes_the_same_runtime_flags_as_dashboard(): + argv = ["--host", "0.0.0.0", "--port", "0", "--insecure", "--skip-build", "--isolated"] + serve = _parser().parse_args(["serve", *argv]) + dash = _parser().parse_args(["dashboard", *argv]) + for field in ("host", "port", "insecure", "skip_build", "isolated"): + assert getattr(serve, field) == getattr(dash, field) + + +@pytest.mark.parametrize("flag", ["--stop", "--status"]) +def test_serve_supports_the_lifecycle_flags(flag): + assert getattr(_parser().parse_args(["serve", flag]), flag.lstrip("-")) is True