Docs / cli-io-sanity-qe · Edit on GitHub

CLI I/O sanity audit

Engineers use this document to understand what the CLI I/O test suites verify, how the subprocess harness works, and where to add coverage when changing serializers or flags.

The goal is a stable contract for:

  • Human operators — text on stdout/stderr, progress on stderr, sensible exit codes.
  • Machine consumers — deterministic JSON with versioned schemas, omitted keys instead of null, and composable topology arrays.

Test architecture (three layers)

flowchart TB
  subgraph L1["Layer 1 — Unit schema tests"]
    U["tests/cli_output/*.rs"]
    S["src/cli/*_output.rs fixtures"]
    U --> S
  end

  subgraph L2["Layer 2 — Golden-path subprocess"]
    G["subprocess_golden_path.rs"]
    G --> B["CARGO_BIN_EXE_rg_build"]
  end

  subgraph L3["Layer 3 — Full-platform subprocess"]
    A["all_commands_sanity.rs"]
    A --> B
  end

  F["tests/fixtures/tiny_polyglot_repo"]
  G --> F
  A --> F

  L1 -.->|"fast, no binary"| L2
  L2 -.->|"narrow regressions"| L3
LayerCargo targetSpeedInvokes binary?Purpose
1 — Unit schemacli_outputFast (~ms)NoAssert serde shapes from typed fixtures in *_output.rs
2 — Golden pathsubprocess_golden_pathMediumYesNarrow end-to-end paths: discover ingest, blast-radius v2, policy exit 1
3 — Full sanityall_commands_sanitySlower (~1s)YesOne subprocess loop covering every JSON command + key platform rules

Run everything:

cargo test --test cli_output --test subprocess_golden_path --test all_commands_sanity

CI: Maintainers order .github/workflows/ci.yml via workflow_dispatch or by adding the ci label on a PR. That job runs format/clippy, workspace tests, named QE steps (map_collision_qe, graph_correctness, semantic_search_qe, cross_feature_qe), and the three CLI I/O targets plus blast-radius perf. There is no automatic run on every PR open.

Individual targets:

cargo test --test cli_output              # serializers only
cargo test --test subprocess_golden_path  # discover + blast-radius golden paths
cargo test --test all_commands_sanity     # comprehensive subprocess audit

Subprocess harness (all_commands_sanity.rs)

Design goals

  1. Never touch a developer tree — each test copies tests/fixtures/tiny_polyglot_repo into a tempfile::TempDir.
  2. Explicit sandbox database — graph writes go to {temp}/sandbox_graph.db via -d, not {repo}/.rgbuilder/.
  3. Real binary — uses env!("CARGO_BIN_EXE_rg_build") so cargo test always runs the binary built for the current profile.
  4. Shared repo root-r {temp_repo} keeps paths stable for slice/inspect file arguments.

Sandbox helper

Method / fieldRole
Sandbox::new()Copies fixture into temp dir; sets db = {temp}/sandbox_graph.db
sandbox.repoRoot of the copied polyglot repo (Java + Rust)
sandbox.dbIsolated legacy JSON graph path (-d; not SQLite)
sandbox.run(args)Spawns rg-build -r {repo} -d {db} …args and returns Output
parse_stdout_json(output)Parses stdout as JSON; panics with stdout/stderr on failure

Assertion helpers

HelperEnforces
assert_successExit code 0
assert_exit_code(output, code, label)Exact UNIX exit (0 or 1)
assert_schema_version(doc, n)Top-level schema_version
assert_keys_presentRequired object keys exist
assert_keys_absent_in_strKey names do not appear in serialized string (metrics omission rule)
assert_no_nil_uuidsPayload must not contain 00000000-0000-0000-0000-000000000000
assert_handoffs_empty_arraygatekeeping.handoffs is [] when --with-slices omitted

Single test: test_all_cli_commands_json_schema_sanity

Execution order inside the test (each phase uses a fresh discover ingest unless noted):

