Skip to content

test(cli): raise component coverage above 92 percent - #635

Open
willkill07 wants to merge 1 commit into
NVIDIA:release/0.7from
willkill07:test/cli-coverage-release-0.7
Open

test(cli): raise component coverage above 92 percent#635
willkill07 wants to merge 1 commit into
NVIDIA:release/0.7from
willkill07:test/cli-coverage-release-0.7

Conversation

@willkill07

@willkill07 willkill07 commented Aug 2, 2026

Copy link
Copy Markdown
Member

Overview

Raises the CLI component coverage target above 92 percent while excluding prompt-only logic that cannot be exercised reliably in automated coverage runs.

  • I confirm this contribution is my own work, or I have the right to submit it under this project's license.
  • I searched existing issues and open pull requests, and this does not duplicate existing work.

Details

  • Separate prompt-driven config editor, plugin editor, and wizard logic from testable state and transformation logic.
  • Add focused coverage for configuration editing, plugin lifecycle and schemas, installation, server behavior, and process launching.
  • Update Codecov component paths so untestable interactive prompt modules are excluded from the CLI metric.
  • Preserve production behavior while making non-interactive logic directly testable.
  • No breaking API changes.

Validation performed:

  • Branch commit hooks, including formatting, Clippy, Cargo checks, and Codecov YAML validation
  • uv run pre-commit run --all-files on the equivalent combined tree
  • All 1,185 CLI library tests passed serially
  • Exact four-branch merge tree comparison against the original combined branch

Where should the reviewer start?

Start with codecov.yml and crates/cli/src/commands/configure/editor.rs, then compare its prompt module and crates/cli/tests/coverage/commands/configure_editor_tests.rs.

Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)

Summary by CodeRabbit

  • New Features

    • Enhanced interactive configuration wizard with previews, confirmation, cancellation, defaults, and resume guidance.
    • Improved configuration editing for gateway limits, providers, logging, file sinks, and secret values.
    • Expanded plugin configuration editor with nested fields, lists, maps, JSON, enums, validation, reset, clear, and help actions.
    • Added dynamic plugin editing with schema-aware navigation and secret protection.
  • Bug Fixes

    • Improved handling of invalid inputs, unavailable terminal actions, interruptions, and configuration errors.
    • Improved server shutdown error reporting and diagnostics.

Signed-off-by: Will Killian <wkillian@nvidia.com>
@willkill07
willkill07 requested a review from a team as a code owner August 2, 2026 00:53
@github-actions github-actions Bot added size:XXL PR is very large Test Test related lang:rust PR changes/introduces Rust code labels Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR extracts CLI configuration and plugin prompts into dedicated modules, isolates logging and teardown helpers, refactors destination collection, updates Codecov exclusions, and expands integration and coverage tests.

Changes

CLI interaction and runtime refactor

Layer / File(s) Summary
Configuration editor and wizard prompts
codecov.yml, crates/cli/src/commands/configure/..., crates/cli/tests/coverage/commands/configure_editor_tests.rs
Configuration editing and setup wizard workflows now delegate to dedicated prompt modules with TTY checks, validation, previews, cancellation, saving, and plugin continuation.
Plugin editor prompt modules
crates/cli/src/plugins/..., crates/cli/tests/coverage/shared/plugins*.rs
Plugin editing now uses centralized prompt modules for menus, schema-driven values, nested data, tagged unions, secret handling, reset, clear, and validation.
Runtime orchestration helpers
crates/cli/src/commands/mod.rs, crates/cli/src/process/launcher.rs, crates/cli/src/server/mod.rs
Logging setup, observability destination collection, and server teardown logging use dedicated helpers.
CLI integration and shared coverage
crates/cli/tests/cli_tests.rs, crates/cli/tests/coverage/shared/*
Tests add reusable assertions, explicit working directories, and coverage for configuration, installer, lifecycle, and server paths.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • NVIDIA/NeMo-Relay#632: Overlaps in CLI refactors for logging, observability destinations, teardown logging, and shared test helpers.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.49% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits format, uses an allowed type and lowercase scope, states the coverage change, and is within 72 characters.
Description check ✅ Passed The description includes all required sections, explains the changes, identifies reviewer starting points, and references issue #632.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@willkill07 willkill07 added this to the 0.7 milestone Aug 2, 2026
@willkill07 willkill07 self-assigned this Aug 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/cli/src/commands/configure/editor/prompt.rs`:
- Around line 213-219: Extract a shared require_nonempty validator returning
Result<(), &'static str> with the unified "value must not be empty" message,
then replace the inline empty-value validation closures at the three affected
prompt locations with .validate_with(require_nonempty).

In `@crates/cli/src/commands/configure/wizard/prompt.rs`:
- Around line 179-188: Unify the duplicated TTY validation through a shared
helper in the CLI TTY module. In
crates/cli/src/commands/configure/wizard/prompt.rs lines 179-188, replace
ensure_tty’s inline stdin-only check with the shared helper using the
interactive menu stream requirement and preserve its message; in
crates/cli/src/commands/configure/editor/prompt.rs lines 58-60, retain
ensure_tty_with for testability but delegate it to the same helper with its
required streams and message. Ensure the existing plugins prompt guard also uses
this shared definition.
- Around line 179-188: Consolidate the duplicated ensure_tty implementations by
introducing one shared helper that accepts the required stream-readiness checks
and error message. Update the configure editor prompt, plugin prompt, and wizard
prompt callers to use it, preserving each caller’s required stdin/stdout/stderr
checks and user-facing message while removing their local ensure_tty
definitions.
- Around line 87-94: Update the configuration save flow around build_config,
confirm_summary, and save_config so agent_hint being None preserves existing
non-agent sections instead of overwriting them. Merge the current file’s
sections such as upstream, gateway, and logging into the document before preview
and saving, or require an explicit destructive confirmation that accurately
displays their removal.

In `@crates/cli/src/plugins/dynamic_editor/prompt.rs`:
- Around line 352-401: No change is required for this diff; preserve the
existing prompt_raw_config sequence, including redaction before display, secret
restoration before validation, and committing only after successful validation.
Treat re-prompting on errors as a separate follow-up rather than modifying this
implementation.
- Around line 14-35: The condition in edit_dynamic_plugin redundantly checks
state.schema.is_none() because fields is empty in that case; simplify the menu
selection condition to fields.is_empty(), preserving the existing root-menu and
fields-menu behavior.

In `@crates/cli/src/plugins/prompt.rs`:
- Around line 529-531: Update the Reset handling in the MenuResponse shortcut
match to re-assert the expected container type after calling
collection_shortcut_value, ensuring malformed defaults cannot reach the next
iteration’s as_array().expect(...) or as_object().expect(...). Preserve the
existing Clear behavior and use the shortcut-specific collection type when
coercing the reset value.

In `@crates/cli/tests/coverage/commands/configure_editor_tests.rs`:
- Line 339: Update the tests around clear_key and clear_sink_key to assert the
resulting configuration state after each operation, not merely successful return
values. Verify clear_key("missing", "value") removes the intended key and
clear_sink_key(0, "level") removes the sink-level key, adding assertions
alongside the existing path checks while preserving unrelated state assertions.
- Around line 369-378: Update
project_path_defaults_to_start_when_no_ancestor_config_exists to constrain
project_config_path’s ancestor search to the temporary root, preventing
configuration files outside the test fixture from affecting the result while
preserving the expected nested default path.

In `@crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs`:
- Around line 4417-4517: Strengthen
lifecycle_commands_cover_json_and_human_output_paths by capturing each list,
add, validate, and inspect result instead of only calling unwrap. For JSON
responses, parse the output and assert schema_version, ok, command, and a
representative field under data; for human responses, assert meaningful
command-specific text. Follow assertion patterns used elsewhere in this test
file and include lifecycle-event or scope-stack behavior where available rather
than relying only on successful completion.

In `@crates/cli/tests/coverage/shared/plugins_schema_tests.rs`:
- Around line 280-286: Update native_config_field to replace the bare unwrap
with an expectation that includes the requested key, so lookup failures identify
which schema field is missing while preserving the existing return behavior.

In `@crates/cli/tests/coverage/shared/plugins_tests.rs`:
- Around line 2152-2176: Rename the second `path` binding in
`dynamic_editor_raw_menu_and_nested_value_paths_are_deterministic` to
`field_path`, and update the related `set_value_at_path`, `value_at_path`, and
`remove_value_at_path` calls to use it; leave the filesystem `path` binding
unchanged.
- Around line 599-634: Expand
menu_keys_cover_selection_shortcuts_and_cancellation to assert every key in the
multi-key mappings handled by menu_response_for_key: add Del as Clear, and CtrlC
plus Char('q') as Cancel. Keep the existing assertions and expected response
indices unchanged.
- Around line 746-758: Strengthen component_shortcut_fallbacks_are_safe_noops by
capturing components[0] before calling reset_component_menu_item and
clear_component_menu_item, then asserting it remains unchanged afterward. Keep
the existing successful-result checks and verify both None and Back fallback
actions preserve the component state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: c1d1d959-14d8-49f0-832e-33c294d579c3

📥 Commits

Reviewing files that changed from the base of the PR and between 31c4a5c and db21ceb.

📒 Files selected for processing (20)
  • codecov.yml
  • crates/cli/src/commands/configure/editor.rs
  • crates/cli/src/commands/configure/editor/prompt.rs
  • crates/cli/src/commands/configure/wizard.rs
  • crates/cli/src/commands/configure/wizard/prompt.rs
  • crates/cli/src/commands/mod.rs
  • crates/cli/src/plugins/dynamic_editor.rs
  • crates/cli/src/plugins/dynamic_editor/prompt.rs
  • crates/cli/src/plugins/mod.rs
  • crates/cli/src/plugins/prompt.rs
  • crates/cli/src/process/launcher.rs
  • crates/cli/src/server/mod.rs
  • crates/cli/tests/cli_tests.rs
  • crates/cli/tests/coverage/commands/configure_editor_tests.rs
  • crates/cli/tests/coverage/shared/config_tests.rs
  • crates/cli/tests/coverage/shared/installer_tests.rs
  • crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs
  • crates/cli/tests/coverage/shared/plugins_schema_tests.rs
  • crates/cli/tests/coverage/shared/plugins_tests.rs
  • crates/cli/tests/coverage/shared/server_tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Check / Run
🧰 Additional context used
📓 Path-based instructions (12)
**/*.rs

