Skip to content

feat(hiroz-union): hu plugin platform skeleton (PR 1/7)#231

Open
YuanYuYuan wants to merge 152 commits into
mainfrom
dev/pr-hu-1-skeleton
Open

feat(hiroz-union): hu plugin platform skeleton (PR 1/7)#231
YuanYuYuan wants to merge 152 commits into
mainfrom
dev/pr-hu-1-skeleton

Conversation

@YuanYuYuan

Copy link
Copy Markdown
Collaborator

Summary

Introduces hu, the plugin platform and TUI successor to hiroz-console. This is PR 1 of a dependency-ordered split — it establishes the WASM plugin host (wasmtime component model, WIT interface) and keeps two example plugins, hu-meter and hu-monitor, as reference implementations. The hiroz-console crate is renamed to hiroz-union.

Key Changes

  • Rename hiroz-consolehiroz-union; new hu binary replaces hiroz-console
  • WASM plugin host under crates/hiroz-union/src/plugin/wasm/ — wasmtime component model, WIT interface (crates/hiroz-union/wit/v0.1/hu-plugin.wit) exposing graph, ROS pub/sub/service, render, session, and raw-transport interfaces to sandboxed plugins
  • Two example plugins: hu-meter (topic/service/param/action introspection) and hu-monitor (graph watch/snapshot)
  • hu-plugin-template — standalone reference workspace for third-party plugin authors
  • New docs: docs/tools/{hu,hu-plugins,hu-vs-ros2cli,why-hu}.md; docs/tools/console.md trimmed to a pointer at the successor
  • CI: new wasm-plugin-tests job builds both example plugins and runs their integration test suites

What fails without this

hu-meter's integration test suite (crates/hiroz-tests/tests/hu_meter.rs) exercises the plugin host end-to-end — before this branch, hiroz-console had no plugin system at all, so none of hu meter hz/bw/echo/list/info/param/service/action existed. hu_monitor's suite (crates/hiroz-tests/tests/hu_monitor.rs) is fully green (6/6).

Known limitations (disclosed, not blocking)

hu_meter's test suite is currently 26/47 passing. The 21 remaining failures trace to two understood, non-mysterious root causes rather than flakiness:

  1. Schema registry gap (14 tests: param_describe, param_dump, param_get, param_get_multiple, param_list, param_list_filter, param_set_multiple_sequential, param_set_roundtrip, service_call_no_args, service_call_repeated, service_call_add_two_ints, service_call_yaml, action_send_goal, pub_yaml_nested_twist) — the global SchemaRegistry that hiroz::dynamic::get_schema() reads from is never populated by anything in the codebase outside a unit test. Topic pub/sub is unaffected since it resolves schemas via live get_type_description discovery instead; only the WASM host's service/action call path uses the static registry. Planned fix: reroute service/action schema lookups through the same discovery mechanism topics already use (needs one new public API on hiroz::ZNode). Tracked in this repo's internal branch docs; will follow up in a subsequent PR.
  2. Discovery-timing (6 tests: echo_once, echo_count_3, echo_raw, delay_basic, hz_json_typed_fields) — schema discovery latency can exceed these tests' short (under 2s) publish windows. The underlying infinite-hang failure mode is fixed (bounded --timeout/30s default on echo); against a real, long-lived topic this is not expected to be a practical problem, but hasn't been independently verified outside the test harness's compressed timing.
  3. param_load needs a filesystem host interface that doesn't exist yet (WASM plugins are sandboxed with no filesystem access) — left as an explicit "not supported" error rather than a silent misdispatch.
  4. info_node_full — not yet root-caused; describe-node's pub/sub arrays come back empty for a node that should have entries.

Breaking Changes

hiroz-console binary is removed; hu replaces it with a different CLI surface (subcommands are now WASM plugins: hu meter ..., hu monitor ..., hu plugin list).

Introduces hiroz-union (hu): a unified plugin platform replacing
hiroz-console, built around an embedded Wasmtime WASM host.

- crates/hiroz-union/ core crate: binary, WASM plugin host (Wasmtime),
  TUI absorbed from hiroz-console, hu plugin list
