hero_service: standard tests, E2E test harness, and CLI tool #29

Open
opened 2026-03-17 10:27:16 +00:00 by timur · 1 comment
Owner

Context

With HeroRpcServer and HeroUiServer (from #27) standardizing how all Hero services start, bind sockets, and expose endpoints, we can now build standard tooling on top.

1. Standard Test Helpers

Ship reusable test functions in hero_service::test that any service can import in its integration tests:

use hero_service::test;

#[tokio::test]
async fn test_mandatory_endpoints() {
    // Start the service (spawns process, waits for socket)
    let svc = test::start_service("hero_myservice_server").await;

    // These test the mandatory endpoints every HeroRpcServer provides
    test::assert_health(&svc).await;              // GET /health → 200 + status:ok
    test::assert_openrpc_valid(&svc).await;       // GET /openrpc.json → valid spec
    test::assert_discovery(&svc).await;           // GET /.well-known/heroservice.json
    test::assert_rpc_health(&svc).await;          // POST /rpc rpc.health → ok
    test::assert_rpc_discover(&svc).await;        // POST /rpc rpc.discover → spec

    svc.stop().await;
}

These would connect to ~/hero/var/sockets/{service_name}.sock via HTTP and validate responses.

2. E2E Test Harness

A test runner that starts server + ui, runs all standard tests, then service-specific tests:

// In hero_myservice_examples/tests/e2e.rs
use hero_service::test::ServiceHarness;

#[tokio::test]
async fn e2e() {
    let harness = ServiceHarness::new()
        .server("hero_myservice_server")
        .ui("hero_myservice_ui")
        .start()
        .await;

    // Standard tests run automatically
    // Then service-specific:
    let resp = harness.rpc_call("mymethod.get", json!({"sid": "123"})).await;
    assert!(resp.is_ok());

    harness.stop().await;
}

3. hero_service CLI Binary

A binary in hero_rpc that provides standardized commands across all services:

# Run both server + ui for a service
hero_service run hero_osis          # starts hero_osis_server + hero_osis_ui via zinit
hero_service stop hero_osis         # stops both
hero_service status                 # shows all running hero services

# Test a running service
hero_service test hero_osis          # runs standard endpoint tests
hero_service test --all              # tests all discovered services

# Discover services
hero_service list                    # lists all sockets in ~/hero/var/sockets/
hero_service health                  # health check all services
hero_service discover hero_osis      # fetch and print openrpc spec

This replaces per-repo Makefile targets with a single tool that knows the conventions.

4. Standardize Makefile Targets

With hero_service CLI, Makefiles become thin wrappers:

run:    hero_service run $(SERVICE_NAME)
stop:   hero_service stop $(SERVICE_NAME)
test:   hero_service test $(SERVICE_NAME)
status: hero_service status $(SERVICE_NAME)

Implementation Priority

  1. Test helpers (hero_service::test) — highest value, enables CI
  2. hero_service CLI binary — developer ergonomics
  3. E2E harness — integration testing framework
  4. Makefile standardization — convenience
## Context With `HeroRpcServer` and `HeroUiServer` (from #27) standardizing how all Hero services start, bind sockets, and expose endpoints, we can now build standard tooling on top. ## 1. Standard Test Helpers Ship reusable test functions in `hero_service::test` that any service can import in its integration tests: ```rust use hero_service::test; #[tokio::test] async fn test_mandatory_endpoints() { // Start the service (spawns process, waits for socket) let svc = test::start_service("hero_myservice_server").await; // These test the mandatory endpoints every HeroRpcServer provides test::assert_health(&svc).await; // GET /health → 200 + status:ok test::assert_openrpc_valid(&svc).await; // GET /openrpc.json → valid spec test::assert_discovery(&svc).await; // GET /.well-known/heroservice.json test::assert_rpc_health(&svc).await; // POST /rpc rpc.health → ok test::assert_rpc_discover(&svc).await; // POST /rpc rpc.discover → spec svc.stop().await; } ``` These would connect to `~/hero/var/sockets/{service_name}.sock` via HTTP and validate responses. ## 2. E2E Test Harness A test runner that starts server + ui, runs all standard tests, then service-specific tests: ```rust // In hero_myservice_examples/tests/e2e.rs use hero_service::test::ServiceHarness; #[tokio::test] async fn e2e() { let harness = ServiceHarness::new() .server("hero_myservice_server") .ui("hero_myservice_ui") .start() .await; // Standard tests run automatically // Then service-specific: let resp = harness.rpc_call("mymethod.get", json!({"sid": "123"})).await; assert!(resp.is_ok()); harness.stop().await; } ``` ## 3. `hero_service` CLI Binary A binary in `hero_rpc` that provides standardized commands across all services: ```bash # Run both server + ui for a service hero_service run hero_osis # starts hero_osis_server + hero_osis_ui via zinit hero_service stop hero_osis # stops both hero_service status # shows all running hero services # Test a running service hero_service test hero_osis # runs standard endpoint tests hero_service test --all # tests all discovered services # Discover services hero_service list # lists all sockets in ~/hero/var/sockets/ hero_service health # health check all services hero_service discover hero_osis # fetch and print openrpc spec ``` This replaces per-repo Makefile targets with a single tool that knows the conventions. ## 4. Standardize Makefile Targets With `hero_service` CLI, Makefiles become thin wrappers: ```makefile run: hero_service run $(SERVICE_NAME) stop: hero_service stop $(SERVICE_NAME) test: hero_service test $(SERVICE_NAME) status: hero_service status $(SERVICE_NAME) ``` ## Implementation Priority 1. Test helpers (`hero_service::test`) — highest value, enables CI 2. `hero_service` CLI binary — developer ergonomics 3. E2E harness — integration testing framework 4. Makefile standardization — convenience
Author
Owner

Implementation Context

This issue builds on the unified server lifecycle from #27. Here's what exists now and what needs to be built.

Current State (from #27)

hero_service crate (in hero_rpc repo, branch development_home27):

  • HeroRpcServer — RPC server builder with mandatory endpoints (/health, /openrpc.json, /.well-known/heroservice.json)
  • HeroUiServer — UI server builder (no mandatory endpoint injection)
  • HeroServer — base: Unix socket binding, graceful shutdown, lifecycle CLI
  • HeroLifecycle — hero_init process management (run/start/stop/status/logs)
  • LifecycleCommand — clap enum with standard CLI subcommands
  • serve_unix() — shared UDS accept loop helper
  • NoServeArgs — empty args for simple services

Socket convention: ~/hero/var/sockets/{service_name}.sock

Mandatory endpoints (HeroRpcServer only):

  • GET /health{"status":"ok","service":"...","version":"..."}
  • GET /openrpc.json → the spec passed at construction
  • GET /.well-known/heroservice.json{"protocol":"openrpc","name":"...","version":"...","description":"..."}

Services already migrated:

  • hero_inspector_serverHeroRpcServer
  • hero_inspector_uiHeroUiServer
  • hero_osis_serverHeroRpcServer (single socket, _context in params)
  • hero_osis_uiHeroUiServer

hero_init (forked from zinit, at lhumina_code/hero_init, branch development_home27):

  • hero_init_sdk — Rust SDK for hero_init process supervisor
  • Socket: ~/hero/var/sockets/hero_init_server.sock
  • Env var: HERO_INIT_SOCKET

Key Files

File Purpose
hero_rpc/crates/service/src/hero_server.rs HeroServer, HeroRpcServer, HeroUiServer, serve_unix()
hero_rpc/crates/service/src/lifecycle.rs HeroLifecycle (hero_init integration)
hero_rpc/crates/service/src/cli.rs LifecycleCommand enum
hero_rpc/crates/service/src/lib.rs Public exports
hero_rpc/crates/service/Cargo.toml Dependencies (hero_init_sdk, axum, hyper, etc.)

What to Build

1. Test helpers (hero_service::test module)

Add a new module hero_rpc/crates/service/src/test.rs with:

/// Connect to a service's Unix socket and verify /health returns 200 + status:ok
pub async fn assert_health(service_name: &str) -> Result<()>

/// Connect and verify /openrpc.json returns a valid OpenRPC spec
pub async fn assert_openrpc_valid(service_name: &str) -> Result<()>

/// Connect and verify /.well-known/heroservice.json returns discovery manifest
pub async fn assert_discovery(service_name: &str) -> Result<()>

/// Connect and POST /rpc with rpc.health method
pub async fn assert_rpc_health(service_name: &str) -> Result<()>

/// Connect and POST /rpc with rpc.discover method  
pub async fn assert_rpc_discover(service_name: &str) -> Result<()>

/// Helper to make HTTP requests over Unix socket
pub async fn http_get(service_name: &str, path: &str) -> Result<(StatusCode, String)>
pub async fn http_post(service_name: &str, path: &str, body: &str) -> Result<(StatusCode, String)>

These connect to ~/hero/var/sockets/{service_name}.sock via HTTP over UDS (using hyper + hyperlocal or similar). They're meant for integration tests in service repos.

2. Service harness (hero_service::test::ServiceHarness)

pub struct ServiceHarness { /* ... */ }

impl ServiceHarness {
    pub fn new() -> Self;
    pub fn server(self, binary_name: &str) -> Self;
    pub fn ui(self, binary_name: &str) -> Self;
    pub async fn start(self) -> RunningHarness;
}

impl RunningHarness {
    pub async fn rpc_call(&self, method: &str, params: Value) -> Result<Value>;
    pub async fn stop(self);
}

Starts service binaries as child processes, waits for their sockets to appear, runs standard endpoint tests automatically, provides rpc_call for service-specific tests.

3. hero_service CLI binary

New binary in hero_rpc/crates/service/ (or a new hero_rpc/crates/hero_service_cli/ crate):

hero_service run <name>       # starts {name}_server + {name}_ui via hero_init
hero_service stop <name>      # stops both
hero_service status [name]    # shows status of one or all hero services
hero_service test <name>      # runs standard endpoint tests against running service
hero_service test --all       # tests all discovered services
hero_service list             # lists all .sock files in ~/hero/var/sockets/
hero_service health [name]    # health check one or all services
hero_service discover <name>  # fetch and print openrpc spec

Uses hero_init_sdk for lifecycle, hero_service::test for testing, and HTTP over UDS for endpoint checks.

Dependencies Available

Already in hero_service Cargo.toml:

  • axum, hyper, hyper-util, tower — for HTTP over UDS
  • hero_init_sdk — for lifecycle management
  • serde_json — for JSON parsing
  • dirs — for socket path resolution
  • clap — for CLI

May need to add:

  • hyperlocal — for HTTP client over Unix sockets (test helpers)
  • tokio::process — for spawning service binaries (harness)
## Implementation Context This issue builds on the unified server lifecycle from #27. Here's what exists now and what needs to be built. ### Current State (from #27) **`hero_service` crate** (in `hero_rpc` repo, branch `development_home27`): - `HeroRpcServer` — RPC server builder with mandatory endpoints (`/health`, `/openrpc.json`, `/.well-known/heroservice.json`) - `HeroUiServer` — UI server builder (no mandatory endpoint injection) - `HeroServer` — base: Unix socket binding, graceful shutdown, lifecycle CLI - `HeroLifecycle` — hero_init process management (run/start/stop/status/logs) - `LifecycleCommand` — clap enum with standard CLI subcommands - `serve_unix()` — shared UDS accept loop helper - `NoServeArgs` — empty args for simple services **Socket convention:** `~/hero/var/sockets/{service_name}.sock` **Mandatory endpoints** (HeroRpcServer only): - `GET /health` → `{"status":"ok","service":"...","version":"..."}` - `GET /openrpc.json` → the spec passed at construction - `GET /.well-known/heroservice.json` → `{"protocol":"openrpc","name":"...","version":"...","description":"..."}` **Services already migrated:** - `hero_inspector_server` → `HeroRpcServer` - `hero_inspector_ui` → `HeroUiServer` - `hero_osis_server` → `HeroRpcServer` (single socket, `_context` in params) - `hero_osis_ui` → `HeroUiServer` **hero_init** (forked from zinit, at `lhumina_code/hero_init`, branch `development_home27`): - `hero_init_sdk` — Rust SDK for hero_init process supervisor - Socket: `~/hero/var/sockets/hero_init_server.sock` - Env var: `HERO_INIT_SOCKET` ### Key Files | File | Purpose | |------|--------| | `hero_rpc/crates/service/src/hero_server.rs` | HeroServer, HeroRpcServer, HeroUiServer, serve_unix() | | `hero_rpc/crates/service/src/lifecycle.rs` | HeroLifecycle (hero_init integration) | | `hero_rpc/crates/service/src/cli.rs` | LifecycleCommand enum | | `hero_rpc/crates/service/src/lib.rs` | Public exports | | `hero_rpc/crates/service/Cargo.toml` | Dependencies (hero_init_sdk, axum, hyper, etc.) | ### What to Build #### 1. Test helpers (`hero_service::test` module) Add a new module `hero_rpc/crates/service/src/test.rs` with: ```rust /// Connect to a service's Unix socket and verify /health returns 200 + status:ok pub async fn assert_health(service_name: &str) -> Result<()> /// Connect and verify /openrpc.json returns a valid OpenRPC spec pub async fn assert_openrpc_valid(service_name: &str) -> Result<()> /// Connect and verify /.well-known/heroservice.json returns discovery manifest pub async fn assert_discovery(service_name: &str) -> Result<()> /// Connect and POST /rpc with rpc.health method pub async fn assert_rpc_health(service_name: &str) -> Result<()> /// Connect and POST /rpc with rpc.discover method pub async fn assert_rpc_discover(service_name: &str) -> Result<()> /// Helper to make HTTP requests over Unix socket pub async fn http_get(service_name: &str, path: &str) -> Result<(StatusCode, String)> pub async fn http_post(service_name: &str, path: &str, body: &str) -> Result<(StatusCode, String)> ``` These connect to `~/hero/var/sockets/{service_name}.sock` via HTTP over UDS (using hyper + hyperlocal or similar). They're meant for integration tests in service repos. #### 2. Service harness (`hero_service::test::ServiceHarness`) ```rust pub struct ServiceHarness { /* ... */ } impl ServiceHarness { pub fn new() -> Self; pub fn server(self, binary_name: &str) -> Self; pub fn ui(self, binary_name: &str) -> Self; pub async fn start(self) -> RunningHarness; } impl RunningHarness { pub async fn rpc_call(&self, method: &str, params: Value) -> Result<Value>; pub async fn stop(self); } ``` Starts service binaries as child processes, waits for their sockets to appear, runs standard endpoint tests automatically, provides `rpc_call` for service-specific tests. #### 3. `hero_service` CLI binary New binary in `hero_rpc/crates/service/` (or a new `hero_rpc/crates/hero_service_cli/` crate): ``` hero_service run <name> # starts {name}_server + {name}_ui via hero_init hero_service stop <name> # stops both hero_service status [name] # shows status of one or all hero services hero_service test <name> # runs standard endpoint tests against running service hero_service test --all # tests all discovered services hero_service list # lists all .sock files in ~/hero/var/sockets/ hero_service health [name] # health check one or all services hero_service discover <name> # fetch and print openrpc spec ``` Uses `hero_init_sdk` for lifecycle, `hero_service::test` for testing, and HTTP over UDS for endpoint checks. ### Dependencies Available Already in `hero_service` Cargo.toml: - `axum`, `hyper`, `hyper-util`, `tower` — for HTTP over UDS - `hero_init_sdk` — for lifecycle management - `serde_json` — for JSON parsing - `dirs` — for socket path resolution - `clap` — for CLI May need to add: - `hyperlocal` — for HTTP client over Unix sockets (test helpers) - `tokio::process` — for spawning service binaries (harness)
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/home#29
No description provided.