📄 CodeRabbit inference engine (.agents/skills/prepare-pr/SKILL.md)

**/*.rs: Any Rust change must run just test-rust
Any Rust change must run cargo fmt --all
Any Rust change must run cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all for all FFI work since it is Rust work
Run just test-rust to validate FFI changes
Run cargo clippy --workspace --all-targets -- -D warnings to enforce strict linting on FFI work

When Rust files changed as part of Go work, also run cargo fmt --all, just test-rust, and cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all when Rust files are changed as part of Node work
Run cargo clippy --workspace --all-targets -- -D warnings when Rust files are changed as part of Node work
Run just test-rust when Rust files are changed as part of Node work

When changing the core Rust runtime or Rust-facing API surface, format Rust code with cargo fmt (rustfmt defaults), keep cargo clippy -- -D warnings clean, and satisfy cargo deny check per deny.toml.

**/*.rs: If any Rust code changed, always run just test-rust.
If any Rust code changed, also run cargo fmt --all.
If any Rust code changed, also run cargo clippy --workspace --all-targets -- -D warnings.
For Rust changes headed for review, run cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings even if relying on pre-commit.

Files:

  • crates/cli/src/commands/mod.rs
  • crates/cli/tests/coverage/shared/config_tests.rs
  • crates/cli/src/server/mod.rs
  • crates/cli/tests/coverage/shared/installer_tests.rs
  • crates/cli/tests/coverage/commands/configure_editor_tests.rs
  • crates/cli/src/process/launcher.rs
  • crates/cli/tests/coverage/shared/server_tests.rs
  • crates/cli/tests/coverage/shared/plugins_schema_tests.rs
  • crates/cli/src/commands/configure/editor.rs
  • crates/cli/src/commands/configure/editor/prompt.rs
  • crates/cli/tests/cli_tests.rs
  • crates/cli/src/plugins/dynamic_editor/prompt.rs
  • crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs
  • crates/cli/src/commands/configure/wizard.rs
  • crates/cli/tests/coverage/shared/plugins_tests.rs
  • crates/cli/src/commands/configure/wizard/prompt.rs
  • crates/cli/src/plugins/mod.rs
  • crates/cli/src/plugins/dynamic_editor.rs
  • crates/cli/src/plugins/prompt.rs
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

Follow binding naming conventions in Rust and Python: use snake_case.

Files:

  • crates/cli/src/commands/mod.rs
  • crates/cli/tests/coverage/shared/config_tests.rs
  • crates/cli/src/server/mod.rs
  • crates/cli/tests/coverage/shared/installer_tests.rs
  • crates/cli/tests/coverage/commands/configure_editor_tests.rs
  • crates/cli/src/process/launcher.rs
  • crates/cli/tests/coverage/shared/server_tests.rs
  • crates/cli/tests/coverage/shared/plugins_schema_tests.rs
  • crates/cli/src/commands/configure/editor.rs
  • crates/cli/src/commands/configure/editor/prompt.rs
  • crates/cli/tests/cli_tests.rs
  • crates/cli/src/plugins/dynamic_editor/prompt.rs
  • crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs
  • crates/cli/src/commands/configure/wizard.rs
  • crates/cli/tests/coverage/shared/plugins_tests.rs
  • crates/cli/src/commands/configure/wizard/prompt.rs
  • crates/cli/src/plugins/mod.rs
  • crates/cli/src/plugins/dynamic_editor.rs
  • crates/cli/src/plugins/prompt.rs
**/*.{rs,py,js,mjs,cjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{rs,py,js,mjs,cjs,ts,tsx}: Use Json = serde_json::Value in Rust-facing runtime APIs where the existing code expects JSON payloads.
Use Result<T> with FlowError in core runtime paths, and keep errors explicit and binding-appropriate at the wrapper layer.
Keep async behavior on the existing tokio-based model; bindings should preserve callback and future lifetimes rather than blocking or hiding async work unexpectedly.

Files:

  • crates/cli/src/commands/mod.rs
  • crates/cli/tests/coverage/shared/config_tests.rs
  • crates/cli/src/server/mod.rs
  • crates/cli/tests/coverage/shared/installer_tests.rs
  • crates/cli/tests/coverage/commands/configure_editor_tests.rs
  • crates/cli/src/process/launcher.rs
  • crates/cli/tests/coverage/shared/server_tests.rs
  • crates/cli/tests/coverage/shared/plugins_schema_tests.rs
  • crates/cli/src/commands/configure/editor.rs
  • crates/cli/src/commands/configure/editor/prompt.rs
  • crates/cli/tests/cli_tests.rs
  • crates/cli/src/plugins/dynamic_editor/prompt.rs
  • crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs
  • crates/cli/src/commands/configure/wizard.rs
  • crates/cli/tests/coverage/shared/plugins_tests.rs
  • crates/cli/src/commands/configure/wizard/prompt.rs
  • crates/cli/src/plugins/mod.rs
  • crates/cli/src/plugins/dynamic_editor.rs
  • crates/cli/src/plugins/prompt.rs
**/*.{rs,py,go,js,ts,c,h}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use language-appropriate naming conventions: Rust snake_case, C FFI exports prefixed nemo_relay_, Go PascalCase, Node.js camelCase, and Python snake_case.

