testing
The cross-cutting testing umbrella for robium. Robotics apps fail in layers a
generic test suite misses — a node that never receives a message, a launch
file that starts everything except the one node that mattered, a policy that
looks fine on paper but never reaches the goal in sim. This skill frames the
robotics-specific test pyramid and states the non-negotiable bar: a robium
build is not done until its smoke test passes. It does not re-teach
launch_testing/pytest mechanics for ROS 2 (that's ros2) or the
lerobot-eval CLI (that's lerobot) — it frames both as test-pyramid layers
and routes to them.
When to use this skill
- Setting up tests for any new robium project — this should be planned into
the build from the start, not bolted on after the fact.
- The trigger phrases in the description: 'test the robot app', 'how do I test
this node', 'smoke test', 'regression test in sim'.
- Before claiming any robotics app or sample works — a claim of "done" without
a passing smoke test is not verified, it's a guess.
- Load alongside whatever skill is building the thing under test —
ros2/
nav2/gazebo for the navigation vertical, lerobot for the manipulation
vertical — this skill supplies the test framing, not a replacement for
either.
- Cross-references — go to the sibling skill instead when the question is:
- ROS 2
launch_testing/pytest mechanics themselves (fixtures, process
actions, assertions) → the upstream launch_testing README and ROS 2
testing docs (linked in References). The ros2 skill does not yet
carry launch_testing content — until it does, go upstream directly
rather than hunting for it there. This skill frames launch/node testing
as a pyramid layer either way.
- The
lerobot-eval CLI and its flags → lerobot. This skill frames policy
eval as a test-pyramid layer with a pass/fail bar; lerobot owns the eval
mechanics.
- Setting up the simulator a regression test runs against →
gazebo or
isaac-sim (or simulation if the choice isn't made yet).
- General (non-robotics) testing practices — unrelated to this skill; use
whatever generic testing guidance already applies to the language/
framework in question.
- The whole-stack decision this feeds into →
architect (plans testing into
the brief, doesn't bolt it on later).
Key directives
- Delegation posture: route + embed the pyramid framing. The test-pyramid
structure, what belongs at each layer, and the trial-run bar live here;
launch_testing/pytest syntax and lerobot-eval flags live downstream.
Never re-teach either mechanism's syntax in this skill.
- A sample app is not done until its smoke test passes. This is the
concrete acceptance bar for any robium trial run: "it builds" or "it starts
without crashing" is not sufficient — a launch/node-level smoke test
actually passing is the minimum evidence of "done." Treat a missing or
failing smoke test as an open task, not a detail to fix later.
- Test at the right layer — don't push everything into sim. Logic that
doesn't depend on ROS or a running robot belongs in a plain unit test, not a
slow sim scenario; reserve sim-based regression tests for behavior that
genuinely requires the simulated environment. See the pyramid in Decision
guidance.
- Determinism is what makes a test a test. A sim scenario or policy eval
that isn't seeded/repeatable produces flaky results that get blamed on the
code instead of the test — apply
simulation's determinism guidance (seeded
physics, use_sim_time) and lerobot's small-scale-before-long-run
discipline (a short, deterministic eval run as a CI gate, not a long
training-scale rollout) to every automated test.
- Both verticals get equal treatment. The ROS/navigation vertical tests
with
launch_testing + pytest; the manipulation vertical tests with
deterministic small-scale policy eval runs. Neither is "the real test
framework" and the other an afterthought — plan both when a project spans
both verticals.
- Assert against config constants, not re-typed literals. A regression
test that hardcodes a rendered value (e.g. asserting the literal string
--steps=2000) can keep "passing" while the fact it checks goes stale — a
config constant dropped to 100 and the test never noticed. When a test
asserts a value derived from a config constant, import and assert against
the constant itself, not a re-typed copy (vla-trial).
- A measurement harness needs a guard that it didn't silently measure
nothing. A benchmark CLI that catches and prints per-device exceptions can
still exit 0 having measured zero devices — this happened on a transient HF
401 with no real code bug. Add a regression test for the harness itself, or a
return 1 if measured == 0 inside it, so "ran clean" and "measured nothing"
can't look the same (vla-trial).
- Gate every paid remote run behind a free local dry-run. Before submitting
any paid remote job (a cloud GPU training run, a paid inference batch), run
the pipeline-smoke layer end-to-end locally first — same pipeline, tiny
scale, CPU is fine. It catches plumbing bugs (a camera-feature mismatch, a
bad local-output-dir path) before they cost a paid run, not just before they
cost wall-clock time (vla-trial).
Quick start
1. Identify what layer a new piece of behavior belongs at using the
pyramid in Decision guidance — don't default to "write a sim test" for
everything.
2. For ROS 2 apps: write plain pytest for ROS-independent logic, and a
launch_testing case for anything that needs a running node or launch file —
mechanics in the upstream launch_testing docs (see References). Run both
through colcon test as part of the build.
3. For ML/policy apps: treat a short, deterministic lerobot-eval run
against a fixed dataset/seed as the smoke test — mechanics in lerobot.
Two distinct pass bars; don't mix them: a pipeline smoke (tiny
train-from-scratch, then a few eval episodes) asserts exit codes and
numeric metrics only — no success threshold, because an undertrained
policy legitimately scores 0 (verified 2026-07-12, manip-trial); a
regression gate (a handful of episodes against a known-good
checkpoint) is where a stated success-rate threshold belongs. Save
large-scale eval runs for manual validation, not every CI run — and don't
let a benchmark masquerade as a regression test: a benchmark that
re-derives an already-recorded number or loads a large model belongs
marked slow/deselected from the default suite (e.g. pytest -m "not slow"), run on demand, not on every push. Leaving benchmarks in the
default suite bloated one suite's runtime unnecessarily (3.5min → 8min,
vla-trial).
4. Before declaring a sample app or feature done, run its smoke test and
confirm it passes — this is the trial-run bar, not optional polish.
5. Wire the passing tests into CI so the bar holds on every change, not
just the first time.
Decision guidance
The test pyramid for robotics (narrower and faster at the top, broader and
slower at the bottom — most changes should be caught by a unit test, not a
full sim run):
| Layer |
What it checks |
Tool / pattern |
Owning skill |
| Unit |
Pure logic with no ROS/robot/sim dependency (a planner's math, a state-machine transition, a data-formatting function) |
pytest |
(generic — no robium skill needed) |
| Node / launch smoke |
A node starts, a launch file brings up the expected set of nodes without crashing, expected topics/services appear |
launch_testing + pytest |
upstream launch_testing docs (see References; not yet in ros2) |
| Sim scenario / regression |
End-to-end behavior in a scripted scenario (robot reaches a goal, avoids an obstacle, completes a manipulation task) run headless and deterministically |
A seeded Gazebo/Isaac Sim run driven by a test script, checked against expected outcomes |
gazebo, isaac-sim, simulation |
| Policy eval |
Pipeline smoke: a tiny train+eval completes and emits numeric metrics (no threshold). Regression gate: a known-good policy's success rate over a small, fixed, seeded episode set meets a stated threshold |
Deterministic small-scale lerobot-eval run as a pass/fail gate |
lerobot |
The trial-run bar: for any sample app or feature this pyramid covers, the
minimum passing bar before calling it done is the node/launch smoke layer (ROS
vertical) or a policy-eval smoke run (manipulation vertical) — sim-scenario
and full-scale eval are the next layer up, expected for anything beyond a
first working sample.
Platform gotchas
- CI runners are headless by default — sim-scenario tests must run
headless (Gazebo server-only mode, Isaac Sim's
runheadless.sh) rather than
assuming a display; route the general headless strategy to environments if
it isn't already decided.
- GPU-gated eval inherits its gate in CI too. A policy-eval smoke test
that needs Isaac Sim inherits that skill's NVIDIA RTX GPU floor — confirm
the CI runner actually has a qualifying GPU before wiring an Isaac-Sim-backed
eval into every pipeline run, or keep CI on a
gazebo/CPU-only eval path and
reserve GPU-backed runs for manual/scheduled checks.
- macOS CI runners can't run the ROS 2 layers natively — the
node/launch-smoke and sim-scenario layers need Docker on macOS, same as
local development (see
ros2's and gazebo's macOS gotchas); plan CI
images accordingly rather than assuming a native macOS runner works.
Customization
- Small demo vs. a fleet-scale project: a one-robot sim demo may only need
the top two pyramid layers (unit + smoke) to meet the trial-run bar; a
production-bound project should build out sim-scenario and eval-threshold
layers too — scale the pyramid's depth to the project's stakes, but never
skip the smoke layer entirely.
- Adding a new vertical or module: extend the same four-layer pyramid
rather than inventing a project-specific test taxonomy — the layer names and
owning skills stay consistent across robium projects.
References
Changelog
- 1.2.0 (2026-07-15): vla-trial absorption — Key directives gains three
bullets (benchmarks aren't regression tests / mark
slow; assert against
config constants not literals; measurement harness zero-measured guard)
and a free-local-gate-before-paid-remote-run rule; Quick start's
large-scale-eval line extended with the slow-marker mechanism and
3.5→8min suite-bloat evidence.
- 1.1.0 (2026-07-12): manip-trial absorption — policy-eval layer split
into pipeline-smoke (exit codes + numeric metrics, no success threshold)
vs regression-gate (threshold against a known-good checkpoint); the
previous universal-threshold phrasing made train-from-scratch smoke
tests impossible to pass honestly.
- 1.0.1 (2026-07-11): nav-trial absorption — fixed the launch_testing
routing dead-end: three spots routed "mechanics → ros2" but the ros2
skill has no launch_testing content; routes now point at the upstream
docs until ros2 grows that section. Trial-run bar + smoke shape confirmed
✓ under real load (one-command compose smoke, exit-code chain).
1---2name: testing-53description: Test-driven robotics development: smoke tests for launch files, sim-based regression tests, node-level unit tests, policy eval as a test, and CI patterns for robotics repos. Use when: 'test the robot app', 'how do I test this node', 'smoke test', 'regression test in sim', setting up tests for a new robotics project, or before claiming any robotics app works. Applies to both verticals: launch_testing and pytest for ROS 2 apps; deterministic small-scale eval runs for ML policies. Load alongside whatever skill is building the thing under test. Not for: general (non-robotics) testing practices.4---56# testing78The cross-cutting testing umbrella for robium. Robotics apps fail in layers a9generic test suite misses — a node that never receives a message, a launch10file that starts everything except the one node that mattered, a policy that11looks fine on paper but never reaches the goal in sim. This skill frames the12robotics-specific test pyramid and states the non-negotiable bar: a robium13build is not done until its smoke test passes. It does not re-teach14`launch_testing`/pytest mechanics for ROS 2 (that's `ros2`) or the15`lerobot-eval` CLI (that's `lerobot`) — it frames both as test-pyramid layers16and routes to them.1718## When to use this skill1920- Setting up tests for any new robium project — this should be planned into21 the build from the start, not bolted on after the fact.22- The trigger phrases in the description: 'test the robot app', 'how do I test23 this node', 'smoke test', 'regression test in sim'.24- Before claiming any robotics app or sample works — a claim of "done" without25 a passing smoke test is not verified, it's a guess.26- Load alongside whatever skill is building the thing under test — `ros2`/27 `nav2`/`gazebo` for the navigation vertical, `lerobot` for the manipulation28 vertical — this skill supplies the test framing, not a replacement for29 either.30- Cross-references — go to the sibling skill instead when the question is:31 - ROS 2 `launch_testing`/pytest mechanics themselves (fixtures, process32 actions, assertions) → the upstream launch_testing README and ROS 233 testing docs (linked in References). The `ros2` skill does not yet34 carry launch_testing content — until it does, go upstream directly35 rather than hunting for it there. This skill frames launch/node testing36 as a pyramid layer either way.37 - The `lerobot-eval` CLI and its flags → `lerobot`. This skill frames policy38 eval as a test-pyramid layer with a pass/fail bar; `lerobot` owns the eval39 mechanics.40 - Setting up the simulator a regression test runs against → `gazebo` or41 `isaac-sim` (or `simulation` if the choice isn't made yet).42 - General (non-robotics) testing practices — unrelated to this skill; use43 whatever generic testing guidance already applies to the language/44 framework in question.45 - The whole-stack decision this feeds into → `architect` (plans testing into46 the brief, doesn't bolt it on later).4748## Key directives4950- **Delegation posture: route + embed the pyramid framing.** The test-pyramid51 structure, what belongs at each layer, and the trial-run bar live here;52 `launch_testing`/pytest syntax and `lerobot-eval` flags live downstream.53 Never re-teach either mechanism's syntax in this skill.54- **A sample app is not done until its smoke test passes.** This is the55 concrete acceptance bar for any robium trial run: "it builds" or "it starts56 without crashing" is not sufficient — a launch/node-level smoke test57 actually passing is the minimum evidence of "done." Treat a missing or58 failing smoke test as an open task, not a detail to fix later.59- **Test at the right layer — don't push everything into sim.** Logic that60 doesn't depend on ROS or a running robot belongs in a plain unit test, not a61 slow sim scenario; reserve sim-based regression tests for behavior that62 genuinely requires the simulated environment. See the pyramid in Decision63 guidance.64- **Determinism is what makes a test a test.** A sim scenario or policy eval65 that isn't seeded/repeatable produces flaky results that get blamed on the66 code instead of the test — apply `simulation`'s determinism guidance (seeded67 physics, `use_sim_time`) and `lerobot`'s small-scale-before-long-run68 discipline (a short, deterministic eval run as a CI gate, not a long69 training-scale rollout) to every automated test.70- **Both verticals get equal treatment.** The ROS/navigation vertical tests71 with `launch_testing` + pytest; the manipulation vertical tests with72 deterministic small-scale policy eval runs. Neither is "the real test73 framework" and the other an afterthought — plan both when a project spans74 both verticals.75- **Assert against config constants, not re-typed literals.** A regression76 test that hardcodes a rendered value (e.g. asserting the literal string77 `--steps=2000`) can keep "passing" while the fact it checks goes stale — a78 config constant dropped to `100` and the test never noticed. When a test79 asserts a value derived from a config constant, import and assert against80 the constant itself, not a re-typed copy (vla-trial).81- **A measurement harness needs a guard that it didn't silently measure82 nothing.** A benchmark CLI that catches and prints per-device exceptions can83 still exit 0 having measured zero devices — this happened on a transient HF84 401 with no real code bug. Add a regression test for the harness itself, or a85 `return 1 if measured == 0` inside it, so "ran clean" and "measured nothing"86 can't look the same (vla-trial).87- **Gate every paid remote run behind a free local dry-run.** Before submitting88 any paid remote job (a cloud GPU training run, a paid inference batch), run89 the pipeline-smoke layer end-to-end locally first — same pipeline, tiny90 scale, CPU is fine. It catches plumbing bugs (a camera-feature mismatch, a91 bad local-output-dir path) before they cost a paid run, not just before they92 cost wall-clock time (vla-trial).9394## Quick start9596**1. Identify what layer a new piece of behavior belongs at** using the97pyramid in Decision guidance — don't default to "write a sim test" for98everything.99100**2. For ROS 2 apps:** write plain pytest for ROS-independent logic, and a101`launch_testing` case for anything that needs a running node or launch file —102mechanics in the upstream launch_testing docs (see References). Run both103through `colcon test` as part of the build.104105**3. For ML/policy apps:** treat a short, deterministic `lerobot-eval` run106against a fixed dataset/seed as the smoke test — mechanics in `lerobot`.107Two distinct pass bars; don't mix them: a **pipeline smoke** (tiny108train-from-scratch, then a few eval episodes) asserts exit codes and109numeric metrics only — no success threshold, because an undertrained110policy legitimately scores 0 (verified 2026-07-12, manip-trial); a111**regression gate** (a handful of episodes against a known-good112checkpoint) is where a stated success-rate threshold belongs. Save113large-scale eval runs for manual validation, not every CI run — and don't114let a benchmark masquerade as a regression test: a benchmark that115re-derives an already-recorded number or loads a large model belongs116marked `slow`/deselected from the default suite (e.g. pytest `-m "not117slow"`), run on demand, not on every push. Leaving benchmarks in the118default suite bloated one suite's runtime unnecessarily (3.5min → 8min,119vla-trial).120121**4. Before declaring a sample app or feature done, run its smoke test and122confirm it passes** — this is the trial-run bar, not optional polish.123124**5. Wire the passing tests into CI** so the bar holds on every change, not125just the first time.126127## Decision guidance128129**The test pyramid for robotics** (narrower and faster at the top, broader and130slower at the bottom — most changes should be caught by a unit test, not a131full sim run):132133| Layer | What it checks | Tool / pattern | Owning skill |134|---|---|---|---|135| Unit | Pure logic with no ROS/robot/sim dependency (a planner's math, a state-machine transition, a data-formatting function) | pytest | (generic — no robium skill needed) |136| Node / launch smoke | A node starts, a launch file brings up the expected set of nodes without crashing, expected topics/services appear | `launch_testing` + pytest | upstream launch_testing docs (see References; not yet in `ros2`) |137| Sim scenario / regression | End-to-end behavior in a scripted scenario (robot reaches a goal, avoids an obstacle, completes a manipulation task) run headless and deterministically | A seeded Gazebo/Isaac Sim run driven by a test script, checked against expected outcomes | `gazebo`, `isaac-sim`, `simulation` |138| Policy eval | Pipeline smoke: a tiny train+eval completes and emits numeric metrics (no threshold). Regression gate: a known-good policy's success rate over a small, fixed, seeded episode set meets a stated threshold | Deterministic small-scale `lerobot-eval` run as a pass/fail gate | `lerobot` |139140**The trial-run bar:** for any sample app or feature this pyramid covers, the141minimum passing bar before calling it done is the node/launch smoke layer (ROS142vertical) or a policy-eval smoke run (manipulation vertical) — sim-scenario143and full-scale eval are the next layer up, expected for anything beyond a144first working sample.145146## Platform gotchas147148- **CI runners are headless by default** — sim-scenario tests must run149 headless (Gazebo server-only mode, Isaac Sim's `runheadless.sh`) rather than150 assuming a display; route the general headless strategy to `environments` if151 it isn't already decided.152- **GPU-gated eval inherits its gate in CI too.** A policy-eval smoke test153 that needs Isaac Sim inherits that skill's NVIDIA RTX GPU floor — confirm154 the CI runner actually has a qualifying GPU before wiring an Isaac-Sim-backed155 eval into every pipeline run, or keep CI on a `gazebo`/CPU-only eval path and156 reserve GPU-backed runs for manual/scheduled checks.157- **macOS CI runners can't run the ROS 2 layers natively** — the158 node/launch-smoke and sim-scenario layers need Docker on macOS, same as159 local development (see `ros2`'s and `gazebo`'s macOS gotchas); plan CI160 images accordingly rather than assuming a native macOS runner works.161162## Customization163164- **Small demo vs. a fleet-scale project:** a one-robot sim demo may only need165 the top two pyramid layers (unit + smoke) to meet the trial-run bar; a166 production-bound project should build out sim-scenario and eval-threshold167 layers too — scale the pyramid's depth to the project's stakes, but never168 skip the smoke layer entirely.169- **Adding a new vertical or module:** extend the same four-layer pyramid170 rather than inventing a project-specific test taxonomy — the layer names and171 owning skills stay consistent across robium projects.172173## References174175- Upstream: [ROS 2 testing overview](https://docs.ros.org/en/jazzy/Tutorials/Intermediate/Testing/Testing-Main.html),176 [ROS 2 launch package (launch_testing source)](https://github.com/ros2/launch/blob/rolling/launch_testing/README.md),177 [colcon test-result / test workflow](https://colcon.readthedocs.io/en/released/reference/verb/test.html),178 [pytest documentation](https://docs.pytest.org/en/stable/).179- Sibling skills: `ros2` (`launch_testing`/pytest mechanics), `lerobot`180 (`lerobot-eval` mechanics and small-scale-run discipline), `gazebo` and181 `isaac-sim` (sim-scenario execution), `simulation` (determinism guidance182 this skill's sim-scenario layer relies on), `environments` (headless CI183 setup), `architect` (plans testing into the brief).184185## Changelog186187<!-- One dated line per battle-tested change, added by skill-author hardening sessions. -->188189- 1.2.0 (2026-07-15): vla-trial absorption — Key directives gains three190 bullets (benchmarks aren't regression tests / mark `slow`; assert against191 config constants not literals; measurement harness zero-measured guard)192 and a free-local-gate-before-paid-remote-run rule; Quick start's193 large-scale-eval line extended with the `slow`-marker mechanism and194 3.5→8min suite-bloat evidence.195- 1.1.0 (2026-07-12): manip-trial absorption — policy-eval layer split196 into pipeline-smoke (exit codes + numeric metrics, no success threshold)197 vs regression-gate (threshold against a known-good checkpoint); the198 previous universal-threshold phrasing made train-from-scratch smoke199 tests impossible to pass honestly.200- 1.0.1 (2026-07-11): nav-trial absorption — fixed the launch_testing201 routing dead-end: three spots routed "mechanics → ros2" but the ros2202 skill has no launch_testing content; routes now point at the upstream203 docs until ros2 grows that section. Trial-run bar + smoke shape confirmed204 ✓ under real load (one-command compose smoke, exit-code chain).