M7: CLI RPC coverage matrix — prove every nuncio.v1 RPC has an offline CLI-driven E2E #207

Open
opened 2026-07-28 14:04:57 +00:00 by KofTwentyTwo · 0 comments
KofTwentyTwo commented 2026-07-28 14:04:57 +00:00 (Migrated from github.com)

Context

Story for milestone M7. Read docs/STORY-WORKFLOW.md first — it defines the process, gates, and patterns; this issue defines only the specific work.

crates/nuncio-cli is the in-repo reference client. Checking crates/nuncio-cli/src/runner.rs against every RPC in crates/nuncio-proto/proto/nuncio/v1/nuncio.proto: System.GetStatus (line 1638-1646), Accounts.AddAccount/ListAccounts (lines 1441-1499), Mail.Sync/ListMessages/SendMessage/SearchMessages/ListFolders/GetMessage/MarkRead (lines 692-981), Filters.ListRules/CreateRule/DeleteRule/ValidateRule/PreviewRule (lines 1173-1349), Export.ExportMailbox (lines 1006-1032), and Audit.ListRecords/VerifyChain (lines 1072-1131) are all called from CLI command handlers. System.Subscribe is the one exception: it has a client-side helper (nuncio_proto::client::subscribe_events) and is exercised only by nunciod's own test double at crates/nuncio-cli/src/runner.rs:1857-1891 (a mock System service implementing SubscribeStream for a different test), never by an actual CLI command a user could run. There is also no test today that fails automatically if a future RPC is added to the proto and never wired into any CLI command.

Goal

Close both gaps: add a real CLI command that drives System.Subscribe end-to-end (reads the live event stream and prints events until closed/timeout), and add a coverage-matrix test that decodes the compiled nuncio.v1 descriptor (reusing the prost-types walking approach from the M7-S1 contract-coverage story) and fails if any (service, method) pair in the contract is not listed in a maintained "exercised by" matrix. This is the proof M7 needs that the reference client is complete, not just mostly complete.

Files to touch / patterns to mirror

  • crates/nuncio-cli/src/args.rs — SystemSubcommand enum (lines 348-359, currently Status and Audit); add a Watch variant (e.g. with an optional --count <N> / --timeout-secs <N> bound so the command terminates deterministically in tests instead of hanging forever on a live stream).
  • crates/nuncio-cli/src/runner.rs — dispatch match at lines 363-364 (mirror SystemSubcommand::Status => self.handle_system_status(...)); add handle_system_watch, mirroring query_daemon_status (lines 1631-1649) but calling nuncio_proto::client::subscribe_events and iterating the returned Streaming<Event> with Streaming::message() until it closes, hits the count/timeout bound, or errors.
  • crates/nunciod/tests/spine_e2e_test.rs — reference for booting an in-process daemon with mocked backends (boot_daemon, lines 93-140) to drive the new CLI command against a real (test) daemon rather than mocking the CLI's gRPC client directly.
  • New test module (in crates/nunciod/tests/, e.g. rpc_coverage_matrix_test.rs, or alongside the M7-S1 tests in crates/nuncio-proto/src/lib.rs if that placement proves cleaner — pick whichever crate can see both the compiled descriptor and a maintained matrix without a dependency cycle) — the new coverage-matrix test.