Files:

  • crates/cli/src/commands/mod.rs
  • crates/cli/tests/coverage/shared/config_tests.rs
  • crates/cli/src/server/mod.rs
  • crates/cli/tests/coverage/shared/installer_tests.rs
  • crates/cli/tests/coverage/commands/configure_editor_tests.rs
  • crates/cli/src/process/launcher.rs
  • crates/cli/tests/coverage/shared/server_tests.rs
  • crates/cli/tests/coverage/shared/plugins_schema_tests.rs
  • crates/cli/src/commands/configure/editor.rs
  • crates/cli/src/commands/configure/editor/prompt.rs
  • crates/cli/tests/cli_tests.rs
  • crates/cli/src/plugins/dynamic_editor/prompt.rs
  • crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs
  • crates/cli/src/commands/configure/wizard.rs
  • crates/cli/tests/coverage/shared/plugins_tests.rs
  • crates/cli/src/commands/configure/wizard/prompt.rs
  • crates/cli/src/plugins/mod.rs
  • crates/cli/src/plugins/dynamic_editor.rs
  • crates/cli/src/plugins/prompt.rs
**/*.{rs,go,js,ts}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Add the SPDX license header to all Rust, Go, JavaScript, and TypeScript source files using the corresponding // comment form.

Files:

  • crates/cli/src/commands/mod.rs
  • crates/cli/tests/coverage/shared/config_tests.rs
  • crates/cli/src/server/mod.rs
  • crates/cli/tests/coverage/shared/installer_tests.rs
  • crates/cli/tests/coverage/commands/configure_editor_tests.rs
  • crates/cli/src/process/launcher.rs
  • crates/cli/tests/coverage/shared/server_tests.rs
  • crates/cli/tests/coverage/shared/plugins_schema_tests.rs
  • crates/cli/src/commands/configure/editor.rs
  • crates/cli/src/commands/configure/editor/prompt.rs
  • crates/cli/tests/cli_tests.rs
  • crates/cli/src/plugins/dynamic_editor/prompt.rs
  • crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs
  • crates/cli/src/commands/configure/wizard.rs
  • crates/cli/tests/coverage/shared/plugins_tests.rs
  • crates/cli/src/commands/configure/wizard/prompt.rs
  • crates/cli/src/plugins/mod.rs
  • crates/cli/src/plugins/dynamic_editor.rs
  • crates/cli/src/plugins/prompt.rs
{crates/**/src/**/*.rs,python/**/*.py}

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

Do not add tests under src; Rust tests belong in crate tests/ trees, and Python SDK tests belong under python/tests.

Files:

  • crates/cli/src/commands/mod.rs
  • crates/cli/src/server/mod.rs
  • crates/cli/src/process/launcher.rs
  • crates/cli/src/commands/configure/editor.rs
  • crates/cli/src/commands/configure/editor/prompt.rs
  • crates/cli/src/plugins/dynamic_editor/prompt.rs
  • crates/cli/src/commands/configure/wizard.rs
  • crates/cli/src/commands/configure/wizard/prompt.rs
  • crates/cli/src/plugins/mod.rs
  • crates/cli/src/plugins/dynamic_editor.rs
  • crates/cli/src/plugins/prompt.rs
**/*

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

**/*: Format changed files with the language-native formatter before the final lint/test pass.
If dynamic plugin behavior changed, use maintain-dynamic-plugins and include the native SDK, worker protocol, Python SDK, docs, packaging, and Codecov surfaces in the validation plan.
If code changes alter APIs, bindings, commands, paths, packaging behavior, observability/adaptive semantics, or documented best practices, update any dependent maintainer or consumer skills in the same branch.
During iteration, prefer uv run pre-commit run --files <changed files...>.
Before review or handoff, run uv run pre-commit run --all-files.

Files:

  • crates/cli/src/commands/mod.rs
  • codecov.yml
  • crates/cli/tests/coverage/shared/config_tests.rs
  • crates/cli/src/server/mod.rs
  • crates/cli/tests/coverage/shared/installer_tests.rs
  • crates/cli/tests/coverage/commands/configure_editor_tests.rs
  • crates/cli/src/process/launcher.rs
  • crates/cli/tests/coverage/shared/server_tests.rs
  • crates/cli/tests/coverage/shared/plugins_schema_tests.rs
  • crates/cli/src/commands/configure/editor.rs
  • crates/cli/src/commands/configure/editor/prompt.rs
  • crates/cli/tests/cli_tests.rs
  • crates/cli/src/plugins/dynamic_editor/prompt.rs
  • crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs
  • crates/cli/src/commands/configure/wizard.rs
  • crates/cli/tests/coverage/shared/plugins_tests.rs
  • crates/cli/src/commands/configure/wizard/prompt.rs
  • crates/cli/src/plugins/mod.rs
  • crates/cli/src/plugins/dynamic_editor.rs
  • crates/cli/src/plugins/prompt.rs
**/*.{rs,py,go,js,ts}

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

If a language surface changed, always run that language's test target even when Rust core did not change.

**/*.{rs,py,go,js,ts}: When observability configuration or lifecycle is exposed, keep FFI and Python, Go, and Node.js binding-native config objects and subscriber/exporter methods aligned in logical knobs and semantics.
Require every OpenTelemetry endpoint to have a type and nonblank destination; resolve header_env values at activation and reject missing, blank, or duplicate headers.
Concatenate layered ATOF sink, ATIF storage, and OpenTelemetry endpoint lists with higher-precedence entries first.
Preserve correct handling of mark events, start/end events, orphan cases, and span or trajectory fields derived from intended event data.
Run affected Rust tests and just test-rust when event fields change; run just test-python, just test-go, and just test-node when binding-native configuration or lifecycle changes.

Files:

  • crates/cli/src/commands/mod.rs
  • crates/cli/tests/coverage/shared/config_tests.rs
  • crates/cli/src/server/mod.rs
  • crates/cli/tests/coverage/shared/installer_tests.rs
  • crates/cli/tests/coverage/commands/configure_editor_tests.rs
  • crates/cli/src/process/launcher.rs
  • crates/cli/tests/coverage/shared/server_tests.rs
  • crates/cli/tests/coverage/shared/plugins_schema_tests.rs
  • crates/cli/src/commands/configure/editor.rs
  • crates/cli/src/commands/configure/editor/prompt.rs
  • crates/cli/tests/cli_tests.rs
  • crates/cli/src/plugins/dynamic_editor/prompt.rs
  • crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs
  • crates/cli/src/commands/configure/wizard.rs
  • crates/cli/tests/coverage/shared/plugins_tests.rs
  • crates/cli/src/commands/configure/wizard/prompt.rs
  • crates/cli/src/plugins/mod.rs
  • crates/cli/src/plugins/dynamic_editor.rs
  • crates/cli/src/plugins/prompt.rs
**/*.{rs,py,js,ts,tsx,go,java,kt,swift}

📄 CodeRabbit inference engine (.agents/skills/add-middleware/SKILL.md)

Add tests covering registration and duplicate names, deregistration and missing names, priority ordering, callback failure policy, scope-local inheritance and cleanup, event payload semantics, immutable mark and scope fields, and parity across affected bindings.

Files:

  • crates/cli/src/commands/mod.rs
  • crates/cli/tests/coverage/shared/config_tests.rs
  • crates/cli/src/server/mod.rs
  • crates/cli/tests/coverage/shared/installer_tests.rs
  • crates/cli/tests/coverage/commands/configure_editor_tests.rs
  • crates/cli/src/process/launcher.rs
  • crates/cli/tests/coverage/shared/server_tests.rs
  • crates/cli/tests/coverage/shared/plugins_schema_tests.rs
  • crates/cli/src/commands/configure/editor.rs
  • crates/cli/src/commands/configure/editor/prompt.rs
  • crates/cli/tests/cli_tests.rs
  • crates/cli/src/plugins/dynamic_editor/prompt.rs
  • crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs
  • crates/cli/src/commands/configure/wizard.rs
  • crates/cli/tests/coverage/shared/plugins_tests.rs
  • crates/cli/src/commands/configure/wizard/prompt.rs
  • crates/cli/src/plugins/mod.rs
  • crates/cli/src/plugins/dynamic_editor.rs
  • crates/cli/src/plugins/prompt.rs
codecov.yml

📄 CodeRabbit inference engine (.agents/skills/maintain-ci/SKILL.md)

Keep Codecov component paths aligned with new crates, packages, and generated outputs; dynamic plugin SDK/protocol paths belong in the plugin component.

Files:

  • codecov.yml
{justfile,codecov.yml,codecov.yaml,.github/workflows/**/*.yml,.github/workflows/**/*.yaml}

