feat(scaffold): per-root-object workspace-root E2E tests crate (closes #115) #119

Merged
timur merged 2 commits from issue-115-e2e-tests into development 2026-05-22 00:51:40 +00:00
Owner

Summary

  • Emits one <entity>_e2e.rs per OSchema rootobject under <workspace>/tests/generated/ — each driving the full assembled service through the typed SDK over a real UDS socket.
  • Scaffolds the parent tests/ crate (Cargo.toml, .gitignore, src/lib.rs with spin_up_service() helper) preserved-once via the canonical with_tests() / --no-tests wiring mirroring --no-web (#98) / --no-benches (#113).
  • Spin-up uses hero_rpc_osis::rpc::bootstrap::run_for_test from #117 — tests never duplicate the production bootstrap. tests/src/lib.rs::spin_up_service() is a 5-line wrapper.

Side cleanup

  • Old per-domain dispatch tests emitter (generate/tests_emit.rs) deleted — the wire-level E2E suite transitively covers OSIS dispatch. Three-layer test taxonomy collapses to two (inline unit + workspace-root E2E).
  • Shared scaffolder data model (FieldKind, FieldInfo, RootObjectInfo, discover_root_objects) extracted from build/ui_emit.rs into new build/fixture.rs so the three sibling emitters (ui, tests, future benches) consume one source of truth. ui_emit.rs keeps its HTML-specific renderers as free functions taking &FieldKind.

Generated test shape

// tests/generated/recipe_e2e.rs — auto-generated
use hero_recipes_sdk::recipes::{Recipe, RecipeInput, RecipesClient};
use hero_recipes_tests::spin_up_service;

#[tokio::test]
async fn recipe_full_lifecycle() {
    let (svc, _data_dir) = spin_up_service().await.expect("spin up service");
    let sid = RecipesClient::recipe_new(&*svc.client, None, RecipeInput::default()).await.expect("recipe_new");
    let got = RecipesClient::recipe_get(&*svc.client, None, sid.clone()).await.expect("recipe_get");
    assert_eq!(got.sid.as_str(), sid);
    RecipesClient::recipe_set(&*svc.client, None, sid.clone(), RecipeInput { name: "updated".into(), ..Default::default() }).await.expect("recipe_set");
    // … list / list_full / exists / delete / exists-false …
    svc.shutdown().await;
}

Why svc.shutdown().await explicitly

RunningServer::Drop deadlocks inside #[tokio::test] (block_on inside the runtime thread). Generated tests call svc.shutdown().await explicitly — the upstream Drop impl can stay for production binaries that drop outside a tokio runtime; the test-side path takes the async route. Follow-up: the Drop impl in hero_rpc_osis::rpc::bootstrap should be fixed (detach the shutdown future when no async context is safe).

Acceptance

  • recipe_server/tests/ exists; cargo test -p hero_recipes_tests runs recipe_full_lifecycle + collection_full_lifecycle green against an in-process recipe_server.
  • cargo test -p hero_rpc_generator --lib — 137 tests passing.
  • git status clean after cargo build (tests/generated/ ignored).
  • --no-tests skips the entire tests/ scaffold.
  • Old generate/tests_emit.rs (dispatch tests) deleted; per-domain <server>/<domain>/generated/tests.rs stops being emitted.

Test plan

  • cd examples/recipe_server && cargo test -p hero_recipes_tests — both lifecycle tests pass.
  • cargo test -p hero_rpc_generator --lib — generator unit tests green.
  • cargo build -p hero_recipes — build.rs regenerates tests/generated/{mod.rs, recipe_e2e.rs, collection_e2e.rs}.
  • git status clean post-build (the generated/ tree is gitignored).
  • Reviewer: spot-check tests/generated/recipe_e2e.rs content matches the shape above.
  • Reviewer: confirm the lab infocheck audit grep rules still hold (server crate stays bin-only; main.rs unchanged).

Follow-up

  • Fix RunningServer::Drop deadlock inside #[tokio::test] (track separately).
  • hero_skills companion PR: update hero_service_scaffold.md skill with --no-tests flag + tests/ directory tree + "End-to-end tests — out of the box" section (will open after this merges).

🤖 Generated with Claude Code

## Summary - Emits one `<entity>_e2e.rs` per OSchema rootobject under `<workspace>/tests/generated/` — each driving the full assembled service through the typed SDK over a real UDS socket. - Scaffolds the parent `tests/` crate (Cargo.toml, .gitignore, `src/lib.rs` with `spin_up_service()` helper) preserved-once via the canonical `with_tests()` / `--no-tests` wiring mirroring `--no-web` (#98) / `--no-benches` (#113). - Spin-up uses `hero_rpc_osis::rpc::bootstrap::run_for_test` from #117 — tests never duplicate the production bootstrap. `tests/src/lib.rs::spin_up_service()` is a 5-line wrapper. ### Side cleanup - Old per-domain dispatch tests emitter (`generate/tests_emit.rs`) **deleted** — the wire-level E2E suite transitively covers OSIS dispatch. Three-layer test taxonomy collapses to two (inline unit + workspace-root E2E). - Shared scaffolder data model (`FieldKind`, `FieldInfo`, `RootObjectInfo`, `discover_root_objects`) extracted from `build/ui_emit.rs` into new `build/fixture.rs` so the three sibling emitters (ui, tests, future benches) consume one source of truth. `ui_emit.rs` keeps its HTML-specific renderers as free functions taking `&FieldKind`. ### Generated test shape ```rust // tests/generated/recipe_e2e.rs — auto-generated use hero_recipes_sdk::recipes::{Recipe, RecipeInput, RecipesClient}; use hero_recipes_tests::spin_up_service; #[tokio::test] async fn recipe_full_lifecycle() { let (svc, _data_dir) = spin_up_service().await.expect("spin up service"); let sid = RecipesClient::recipe_new(&*svc.client, None, RecipeInput::default()).await.expect("recipe_new"); let got = RecipesClient::recipe_get(&*svc.client, None, sid.clone()).await.expect("recipe_get"); assert_eq!(got.sid.as_str(), sid); RecipesClient::recipe_set(&*svc.client, None, sid.clone(), RecipeInput { name: "updated".into(), ..Default::default() }).await.expect("recipe_set"); // … list / list_full / exists / delete / exists-false … svc.shutdown().await; } ``` ### Why `svc.shutdown().await` explicitly `RunningServer::Drop` deadlocks inside `#[tokio::test]` (block_on inside the runtime thread). Generated tests call `svc.shutdown().await` explicitly — the upstream Drop impl can stay for production binaries that drop outside a tokio runtime; the test-side path takes the async route. **Follow-up:** the Drop impl in `hero_rpc_osis::rpc::bootstrap` should be fixed (detach the shutdown future when no async context is safe). ### Acceptance - [x] `recipe_server/tests/` exists; `cargo test -p hero_recipes_tests` runs `recipe_full_lifecycle` + `collection_full_lifecycle` green against an in-process recipe_server. - [x] `cargo test -p hero_rpc_generator --lib` — 137 tests passing. - [x] `git status` clean after `cargo build` (`tests/generated/` ignored). - [x] `--no-tests` skips the entire `tests/` scaffold. - [x] Old `generate/tests_emit.rs` (dispatch tests) deleted; per-domain `<server>/<domain>/generated/tests.rs` stops being emitted. ## Test plan - [x] `cd examples/recipe_server && cargo test -p hero_recipes_tests` — both lifecycle tests pass. - [x] `cargo test -p hero_rpc_generator --lib` — generator unit tests green. - [x] `cargo build -p hero_recipes` — build.rs regenerates `tests/generated/{mod.rs, recipe_e2e.rs, collection_e2e.rs}`. - [x] `git status` clean post-build (the generated/ tree is gitignored). - [ ] Reviewer: spot-check `tests/generated/recipe_e2e.rs` content matches the shape above. - [ ] Reviewer: confirm the `lab infocheck` audit grep rules still hold (server crate stays bin-only; main.rs unchanged). ## Follow-up - Fix `RunningServer::Drop` deadlock inside `#[tokio::test]` (track separately). - hero_skills companion PR: update `hero_service_scaffold.md` skill with `--no-tests` flag + `tests/` directory tree + "End-to-end tests — out of the box" section (will open after this merges). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(scaffold): per-root-object workspace-root E2E tests crate (closes #115)
Some checks failed
Test / test (push) Failing after 2m18s
Test / test (pull_request) Failing after 1m57s
90dc4599bc
Emits one `<entity>_e2e.rs` per OSchema rootobject under `<workspace>/tests/generated/`,
each driving the full assembled service through the typed SDK over a real UDS
socket. Output mirrors how #98 emits per-entity UI scaffolds + how #113 will emit
benches — the generated tree is gitignored under `tests/generated/`, the parent
`tests/` crate (Cargo.toml, .gitignore, src/lib.rs with `spin_up_service()`
helper) is scaffolded once and contributor-owned.

Spin-up lives in `hero_rpc_osis::rpc::bootstrap::run_for_test` from #117 — tests
do not duplicate the production bootstrap. `tests/src/lib.rs::spin_up_service()`
is a five-line wrapper that calls `run_for_test::<OsisRecipes>("recipes", 0)`
then `ClientBuilder::new().connect_http(...)`. Generated tests explicitly
`svc.shutdown().await` at the end because `RunningServer::Drop` deadlocks inside
`#[tokio::test]` (block_on inside the runtime thread).

Scaffolder wiring matches the `--no-web` / `--no-benches` pattern: `with_tests()`
/ `without_tests()` builders, `--no-tests` CLI flag, `"tests"` added to
`[workspace] members`, build.rs gains `.tests_crate_dir("../../tests")` +
`.tests_crate_name(...)` + `.sdk_crate_name(...)` when tests are enabled.

Side cleanup:
* Old per-domain dispatch tests emitter (`generate/tests_emit.rs`) deleted —
  the wire-level E2E suite transitively covers OSIS dispatch; the three-layer
  test taxonomy collapses to two (inline unit + workspace-root E2E).
* Shared scaffolder data model (FieldKind, FieldInfo, RootObjectInfo,
  discover_root_objects) extracted from `build/ui_emit.rs` into new
  `build/fixture.rs` so the three sibling emitters (ui, tests, future benches)
  consume one source of truth. `ui_emit.rs` keeps its HTML-specific renderers
  as free functions taking `&FieldKind`.
* Edge-case unknown-sid assertions intentionally not emitted — OSIS handler
  semantics for arbitrary sids are not part of the post-#117 contract
  (`delete` is idempotent and returns `Ok(true)` for non-existent sids).
  Contributors add domain-specific edge-case tests by hand; the generated
  lifecycle test covers the wire path end-to-end.

Acceptance:
* `cargo test -p hero_recipes_tests` runs `recipe_full_lifecycle` +
  `collection_full_lifecycle` green against an in-process recipe_server.
* `cargo test -p hero_rpc_generator --lib` — 137 tests passing.
* `git status` clean after `cargo build` (`tests/generated/` ignored).

Known follow-up: `RunningServer::Drop` in `hero_rpc_osis::rpc::bootstrap`
deadlocks inside `#[tokio::test]` because `std:🧵:scope` + `Handle::block_on`
can't make progress when the calling thread is the runtime thread. Workaround
here is explicit `svc.shutdown().await` in generated tests; the upstream Drop
impl should be fixed (e.g. detach the shutdown future when no async context is
safe).
docs(scaffold): README mentions tests/ + the two-pattern split
Some checks failed
Test / test (pull_request) Failing after 2m56s
Test / test (push) Failing after 2m57s
5dabd92f63
Adds `tests/` to the scaffolded service's `## Layout` block and a new
`## Tests` section that explicitly names the two coexisting patterns:

- `tests/` (this PR) — codegen E2E, `cargo test`, in-process via
  `run_for_test`, no `lab service --start` required.
- `crates/{name}_test/` (optional, handwritten per the canonical
  `hero_tests_create` skill) — service integration, against a running
  service started via `lab service --start`.

Neither replaces the other. The README points contributors at the
hero_skills doc for the canonical pattern so the split is intentional
+ discoverable rather than accidental.
timur merged commit a97acd100a into development 2026-05-22 00:51:40 +00:00
Sign in to join this conversation.
No reviewers
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/hero_rpc!119
No description provided.