Implementation steps

  1. Add SystemSubcommand::Watch with a bounded exit condition (count of events and/or a timeout) — an unbounded nuncio system watch that never returns would be unusable in an offline E2E test and a footgun interactively.
  2. Implement handle_system_watch in runner.rs: connect via connect_system, call subscribe_events, loop reading Events, print each (respecting --json), stop on the bound or stream close, and return a clear error if the daemon is unreachable — mirror the existing Result<_, String> error-surfacing style used by query_daemon_status and the other handle_* methods, no panics.
  3. Write an offline E2E test (in crates/nunciod/tests/, following spine_e2e_test.rs's boot_daemon pattern) that boots a daemon with a mocked backend that emits at least one real Event onto the shared EventBus, runs the CLI's new system watch --count 1 command against it over real gRPC with the real bearer token, and asserts the event was received and printed — proving Subscribe is reachable from the reference client end to end, not just from nunciod's internal test double.
  4. Add an unauthenticated-rejection check for Subscribe if one does not already exist as part of grpc_auth_test.rs's coverage (verify against crates/nunciod/tests/grpc_auth_test.rs; add it if Subscribe is not already covered there — every RPC, including streaming ones, must reject a missing/invalid bearer token per the security invariant in docs/STORY-WORKFLOW.md §4).
  5. Build the coverage-matrix test: decode the compiled descriptor's (service, method) pairs the same way as the M7-S1 full_surface_is_covered_by_golden test (depends on that story's prost-types decoding groundwork — coordinate order with the PO if M7-S1 hasn't landed yet, or duplicate the small decode helper locally if it must ship first). Maintain an explicit const EXERCISED_RPCS: &[(&str, &str, &str)] array of (service, method, e2e_test_name) triples covering every RPC exercised by the CLI's offline E2E suite (spine_e2e_test.rs, daemon_e2e_test.rs, grpc_auth_test.rs, and the new watch test), and assert every (service, method) pair from the decoded descriptor appears in that array. A new RPC added later without a matching CLI E2E entry fails this test immediately with the missing pair named.
  6. Run cargo verify.

Acceptance criteria

  • nuncio system watch (or the chosen subcommand name) exists, calls System.Subscribe over real gRPC, and terminates deterministically (count/timeout bound) rather than hanging.
  • A new offline E2E test boots a real (test) daemon, drives the new CLI command against it, and asserts a real Event was received and rendered — no fabricated/hardcoded event output.
  • Subscribe is covered by an unauthenticated-rejection E2E assertion (existing or newly added).
  • A coverage-matrix test exists that decodes the live nuncio.v1 descriptor and fails if any (service, method) pair is missing from the maintained EXERCISED_RPCS list; verify by temporarily removing one entry locally and confirming the test fails, then restoring it.
  • Every RPC currently in nuncio.proto (System.GetStatus, System.Subscribe, Accounts.AddAccount, Accounts.ListAccounts, Mail.ListFolders, Mail.ListMessages, Mail.GetMessage, Mail.MarkRead, Mail.SearchMessages, Mail.SendMessage, Mail.Sync, Filters.CreateRule, Filters.ListRules, Filters.DeleteRule, Filters.ValidateRule, Filters.PreviewRule, Export.ExportMailbox, Audit.ListRecords, Audit.VerifyChain) has an entry in EXERCISED_RPCS and a genuine passing offline E2E backing it.
  • cargo verify green (fmt + clippy -D warnings all-targets + test-all), 0 ignored.

Definition of done & process

Follow docs/STORY-WORKFLOW.md: no fabricated data; comment convention (intent-only, no story/GH/phase refs in .rs/.proto); mock-only tests, no live network. Branch story/m7-s3-cli-coverage-matrix off dev; PR to dev with the gate output pasted as evidence; the PO reviews before the issue is closed.

Dependencies / notes

Not a [PROTO] story — adds a CLI command and tests only, no .proto changes. Shares its descriptor-decoding approach with M7-S1; if M7-S1 has not landed yet, either sequence this story after it or inline a small local copy of the decode helper rather than blocking — note whichever choice was made in the PR. If M1–M6 land new RPCs (Calendar, Contacts, filter management/bulk-triage, account lifecycle, explicit-send-account) before this story starts, extend both the CLI coverage and EXERCISED_RPCS to include them — the acceptance criterion is "every RPC in the contract at the time this merges," not a frozen list. Size M.

## Context Story for milestone M7. **Read [`docs/STORY-WORKFLOW.md`](https://github.com/KofTwentyTwo/nuncio/blob/dev/docs/STORY-WORKFLOW.md) first** — it defines the process, gates, and patterns; this issue defines only the specific work. `crates/nuncio-cli` is the in-repo reference client. Checking `crates/nuncio-cli/src/runner.rs` against every RPC in `crates/nuncio-proto/proto/nuncio/v1/nuncio.proto`: `System.GetStatus` (line 1638-1646), `Accounts.AddAccount`/`ListAccounts` (lines 1441-1499), `Mail.Sync`/`ListMessages`/`SendMessage`/`SearchMessages`/`ListFolders`/`GetMessage`/`MarkRead` (lines 692-981), `Filters.ListRules`/`CreateRule`/`DeleteRule`/`ValidateRule`/`PreviewRule` (lines 1173-1349), `Export.ExportMailbox` (lines 1006-1032), and `Audit.ListRecords`/`VerifyChain` (lines 1072-1131) are all called from CLI command handlers. **`System.Subscribe` is the one exception**: it has a client-side helper (`nuncio_proto::client::subscribe_events`) and is exercised only by `nunciod`'s own test double at `crates/nuncio-cli/src/runner.rs:1857-1891` (a mock `System` service implementing `SubscribeStream` for a *different* test), never by an actual CLI command a user could run. There is also no test today that fails automatically if a future RPC is added to the proto and never wired into any CLI command. ## Goal Close both gaps: add a real CLI command that drives `System.Subscribe` end-to-end (reads the live event stream and prints events until closed/timeout), and add a coverage-matrix test that decodes the compiled `nuncio.v1` descriptor (reusing the `prost-types` walking approach from the M7-S1 contract-coverage story) and fails if any `(service, method)` pair in the contract is not listed in a maintained "exercised by" matrix. This is the proof M7 needs that the reference client is complete, not just mostly complete. ## Files to touch / patterns to mirror - `crates/nuncio-cli/src/args.rs` — `SystemSubcommand` enum (lines 348-359, currently `Status` and `Audit`); add a `Watch` variant (e.g. with an optional `--count <N>` / `--timeout-secs <N>` bound so the command terminates deterministically in tests instead of hanging forever on a live stream). - `crates/nuncio-cli/src/runner.rs` — dispatch match at lines 363-364 (mirror `SystemSubcommand::Status => self.handle_system_status(...)`); add `handle_system_watch`, mirroring `query_daemon_status` (lines 1631-1649) but calling `nuncio_proto::client::subscribe_events` and iterating the returned `Streaming<Event>` with `Streaming::message()` until it closes, hits the count/timeout bound, or errors. - `crates/nunciod/tests/spine_e2e_test.rs` — reference for booting an in-process daemon with mocked backends (`boot_daemon`, lines 93-140) to drive the new CLI command against a real (test) daemon rather than mocking the CLI's gRPC client directly. - New test module (in `crates/nunciod/tests/`, e.g. `rpc_coverage_matrix_test.rs`, or alongside the M7-S1 tests in `crates/nuncio-proto/src/lib.rs` if that placement proves cleaner — pick whichever crate can see both the compiled descriptor and a maintained matrix without a dependency cycle) — the new coverage-matrix test. ## Implementation steps 1. Add `SystemSubcommand::Watch` with a bounded exit condition (count of events and/or a timeout) — an unbounded `nuncio system watch` that never returns would be unusable in an offline E2E test and a footgun interactively. 2. Implement `handle_system_watch` in `runner.rs`: connect via `connect_system`, call `subscribe_events`, loop reading `Event`s, print each (respecting `--json`), stop on the bound or stream close, and return a clear error if the daemon is unreachable — mirror the existing `Result<_, String>` error-surfacing style used by `query_daemon_status` and the other `handle_*` methods, no panics. 3. Write an offline E2E test (in `crates/nunciod/tests/`, following `spine_e2e_test.rs`'s `boot_daemon` pattern) that boots a daemon with a mocked backend that emits at least one real `Event` onto the shared `EventBus`, runs the CLI's new `system watch --count 1` command against it over real gRPC with the real bearer token, and asserts the event was received and printed — proving `Subscribe` is reachable from the reference client end to end, not just from `nunciod`'s internal test double. 4. Add an **unauthenticated-rejection** check for `Subscribe` if one does not already exist as part of `grpc_auth_test.rs`'s coverage (verify against `crates/nunciod/tests/grpc_auth_test.rs`; add it if `Subscribe` is not already covered there — every RPC, including streaming ones, must reject a missing/invalid bearer token per the security invariant in `docs/STORY-WORKFLOW.md` §4). 5. Build the coverage-matrix test: decode the compiled descriptor's `(service, method)` pairs the same way as the M7-S1 `full_surface_is_covered_by_golden` test (depends on that story's `prost-types` decoding groundwork — coordinate order with the PO if M7-S1 hasn't landed yet, or duplicate the small decode helper locally if it must ship first). Maintain an explicit `const EXERCISED_RPCS: &[(&str, &str, &str)]` array of `(service, method, e2e_test_name)` triples covering every RPC exercised by the CLI's offline E2E suite (`spine_e2e_test.rs`, `daemon_e2e_test.rs`, `grpc_auth_test.rs`, and the new watch test), and assert every `(service, method)` pair from the decoded descriptor appears in that array. A new RPC added later without a matching CLI E2E entry fails this test immediately with the missing pair named. 6. Run `cargo verify`. ## Acceptance criteria - [ ] `nuncio system watch` (or the chosen subcommand name) exists, calls `System.Subscribe` over real gRPC, and terminates deterministically (count/timeout bound) rather than hanging. - [ ] A new offline E2E test boots a real (test) daemon, drives the new CLI command against it, and asserts a real `Event` was received and rendered — no fabricated/hardcoded event output. - [ ] `Subscribe` is covered by an unauthenticated-rejection E2E assertion (existing or newly added). - [ ] A coverage-matrix test exists that decodes the live `nuncio.v1` descriptor and fails if any `(service, method)` pair is missing from the maintained `EXERCISED_RPCS` list; verify by temporarily removing one entry locally and confirming the test fails, then restoring it. - [ ] Every RPC currently in `nuncio.proto` (`System.GetStatus`, `System.Subscribe`, `Accounts.AddAccount`, `Accounts.ListAccounts`, `Mail.ListFolders`, `Mail.ListMessages`, `Mail.GetMessage`, `Mail.MarkRead`, `Mail.SearchMessages`, `Mail.SendMessage`, `Mail.Sync`, `Filters.CreateRule`, `Filters.ListRules`, `Filters.DeleteRule`, `Filters.ValidateRule`, `Filters.PreviewRule`, `Export.ExportMailbox`, `Audit.ListRecords`, `Audit.VerifyChain`) has an entry in `EXERCISED_RPCS` and a genuine passing offline E2E backing it. - [ ] `cargo verify` green (fmt + clippy `-D warnings` all-targets + test-all), 0 ignored. ## Definition of done & process Follow [`docs/STORY-WORKFLOW.md`](https://github.com/KofTwentyTwo/nuncio/blob/dev/docs/STORY-WORKFLOW.md): no fabricated data; comment convention (intent-only, no story/GH/phase refs in `.rs`/`.proto`); mock-only tests, no live network. Branch `story/m7-s3-cli-coverage-matrix` off `dev`; PR to `dev` with the gate output pasted as evidence; the PO reviews before the issue is closed. ## Dependencies / notes Not a `[PROTO]` story — adds a CLI command and tests only, no `.proto` changes. Shares its descriptor-decoding approach with M7-S1; if M7-S1 has not landed yet, either sequence this story after it or inline a small local copy of the decode helper rather than blocking — note whichever choice was made in the PR. If M1–M6 land new RPCs (Calendar, Contacts, filter management/bulk-triage, account lifecycle, explicit-send-account) before this story starts, extend both the CLI coverage and `EXERCISED_RPCS` to include them — the acceptance criterion is "every RPC in the contract at the time this merges," not a frozen list. Size M.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: KofTwentyTwo/nuncio#207