Generator + scaffolder: emit end-to-end tests crate from OSchema (workspace-root tests/) #115
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#115
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?
Context
The scaffolder already emits a complete shape from an OSchema:
_server(tests_emit.rs→<domain>/generated/tests.rs) — these are OSIS-dispatch unit testsexamples.rs)_admin+_webUI scaffolds (#98)What's still missing: a workspace-root end-to-end test crate that exercises the whole assembled service as a black box — real UDS socket, real
hero_rpc2transport, real typed SDK calls, full CRUD lifecycle across the scaffolded surface. Today the only thing that proves the assembled binary actually works islab service <name> --start+ manual curl. That's not regression-safe.This is not a replacement for inline
#[cfg(test)] mod testsunit tests inside source files, nor for the existing generator-emitted dispatch unit tests in_server. It's the missing peer: integration that crosses crate boundaries.What this does
crates/generator/src/generate/e2e_emit.rs(mirrorstests_emit.rsand the upcomingbenches_emit.rsfrom #113).tests/crate — handwrittensrc/lib.rswith in-process service spin-up helpers, generatedgenerated/<entity>_e2e.rsper root object, generatedgenerated/<domain>_methods_e2e.rsper domain for non-CRUD service methods.new → get → set → list → list_full → exists → delete → verify gone) over the real UDS socket, asserted through the typed SDK trait.#[tokio::test]per declared.oschemaservice method, calling through the typed SDK.Concrete deliverables
Workspace-root layout
Each generated
[[test]]file usesuse hero_<name>_tests::spin_up_service;from the local lib. Cargo's standard test harness runs them in parallel by default;serial_test::serialcovers any that share global state.Per-entity E2E shape (one generated
<entity>_e2e.rs)Fixture generation
Reuse the same per-OSchema-type defaults as #98's form-input table and #113's bench fixtures. Don't re-invent — share a
crates/generator/src/fixture.rshelper if these three emitters (UI / benches / e2e) end up duplicating the logic.What to do
[[test]]Cargo manifest shape).spin_up_service()helper API — does it return(Client, JoinHandle)or aServiceHandleRAII guard? How does it dispose the UDS socket between tests?#[serial]annotation? Global semaphore?).<entity>_e2e.rs.crates/generator/src/generate/e2e_emit.rsmirroring the existing emitters.crates/generator/src/build/scaffold.rs+crates/generator/src/bin/scaffold.rswithwith_tests()(default-on) and--no-testsflag — match the exact wiring pattern used for--no-web(#98) /--no-benches(#113).example/recipe_server/—cargo testat workspace root runs the new E2E suite green.hero_service_scaffold.mdskill inhero_skillsto documenttests/+--no-tests, distinguishing it from the inline unit tests and the_server-internal dispatch tests.Acceptance
recipe_server/tests/exists, gets regenerated oncargo build, andcargo testat workspace root runs the new E2E suite end-to-end against an in-process service with no manual setup.git statusclean aftercargo build(tests/generated/ignored).--no-testsskips the entiretests/scaffold.#[cfg(test)]unit tests in source files, (2) generator-emitted dispatch tests inside_server/<domain>/generated/tests.rs, (3) this new workspace-root E2E suite.lab service new <name>produces a complete E2E scaffold alongside test/example/UI/benches scaffolds.Out of scope
ServerBuilderis sufficient for now._admin/_webHTML surfaces — separate issue; this covers the RPC contract only.Related
crates/generator/src/generate/tests_emit.rs,examples.rs,e2e.rsDesign — workspace-root
tests/crate scaffolderPosting before coding per the workflow ask. Worktree at
/tmp/hero_rpc_115on branchissue-115-e2e-tests, off latest dev (87289a1). Flagging one upstream dependency at the bottom — needs a decision before this can ship cleanly.1. Emitter layout
New emitter
crates/generator/src/generate/e2e_emit.rs, sibling oftests_emit.rs,examples.rs, and the (forthcoming)benches_emit.rsfrom #113. Same module shape:Codegen-triggered (called from the per-domain codegen path, not from
WorkspaceScaffolder). WalksSchema::definitions, filtersobj.is_root_object || has_sid, emits one<entity>_e2e.rsper root object plus one<domain>_methods_e2e.rsper domain (for non-CRUD service methods). Output lives under<workspace>/tests/generated/.Note on naming: there's an existing
crates/generator/src/generate/e2e.rs— it emits the per-domain runnable example (examples/rust/<domain>/client_server.rs), unrelated to tests. I'll name the new filee2e_emit.rsto match thetests_emit.rs/benches_emit.rsconvention and avoid the collision.2. Scaffolder wiring —
tests/workspace memberThe scaffolder writes the parent
tests/crate as a workspace member (preserved-once, contributor-owned):Wiring exactly parallels
--no-web(PR #103,ac70c7e) and the upcoming--no-benchesfrom #113. Specifically:WorkspaceScaffolder::generate_tests: boolfield, defaulttrue.with_tests()/without_tests()fluent builders.bin/scaffold.rs:--no-testsflag →scaffolder.without_tests().scaffold()callsgenerate_tests_crate(&workspace_dir, &mut result)?aftergenerate_examples_crate.create_workspace_dirsaddstests/{,src,generated}.generate_workspace_cargo_tomladds"tests"to[workspace] memberswhen the flag is on.generate_tests_cratewrites the preserved-once files:Cargo.toml,.gitignore(generated/),src/lib.rs.Tests added (mirroring
test_scaffolder_web_default_on_with_without_toggle+test_scaffold_emits_examples_crate):test_scaffolder_tests_default_on_with_without_toggletest_scaffold_emits_tests_cratetest_scaffold_no_tests_skips_dir3.
tests/Cargo.tomlshapeharness = trueis the Cargo default — left implicit for brevity. Each[[test]]compiles to its own binary, which means Cargo can parallelize across files. Inside each file,#[tokio::test]annotations let multiple test fns share the binary's tokio runtime.4.
src/lib.rs—spin_up_service()helper (scaffolded once, handwritten)The contributor owns this file. If they want shared fixtures, custom claims headers, or pre-seeded data, they extend
spin_up_service()here — codegen never overwrites it.5. Parallel vs serial
Parallel by default. Each test owns its own tmp data dir + UDS socket path (per-test atomic counter). No shared global state, no
serial_test::serialneeded. The contributor adds#[serial]themselves on any test that touches a singleton (env vars, global metrics, etc.).6. Sample fully-rendered
<entity>_e2e.rsAgainst the post-#117 unified shape (CREATE/UPDATE split):
The "fixture" used in
RecipeInput { name: "smoke".into(), ..Default::default() }is the same per-FieldKind default-value table used by #98 / #113 — see §7.7. Shared fixture helper
The fixture-default logic for OSchema fields is currently entangled in
ui_emit.rs::FieldKind(HTML inputs). #113 hasn't shipped, so the helper isn't extracted yet. I'll do the extraction as part of this PR:New file
crates/generator/src/fixture.rs:FieldKind,FieldInfo,RootObjectInfo,discover_root_objectsout ofbuild/ui_emit.rsinto here. These are emitter-neutral data; the UI emitter, the e2e emitter, and (soon) the bench emitter all consume them.impl FieldKind { pub fn rust_default_expr(&self) -> String }— emits e.g."String::new()"forStr,"OTime::default()"forOTime,"0u32"forUnsignedInt,"Vec::new()"forPrimitiveList, etc. Used by e2e + benches to populate fixture struct literals.ui_emit.rsre-exports the types it moved (for back-compat with any external callers), keeps its UI-specificinput_html/detail_exprmethods.This eliminates the duplication risk the issue body called out and is a low-risk refactor (
grepshows ~5 internal call sites).8. Hero_skills follow-up
After the hero_rpc PR merges:
--no-teststo the CLI flags table + library example inskills/hero/service/hero_service_scaffold.md.tests/to the workspace-root file tree.#[cfg(test)] mod testsin sourcecrates/<name>_server/src/<domain>/generated/tests.rstests/generated/<entity>_e2e.rsOpen question — upstream dependency on #117
The sample renders in §6 assume the post-#117 unified CRUD shape (
recipe_new(input) -> Sid,recipe_set(sid, input) -> (),RecipeInputdistinct fromRecipe). Against today's pre-#117 trait (recipe_new() -> Recipe,recipe_set(Recipe) -> Sid), the renders look meaningfully different.Three sequencing options:
--with-unified-crudflag until #117 merges. Lets #115 ship today but adds a temporary feature flag.I lean (a). #117 is a foundational reshape; redoing every render string twice is more churn than waiting. Happy to be overridden — if you want #115 to land first, I'll write today against the pre-#117 trait and queue the refactor as a #117-blocker follow-up.
Sign-off needed on:
e2e_emit.rsnaming to avoid collision with existinge2e.rs(which is the runnable example emitter, not tests). OK?serial_testdependency. OK?fixture.rsas part of this PR (since #113 hasn't shipped). OK?Will hold on coding until the green light.
Design revisions — three decisions confirmed by the user
Folding follow-ups into the #115 design. Three substantial changes from my first design comment:
1. Drop the existing
tests_emit.rsentirelyUser signed off on collapsing the test taxonomy from three layers to two. The existing
crates/generator/src/generate/tests_emit.rs— which emits OSIS-direct dispatch tests inside<server>/src/<domain>/generated/tests.rs— goes away as part of this PR. Its CRUD coverage is transitively covered by the new wire-level E2E suite; per-OSIS-layer correctness lives in inline#[cfg(test)]blocks where it belongs.Updated three-layer table from §8 of my first comment collapses to:
#[cfg(test)] mod testsin sourcetests/generated/<entity>_e2e.rsDeletion scope as part of this PR:
crates/generator/src/generate/tests_emit.rs— entire file.<server>/src/<domain>/generated/tests.rsoutputs become stale and stop being emitted; no migration needed since they're regenerated on every build (per #96).2. Emitter file naming — parallel
The
generate/subdirectory has drifted on naming. Cleaning up alongside this PR:tests_emit.rs(dispatch tests)examples.rs(workspace-levelbasic_crud.rs)examples_emit.rse2e.rs(per-domainclient_server.rsexample)examples_emit.rstests_emit.rs(new — workspace-root E2E tests, this issue)benches_emit.rs(new — #113)Final layout:
examples_emit.rs,tests_emit.rs,benches_emit.rs— three peers. Workspace-root output directories already line up:examples/,tests/,benches/.3. Spin-up uses
<service>_server::run(ServerConfig::for_test())from #117This is the biggest change. My §4 in the first comment described a custom
spin_up_service()helper that duplicated the foreground bootstrap frommain.rs. User correctly pushed back on this — custom spin-up paths drift from production and don't reflect the canonical lifecycle.#117 now covers (per this comment) promoting the scaffolded server crate from
bin-only tolib + binand extracting the foreground bootstrap into<service>_server::run(ServerConfig). The samerun()is used bymain.rs(prod), this issue'stests/src/lib.rs, #113'sbenches/runner.rs, and the runnableexamples/rust/<domain>/client_server.rs.Updated
tests/src/lib.rs:spin_up_service()is now a 5-line wrapper around #117'srun(). No duplicatedServerBuilderblock, no duplicated socket-path math, no duplicated header-lifting config. If the production bootstrap changes shape (lifted headers, service-info fields, anything), the test bootstrap inherits the change automatically.4. Sequencing — confirmed
Sequence (a) from the first comment is locked in: #115 blocks on #117 (which now bundles types + seeding + bootstrap unification). I won't cut e2e_emit.rs code until #117 lands.
Updated open questions
The four sign-off points from my first comment are answered:
tests_emit.rsnaming: yes (reusing the freed name after deleting the old one).TempDir+ per-test atomic-counter socket viaServerConfig::for_test).fixture.rsas part of this PR: still applies.No new open questions. Will resume coding once #117's expanded scope lands (worktree
/tmp/hero_rpc_115already prepared off latest dev87289a1; will rebase on top of #117's PR when it merges).Correction —
<service>_serverstays bin-onlyQuick correction on the spin-up shape from my revisions comment. The "tests call
hero_recipes_server::run(ServerConfig::for_test())" line is wrong because it implied promoting the server crate tolib + bin— which it must not be (see #117 correction for the full reasoning).Corrected: the canonical bootstrap lives in
hero_rpc_osis::rpc::server::{run, run_for_test}. Tests call:Five lines of bootstrap glue. No duplicated
ServerBuilderblock, no custom socket-path math, and crucially — the productionmain.rsand the test path go through the samehero_rpc_osis::rpc::servermodule, so any drift in the OSIS-over-rpc2 boot sequence is caught by tests automatically.hero_recipes_serverstaysbin-only. Its Cargo.toml gains no[lib]section. The scaffoldedtests/Cargo.toml(#115) does pullhero_rpc_osisdirectly (already a transitive dep, just made direct), with thetest-supportfeature enabled.Everything else in my revisions comment stands: drop the old
tests_emit.rs, rename emitters toexamples_emit.rs/tests_emit.rs/benches_emit.rs, parallel tests by default, sequence (a). Worktree at/tmp/hero_rpc_115still ready.