StepCommand (abbrev.)Assertions
1discover . --languages java,rust (text)Success; stdout does not start with {
2-f json discover . --languages java,rustschema_version: 2, command: discover, metrics block keys
3-f json blast-radius OrderService::processv2 sections; Java language + canonical_fqn; empty handoffs; no nil UUIDs
3b-f json blast-radius publishEvent --depth 1metrics.caller_depth_limit: 1; impact_zone_size ≤ full closure
4-f json gql … / --explainv1 bindings; explain: false then true
5-f json metrics --pagerank / --betweenness / --communitiesEach flag omits unrequested section keys
6–6b-f json check permissive / strictexit 0 pass; exit 1 + publishEvent violation
7–8-f json slice cfg / pdg / --tainttopology arrays vs flat taint schema
9-f json inspect checkout cfg / pdg / domstructured layers; integer block_index
10blast-radius --with-slices publishEventnon-empty gatekeeping.handoffs
11blast-radius policy violationexit 1 + VIOLATED

Separate test: test_discover_cli_flags--exclude, -v, --security, --with-cfg, --with-taint.

CLI note: inspect takes a layer subcommand (inspect SYMBOL dom), not --layer dom.


Fixture: tests/fixtures/tiny_polyglot_repo

Minimal polyglot repo used by all subprocess suites.

PathContents
java/com/example/OrderService.javaOrderService::process — primary blast-radius / disambiguation target
java/com/example/OrderController.javacheckout (inspect dom), publishEvent (unique symbol with caller for check policy tests)
rust/src/lib.rsprocess_labeled, call chain for slice CFG/taint
rust/src/main.rsEntry point for Rust discover

Known limits (documented so engineers do not chase false failures):

  1. Rust Calls edges — Rust plugin may not emit call edges in this tiny fixture; blast-radius/check upstream counts for Rust symbols can be zero.
  2. Duplicate bare namesprocess, helper, etc. exist in both languages; blast-radius needs Class::method or --class; check skips ambiguous symbols via resolve_unique_symbol. Use publishEvent for subprocess scale-failure coverage.
  3. Re-discover after cache schema changes — subprocess tests always run fresh discover; stale local .rgbuilder/ is not used.

Layer 1 — Unit schema tests (cargo test --test cli_output)

These tests call serializer fixtures in src/cli/*_output.rs directly. They do not spawn the CLI. Add or extend a test here when changing JSON field names, optional-key rules, or fixture builders.

Module map

FileSerializer under testTests
discover.rsdiscover_output.rstest_discover_json_schema_sanity, test_discover_build_maps_pipeline_stats
blast_radius.rsblast_radius_output.rstest_blast_radius_json_schema_sanity, test_caller_depth_limit_serializes_when_set, test_blast_radius_symbol_context_shape, test_skipped_gatekeeping_always_has_empty_handoffs, test_blast_radius_target_v2_metadata
uuid_resolution.rsblast_radius_output.rstest_cache_entry_omits_unresolved_topology_without_nil_uuid
gql.rsgql_output.rstest_gql_json_schema_sanity, test_gql_empty_rows_explicit_array
metrics.rsmetrics_output.rstest_metrics_json_schema_sanity, test_metrics_wrap_adds_schema_version, test_metrics_pagerank_only_omits_other_sections
check.rscheck_output.rstest_check_json_schema_sanity, test_check_violations_always_array_when_passing, test_check_passed_false_contract
slice.rsslice_output.rstest_slice_cfg_json_schema_sanity, test_slice_cfg_topology_not_counts
inspect.rsinspect_output.rstest_inspect_cfg_json_schema_sanity, test_inspect_cfg_block_has_index

What each command’s unit tests prove

discover

  • schema_version: 2, command: discover
  • Metrics object always includes: files_discovered, files_indexed, files_skipped, nodes_generated, edges_generated, duration_ms
  • build_discover_response maps PipelineStats → JSON fields correctly

blast-radius (v2)

  • Top-level: target, metrics, topology, gatekeeping
  • gatekeeping.handoffs is always a present empty array when slices skipped
  • Topology caller entries expose id, fqn, file_path
  • Target v2: language, canonical_fqn; signature omitted when None
  • metrics.caller_depth_limit present only when --depth N passed; impact_zone_size matches filtered zone
  • --depth N post-filters cached/engine impact zones by incoming call hops (see json-api.md blast-radius catalog)
  • Unresolved UUIDs in cache → caller dropped from topology (nil-UUID guardrail)

gql

  • schema_version: 1, rows, count, explain
  • Row cells: binding, node, type, file
  • Empty result → rows: [], not omitted

metrics

  • Full response includes all three sections when built with data
  • Pagerank-only build: betweenness and communities keys absent (not null)
  • wrap_metrics_payload injects schema_version

check

  • Root: policy, violations, passed
  • Passing run: violations: []
  • test_check_passed_false_contract: serializer contract for passed: false
  • Subprocess: publishEvent + max_impact_nodes: 0 → exit 1 (see Layer 2 / Layer 3)

slice

  • CFG view: view, nodes, edges arrays — not legacy scalar block counts
  • blocks key must not appear in CFG JSON

inspect

  • CFG layer fixture: symbol, layer, nodes, edges
  • Nodes use stable block_index + start_line (not internal debug pointers)

Layer 2 — Golden-path subprocess (subprocess_golden_path.rs)

Focused regressions that proved fragile during P2 work. Uses the same temp-copy fixture pattern but default -d (graph under {repo}/.rgbuilder/) except where noted.

TestWhat it proves
discover_json_emits_telemetry_on_stdoutJSON mode: single telemetry object on stdout; no human [✓] Indexed lines on stdout
discover_initializes_tiny_polyglot_repoText discover creates .rgbuilder/graph.db or snapshot
blast_radius_json_exit_zero_after_discoverJava OrderService::process via --class; v2 target metadata including signature
blast_radius_policy_violation_fails_closed_with_exit_one--policy-file with max_impact_nodes: 0 → exit 1, policy_status: VIOLATED
blast_radius_with_slices_populates_handoffs--with-slices on publishEvent → non-empty handoffs
blast_radius_with_slices_under_30s_after_cfg_discoverdiscover --with-cfg then --with-slices under 30s (br.slice.total_ms)
check_policy_violation_fails_closed_with_exit_onecheck with max_impact_nodes: 0 → exit 1

Add a golden-path test when a specific discover → command pipeline breaks in production but unit fixtures still pass.


Global platform rules (enforced where marked)

RuleUnitSubprocessNotes
Deterministic schema_versionv2: discover, blast-radius; v1: others
Strict null eliminationMetrics sections omitted; handoffs/violations/rows as []
No engine refactoring in I/O scopeTests only touch src/cli/*_output.rs + discover emit
Isolated DB in full sanityall_commands_sanity uses -d sandbox_graph.db
Exit 0 on successAll success paths
Exit 1 on policy breach✅ check serializer✅ check + blast-radius subprocess

Architecture alignment: Code_structure.md — CLI thin, serializers in *_output.rs, cache enrichment in rgbuilder-analysis.


Coverage gaps

All items from the original audit matrix are now covered by subprocess and/or unit tests. When adding new CLI flags or JSON fields, extend:

  • tests/cli_output/all_commands_sanity.rs — full-platform subprocess loop + test_discover_cli_flags
  • tests/cli_output/subprocess_golden_path.rs — focused regressions
  • tests/cli_output/*.rs — serializer unit fixtures

Future optional expansions (not required for baseline compliance):

AreaIdea
discover --verbose -f jsonAssert telemetry JSON when logging is redirected off stdout
gql --explain plan payloadSerialize QueryResult.plan in JSON when --explain is set
Rust Calls edges in fixtureRicher blast-radius/check paths for Rust symbols

Extending coverage

Changed a JSON field in *_output.rs

  1. Update the typed struct and fixture_* builder in the same file.
  2. Fix the matching module under tests/cli_output/.
  3. If the field is user-visible in subprocess output, add an assertion to all_commands_sanity.rs or subprocess_golden_path.rs.

Added a new CLI JSON command

  1. Create src/cli/<cmd>_output.rs with SCHEMA_VERSION constant and fixture.
  2. Add tests/cli_output/<cmd>.rs and mod <cmd>; in main.rs.
  3. Append a step to test_all_cli_commands_json_schema_sanity.
  4. Document the schema in json-api.md field catalogs.

Added a subprocess-only flag

Prefer asserting in all_commands_sanity.rs if the flag affects JSON shape or exit code; use subprocess_golden_path.rs for one critical pipeline only.


Related docs