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
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.
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.
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.
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).
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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Context
Story for milestone M7. Read
docs/STORY-WORKFLOW.mdfirst — it defines the process, gates, and patterns; this issue defines only the specific work.crates/nuncio-cliis the in-repo reference client. Checkingcrates/nuncio-cli/src/runner.rsagainst every RPC incrates/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), andAudit.ListRecords/VerifyChain(lines 1072-1131) are all called from CLI command handlers.System.Subscribeis the one exception: it has a client-side helper (nuncio_proto::client::subscribe_events) and is exercised only bynunciod's own test double atcrates/nuncio-cli/src/runner.rs:1857-1891(a mockSystemservice implementingSubscribeStreamfor 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.Subscribeend-to-end (reads the live event stream and prints events until closed/timeout), and add a coverage-matrix test that decodes the compilednuncio.v1descriptor (reusing theprost-typeswalking 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—SystemSubcommandenum (lines 348-359, currentlyStatusandAudit); add aWatchvariant (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 (mirrorSystemSubcommand::Status => self.handle_system_status(...)); addhandle_system_watch, mirroringquery_daemon_status(lines 1631-1649) but callingnuncio_proto::client::subscribe_eventsand iterating the returnedStreaming<Event>withStreaming::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.crates/nunciod/tests/, e.g.rpc_coverage_matrix_test.rs, or alongside the M7-S1 tests incrates/nuncio-proto/src/lib.rsif 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
SystemSubcommand::Watchwith a bounded exit condition (count of events and/or a timeout) — an unboundednuncio system watchthat never returns would be unusable in an offline E2E test and a footgun interactively.handle_system_watchinrunner.rs: connect viaconnect_system, callsubscribe_events, loop readingEvents, print each (respecting--json), stop on the bound or stream close, and return a clear error if the daemon is unreachable — mirror the existingResult<_, String>error-surfacing style used byquery_daemon_statusand the otherhandle_*methods, no panics.crates/nunciod/tests/, followingspine_e2e_test.rs'sboot_daemonpattern) that boots a daemon with a mocked backend that emits at least one realEventonto the sharedEventBus, runs the CLI's newsystem watch --count 1command against it over real gRPC with the real bearer token, and asserts the event was received and printed — provingSubscribeis reachable from the reference client end to end, not just fromnunciod's internal test double.Subscribeif one does not already exist as part ofgrpc_auth_test.rs's coverage (verify againstcrates/nunciod/tests/grpc_auth_test.rs; add it ifSubscribeis not already covered there — every RPC, including streaming ones, must reject a missing/invalid bearer token per the security invariant indocs/STORY-WORKFLOW.md§4).(service, method)pairs the same way as the M7-S1full_surface_is_covered_by_goldentest (depends on that story'sprost-typesdecoding 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 explicitconst 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.cargo verify.Acceptance criteria
nuncio system watch(or the chosen subcommand name) exists, callsSystem.Subscribeover real gRPC, and terminates deterministically (count/timeout bound) rather than hanging.Eventwas received and rendered — no fabricated/hardcoded event output.Subscribeis covered by an unauthenticated-rejection E2E assertion (existing or newly added).nuncio.v1descriptor and fails if any(service, method)pair is missing from the maintainedEXERCISED_RPCSlist; verify by temporarily removing one entry locally and confirming the test fails, then restoring it.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 inEXERCISED_RPCSand a genuine passing offline E2E backing it.cargo verifygreen (fmt + clippy-D warningsall-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. Branchstory/m7-s3-cli-coverage-matrixoffdev; PR todevwith 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.protochanges. 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 andEXERCISED_RPCSto include them — the acceptance criterion is "every RPC in the contract at the time this merges," not a frozen list. Size M.