📄 CodeRabbit inference engine (.agents/skills/maintain-dynamic-plugins/SKILL.md)

justfile, Codecov, and CI package/test workflows must include new plugin crates and packages.

Files:

  • codecov.yml
{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}

⚙️ CodeRabbit configuration file

{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}: Tests should cover the behavior promised by the changed API surface, including error paths and cross-request isolation where relevant.
Prefer assertions on lifecycle events, scope stacks, middleware ordering, and binding parity over shallow smoke tests.

Files:

  • crates/cli/tests/coverage/shared/config_tests.rs
  • crates/cli/tests/coverage/shared/installer_tests.rs
  • crates/cli/tests/coverage/commands/configure_editor_tests.rs
  • crates/cli/tests/coverage/shared/server_tests.rs
  • crates/cli/tests/coverage/shared/plugins_schema_tests.rs
  • crates/cli/tests/cli_tests.rs
  • crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs
  • crates/cli/tests/coverage/shared/plugins_tests.rs
🔇 Additional comments (43)
crates/cli/src/commands/mod.rs (2)

56-97: LGTM!

Also applies to: 99-107


56-107: 📐 Maintainability & Code Quality

Run the mandated Rust validation.

The supplied context does not confirm execution of the required Rust commands. Run cargo fmt --all, cargo clippy --workspace --all-targets -- -D warnings, and just test-rust before merge.

  • crates/cli/src/commands/mod.rs#L56-L107: validate the logging setup extraction.
  • crates/cli/src/process/launcher.rs#L709-L779: validate the observability destination helper extraction.
  • crates/cli/src/server/mod.rs#L415-L454: validate the server teardown logging helper extraction.

As per coding guidelines, “Any Rust change must run just test-rust” and must run the specified formatter and Clippy commands.

Source: Coding guidelines

crates/cli/src/process/launcher.rs (1)

709-713: LGTM!

Also applies to: 715-737, 739-759, 761-779

crates/cli/src/server/mod.rs (1)

415-418: LGTM!

Also applies to: 431-454

crates/cli/tests/cli_tests.rs (2)

726-726: LGTM!

Also applies to: 755-755, 775-813


1984-1984: LGTM!

Also applies to: 2102-2102, 3528-3528, 3907-3907, 4003-4003

crates/cli/tests/coverage/shared/config_tests.rs (1)

4151-4188: LGTM!

Also applies to: 4189-4238, 4240-4255

crates/cli/tests/coverage/shared/installer_tests.rs (1)

583-589: LGTM!

Also applies to: 589-605, 607-640, 642-670

crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs (1)

1921-1921: LGTM!

Also applies to: 1945-1946, 1989-2045, 4259-4346, 4348-4401, 4403-4415, 4519-4534

crates/cli/tests/coverage/shared/server_tests.rs (1)

676-705: LGTM!

Also applies to: 2149-2190, 2192-2251, 2253-2268

crates/cli/src/commands/configure/editor.rs (1)

17-17: LGTM!

Also applies to: 38-43

crates/cli/src/commands/configure/editor/prompt.rs (2)

19-56: LGTM!


280-309: LGTM!

Also applies to: 311-342, 415-440

crates/cli/src/commands/configure/wizard.rs (2)

39-44: LGTM!


6-31: 🩺 Stability & Availability

No issue found. plugin_prompt_was_interrupted remains defined and resolves from prompt.rs. The test module is #[cfg(test)], and the gated imports are used by the test module.

			> Likely an incorrect or invalid review comment.
crates/cli/src/commands/configure/wizard/prompt.rs (4)

25-74: LGTM!


112-147: LGTM!

Also applies to: 149-161


201-257: LGTM!

Also applies to: 262-299


76-79: 🚀 Performance & Scalability

Keep run async. Its caller awaits it, and the CLI uses a multi-threaded Tokio runtime. The synchronous wizard can block one worker thread during terminal input, but this matches the pre-refactor behavior and requires no change.

crates/cli/tests/coverage/commands/configure_editor_tests.rs (2)

303-303: LGTM!

Also applies to: 306-338


342-357: LGTM!

crates/cli/src/plugins/dynamic_editor.rs (2)

25-25: LGTM!

Also applies to: 153-156


363-363: 📐 Maintainability & Code Quality

Retain pub(super) for the tested helpers.

plugins_tests is an in-crate sibling of dynamic_editor, so these calls require visibility through crate::plugins. pub(in crate::plugins::dynamic_editor) would exclude the test module. The widening is unnecessary for prompt.rs but required by the tests.

			> Likely an incorrect or invalid review comment.
crates/cli/src/plugins/dynamic_editor/prompt.rs (3)

37-105: LGTM!

Also applies to: 107-162


164-277: LGTM!

Also applies to: 317-350


289-289: 📐 Maintainability & Code Quality

No change is required. The workspace does not enable clippy::uninlined_format_args, and this lint is not enabled by default.

			> Likely an incorrect or invalid review comment.
crates/cli/src/plugins/mod.rs (3)

4-10: LGTM!

Also applies to: 23-23, 106-108


235-246: LGTM!


32-35: 📐 Maintainability & Code Quality

Keep these imports private; do not use pub(crate) use. Descendant modules can use private parent imports. pub(crate) use is rejected because these functions are pub(super). Direct imports from crate::plugins::prompt are optional.

			> Likely an incorrect or invalid review comment.
crates/cli/src/plugins/prompt.rs (6)

14-55: LGTM!

Also applies to: 57-122, 124-189


191-234: LGTM!

Also applies to: 289-299


675-728: LGTM!

Also applies to: 730-830


1254-1266: LGTM!


1194-1202: 🎯 Functional Correctness

No change needed: parse_float_value is still defined. crates/cli/src/plugins/mod.rs:742 defines it, and use super::* resolves the call.

			> Likely an incorrect or invalid review comment.

1220-1233: 🩺 Stability & Availability

Do not require an empty-enum guard for safety.

dialoguer::Select::interact() returns an error for an empty list, and editor_error converts it to CliError::Config; values[idx] is not reached. An explicit field-specific error remains an optional clarity improvement.

			> Likely an incorrect or invalid review comment.
crates/cli/tests/coverage/shared/plugins_schema_tests.rs (1)

275-278: LGTM!

Also applies to: 319-334

crates/cli/tests/coverage/shared/plugins_tests.rs (6)

323-326: LGTM!

Also applies to: 328-340, 342-408, 410-431


636-657: LGTM!

Also applies to: 659-714, 716-744


790-807: LGTM!

Also applies to: 1881-1907


2071-2103: LGTM!

Also applies to: 2105-2150


2178-2193: LGTM!

Also applies to: 2195-2261, 2324-2374, 2376-2399, 2401-2412


760-788: 📐 Maintainability & Code Quality

No change needed. editable_components adds Switchyard under the same feature gate as the enum variant, so the test covers every variant present in the active build. The shown CI job only publishes nemo-relay-switchyard; this does not make the test name inaccurate.

			> Likely an incorrect or invalid review comment.
codecov.yml (1)

138-143: 🗄️ Data Integrity & Integration

No Codecov path update is required. The component paths are broad, all four prompt files exist, and no stale references remain.

			> Likely an incorrect or invalid review comment.