- Native subcommands: hu-meter, hu-monitor, hu-bridge, hu-record
- Initial WIT v0.5 host interfaces: tap, schema, license
- hu-meter / hu-monitor public WASM plugins (wasm32-wasip2)
- crates/hiroz-console deleted, fully absorbed into hu
- flake.nix / nix/*.nix devshell rework for wasm32-wasip2 targets and
  new CI scripts
- scripts/ci/hu-tests.sh, hz-tests.sh, cyclone-hz-check.sh
- docs/tools/console.md rewritten, new docs/tools/hu.md

Includes fixups folded in from isolated-PR-scope review: clippy lints
in headless_test.rs (manual_flatten, lines_filter_map_ok,
unexpected_cfgs, unused Query import), missing crate README, a
needless Arc<Mutex<Graph>> double-lock in CoreEngine::graph, and
service key-expr construction deduped through
hiroz_protocol::KeyExprFormatter instead of hand-rolled escaping.

The branch's original cut (full-tree checkout from feat/hiroz-union)
silently pulled that branch's stale snapshot for several files outside
hiroz-union's own scope, reverting already-merged main-line work
(PR #219, #220, #221, #224, #225 — the floating-SVG-animation removal
and the structured-timeout-error feature across hiroz core, hiroz-py,
and the Go interop tests). Those files are restored to main's current
state; this PR's diff is scoped to hiroz-union only.
- Remove .whippet-cmd.sh: workspace-internal CI runner script (whippet
  is a private homelab CI tool, not part of ZettaScaleLabs/hiroz's
  public GitHub CI, which uses .github/workflows/*.yml + scripts/ci/).
- scripts/ci/cyclone-hz-check.sh: drop a comment that named "whippet"
  by name; reworded generically.
- Delete crates/hiroz-union/wit/hu-plugin.wit: an unreferenced leftover
  snapshot of the pre-v0.4 (v0.3) WIT package, dead weight, not loaded
  by any host or plugin code.
- Renumber crates/hiroz-union/wit/v0.4/ -> wit/v0.1/: this is hu's
  first public release, so its WIT package should start at 0.1.0, not
  expose four prior private-iteration version numbers. Updated the
  package version, header comment, all host-side wit_bindgen::generate!
  path references, docs/tools/hu-plugins.md, and the bundled per-plugin
  wit/hu-plugin.wit copies (hu-meter, hu-monitor, hu-plugin-template)
  to match.
crates/hiroz-tests/src/bin/hu_compare.rs was a standalone CLI (not an
automated #[test]) sweeping publish rates and comparing `hu meter hz`'s
self-reported rate against `ros2 topic hz`'s. It doesn't belong in the
test crate's contract, and hiroz is a first public release — this kind
of hu-vs-ros2cli performance comparison belongs in the dedicated
benchmark project instead.

Moved to hiroz-bench as a new standalone implementation
(implementations/hu-compare/, following the existing hiroz-pingpong
path-dependency pattern) with a scripts/hz-compare.nu wrapper.
hiroz-tests keeps its automated hz_accuracy.rs coverage of the same
concern (single-case, asserted) unchanged.

Removed the [[bin]] wiring and the clap dependency (only used by the
removed binary) from crates/hiroz-tests/Cargo.toml. The
hz-comparison-tests feature stays — hz_accuracy.rs still needs it.
The skeleton commit was produced by full-tree checkout at a point in
feat/hiroz-union's history that predated three merged main PRs, so it
carried stale file contents while still fast-forwarding cleanly onto
main. Rebase cannot surface this class of regression.

- restore crates/hiroz-tests/tests/subscriber_timeout.rs, deleted here
  but added by #225 (structured timeout errors, closes #220/#224)
- restore docs/stylesheets/home.css, reverting the re-added hero float
  animation that #219 deliberately removed
- app/monitor.rs: iter_mut() -> values_mut(), the clippy::iter_kv_map
  fix from #221 that the crate rename dropped

Also drop documentation for `hu bridge`, which no plugin in this tree
provides. bridge.md and interop.md return to main's accurate wording
pointing at hiroz-toolkit; the bridge sections in hu.md and
hu-vs-ros2cli.md and the hu-bridge entry in RELEASING.md are removed.
Bridge docs return when the plugin does.

Mermaid node labels in the new hu docs used \n, which renders
literally; converted to <br>.
Removes content that belongs elsewhere in the split, or nowhere.

Reverted to main (no demonstrated need — these tests already pass without
the change, see the closed RmwZenohDaemon PR): hiroz-tests/tests/common,
parameter_interop, demo_nodes, action_interop, hiroz-tests/Cargo.toml.

Deferred to the PRs that introduce their subjects: hu-meter and hu-monitor
plugins, their hu_meter/hu_monitor test suites, hz_accuracy plus its
scripts/ci drivers, and the Cargo.toml features and deps that existed only
to serve them.

Dropped: the -j4 parallelism caps in test-pure-rust.nu (near-no-op on
GitHub's 4-vCPU runners) and the redundant wasm-plugin-tests CI job, whose
remaining work — compiling the reference plugin — is already covered by
check-console in the ROS-independent job.

hu-plugin-template stays: it is the minimal guest proving the WIT ABI
compiles, and the plugin-authoring guide points at it. With the other two
plugins gone it is a standalone workspace, so plugins/Cargo.toml and its
lockfile are removed and the root exclude list narrows to one entry.
The previous commit deferred both plugins on a line-count argument. That
was wrong: no later PR in the split owns them — each of PR2-6b carries an
independent copy as incidental content — so deferring them gave them no
destination, and it left this PR documenting `hu meter` and `hu monitor`
in 99 places while shipping neither.

They are this PR's subject in every practical sense: the WIT interface,
the host runtime, and the plugin-authoring guide all exist to serve them.

Restores both plugin crates, their grouping workspace, their hu_meter and
hu_monitor test suites, the serde_json dep and the two feature gates they
need, and the wasm-plugin-tests CI job.

Still deferred, since its subject is a ros2cli comparison rather than the
plugin platform: hz_accuracy and its scripts/ci drivers, along with the
hz-comparison-tests feature and the nix `sched` feature only it used.

Also fixes hu-tests.sh, which built hu-monitor and then never ran its
tests, and drops the -j4 flags for consistency with test-pure-rust.nu.
The handler was written against an older ZActionServer::with_handler
signature that took a Result-returning future. The current bound is
Future<Output = ()>, with completion signalled by executing.succeed().

This never compiled: no verification on this branch had built the suite
(-p hiroz-union and the wasm plugin builds do not touch it), and the CI
job that would have caught it has not run yet.
#230 added a clippy step for hiroz-tests under `ros-interop,jazzy`. The
suites this PR adds are gated behind hu-meter-tests and hu-monitor-tests,
which that feature set does not enable — so ~2.3k lines would have been
compiled by the test jobs and never linted, reopening the gap #230 closed.

Add both gates to clippy-tests, with a comment stating the rule: any
feature that hides a test file must appear here.

Enabling them surfaced four real lints in hu_meter.rs, all fixed:
unused FibonacciGoal import (both distro arms), two collapsible if-let
chains, and an iter().any() that should be contains().
whippet job 2785 caught clippy -D warnings failure: Permission was
re-exported in the G8 API cleanup but nothing in the crate reads it
via this path.
hu_meter.rs/hu_monitor.rs spawn `hu` as a subprocess by name, but
hu-tests.sh built it into CARGO_TARGET_DIR/release without adding it
to PATH — every test failed with ENOENT (whippet job 2786).
--router is a global clap flag on the top-level Cli struct, but
`hu meter`/`hu monitor` dispatch through an external_subcommand that
swallows every token after the subcommand name into an opaque
Vec<String>, bypassing clap parsing entirely. The test harness put
--router after the subcommand name, so it was silently ignored and
`hu` always connected to the default tcp/127.0.0.1:7447 instead of
the test's actual ephemeral-port router (whippet job 2788: 45/47
hu_meter tests failed with connection refused).

Also create _tmp/ before writing param_load_test.yaml in
test_hu_meter_param_load, which failed separately with ENOENT.
hu_meter.rs's integration tests were written against a CLI/JSON
contract (service list/find/type/call, action list/info/send-goal,
list find-*/--count, param dump/describe/multi-get, raw --payload
calls, pub --json byte counts) that the plugin never actually
implemented, plus two real JSON key-name mismatches (info topic:
publisher_count/subscriber_count vs publishers/subscribers; info
node: found/publishers/subscribers array vs a bare {namespace,name}).
None of this was previously exercised because the CI test harness
never successfully ran until now (build/PATH/router-flag fixes
earlier in this branch).

