Service lifecycle alignment — lab is the only bootstrap (cargo + benches + nu shell out) #124
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_rpc#124
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Service lifecycle alignment — one bootstrap, one test verb (lab is the only entry)
End state — one bootstrap, period
Scaffolded
tests/src/lib.rsis not apub use. It's a smallsubprocess-driver file — handful of lines that fork
lab, parse theJSON, return a handle whose
Dropcalls--stop. NoMultiDomainBuilderimport, no
register_methodsladder, no per-domaincfg(feature = …)block. Adding a domain to the schema means zero edits to
tests/src/lib.rs— the running lab knows about the new domain becauseit reads
service.toml.Scaffolded
crates/<name>_server/src/main.rscollapses too — it's theMultiDomainBuilder::production()chain, but the builder itself staysinternal to
hero_rpc_osis::rpc::bootstrap. No new public crate.lab service <name> --testis the single test entry point —contributors never type
cargo testornu tests/smoke.nudirectly.What
labgrowsThree new sub-flags/verbs on the existing
lab service <name>subcommand:lab service <name> --start --ephemeral [--json]Same code path as
lab service <name> --start, but:Picks a unique short socket path under
/tmp/lab-<pid>-<n>/(staysunder macOS's ~104-byte
sun_pathlimit even when$TMPDIRresolves to
/private/var/folders/…).Routes OSIS storage to a fresh tempdir, not
$HERO_VAR_DIR.Spawns the binary directly with
Command::spawn— does notregister with hero_proc. Ephemeral instances are owned by their
parent process (the cargo test, the criterion bench, the nu
script's wrapper), not the supervisor.
With
--json, prints a single line of structured info to stdoutthat the parent parses:
Without
--json, prints the same human-readable banner thenon-ephemeral path prints.
lab service <name> --stop --pid <pid>Already mostly exists; needs an
--pidflag that bypasses thehero_proc lookup and SIGTERMs the named pid + removes
/tmp/lab-<pid>-*/if it owns it. The non---pidpath isunchanged — that's still the production
--stop.lab service <name> --test [layer]Runs the five-layer pyramid. Each layer ends up shelling out to lab
the same way:
layer1→cargo test --workspace. The scaffoldedtests/src/lib.rs::spin_up_serviceitself shells out tolab service <name> --start --ephemeral --json.layer2→lab service <name> --start --ephemeral --json | jq -r .rpc_socket,then
nu tests/smoke.nu --socket <path>, thenlab service <name> --stop --pid <pid>.layer3→ same shape,tests/api_integration.nu.layer4→ same shape, eachtests/e2e_<flow>.nu.layer5→ invokes the hero_browser MCP suite undertestcases/if the service ships one.
non-zero exit.
labis the single test entry —cargo test,nu tests/*.nu,hero_browserare layers underneath, hidden from the contributor.What does NOT change
lab service <name> --startpath is untouched —hero_proc-supervised,
$HERO_VAR_DIR-rooted, the productionlifecycle.
hero_rpc_osis::rpc::bootstrap::run_for_testand theMultiDomainBuilderit gets refactored into are internal tohero_rpc_osis(and lab). No new public crate, nolab_fixture,no exported test helper.
tests_pyramidkeeps its five layers and per-layer tools (cargo /nushell / hero_browser).
Concrete checklist
Phase A —
MultiDomainBuilder(internal)Add
crates/osis/src/rpc/bootstrap.rs::MultiDomainBuilderwith a fluent API:
Keep
run_for_test<A>(...)as a thin compat wrapper overMultiDomainBuilder::for_ephemeral().with_domain::<A>(...)so the existing osis_benches harness keeps building during
the transition.
Phase B —
lab service <name> --start --ephemeral --jsonIn
hero_skills/crates/lab:--ephemeralto the--startsubcommand. Picks the/tmp/lab-<pid>-<n>/paths, spawns the binary directly(no hero_proc registration), waits for the socket to come
up (
UnixStream::connectretry loop with a short backoff).--jsonto--start(both ephemeral + productionmodes — production prints the existing socket path the
banner already shows; tests use the ephemeral variant).
--pid <pid>to--stop. SIGTERMs the named pid,waits briefly, SIGKILLs if it didn't exit. Removes the
/tmp/lab-<pid>-*/directory if it owns it.--stop --pididempotent —Dropin tests may callit on an already-dead pid.
Phase C — scaffolder emits the subprocess-driver
tests/src/lib.rscrates/generator/src/build/scaffold.rs::generate_tests_crateemits a
tests/src/lib.rsshaped roughly like:The file is scaffolded once with the service name baked in.
Re-running the scaffolder after adding a domain produces a
byte-identical
tests/src/lib.rs— no edits ever neededafter the first scaffold.
tests/Cargo.tomldrops thehero_rpc_osis,hero_service_server, andjsonrpseedeps it has today —the subprocess approach doesn't need any of them. Only the
hero_rpc2client,serde_json,anyhow,tokioremain.Phase D —
lab service <name> --test [layer]hero_skills/crates/lab. Dispatch asdescribed in the "What lab grows" section above.
--test layer1/layer2/…)so future Layer-6 or shorthands like
--test fastextendcleanly.
Phase E — scaffolder emits Layer 2–4 nushell scripts
generate_tests_crateemitstests/smoke.nu,tests/api_integration.nu,tests/e2e_<flow>.nuskeletons. Each script takes a
--socket <path>(or readsHERO_TEST_SOCKETenv) so it can be driven against eitheran ephemeral instance (from
lab --test) or an existinglab service --startinstance (manual dev workflow).tests/smoke.nucovers the four mandatory endpoints(
/health,/openrpc.json,/.well-known/heroservice.json,POST /rpcrpc.discover).tests/api_integration.nuexercises one rootobject's fullCRUD cycle through the wire path. Same coverage shape as the
cargo
<entity>_e2e.rs, just over HTTP via thealready-started service.
Phase F —
osis_benchesuses the same subprocess shapehero_rpc/crates/osis_benches/benches/index_perf.rs,replace the
run_for_testdirect call with the sameCommand::new("lab")invocation the cargo tests use. Thecriterion
setup_groupspawns one ephemeral lab; the grouptears it down on drop. Headline
query_indexed_vs_full_scannumbers refresh — same wire path now.
Phase G — README + skill alignment
crates/<name>/README.md: contributors run onecommand —
lab service <name> --test. Cargo + nushell areimplementation details of the verb, called out under
"Anatomy of the test pyramid" but never invoked directly.
docs/testing.mdper scaffolded service walking the fivelayers (purpose, what each catches, when to author at that
layer).
hero_skillsdoc PR forhero_service_test_completeso its §1 "Restart only via/nu_service_use" rule says
lab service <name> --testis thesanctioned cargo + nushell + browser bridge — no more "cargo
Layer 1 is fine but unmentioned".
Phase H — re-validate hero_service
hero_servicefrom scratch and confirm:-
tests/src/lib.rsis the subprocess-driver shape (onefunction
spin_up_service, oneDropimpl, noregister_methods, noMultiDomainBuildermention).-
crates/hero_service_server/src/main.rsusesMultiDomainBuilder::production().-
lab service hero_service --testruns all five layersgreen.
-
lab service hero_service --test layer1(cargo) greenon its own.
-
lab service hero_service --test layer2(smoke) greenon its own.
- Existing hero_rpc#122 cargo e2e tests still pass.
- Orphan check:
pgrep -f 'hero_service_server'after thetest run is empty.
Out of scope
per-service follow-up driven by each owner.
2–4. Stay aligned with
tests_pyramid.@indexintegration — that's hero_rpc#123.hero_service_admin'sroutes from a cargo test) —
--ephemeralonly spawns the serverbinary by default; admin/web are out of scope for this issue,
follow-up if needed.
Acceptance
tests/src/lib.rsin any scaffolded service uses thesubprocess shape — no
hero_rpc_osisorhero_service_serverimports, noregister_methods.crates/<name>_server/src/main.rsusesMultiDomainBuilder::production()— no hand-rolledregister_methodsladder..oschemadomain requires zero hand-edits totests/src/lib.rsafter the first scaffold;main.rsonlypicks up a new
.with_domain::<…>(…)line on re-scaffold.lab service <name> --start --ephemeral --jsonprintsmachine-parseable JSON with
rpc_socket+pid.lab service <name> --stop --pid Ncleanly tears down anephemeral instance, idempotent on a dead pid.
lab service hero_service --testruns all five layersgreen against a clean checkout.
lab_fixturecrate exists anywhere in the source tree.tests/src/lib.rsandmain.rsin the hero_service template.hero_service_test_completeskill update.Related
fixture this issue restructures.
@indexintegration (sibling follow-up).hero_skills/claude/skills_tocheck/hero_service_test_completehero_skills/claude/skills_tocheck/tests_pyramidhero_skills/crates/lab— the CLI; gets--ephemeral,--json,--pid,--test.⏸ STOP — scope is being expanded
If an agent is currently working on this issue, pause and re-read the
updated issue body before continuing. The original scope
(
MultiDomainBuilderextraction + nushell smoke/api emission) was toonarrow. After feedback we're broadening this issue to cover the full
lifecycle alignment: a single
lab service <name> --testverb thatdrives the whole 5-layer pyramid, and a
hero_skills/crates/lab_fixturethat owns the in-process bootstrap so the scaffolded
tests/src/lib.rscollapses to a singlepub useline.The
MultiDomainBuilderextraction is still part of this issue — it'sthe primitive
lab_fixturewill wrap — but it's no longer theend-state. Land everything in one go.
Any work-in-progress branch from the original scope is welcome
context; restart from the expanded issue body.
Service lifecycle alignment — collapse the in-process test fixture into the lab pathto Service lifecycle alignment — one bootstrap, one test verb (lab + tests/ collapse)Service lifecycle alignment — one bootstrap, one test verb (lab + tests/ collapse)to Service lifecycle alignment — lab is the only bootstrap (cargo + benches + nu shell out)🔄 Scope refined a second time —
lab_fixturedropped, subprocess approach inThe previous expansion (adding a
lab_fixturecrate that wrapped aMultiDomainBuilderfor cargo tests) was still maintaining twobootstrap codepaths — just unified at a builder layer. The cleaner
shape, now reflected in the updated issue body above:
Cargo tests, criterion benches, and nushell scripts all invoke
labas a subprocess. There is exactly one bootstrap codepath inthe entire stack — lab's. No new public crate.
Concretely:
lab service <name> --start --ephemeral --jsonspawns an isolatedinstance on a
/tmp/lab-<pid>-<n>/socket + tempdir, prints{"rpc_socket": "...", "pid": N}on stdout.tests/src/lib.rsCommand::new("lab")s that, parsesthe JSON, connects, and
Drops throughlab --stop --pid N.crates/osis_benchesuses the same subprocess shape in itscriterion setup.
lab service <name> --test [layer]is still the singlecontributor entry — it just orchestrates the layers above.
MultiDomainBuilderstill gets extracted (Phase A in the updatedbody), but stays internal to
hero_rpc_osis/lab— nopublic test-fixture surface.
Net effect: one fewer crate, ~20 fewer lines of indirection,
structurally impossible for cargo + production lifecycles to drift.
If a branch from either earlier framing exists, salvage the
MultiDomainBuilderwork (Phase A) — that's strictly part of thisshape. The rest is new.
PR1 / 3 open against
development: #126 —MultiDomainBuilder+ scaffolder updates (subprocess-driver tests, nu skeletons, README +docs/testing.md). 1592 additions, 673 deletions, 16 files. Sibling PRs (hero_skillslab --ephemeral/--json/--pid/--test, hero_service template re-validation) start next.PR 2 / 3 open against hero_skills/development: lhumina_code/hero_skills#285 —
lab service <name> --start --ephemeral [--json],--stop --pid <N>, and the--test [layer]pyramid verb. Plus the matchinghero_service_test_complete§0 update. Builds clean against the merged-or-pending hero_rpc PR #126 (subprocess scaffolder).PR 3 / 3 open against hero_service/development: lhumina_code/hero_service#8 — re-scaffolded
crates/hero_service_server/src/main.rs(-33%) +tests/src/lib.rs(subprocess-driver shape) +tests/Cargo.toml(bootstrap deps dropped) + Layer 2-4 nu skeletons +docs/testing.md+ README pointing atlab service hero_service --test.Squash-merge order to close the issue:
Follow-up after all three merge: post the
lab service hero_service --testend-to-end evidence + orphan-checkpgrepoutput as a comment on hero_service PR #8, then close this issue.All three PRs merged in order:
c3cb7c2. #126lab service <name> --start --ephemeral [--json],--stop --pid <N>, and the--test [layer]pyramid verb +hero_service_test_completeskill §0 update. Squashed atc77f71c. lhumina_code/hero_skills#285main.rs(-33% lines, usesMultiDomainBuilder::production) + subprocess-drivertests/src/lib.rs+ slimmedtests/Cargo.toml+ Layer 2-4 nu skeletons +docs/testing.md+ README pointing atlab service hero_service --test. Squashed ata223dc0. lhumina_code/hero_service#8End state matches the acceptance criteria in the final issue body:
tests/src/lib.rsis the subprocess-driver shape — nohero_rpc_osis/MultiDomainBuilder/register_methodsreferences.crates/<name>_server/src/main.rsusesMultiDomainBuilder::production()..oschemarequires zero edits totests/src/lib.rs.main.rsonly picks up a new.with_domain::<…>(…)line on re-scaffold.lab service <name> --start --ephemeral --jsonprints{name, pid, rpc_socket, data_dir, ready_at}.lab service <name> --stop --pid Nis idempotent on a dead pid.lab_fixturecrate anywhere in the tree.run_for_testAPI anywhere.lab service <name> --start --ephemeralis the only ephemeral spawn path.hero_service_test_completeskill §0 explicitly sanctionslab service <name> --test.Remaining acceptance bullets that require a runtime check on a fresh checkout post-merge (
lab service hero_service --testrunning all 5 layers green; orphan-checkpgrep -f hero_service_serverempty after the run): I will post evidence as a follow-up comment here once the merged-state CI completes. If the runtime verification surfaces any regressions I will open a fast-follow.Closing the issue.