Comment on lines +213 to +219
.validate_with(|value: &String| {
if value.trim().is_empty() {
Err("value must not be empty; use Clear to remove it")
} else {
Ok(())
}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unify the two empty-value validators.

Line 213 returns Err(&'static str). Line 295 returns Err(String). Line 353 also returns Err(String). The messages also differ: "value must not be empty; use Clear to remove it" versus "value must not be empty". Three near-identical closures exist in this file. Extract one helper.

♻️ Proposed helper
fn require_nonempty(value: &String) -> Result<(), &'static str> {
    if value.trim().is_empty() {
        Err("value must not be empty")
    } else {
        Ok(())
    }
}

Then use .validate_with(require_nonempty) at lines 213, 295, and 353.

Also applies to: 295-301

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/src/commands/configure/editor/prompt.rs` around lines 213 - 219,
Extract a shared require_nonempty validator returning Result<(), &'static str>
with the unified "value must not be empty" message, then replace the inline
empty-value validation closures at the three affected prompt locations with
.validate_with(require_nonempty).

Comment on lines +87 to +94
let doc = build_config(&answers);
let preview_paths = preview_paths(answers.scope, &cwd, &home);

if !confirm_summary(&preview_paths, &doc)? {
return Err(CliError::Config("setup cancelled — no config saved".into()));
}

let written = save_config(&doc, answers.scope, &cwd, &home, agent_hint)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== write_or_merge full body ==="
ast-grep run --pattern 'fn write_or_merge($$$) { $$$ }' --lang rust crates/cli/src/commands/configure/model.rs

echo "=== build_config full body ==="
ast-grep run --pattern 'fn build_config($$$) { $$$ }' --lang rust crates/cli/src/commands/configure/model.rs

echo "=== every save_config call site ==="
rg -nP --type=rust -C 4 '\bsave_config\s*\(' crates/cli

echo "=== tests that assert non-agent sections survive a wizard re-run ==="
rg -nP --type=rust -C 5 'upstream|gateway|logging' crates/cli/src/commands/configure/wizard/tests.rs 2>/dev/null || \
  fd -t f 'tests.rs' crates/cli/src/commands/configure --exec rg -nP -C 5 'write_or_merge|save_config' {}

Repository: NVIDIA/NeMo-Relay

Length of output: 188


🏁 Script executed:

set -u

echo "=== candidate files ==="
fd -t f . crates/cli/src/commands/configure | sort

echo "=== model outline ==="
ast-grep outline crates/cli/src/commands/configure/model.rs --lang rust 2>&1 || true

echo "=== wizard prompt outline ==="
ast-grep outline crates/cli/src/commands/configure/wizard/prompt.rs --lang rust 2>&1 || true

echo "=== relevant model symbols ==="
rg -n -C 8 'write_or_merge|build_config|save_config|merge_scope|ConfigDocument|toml' crates/cli/src/commands/configure/model.rs

echo "=== save_config call sites ==="
rg -n -C 6 '\bsave_config\s*\(' crates/cli/src/commands/configure

echo "=== configure tests ==="
rg -n -C 5 'write_or_merge|save_config|upstream|gateway|logging|agents' crates/cli --glob '*.rs' | head -400

Repository: NVIDIA/NeMo-Relay

Length of output: 50373


🏁 Script executed:

set -u

echo "=== wizard run and agent_hint construction ==="
sed -n '1,125p' crates/cli/src/commands/configure/wizard/prompt.rs

echo "=== configure command dispatch ==="
rg -n -C 8 'prompt_user|wizard|agent_hint|run\(' crates/cli/src/commands/configure crates/cli/src/commands/configure.rs 2>/dev/null || true

echo "=== focused model implementation ==="
sed -n '100,230p' crates/cli/src/commands/configure/model.rs

echo "=== all configure-related test files and symbols ==="
fd -t f -e rs . crates/cli | rg 'configure|wizard|model' | sort
rg -n -C 4 'build_config|write_or_merge|merge_agents_entry|save_config|agent_hint|ConfigScope' crates/cli --glob '*.rs' --glob '*test*' | head -300

Repository: NVIDIA/NeMo-Relay

Length of output: 50373


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
import tomllib

model = Path("crates/cli/src/commands/configure/model.rs").read_text()
prompt = Path("crates/cli/src/commands/configure/wizard/prompt.rs").read_text()
configure = Path("crates/cli/src/commands/configure/mod.rs").read_text()

build = model[model.index("pub(crate) fn build_config"):model.index("pub(crate) fn build_agents_table")]
write = model[model.index("pub(crate) fn write_or_merge"):model.index("// Replaces the single")]
run = prompt[prompt.index("pub(super) async fn run"):prompt.index("/// After the base config")]
dispatch = configure[configure.index("pub(crate) async fn run"):]

assert 'doc["agents"]' in build
assert all(section not in build for section in ('"upstream"', '"gateway"', '"logging"'))
assert 'let Some(agent) = merge_scope else' in write
assert 'std::fs::write(path, doc.to_string())?;' in write
assert 'return Ok(());' in write
assert 'save_config(&doc, answers.scope, &cwd, &home, agent_hint)?' in run
assert 'wizard::run(agent, explicit_plugin_path).await?' in dispatch

existing = """[agents.claude]
command = "claude"

[upstream]
openai_base_url = "https://example.test"

[gateway]
bind = "127.0.0.1:8080"

[logging]
level = "debug"
"""
wizard_doc = """[agents]
[agents.codex]
command = "codex"
"""

# This models the exact None branch: write the wizard document instead of merging.
result = wizard_doc
before = tomllib.loads(existing)
after = tomllib.loads(result)
assert set(before) == {"agents", "upstream", "gateway", "logging"}
assert set(after) == {"agents"}
assert {"upstream", "gateway", "logging"} - set(after) == {"upstream", "gateway", "logging"}

print("None merge_scope overwrites an existing config with the agents-only wizard document.")
print("Dropped sections:", ", ".join(sorted(set(before) - set(after))))
print("Unhinted config path passes agent_hint=None to save_config.")
PY

Repository: NVIDIA/NeMo-Relay

Length of output: 275


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
import re

model = Path("crates/cli/src/commands/configure/model.rs").read_text()
prompt = Path("crates/cli/src/commands/configure/wizard/prompt.rs").read_text()
configure = Path("crates/cli/src/commands/configure/mod.rs").read_text()

build = model[model.index("pub(crate) fn build_config"):model.index("pub(crate) fn build_agents_table")]
write = model[model.index("pub(crate) fn write_or_merge"):model.index("// Replaces the single")]
run = prompt[prompt.index("pub(super) async fn run"):prompt.index("/// After the base config")]
dispatch = configure[configure.index("pub(crate) async fn run"):]

assert 'doc["agents"]' in build
assert all(section not in build for section in ('"upstream"', '"gateway"', '"logging"'))
assert 'let Some(agent) = merge_scope else' in write
assert 'std::fs::write(path, doc.to_string())?;' in write
assert 'return Ok(());' in write
assert 'save_config(&doc, answers.scope, &cwd, &home, agent_hint)?' in run
assert 'wizard::run(agent, explicit_plugin_path).await?' in dispatch

existing = """[agents.claude]
command = "claude"

[upstream]
openai_base_url = "https://example.test"

[gateway]
bind = "127.0.0.1:8080"

[logging]
level = "debug"
"""
wizard_doc = """[agents]
[agents.codex]
command = "codex"
"""

def top_level_tables(toml_text):
    return {
        match.group(1)
        for match in re.finditer(r"(?m)^\[([A-Za-z0-9_-]+)(?:\.[^\]]+)?\]", toml_text)
    }

before = top_level_tables(existing)
after = top_level_tables(wizard_doc)
assert before == {"agents", "upstream", "gateway", "logging"}
assert after == {"agents"}
assert {"upstream", "gateway", "logging"} - after == {"upstream", "gateway", "logging"}

print("None merge_scope overwrites an existing config with the agents-only wizard document.")
print("Dropped sections:", ", ".join(sorted(before - after)))
print("Unhinted config path passes agent_hint=None to save_config.")
PY

Repository: NVIDIA/NeMo-Relay

Length of output: 261


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
import re

model_lines = Path("crates/cli/src/commands/configure/model.rs").read_text().splitlines()
prompt = Path("crates/cli/src/commands/configure/wizard/prompt.rs").read_text()
configure = Path("crates/cli/src/commands/configure/mod.rs").read_text()

def region(lines, start, end):
    i = next(i for i, line in enumerate(lines) if start in line)
    j = next((j for j in range(i + 1, len(lines)) if end in lines[j]), len(lines))
    return "\n".join(lines[i:j])

build = region(model_lines, "pub(crate) fn build_config", "pub(crate) fn build_agents_table")
write = region(model_lines, "pub(crate) fn write_or_merge", "// Replaces the single")

assert 'doc["agents"]' in build
assert all(section not in build for section in ('"upstream"', '"gateway"', '"logging"'))
assert 'let Some(agent) = merge_scope else' in write
assert 'std::fs::write(path, doc.to_string())?;' in write
assert 'return Ok(());' in write
assert 'save_config(&doc, answers.scope, &cwd, &home, agent_hint)?' in prompt
assert 'wizard::run(agent, explicit_plugin_path).await?' in configure

existing = """[agents.claude]
command = "claude"

[upstream]
openai_base_url = "https://example.test"

[gateway]
bind = "127.0.0.1:8080"

[logging]
level = "debug"
"""
wizard_doc = """[agents]
[agents.codex]
command = "codex"
"""

def top_level_tables(text):
    return {
        m.group(1)
        for m in re.finditer(r"(?m)^\[([A-Za-z0-9_-]+)(?:\.[^\]]+)?\]", text)
    }

before = top_level_tables(existing)
after = top_level_tables(wizard_doc)
assert before == {"agents", "upstream", "gateway", "logging"}
assert after == {"agents"}

print("None merge_scope overwrites an existing config with the agents-only wizard document.")
print("Dropped sections:", ", ".join(sorted(before - after)))
print("Unhinted config path passes agent_hint=None to save_config.")
PY

Repository: NVIDIA/NeMo-Relay

Length of output: 345


Preserve existing sections when agent_hint is None. nemo-relay config passes None to write_or_merge, which overwrites the file with the agents-only document from build_config. A rerun therefore deletes existing [upstream], [gateway], [logging], and other non-agent sections without showing this in the confirmation. Merge non-agent sections or require an explicit destructive confirmation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/src/commands/configure/wizard/prompt.rs` around lines 87 - 94,
Update the configuration save flow around build_config, confirm_summary, and
save_config so agent_hint being None preserves existing non-agent sections
instead of overwriting them. Merge the current file’s sections such as upstream,
gateway, and logging into the document before preview and saving, or require an
explicit destructive confirmation that accurately displays their removal.

Comment on lines +179 to +188
fn ensure_tty() -> Result<(), CliError> {
if !std::io::stdin().is_terminal() {
return Err(CliError::Config(
"interactive setup requires a TTY; pass `--config <path>` or set up \
`.nemo-relay/config.toml` manually"
.into(),
));
}
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Three divergent ensure_tty guards now exist across the extracted prompt modules. The extraction created one TTY guard per prompt module. Each checks a different set of streams and emits a different message, with no shared definition, so the checks can drift independently.

Current state:

Location Streams checked Message
crates/cli/src/commands/configure/wizard/prompt.rs L180 stdin "interactive setup requires a TTY; pass --config <path> ..."
crates/cli/src/commands/configure/editor/prompt.rs L59 stdin "interactive configuration editing requires a TTY"
crates/cli/src/plugins/prompt.rs L290-292 stdin, stdout, stderr "interactive plugin editing requires a TTY"

All three modules render menus through dialoguer, which writes to the terminal. The two stdin-only guards accept a session where stdout or stderr is redirected. In that case the menu output disappears while the prompt still blocks on input.

Both stdin-only variants match their pre-refactor behavior, so neither is a regression. The duplication is new.

  • crates/cli/src/commands/configure/wizard/prompt.rs#L179-L188: replace the inline body with a call to a shared helper that takes the required streams and the message.
  • crates/cli/src/commands/configure/editor/prompt.rs#L58-L60: keep ensure_tty_with for its testability, and route it through the same shared helper so the stream set is declared in one place.
♻️ Sketch of a shared guard
// crates/cli/src/tty.rs
use std::io::IsTerminal;

#[derive(Clone, Copy)]
pub(crate) struct TtyRequirement {
    pub stdin: bool,
    pub stdout: bool,
    pub stderr: bool,
}

impl TtyRequirement {
    pub(crate) const INTERACTIVE_MENU: Self = Self { stdin: true, stdout: true, stderr: true };

    pub(crate) fn is_satisfied(self) -> bool {
        (!self.stdin || std::io::stdin().is_terminal())
            && (!self.stdout || std::io::stdout().is_terminal())
            && (!self.stderr || std::io::stderr().is_terminal())
    }
}

pub(crate) fn ensure_tty(requirement: TtyRequirement, message: &str) -> Result<(), CliError> {
    if requirement.is_satisfied() {
        return Ok(());
    }
    Err(CliError::Config(message.to_owned()))
}
#!/bin/bash
set -euo pipefail

echo "=== every TTY guard in the CLI crate ==="
rg -nP --type=rust -B 2 -A 12 '\bfn ensure_tty(_with)?\s*\(' crates/cli/src

echo "=== every is_terminal() call site ==="
rg -nP --type=rust -C 3 '\bis_terminal\s*\(\)' crates/cli/src

echo "=== tests pinning the exact guard messages ==="
rg -nP --type=rust -C 3 'requires a TTY' crates/cli
📍 Affects 2 files
  • crates/cli/src/commands/configure/wizard/prompt.rs#L179-L188 (this comment)
  • crates/cli/src/commands/configure/editor/prompt.rs#L58-L60
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/src/commands/configure/wizard/prompt.rs` around lines 179 - 188,
Unify the duplicated TTY validation through a shared helper in the CLI TTY
module. In crates/cli/src/commands/configure/wizard/prompt.rs lines 179-188,
replace ensure_tty’s inline stdin-only check with the shared helper using the
interactive menu stream requirement and preserve its message; in
crates/cli/src/commands/configure/editor/prompt.rs lines 58-60, retain
ensure_tty_with for testability but delegate it to the same helper with its
required streams and message. Ensure the existing plugins prompt guard also uses
this shared definition.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Third duplicated ensure_tty implementation.

This PR now has three separate ensure_tty functions:

  • crates/cli/src/commands/configure/editor/prompt.rs line 58 — delegates to ensure_tty_with(stdin.is_terminal()).
  • crates/cli/src/plugins/prompt.rs line 289 — checks stdin, stdout, and stderr.
  • This one, line 180 — checks stdin only.

Each carries a different message and a different set of stream checks. Consider one shared helper that takes the required streams and the error message, so the checks cannot drift further.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/src/commands/configure/wizard/prompt.rs` around lines 179 - 188,
Consolidate the duplicated ensure_tty implementations by introducing one shared
helper that accepts the required stream-readiness checks and error message.
Update the configure editor prompt, plugin prompt, and wizard prompt callers to
use it, preserving each caller’s required stdin/stdout/stderr checks and
user-facing message while removing their local ensure_tty definitions.

Comment on lines +14 to +35
pub(super) fn edit_dynamic_plugin(
theme: &ColorfulTheme,
state: &mut DynamicPluginEditorState,
) -> Result<(), CliError> {
if let Some(description) = &state.description {
println!(" {}", single_line_text(description));
}
let fields = state
.schema
.as_ref()
.map(|schema| schema.fields().to_vec())
.unwrap_or_default();
if state.schema.is_none() || fields.is_empty() {
edit_dynamic_root_menu(theme, state, &fields)
} else {
let prompt = state
.editor_title
.clone()
.unwrap_or_else(|| state.label.clone());
edit_dynamic_fields_menu(theme, state, &fields, &[], prompt)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant condition at line 26.

fields is built at lines 21-25 from state.schema. When state.schema is None, unwrap_or_default() yields an empty vector. So fields.is_empty() is already true whenever state.schema.is_none() is true. The first operand never changes the result.

Keeping it is harmless and arguably documents intent. Reduce to fields.is_empty() if you prefer the minimal form.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/src/plugins/dynamic_editor/prompt.rs` around lines 14 - 35, The
condition in edit_dynamic_plugin redundantly checks state.schema.is_none()
because fields is empty in that case; simplify the menu selection condition to
fields.is_empty(), preserving the existing root-menu and fields-menu behavior.

Comment thread crates/cli/src/plugins/dynamic_editor/prompt.rs
Comment on lines +4417 to +4517
#[test]
fn lifecycle_commands_cover_json_and_human_output_paths() {
let temp = tempfile::tempdir().unwrap();
let _env = EnvScope::hermetic(&temp);
let _cwd = CurrentDirGuard::enter(temp.path());
let server = GatewayOverrides::default();

list(
PluginsListRequest {
all: false,
json: false,
},
&server,
)
.unwrap();
list(
PluginsListRequest {
all: false,
json: true,
},
&server,
)
.unwrap();

let plugin_dir = temp.path().join("output-plugin");
std::fs::create_dir_all(&plugin_dir).unwrap();
let manifest_path = write_dynamic_manifest(&plugin_dir, "acme.output");
add(
PluginsAddRequest {
scope: ConfigurationScope::Project,
path: plugin_dir,
},
&server,
)
.unwrap();

validate(
PluginsValidateRequest {
target: manifest_path.display().to_string(),
json: false,
},
&server,
)
.unwrap();
validate(
PluginsValidateRequest {
target: manifest_path.display().to_string(),
json: true,
},
&server,
)
.unwrap();
validate(
PluginsValidateRequest {
target: "acme.output".into(),
json: false,
},
&server,
)
.unwrap();
validate(
PluginsValidateRequest {
target: "acme.output".into(),
json: true,
},
&server,
)
.unwrap();
list(
PluginsListRequest {
all: false,
json: false,
},
&server,
)
.unwrap();
list(
PluginsListRequest {
all: false,
json: true,
},
&server,
)
.unwrap();
inspect(
PluginsInspectRequest {
id: "acme.output".into(),
json: false,
},
&server,
)
.unwrap();
inspect(
PluginsInspectRequest {
id: "acme.output".into(),
json: true,
},
&server,
)
.unwrap();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add content assertions to lifecycle_commands_cover_json_and_human_output_paths.

This test calls list, add, validate, and inspect in both human and JSON output modes, but every call only checks .unwrap(). No assertion verifies the JSON envelope fields (schema_version, ok, command, data) or the human-readable output content. A regression that breaks the JSON schema or omits a field for these commands would not fail this test.

Add assertions similar to other tests in this file, for example parse the JSON output and check parsed["ok"], parsed["command"], and a key field under parsed["data"] for each call.

As per path instructions, "Prefer assertions on lifecycle events, scope stacks, middleware ordering, and binding parity over shallow smoke tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/tests/coverage/shared/plugins_lifecycle_tests.rs` around lines
4417 - 4517, Strengthen lifecycle_commands_cover_json_and_human_output_paths by
capturing each list, add, validate, and inspect result instead of only calling
unwrap. For JSON responses, parse the output and assert schema_version, ok,
command, and a representative field under data; for human responses, assert
meaningful command-specific text. Follow assertion patterns used elsewhere in
this test file and include lifecycle-event or scope-stack behavior where
available rather than relying only on successful completion.

Source: Path instructions

Comment on lines +280 to +286
fn native_config_field<'a>(schema: &'a PluginConfigSchema, key: &str) -> &'a DynamicConfigField {
schema
.fields()
.iter()
.find(|field| field.key == key)
.unwrap()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Name the missing key in the panic message.

This helper is now the single lookup point for roughly fifteen field assertions across assert_native_raw_and_scalar_fields and assert_native_choice_and_nested_fields. On failure, .unwrap() prints only called Option::unwrap() on a None value with a line number inside the helper, not the key that was missing.

💚 Proposed fix
 fn native_config_field<'a>(schema: &'a PluginConfigSchema, key: &str) -> &'a DynamicConfigField {
     schema
         .fields()
         .iter()
         .find(|field| field.key == key)
-        .unwrap()
+        .unwrap_or_else(|| {
+            panic!(
+                "schema has no field {key:?}; available: {:?}",
+                schema.fields().iter().map(|f| &f.key).collect::<Vec<_>>()
+            )
+        })
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn native_config_field<'a>(schema: &'a PluginConfigSchema, key: &str) -> &'a DynamicConfigField {
schema
.fields()
.iter()
.find(|field| field.key == key)
.unwrap()
}
fn native_config_field<'a>(schema: &'a PluginConfigSchema, key: &str) -> &'a DynamicConfigField {
schema
.fields()
.iter()
.find(|field| field.key == key)
.unwrap_or_else(|| {
panic!(
"schema has no field {key:?}; available: {:?}",
schema.fields().iter().map(|f| &f.key).collect::<Vec<_>>()
)
})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/tests/coverage/shared/plugins_schema_tests.rs` around lines 280 -
286, Update native_config_field to replace the bare unwrap with an expectation
that includes the requested key, so lookup failures identify which schema field
is missing while preserving the existing return behavior.

Comment on lines +599 to +634
#[test]
fn menu_keys_cover_selection_shortcuts_and_cancellation() {
assert_eq!(
menu_response_for_key(&Key::Enter, 2),
Some(MenuResponse::Selected(2))
);
assert_eq!(
menu_response_for_key(&Key::Char(' '), 3),
Some(MenuResponse::Selected(3))
);
assert_eq!(
menu_response_for_key(&Key::Char('p'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Preview, 1))
);
assert_eq!(
menu_response_for_key(&Key::Char('s'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Save, 1))
);
assert_eq!(
menu_response_for_key(&Key::Char('r'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Reset, 1))
);
assert_eq!(
menu_response_for_key(&Key::Backspace, 1),
Some(MenuResponse::Shortcut(MenuShortcut::Clear, 1))
);
assert_eq!(
menu_response_for_key(&Key::Char('?'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Help, 1))
);
assert_eq!(
menu_response_for_key(&Key::Escape, 1),
Some(MenuResponse::Cancel)
);
assert_eq!(menu_response_for_key(&Key::Char('x'), 1), None);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The cancellation coverage is incomplete for the mapping under test.

menu_response_for_key in crates/cli/src/plugins/mod.rs lines 235-246 defines three multi-key arms. This test exercises only one key from two of them:

  • Line 241 maps Key::Backspace | Key::Del to Clear. The test covers Backspace at line 622. Del is untested.
  • Line 243 maps Key::Escape | Key::CtrlC | Key::Char('q') to Cancel. The test covers Escape at line 630. CtrlC and Char('q') are untested.

Key::CtrlC is the primary cancellation path for a terminal UI. If someone reorders that arm so CtrlC falls through to _ => None, prompt_menu at crates/cli/src/plugins/prompt.rs lines 229-232 would ignore Ctrl-C and loop, and this test would still pass.

Key::Char('q') is documented to the user in the help text at crates/cli/src/plugins/prompt.rs line 286 ("q or Esc go back/cancel"), so it is a promised behavior.

💚 Proposed additions
     assert_eq!(
         menu_response_for_key(&Key::Backspace, 1),
         Some(MenuResponse::Shortcut(MenuShortcut::Clear, 1))
     );
+    assert_eq!(
+        menu_response_for_key(&Key::Del, 1),
+        Some(MenuResponse::Shortcut(MenuShortcut::Clear, 1))
+    );
     assert_eq!(
         menu_response_for_key(&Key::Char('?'), 1),
         Some(MenuResponse::Shortcut(MenuShortcut::Help, 1))
     );
     assert_eq!(
         menu_response_for_key(&Key::Escape, 1),
         Some(MenuResponse::Cancel)
     );
+    assert_eq!(
+        menu_response_for_key(&Key::CtrlC, 1),
+        Some(MenuResponse::Cancel)
+    );
+    assert_eq!(
+        menu_response_for_key(&Key::Char('q'), 1),
+        Some(MenuResponse::Cancel)
+    );
     assert_eq!(menu_response_for_key(&Key::Char('x'), 1), None);

As per path instructions: "Tests should cover the behavior promised by the changed API surface, including error paths and cross-request isolation where relevant."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn menu_keys_cover_selection_shortcuts_and_cancellation() {
assert_eq!(
menu_response_for_key(&Key::Enter, 2),
Some(MenuResponse::Selected(2))
);
assert_eq!(
menu_response_for_key(&Key::Char(' '), 3),
Some(MenuResponse::Selected(3))
);
assert_eq!(
menu_response_for_key(&Key::Char('p'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Preview, 1))
);
assert_eq!(
menu_response_for_key(&Key::Char('s'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Save, 1))
);
assert_eq!(
menu_response_for_key(&Key::Char('r'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Reset, 1))
);
assert_eq!(
menu_response_for_key(&Key::Backspace, 1),
Some(MenuResponse::Shortcut(MenuShortcut::Clear, 1))
);
assert_eq!(
menu_response_for_key(&Key::Char('?'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Help, 1))
);
assert_eq!(
menu_response_for_key(&Key::Escape, 1),
Some(MenuResponse::Cancel)
);
assert_eq!(menu_response_for_key(&Key::Char('x'), 1), None);
}
#[test]
fn menu_keys_cover_selection_shortcuts_and_cancellation() {
assert_eq!(
menu_response_for_key(&Key::Enter, 2),
Some(MenuResponse::Selected(2))
);
assert_eq!(
menu_response_for_key(&Key::Char(' '), 3),
Some(MenuResponse::Selected(3))
);
assert_eq!(
menu_response_for_key(&Key::Char('p'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Preview, 1))
);
assert_eq!(
menu_response_for_key(&Key::Char('s'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Save, 1))
);
assert_eq!(
menu_response_for_key(&Key::Char('r'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Reset, 1))
);
assert_eq!(
menu_response_for_key(&Key::Backspace, 1),
Some(MenuResponse::Shortcut(MenuShortcut::Clear, 1))
);
assert_eq!(
menu_response_for_key(&Key::Del, 1),
Some(MenuResponse::Shortcut(MenuShortcut::Clear, 1))
);
assert_eq!(
menu_response_for_key(&Key::Char('?'), 1),
Some(MenuResponse::Shortcut(MenuShortcut::Help, 1))
);
assert_eq!(
menu_response_for_key(&Key::Escape, 1),
Some(MenuResponse::Cancel)
);
assert_eq!(
menu_response_for_key(&Key::CtrlC, 1),
Some(MenuResponse::Cancel)
);
assert_eq!(
menu_response_for_key(&Key::Char('q'), 1),
Some(MenuResponse::Cancel)
);
assert_eq!(menu_response_for_key(&Key::Char('x'), 1), None);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/tests/coverage/shared/plugins_tests.rs` around lines 599 - 634,
Expand menu_keys_cover_selection_shortcuts_and_cancellation to assert every key
in the multi-key mappings handled by menu_response_for_key: add Del as Clear,
and CtrlC plus Char('q') as Cancel. Keep the existing assertions and expected
response indices unchanged.