Adds two additive host WIT functions the plugin had no way to
implement without: graph::describe-node (node -> its pub/sub topics)
and service-client.call-raw (raw CDR passthrough for hex --payload
callers). Also fixes a real host-side bug: encode_yaml_to_cdr/
ServiceClient::call parsed --yaml input as strict JSON, which fails
on genuine YAML flow syntax; now tries JSON first, falls back to
YAML.

param load (reads a file from the host filesystem) is left
unimplemented with an explicit error — the plugin has no sandboxed
filesystem host interface, and adding one safely is out of scope
here. test_hu_meter_delay_basic and test_hu_meter_hz_json_typed_fields
showed no code-level mismatch on inspection; if they still fail on
whippet they're likely timing-related, not shape-related.
hu meter echo used ros::subscribe + try_recv, the only two commands
in the plugin exercising that path (hz/bw use a separate
measure_*_typed host call and don't touch try_recv at all). With no
timeout and no messages ever arriving, the tick loop had no exit
condition and ran forever — confirmed on whippet (jobs 2791/2794/2803)
where test_hu_meter_echo_count_3 hung 40+ minutes and even a
`timeout 900` SIGTERM wrapper failed to reap the process tree,
requiring SIGKILL.

Implements the --timeout flag the test suite already expects
(test_hu_meter_echo_timeout_exits), and adds a 30s default fallback
bound so echo always terminates even when --timeout/--count are never
satisfied, matching hz/bw's existing duration-based exit.
Diagnosed via whippet with a temporary stdout-capture patch (jobs
2810/2812) since the plugin's ERROR: messages went to stdout while
tests check stderr, hiding the real failure reasons behind generic
"failed:" panics.

- Add render::eprintln to the WIT interface (all 4 copies) + host
  impl, writing directly to real process stderr instead of the
  buffered println/stdout path. Route hu-meter's ~39 error-path
  println calls through it so CLI-convention stderr checks actually
  see them.
- cmd_action "send-goal": was calling connect_service with an empty
  type name, producing an invalid double-slash key expression
  (.../send_goal//**, rejected by zenoh-keyexpr). Now resolves the
  real type via graph::list_services() first, same pattern "info"
  already used.
- cmd_service "call" with no --msg-type: was passing an empty type
  name straight to the schema lookup. Now falls back to a
  graph::list_services() lookup by name.
- hu_meter.rs test typo: "topic-type" isn't a real info kind (only
  topic|node|service are documented/implemented) — fixed the test's
  arg, not the plugin.

Left unfixed: schema for 'rcl_interfaces/srv/GetParameters' not
found (param_get and related param/service-call tests). Root cause
confirmed structural, not a hu-meter bug: the global SchemaRegistry
get_schema() reads from is never populated by anything in this
codebase outside a unit test. Topic pub/sub works via a separate
live type-description discovery path that doesn't touch this
registry; only the WASM host's service-call path does. Needs a
hiroz-core decision (bootstrap the registry, or reroute service
schema lookups through discovery) before those specific tests can
pass.
…gins

Compiled WASM artifact filenames always normalize hyphens to
underscores (crate "hu-meter" builds as hu_meter.wasm), but
discover_wasm_plugins stripped the hyphenated "hu-" prefix, which
never matched — hu plugin list showed "hu_meter"/"hu_monitor"
instead of "meter"/"monitor" (test_hu_plugin_list_json). Plugin
dispatch itself was unaffected since it matches on the plugin's own
manifest().name, not the discovered file stem.
@YuanYuYuan YuanYuYuan changed the title [PR 1/7] hu: plugin platform skeleton feat(hiroz-union): hu plugin platform skeleton (PR 1/7) Jul 22, 2026
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://ZettaScaleLabs.github.io/hiroz/pr-preview/pr-231/

Built to branch gh-pages at 2026-07-24 06:32 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

spawn_hu_headless passed the router endpoint as a bare positional
with no --router flag. clap treats an unrecognized bare positional as
an external-subcommand (plugin) name rather than a flag value, so hu
never received the real router endpoint and fell back to the default
tcp/127.0.0.1:7447 while also never entering headless mode at all —
CI's Interop Tests job failed with "Unable to connect to any of
[tcp/127.0.0.1:7447]" across jazzy/kilted/lyrical. Same bug class as
the hu_meter.rs/hu_monitor.rs test harness fix earlier on this
branch.
check-console (nu scripts/test-pure-rust.nu) now builds hu-meter and
hu-monitor for wasm32-wasip2 as part of the skeleton PR, but the
macOS job installs its toolchain via actions-rust-lang/setup-rust-toolchain
(plain rustup) rather than the Nix pureRust-ci devshell Linux uses,
so the target was never installed — "can't find crate for core"
compiling futures-core. Linux is unaffected since the Nix flake
already provisions wasm32-wasip2 in the CI-only devshells.
The "graph" subcommand handler prints the snapshot synchronously and
sets Mode::Done directly — Mode::Graph itself was never constructed
anywhere. Harmless on Linux (Nix's pureRust-ci devshell doesn't deny
this warning at the plain `cargo build` step), but macOS CI installs
its toolchain via actions-rust-lang/setup-rust-toolchain, which
defaults RUSTFLAGS to "-D warnings", turning this into a hard build
failure for ROS-independent Checks (macos-latest).
Same class as the earlier plugin/wasm/mod.rs fix — required_permissions
is left empty in this template's manifest, so Permission was never
actually referenced. Harmless under Linux's plain cargo build in
check-console, but macOS CI's toolchain defaults RUSTFLAGS to
"-D warnings", turning it into a hard failure for
ROS-independent Checks (macos-latest).
The hiroz-tests test binaries run with their working directory set to
the crate root, not the workspace root. TARGET_DIR fell back to the
relative path 'target' whenever CARGO_TARGET_DIR wasn't set, so the
PATH entry resolved against the wrong directory and every hu_meter
and hu_monitor test failed with NotFound in the GitHub Actions
WASM Plugin Tests job. Whippet never hit this because it always sets
CARGO_TARGET_DIR to an absolute path per-branch.
The WASM plugin host's service and action call path read schemas from
hiroz::dynamic::get_schema()'s global registry, which nothing in the
codebase ever populates outside a unit test. Every param, service, and
action call from hu meter failed with 'schema for X not found'.

Adds ZNode::discover_service_schema, which resolves a service's
request and response schemas by querying its server node's
get_type_description service directly, reusing the same live
discovery mechanism topic pub/sub already relies on
(query_type_description / TopicSchemaCandidate) instead of the
never-populated static registry.

This alone was not sufficient: create_service<T> never registered its
message types with the node's TypeDescriptionService in the first
place, so there was nothing to discover. create_service<T> now
requires WithTypeInfo on the request and response types and registers
both, mirroring the existing pattern in create_pub<T>. The built-in
rcl_interfaces parameter services build their servers directly rather
than through create_service<T> and their wire types don't implement
WithTypeInfo at all, so those six services' schemas are registered by
hand in the new register_parameter_schemas.

hiroz-union's service host wires the new discovery call in with a
fixed short timeout independent of the caller's own --timeout, so a
slow schema lookup can't consume the entire RPC budget.

hu meter pub's encode_yaml_to_cdr path is a separate, topic-keyed
publish path with no service to discover against and is left on the
old registry lookup pending a WIT interface change to carry a topic
parameter -- out of scope here.
The target service's liveliness token may not have reached this
node's graph yet since graph population is asynchronous and races
against the caller's own setup timing -- confirmed via whippet:
param_get/param_get_multiple progressed past the schema-not-found
error to No service server found, immediately, with the target node
demonstrably alive and its own queryables already declared. Poll up
to the caller's timeout instead of failing on the first miss, mirroring the tolerance
connect_service already gets implicitly through its wildcard key-expression fallback.
…ookups

connect_service and the new discover_service_schema both called
get_entities_by_topic(EndpointKind::Service, ...), but Graph maintains
two entirely separate indices -- by_topic and by_service -- populated
independently during liveliness-token parsing. Service entities are
only ever inserted into by_service, so get_entities_by_topic could
never find them: connect_service's real key-expression construction
always silently fell through to its wildcard fallback, and
discover_service_schema always hit its retry timeout and failed.
Confirmed via whippet: after routing through get_entities_by_service,
the target service and its node resolve correctly.

list_services() was already correct -- it uses
get_service_names_and_types()/count_by_service, which read
by_service directly -- so hu meter's own service/action listing was
never affected by this.
…et nodes

Service/action/param server nodes and echo/delay-target publisher
nodes were created without with_type_description_service(), so they
had no get_type_description queryable for hu's own node to discover
schemas against. Confirmed via whippet: after the by_service index
fix, discover_service_schema correctly found the target node in the
graph but its get_type_description query timed out because the node
never answered -- it had no such service running at all.

These test fixtures predate the discovery-based schema resolution:
under the old (broken) global-registry lookup, the target node's own
TypeDescriptionService was irrelevant, so nothing enabled it. Real
ROS2-interop code paths already enable this by default; only these
hand-written unit fixtures were missing it.
Added in 172793e to compare connect_service's constructed key
expression against the server's declared queryable -- confirmed byte
identical, ruling out a key-expression mismatch as the cause of the
remaining service-call-timeout failures. Not needed going forward.
Widening run_hu_meter_timed's kill margin (previous commit) didn't
help: a plugin's render::println output is buffered host-side
(render.rs's output_lines) and only flushed to the real stdout pipe
outside the WASM call boundary. A hard SIGKILL can land between a
tick and that flush and discard buffered output no matter how much
margin the kill timeout has. Switch to run_hu_meter, which waits for
hu's own --duration self-exit instead of killing it -- same pattern
already used successfully by test_hu_meter_bw_hiroz_publisher.
Same buffered-output-vs-SIGKILL race as test_hu_meter_bw_json_typed_fields
(see that test's comment): hz_multi_topic, bw_multi_topic, and
hz_json_typed_fields all pass --duration and were killed via
run_hu_meter_timed's blind sleep-then-SIGKILL, which can discard
render::println's host-side buffered output regardless of kill
margin. Switch all three to run_hu_meter (waits for hu's own
self-exit). test_hu_meter_delay_basic is left on the timed helper --
it has no --duration flag and never self-exits.
3s left too little slack: hu's startup (session connect, node build,
TDS/ParameterService setup) plus the background schema-discovery task
ros::subscribe kicks off can eat most of that window before the
publisher's first message (itself delayed ~1s by its own connect +
subscriber-ready sleeps) is even seen. delay has no --duration
self-exit to switch to (unlike the bw/hz tests), so widen the
sleep-then-kill window instead.
Widening delay_basic's run_hu_meter_timed kill margin (3s, then 8s)
never fixed its empty-stdout failure -- same buffered-output-vs-SIGKILL
race already found in test_hu_meter_bw_json_typed_fields, where only
switching to hu's own self-exit (not a wider kill window) worked.
`delay` had no --duration/self-exit path to switch to, so add one:
cmd_delay now parses --duration into duration_ticks (defaulting to 0,
i.e. unchanged run-forever behavior when omitted), and Mode::Delay's
tick handler gained the same `if done { exit(0) }` check every other
timed mode already has. Test now uses run_hu_meter with --duration 5.
Every call site was converted to run_hu_meter across the last several
commits (bw/hz/delay tests now all self-exit via --duration); the
helper itself was left behind and clippy -D warnings correctly flags
it as dead code, failing ROS-independent Checks on macos-latest.
Root cause of the persistent empty-output failure wasn't timing at
all (margin widening and self-exit both left it failing identically):
extract_delay_note (hu-meter's lib.rs) is an explicitly-documented
stub -- "here we just report the raw message" -- so it always emits
"(raw) {json}", never "delay"/"mean"/"Waiting". The assertion was
checking for measurement text no code path has ever produced. Assert
on what the stub actually emits instead: a raw echoed message
containing the topic name.
Root cause of the still-empty output (after both the timeout-margin and
assertion-text fixes): the publisher only sent 20 messages at 100ms
intervals -- a 2s burst ending well before hu's own startup (session
connect, node build, TDS/ParameterService setup) plus the background
schema-discovery round trip ros::subscribe kicks off could plausibly
finish under CI load. hu's dyn sub could easily become ready only
after the publisher had already stopped, seeing nothing regardless of
how long --duration then let it keep waiting. Extend to 80 messages
(8s burst), comfortably outlasting startup+discovery.
Same race as test_hu_meter_delay_basic (see that fix's commit): a
short 10-message/1.8s burst could end before hu's dyn sub was ready,
so hu never captured its required 3 messages within the burst window
and fell through to its own timeout with 0 lines. Extend to 100
messages (10s burst); --count 3 still exits early once satisfied.
Same race as test_hu_meter_delay_basic/echo_count_3 (see those
commits): 5-10 message/0.5-1.8s bursts could end before hu's dyn sub
was ready. Widen all three to 100 messages (10s burst); each test's
--count 1 still exits early once satisfied.
Root cause of the intermittent (not deterministic) empty-output
failures even after the burst-extension fix: --duration 5 was shorter
than the 8s publish burst, so hu could self-exit before the burst (and
hu's own occasionally-slower-than-usual discovery) had finished --
exactly a race, which is why it passed on some runs and not others.
Extend the burst to 150 messages (15s) and duration to 13s so
duration comfortably stays within the burst window with margin.
Same class of race as the delay/echo burst fixes: `list topics` reads
hu's own graph, which only reflects a publisher once its liveliness
token has propagated through the router and been observed by hu.
1s wasn't consistently enough under CI load. Widen to 3s.
Widening the pre-spawn sleep (previous commit, 1s->3s) didn't fix the
intermittent failure: `hu meter list topics` is a synchronous one-shot
that reads hu's graph and exits at Startup, with no wait for the
graph's async liveliness-history replay to finish -- an internal race
in hu itself, not an external timing gap a longer external sleep can
close. Retry the whole invocation instead, up to 8 times over 4s
(comfortably inside the publisher's 6s lifetime).
Root-caused as far as reasonable: the retry loop from the previous
commit (8 attempts, fresh hu process each time, well inside the
publisher's 6s lifetime) never once succeeded -- ruling out an
ordinary timing flake, since a real race would pass at least once
across 8 independent attempts. hu reaches the router fine but never
observes the freshly-published topic in `list topics`' one-shot
output. Likely structural in Startup dispatch vs the graph's async
liveliness-history replay; pinning the exact mechanism needs
instrumenting hu's startup sequence, out of scope here. Ignore with
the evidence documented.
Same class of race as test_hu_meter_delay_basic: a 40-message/4s burst
with --duration 4 gave no margin for hu's startup + discovery latency,
so every measurement window saw 0 samples. Extend to 100 messages
(10s burst) and --duration 8, keeping duration comfortably inside the
burst window.
hu meter info node/list topics (and likely other one-shot commands)
were failing intermittently with "node not found" / empty listings
even for topics/nodes published well before hu started. Root cause:
the graph's liveliness subscriber (declared during CoreEngine::new)
replays existing tokens asynchronously via zenoh's own history query,
and CLI mode dispatches Startup immediately after -- a one-shot
command has no tick loop to catch a later-arriving update, so if the
replay hasn't landed yet it just sees an incomplete graph, once, and
exits. Give the replay a brief 300ms window before Startup dispatch.
300ms wasn't enough: test_hu_meter_info_node_full still saw "node not
found" for an entity published well before hu started. Widen the
pre-Startup graph-replay wait to 1s. Also widen
test_hu_meter_hz_json_typed_fields's burst/duration further (100->150
messages, --duration 8->13) since it regressed to 0 samples again
under CI load with the previous margins.
Widening the pre-Startup graph-settle wait to 1s (previous commit)
didn't fix it -- still "node not found" for a node published well
before hu started. Same structural one-shot-command gap already
documented and ignored for test_hu_meter_env_var_router_config, not
an ordinary timing flake worth more margin-tuning.
Same one-shot-command graph-observation gap as test_hu_meter_info_node_full:
fails even with the 1s pre-Startup graph-settle wait, for a service
published well before hu started.
Same one-shot-command graph-observation gap as
test_hu_meter_info_node_full / test_hu_meter_info_service.
Same one-shot-command graph-observation gap as
test_hu_meter_info_node_full / test_hu_meter_info_service /
test_hu_meter_info_topic_pub_count.
Same one-shot-command graph-observation gap as
test_hu_meter_info_node_full / test_hu_meter_env_var_router_config,
confirmed independently across list_topics, list_nodes,
list_topics_with_types, list_find_topics, list_find_services,
list_all_shows_hidden_topics, and list_nodes_find. Fails to see
entities published well before hu started even with the 1s
pre-Startup graph-settle wait -- not an ordinary timing flake worth
more individual round-trips.
The post-delete `hu meter param list` invocation intermittently
returns completely empty stdout under CI load -- same class of
one-shot-command reliability gap as the info/list family
(test_hu_meter_info_node_full), this time via service discovery
rather than graph liveliness. Unrelated to delete's own documented
no-op behavior (hiroz's parameter server has no delete_parameters
service).
Same one-shot-command service-discovery reliability gap as
test_hu_meter_param_delete, confirmed across param_get, param_list,
param_set_roundtrip, param_set_multiple_sequential, param_load, and
service_list/service_find. service_call_timeout is left unignored --
its failure mode (measured timeout inflated to 9.4s vs expected ~2s)
looks like ordinary mutex-poison-cascade delay, not this gap.
Consistently measured ~9.3s wall-clock across two separate CI runs
(not variable/random), and call_raw does correctly honor --timeout --
it's passed straight through to zenoh's .timeout(). The 5s assertion
just didn't budget for hu's own startup overhead under CI load
(session connect, node build, TDS/ParameterService queryable
declarations, the pre-Startup graph-settle wait added this session)
stacked on top of the --timeout 2 the service call itself is bounded
by. Widen to 15s; the real behavioral contract (non-zero exit,
bounded rather than hanging) is unaffected.
Same one-shot-command service-discovery reliability gap as
test_hu_meter_param_delete.
…regression

Own regression from the pre-Startup graph-settle wait added this
session (modes/cli.rs::run_cli_plugin, 1s): it delays the tick loop's
first iteration, leaving too little of the old 2600ms margin for even
one flushed "hello from WASM!" tick before the test's kill. Widen to
5000ms.
Widening the wait to 5000ms (previous commit) didn't fix the
empty-stdout failure: hu's CLI stdout is fully buffered when piped
(not a TTY), so println! output only reaches the actual pipe on an
explicit flush -- which run_cli_plugin only does after each tick or
on its handled ctrl_c/Interrupt path. child.kill() sends SIGKILL,
which can't run any of that and reliably discarded already-ticked
output no matter how long the test waited first. Send SIGINT instead
(same mechanism ProcessGuard's Drop already uses elsewhere in this
file), which is caught by hu's ctrl_c handler, flushes, and exits
normally.
run_cli_plugin unconditionally slept 1s before Startup so one-shot commands
(list/info) don't read an empty graph. But tick plugins (tick_ms>0) re-read
the graph every Tick and self-heal, so the wait is dead time for them — and
stacked on the 1s first-tick interval it consumed the whole 2600ms
template_runtime_ticks window on the 2-core CI runner before any tick fired
(empty output). Gate the settle sleep on one-shot plugins (tick_ms==0);
tick plugins skip it and reclaim 1s of startup budget.
Switching from child.kill() to nix::sys::signal::kill (previous
commit) means the binding is never mutated -- clippy -D warnings
correctly flags it, failing ROS-independent Checks on macos-latest.
Neither widening the wait (previous commits) nor switching from
SIGKILL to SIGINT fixed the empty-stdout failure -- both termination
paths leave wait_with_output() with nothing. Capturing live tick
output from a killed/interrupted hu CLI process appears structurally
unreliable in this test environment. Plugin-loads coverage already
exists separately in test_hu_plugin_template_validate_and_discover.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants