M7: Enforce full-surface contract coverage and an additive-only v1 policy in nuncio-proto #205

Open
opened 2026-07-28 14:04:54 +00:00 by KofTwentyTwo · 0 comments
KofTwentyTwo commented 2026-07-28 14:04:54 +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-proto/src/lib.rs already has a contract_stability::descriptor_matches_committed_golden test that byte-diffs the freshly compiled FileDescriptorSet against the committed crates/nuncio-proto/proto/nuncio/v1/descriptor.bin. That test is real and passes today, but it only proves "the descriptor didn't change since the last time someone manually reviewed and re-committed it" — it does not positively enumerate which services/RPCs are supposed to exist, and there is no automated check that a .proto change was additive (as opposed to a deliberately-accepted breaking change). M7 is the gate after which client repos (Swift/C#/TS) start trusting this contract, so this story closes both gaps with real, decoding-based tests, not documentation promises.

Goal

Add two new tests to nuncio-proto that make the M7 promise machine-checked instead of aspirational:

  1. Full-surface enumeration — decode the committed golden descriptor and assert it contains an explicit, hardcoded list of every service and RPC method that is supposed to exist in nuncio.v1 today. This catches the case the byte-diff test cannot: a future .proto file added under proto/nuncio/v1/ but never added to build.rs's compile_with_config input list would produce a fresh descriptor that's unchanged from the golden (both silently omit it) and the existing test would pass while shipping an uncovered service.
  2. Additive-only-since-freeze — freeze a baseline FileDescriptorSet snapshot now (the "M7 freeze") and add a test that structurally diffs the current golden against that baseline: every service, RPC, message, and field (by number) present in the baseline must still be present, with the same name/type/number, in the current descriptor. New services/RPCs/messages/fields are allowed (additive); anything removed, renamed, renumbered, or retyped fails the test.

This must not fake coverage: both tests decode real protobuf descriptor bytes via prost-types and assert against real structural data, not a static string comparison of file contents.

Files to touch / patterns to mirror

  • Cargo.toml (root [workspace.dependencies]) — add prost-types pinned to the same line as prost = "0.14" (line 83).
  • crates/nuncio-proto/Cargo.toml — add prost-types.workspace = true under [dev-dependencies] (only the new tests need it).
  • crates/nuncio-proto/src/lib.rs — extend the existing #[cfg(test)] mod contract_stability block (currently lines 85-117). Keep GOLDEN_DESCRIPTOR and FRESH_DESCRIPTOR as-is; add a new const BASELINE_DESCRIPTOR: &[u8] = include_bytes!("../proto/nuncio/v1/descriptor.baseline.bin"); and the two new #[test] functions.
  • New file: crates/nuncio-proto/proto/nuncio/v1/descriptor.baseline.bin — a byte-for-byte copy of the current descriptor.bin at the moment this story lands. This file is committed once and is not touched by the routine "regenerate the golden" recipe in docs/STORY-WORKFLOW.md §5 — it only ever moves forward via a second, deliberate PR that explicitly re-freezes it (e.g. when nuncio.v2 ships).
  • crates/nuncio-proto/proto/README.md — the "Versioning policy" section (lines 119-144) currently describes the policy in prose only ("The committed descriptor.bin is the enforcement mechanism"); update it to name the two new tests as the actual enforcement.

Implementation steps

  1. Add the prost-types dependency (workspace + crate, dev-only) at the version matching the workspace's prost = "0.14" line.
  2. Copy crates/nuncio-proto/proto/nuncio/v1/descriptor.bin to crates/nuncio-proto/proto/nuncio/v1/descriptor.baseline.bin verbatim.
  3. In lib.rs's contract_stability module, write full_surface_is_covered_by_golden:
    • prost_types::FileDescriptorSet::decode(GOLDEN_DESCRIPTOR) (use prost::Message::decode).
    • Walk .file → .service → collect (service.name(), method.name()) pairs into a Vec<(String, String)>, sort it.
    • Compare against an explicit hardcoded, sorted Vec literal enumerating every current RPC: System::{GetStatus, Subscribe}, Accounts::{AddAccount, ListAccounts}, Mail::{ListFolders, ListMessages, GetMessage, MarkRead, SearchMessages, SendMessage, Sync}, Filters::{CreateRule, ListRules, DeleteRule, ValidateRule, PreviewRule}, Export::{ExportMailbox}, Audit::{ListRecords, VerifyChain} (verified against crates/nuncio-proto/proto/nuncio/v1/nuncio.proto service blocks at lines 17-24, 139-146, 231-262, 437-460, 593-599, 659-667).
    • Assert equality with a failure message telling the reader to add the new RPC to this hardcoded list — the point is that adding an RPC to the .proto without updating this list is a compile-time-visible failure, not a silent gap.
  4. Write descriptor_is_additive_only_since_freeze:
    • Decode both BASELINE_DESCRIPTOR and GOLDEN_DESCRIPTOR into FileDescriptorSet.
    • For every service in baseline: assert a same-named service exists in golden; for every method in that baseline service: assert a same-named method exists on the golden service.
    • For every message_type in baseline (recursing into nested types): assert a same-named message exists in golden; for every field in that baseline message: assert golden has a field with the same number(), and that field's name(), type(), and label() (repeated/optional/required) are unchanged. Do not require the reverse — golden may have more fields/messages/services than baseline.
    • On any mismatch, fail with a message naming the offending service/message/field and stating this is a breaking change that must move to a new nuncio.v2 package per the versioning policy, never be applied in place.
  5. Update proto/README.md's versioning policy section to reference both tests by name and explain the baseline-freeze file's purpose and its separate (deliberate, rare) update process, distinct from the routine golden-regeneration recipe.
  6. Run cargo verify.

Acceptance criteria

  • full_surface_is_covered_by_golden exists in crates/nuncio-proto/src/lib.rs, decodes the real descriptor bytes, and fails if any current service/RPC is missing from its hardcoded expected list (verify by temporarily deleting one entry locally and confirming the test fails, then restoring it).
  • descriptor_is_additive_only_since_freeze exists, decodes both descriptor.bin and the new descriptor.baseline.bin, and fails on any removed/renamed/renumbered/retyped service, RPC, message, or field between them while passing on purely additive differences.
  • crates/nuncio-proto/proto/nuncio/v1/descriptor.baseline.bin is committed as an exact copy of today's descriptor.bin.
  • crates/nuncio-proto/proto/README.md's versioning-policy section names both tests as the enforcement mechanism.
  • No changes to nuncio.proto itself — this story adds test/tooling coverage only; the existing descriptor_matches_committed_golden test is untouched and still passes.
  • 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. Branch story/m7-s1-contract-coverage off dev; PR to dev with the gate output pasted as evidence; the PO reviews before the issue is closed.

Dependencies / notes

This story is proto-adjacent: it touches nuncio-proto's test machinery and adds a new committed binary file, but does not modify nuncio.proto's messages, services, or RPCs, and does not require regenerating descriptor.bin. Per §6 of docs/STORY-WORKFLOW.md, confirm with the PO before starting if a genuine [PROTO] story (M1-S1, M2-S2, M4-S3, M4-S4, M5-S1, or M5-S3) is mid-flight — this story's baseline snapshot should ideally be taken once those land, so the frozen surface reflects the real M1-M6 API rather than a stale mid-migration one. If timing forces this story first, the baseline simply freezes the current (smaller) surface, and later proto stories are additive against it — which is exactly what the test is designed to verify. 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-proto/src/lib.rs` already has a `contract_stability::descriptor_matches_committed_golden` test that byte-diffs the freshly compiled `FileDescriptorSet` against the committed `crates/nuncio-proto/proto/nuncio/v1/descriptor.bin`. That test is real and passes today, but it only proves "the descriptor didn't change since the last time someone manually reviewed and re-committed it" — it does not positively enumerate which services/RPCs are supposed to exist, and there is no automated check that a `.proto` change was *additive* (as opposed to a deliberately-accepted breaking change). M7 is the gate after which client repos (Swift/C#/TS) start trusting this contract, so this story closes both gaps with real, decoding-based tests, not documentation promises. ## Goal Add two new tests to `nuncio-proto` that make the M7 promise machine-checked instead of aspirational: 1. **Full-surface enumeration** — decode the committed golden descriptor and assert it contains an explicit, hardcoded list of every service and RPC method that is supposed to exist in `nuncio.v1` today. This catches the case the byte-diff test cannot: a future `.proto` file added under `proto/nuncio/v1/` but never added to `build.rs`'s `compile_with_config` input list would produce a fresh descriptor that's *unchanged* from the golden (both silently omit it) and the existing test would pass while shipping an uncovered service. 2. **Additive-only-since-freeze** — freeze a baseline `FileDescriptorSet` snapshot now (the "M7 freeze") and add a test that structurally diffs the current golden against that baseline: every service, RPC, message, and field (by number) present in the baseline must still be present, with the same name/type/number, in the current descriptor. New services/RPCs/messages/fields are allowed (additive); anything removed, renamed, renumbered, or retyped fails the test. This must not fake coverage: both tests decode real protobuf descriptor bytes via `prost-types` and assert against real structural data, not a static string comparison of file contents. ## Files to touch / patterns to mirror - `Cargo.toml` (root `[workspace.dependencies]`) — add `prost-types` pinned to the same line as `prost = "0.14"` (line 83). - `crates/nuncio-proto/Cargo.toml` — add `prost-types.workspace = true` under `[dev-dependencies]` (only the new tests need it). - `crates/nuncio-proto/src/lib.rs` — extend the existing `#[cfg(test)] mod contract_stability` block (currently lines 85-117). Keep `GOLDEN_DESCRIPTOR` and `FRESH_DESCRIPTOR` as-is; add a new `const BASELINE_DESCRIPTOR: &[u8] = include_bytes!("../proto/nuncio/v1/descriptor.baseline.bin");` and the two new `#[test]` functions. - New file: `crates/nuncio-proto/proto/nuncio/v1/descriptor.baseline.bin` — a byte-for-byte copy of the current `descriptor.bin` at the moment this story lands. This file is committed once and is **not** touched by the routine "regenerate the golden" recipe in `docs/STORY-WORKFLOW.md` §5 — it only ever moves forward via a second, deliberate PR that explicitly re-freezes it (e.g. when `nuncio.v2` ships). - `crates/nuncio-proto/proto/README.md` — the "Versioning policy" section (lines 119-144) currently describes the policy in prose only ("The committed `descriptor.bin` is the enforcement mechanism"); update it to name the two new tests as the actual enforcement. ## Implementation steps 1. Add the `prost-types` dependency (workspace + crate, dev-only) at the version matching the workspace's `prost = "0.14"` line. 2. Copy `crates/nuncio-proto/proto/nuncio/v1/descriptor.bin` to `crates/nuncio-proto/proto/nuncio/v1/descriptor.baseline.bin` verbatim. 3. In `lib.rs`'s `contract_stability` module, write `full_surface_is_covered_by_golden`: - `prost_types::FileDescriptorSet::decode(GOLDEN_DESCRIPTOR)` (use `prost::Message::decode`). - Walk `.file` → `.service` → collect `(service.name(), method.name())` pairs into a `Vec<(String, String)>`, sort it. - Compare against an explicit hardcoded, sorted `Vec` literal enumerating every current RPC: `System::{GetStatus, Subscribe}`, `Accounts::{AddAccount, ListAccounts}`, `Mail::{ListFolders, ListMessages, GetMessage, MarkRead, SearchMessages, SendMessage, Sync}`, `Filters::{CreateRule, ListRules, DeleteRule, ValidateRule, PreviewRule}`, `Export::{ExportMailbox}`, `Audit::{ListRecords, VerifyChain}` (verified against `crates/nuncio-proto/proto/nuncio/v1/nuncio.proto` service blocks at lines 17-24, 139-146, 231-262, 437-460, 593-599, 659-667). - Assert equality with a failure message telling the reader to add the new RPC to this hardcoded list — the point is that adding an RPC to the `.proto` without updating this list is a compile-time-visible failure, not a silent gap. 4. Write `descriptor_is_additive_only_since_freeze`: - Decode both `BASELINE_DESCRIPTOR` and `GOLDEN_DESCRIPTOR` into `FileDescriptorSet`. - For every `service` in baseline: assert a same-named service exists in golden; for every `method` in that baseline service: assert a same-named method exists on the golden service. - For every `message_type` in baseline (recursing into nested types): assert a same-named message exists in golden; for every `field` in that baseline message: assert golden has a field with the same `number()`, and that field's `name()`, `type()`, and `label()` (repeated/optional/required) are unchanged. Do not require the reverse — golden may have *more* fields/messages/services than baseline. - On any mismatch, fail with a message naming the offending service/message/field and stating this is a breaking change that must move to a new `nuncio.v2` package per the versioning policy, never be applied in place. 5. Update `proto/README.md`'s versioning policy section to reference both tests by name and explain the baseline-freeze file's purpose and its separate (deliberate, rare) update process, distinct from the routine golden-regeneration recipe. 6. Run `cargo verify`. ## Acceptance criteria - [ ] `full_surface_is_covered_by_golden` exists in `crates/nuncio-proto/src/lib.rs`, decodes the real descriptor bytes, and fails if any current service/RPC is missing from its hardcoded expected list (verify by temporarily deleting one entry locally and confirming the test fails, then restoring it). - [ ] `descriptor_is_additive_only_since_freeze` exists, decodes both `descriptor.bin` and the new `descriptor.baseline.bin`, and fails on any removed/renamed/renumbered/retyped service, RPC, message, or field between them while passing on purely additive differences. - [ ] `crates/nuncio-proto/proto/nuncio/v1/descriptor.baseline.bin` is committed as an exact copy of today's `descriptor.bin`. - [ ] `crates/nuncio-proto/proto/README.md`'s versioning-policy section names both tests as the enforcement mechanism. - [ ] No changes to `nuncio.proto` itself — this story adds test/tooling coverage only; the existing `descriptor_matches_committed_golden` test is untouched and still passes. - [ ] `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. Branch `story/m7-s1-contract-coverage` off `dev`; PR to `dev` with the gate output pasted as evidence; the PO reviews before the issue is closed. ## Dependencies / notes This story is **proto-adjacent**: it touches `nuncio-proto`'s test machinery and adds a new committed binary file, but does not modify `nuncio.proto`'s messages, services, or RPCs, and does not require regenerating `descriptor.bin`. Per §6 of `docs/STORY-WORKFLOW.md`, confirm with the PO before starting if a genuine `[PROTO]` story (M1-S1, M2-S2, M4-S3, M4-S4, M5-S1, or M5-S3) is mid-flight — this story's baseline snapshot should ideally be taken once those land, so the frozen surface reflects the real M1-M6 API rather than a stale mid-migration one. If timing forces this story first, the baseline simply freezes the current (smaller) surface, and later proto stories are additive against it — which is exactly what the test is designed to verify. Size M.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: KofTwentyTwo/nuncio#205