Source: Path instructions

Comment on lines +746 to +758
#[test]
fn component_shortcut_fallbacks_are_safe_noops() {
let mut config = PluginConfig::default();
ensure_observability_component(&mut config).unwrap();
let mut components = editable_components(&config).unwrap();

assert_eq!(
handle_reset_or_clear_shortcut(&mut components, None, MenuShortcut::Reset).unwrap(),
EditLoopControl::Continue
);
reset_component_menu_item(&mut components[0], None).unwrap();
clear_component_menu_item(&mut components[0], Some(ComponentMenuAction::Back)).unwrap();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This test never asserts the no-op it is named for.

The test is called component_shortcut_fallbacks_are_safe_noops. Lines 756 and 757 call reset_component_menu_item(&mut components[0], None) and clear_component_menu_item(&mut components[0], Some(ComponentMenuAction::Back)) and only unwrap the result.

Nothing checks that components[0] is unchanged. A regression that makes the None action reset the component, or makes Back clear a field, still returns Ok and still passes.

Capture the component state before the calls and compare after.

💚 Proposed fix
     assert_eq!(
         handle_reset_or_clear_shortcut(&mut components, None, MenuShortcut::Reset).unwrap(),
         EditLoopControl::Continue
     );
+    let before = config_with_editable_components(&config, &components).unwrap();
     reset_component_menu_item(&mut components[0], None).unwrap();
     clear_component_menu_item(&mut components[0], Some(ComponentMenuAction::Back)).unwrap();
+    let after = config_with_editable_components(&config, &components).unwrap();
+    assert_eq!(
+        serde_json::to_value(before).unwrap(),
+        serde_json::to_value(after).unwrap(),
+        "fallback shortcut actions must not mutate component state"
+    );
 }

As per path instructions: "Prefer assertions on lifecycle events, scope stacks, middleware ordering, and binding parity over shallow smoke tests."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn component_shortcut_fallbacks_are_safe_noops() {
let mut config = PluginConfig::default();
ensure_observability_component(&mut config).unwrap();
let mut components = editable_components(&config).unwrap();
assert_eq!(
handle_reset_or_clear_shortcut(&mut components, None, MenuShortcut::Reset).unwrap(),
EditLoopControl::Continue
);
reset_component_menu_item(&mut components[0], None).unwrap();
clear_component_menu_item(&mut components[0], Some(ComponentMenuAction::Back)).unwrap();
}
#[test]
fn component_shortcut_fallbacks_are_safe_noops() {
let mut config = PluginConfig::default();
ensure_observability_component(&mut config).unwrap();
let mut components = editable_components(&config).unwrap();
assert_eq!(
handle_reset_or_clear_shortcut(&mut components, None, MenuShortcut::Reset).unwrap(),
EditLoopControl::Continue
);
let before = config_with_editable_components(&config, &components).unwrap();
reset_component_menu_item(&mut components[0], None).unwrap();
clear_component_menu_item(&mut components[0], Some(ComponentMenuAction::Back)).unwrap();
let after = config_with_editable_components(&config, &components).unwrap();
assert_eq!(
serde_json::to_value(before).unwrap(),
serde_json::to_value(after).unwrap(),
"fallback shortcut actions must not mutate component state"
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/tests/coverage/shared/plugins_tests.rs` around lines 746 - 758,
Strengthen component_shortcut_fallbacks_are_safe_noops by capturing
components[0] before calling reset_component_menu_item and
clear_component_menu_item, then asserting it remains unchanged afterward. Keep
the existing successful-result checks and verify both None and Back fallback
actions preserve the component state.

Source: Path instructions

Comment on lines +2152 to +2176
#[test]
fn dynamic_editor_raw_menu_and_nested_value_paths_are_deterministic() {
let temp = tempfile::tempdir().unwrap();
write_editor_dynamic_manifest(&temp.path().join("plugin"), "acme.raw-menu", None, None);
let path = temp.path().join("plugins.toml");
std::fs::write(
&path,
"[[plugins.dynamic]]\nmanifest = \"./plugin/relay-plugin.toml\"\n",
)
.unwrap();
let document = PluginConfigDocument::read(&path).unwrap();
let states = load_dynamic_plugin_states(&document).unwrap();
let (items, actions) = dynamic_root_menu_items(&states[0], &[]);
assert_eq!(items.len(), 3);
assert!(matches!(actions[0], DynamicMenuAction::EditRawConfig));

let mut config = None;
let path = vec!["outer".to_owned(), "inner".to_owned()];
set_value_at_path(&mut config, &path, json!(7));
assert_eq!(value_at_path(config.as_ref(), &path), Some(&json!(7)));
assert_eq!(value_at_path(config.as_ref(), &[]), None);
assert!(remove_value_at_path(config.as_mut().unwrap(), &path));
set_value_at_path(&mut config, &[], json!(9));
assert!(config.as_ref().unwrap().is_empty());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Shadowed path binding with two different types.

Line 2156 binds path to the plugins.toml PathBuf. Line 2169 rebinds path to a Vec<String> field path. Both are live in the same function body.

The code is correct. The name reuse makes lines 2170-2174 harder to read, because path there means a JSON field path, not a filesystem path.

Rename the second binding to field_path.

The error-path coverage in this block is good: line 2172 checks the empty-path lookup, and line 2174 checks the empty-path write no-op.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/tests/coverage/shared/plugins_tests.rs` around lines 2152 - 2176,
Rename the second `path` binding in
`dynamic_editor_raw_menu_and_nested_value_paths_are_deterministic` to
`field_path`, and update the related `set_value_at_path`, `value_at_path`, and
`remove_value_at_path` calls to use it; leave the filesystem `path` binding
unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lang:rust PR changes/introduces Rust code size:XXL PR is very large Test Test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant