feat(contacts): add Contacts gRPC vertical (proto, daemon, CLI, E2E) #218

Merged
KofTwentyTwo merged 1 commits from story/m2-contacts-api into dev 2026-07-29 23:23:32 +00:00
KofTwentyTwo commented 2026-07-29 23:15:33 +00:00 (Migrated from github.com)

Closes #188.

Summary

Publishes the M2-S1 contacts engine (nuncio-contacts: ContactsBackend/MockContactsBackend, DatabaseEngine::save_contact/list_contacts/get_contact) over the daemon's authenticated gRPC API, mirroring the Calendar vertical (story/m1-calendar-api) throughout.

  • Proto: adds service Contacts (Sync, ListContacts, GetContact, CreateContact) plus Contact/ContactEmail/ContactPhone messages to nuncio.v1. Golden descriptor regenerated; contract_stability::descriptor_matches_committed_golden passes.
  • Daemon: ContactsEngineOverrides + ContactsGrpcService in crates/nunciod/src/grpc.rs, mounted behind its own BearerAuthInterceptor, threaded through serve_on_listener_with_overrides alongside the existing Mail/Calendar overrides. crates/nunciod/src/contacts_sync.rs mirrors calendar_sync.rs (pure fetch-and-persist, no production entry point since AccountConfig has no CardDAV fields yet).
  • Client: connect_contacts/AuthenticatedContactsClient in crates/nuncio-proto/src/client.rs, matching every other connect_* helper's shape and test coverage.
  • CLI: nuncio contact list/search/add/sync are now real gRPC clients (crates/nuncio-cli/src/runner.rs), replacing the old nuncio_contacts::ContactsDatabase::in_memory() throwaway-per-invocation database. nuncio-cli no longer depends on nuncio-contacts at all.
  • E2E: crates/nunciod/tests/contacts_e2e_test.rs boots a real daemon with an injected MockContactsBackend, drives Sync → ListContacts/GetContact, proves CreateContact persists visibly to a second, independent client connection (proving real daemon-side persistence, not process-local state), and asserts an unauthenticated call is rejected with Code::Unauthenticated.

Deviations from the issue's literal proto sketch (flagged for PO review)

  1. Added CreateContact (not listed in the issue's message bullets). The acceptance criteria explicitly requires contact add to persist across CLI invocations, but there is no per-account CardDAV credential to write back to a real server (same gap Calendar has). CreateContact only ever writes to the daemon's own local SQLite store — never a fabricated CardDAV write-back — which is the same honest-surface spirit as Calendar's "no create/delete" note (that note is about never pretending to write to the remote server, not about refusing to persist locally-authored data at all). Without this RPC, contact add could not honestly satisfy the acceptance criteria.
  2. account_id is required on ListContactsRequest/CreateContactRequest (the issue sketched a bare ListContactsRequest {}), because the real DatabaseEngine::list_contacts/save_contact are account-scoped. ContactSubcommand::List/Search/Add/Sync all gained a --account flag to match (mirroring CalSubcommand).
  3. No server-side search RPC. contact search fetches the account's full ListContacts result and filters client-side (display name / organization / email, case-insensitive) — documented in ContactSubcommand::Search's doc comment.
  4. Timestamps omitted. Contact's created_at/updated_at/last_interacted_at are not on the wire — no real client use case identified for them yet; can be added additively later.

Gate evidence

cargo fmt --all -- --check     # clean
cargo check-all                # clippy --all-targets --workspace -D warnings: clean
cargo test-all                 # all suites green, 0 ignored (see below)

Representative counts from the full run: nuncio-proto 23 unit tests (incl. connect_contacts_* and the contract-stability golden check), nunciod lib 100 unit tests (incl. contacts_rpcs_reject_missing_bearer_token, contacts_sync_without_override_reports_honest_error, contacts_sync_with_injected_backend_persists_and_is_immediately_visible, create_contact_persists_and_is_immediately_visible, list_contacts_returns_only_genuinely_persisted_contacts, get_contact_reports_not_found_for_unknown_contact_id), nunciod/tests/contacts_e2e_test.rs 1 full-daemon E2E, nuncio-cli lib 29 unit tests (incl. contacts_rpcs_round_trip_over_grpc_to_a_stub_daemon). Local pre-commit hook (fmt + clippy + full test-all) passed cleanly on the commit.

Test plan

  • cargo fmt --all -- --check
  • cargo check-all
  • cargo test -p nuncio-proto (golden regenerated and passing)
  • cargo test -p nunciod (unit + contacts_e2e_test, calendar_e2e_test, spine_e2e_test, grpc_auth_test, daemon_e2e_test)
  • cargo test -p nuncio-cli
  • cargo test-all (full workspace, 0 ignored)
Closes #188. ## Summary Publishes the M2-S1 contacts engine (`nuncio-contacts`: `ContactsBackend`/`MockContactsBackend`, `DatabaseEngine::save_contact`/`list_contacts`/`get_contact`) over the daemon's authenticated gRPC API, mirroring the Calendar vertical (`story/m1-calendar-api`) throughout. - **Proto**: adds `service Contacts` (`Sync`, `ListContacts`, `GetContact`, `CreateContact`) plus `Contact`/`ContactEmail`/`ContactPhone` messages to `nuncio.v1`. Golden descriptor regenerated; `contract_stability::descriptor_matches_committed_golden` passes. - **Daemon**: `ContactsEngineOverrides` + `ContactsGrpcService` in `crates/nunciod/src/grpc.rs`, mounted behind its **own** `BearerAuthInterceptor`, threaded through `serve_on_listener_with_overrides` alongside the existing Mail/Calendar overrides. `crates/nunciod/src/contacts_sync.rs` mirrors `calendar_sync.rs` (pure fetch-and-persist, no production entry point since `AccountConfig` has no CardDAV fields yet). - **Client**: `connect_contacts`/`AuthenticatedContactsClient` in `crates/nuncio-proto/src/client.rs`, matching every other `connect_*` helper's shape and test coverage. - **CLI**: `nuncio contact list/search/add/sync` are now real gRPC clients (`crates/nuncio-cli/src/runner.rs`), replacing the old `nuncio_contacts::ContactsDatabase::in_memory()` throwaway-per-invocation database. `nuncio-cli` no longer depends on `nuncio-contacts` at all. - **E2E**: `crates/nunciod/tests/contacts_e2e_test.rs` boots a real daemon with an injected `MockContactsBackend`, drives `Sync` → `ListContacts`/`GetContact`, proves `CreateContact` persists visibly to a **second, independent** client connection (proving real daemon-side persistence, not process-local state), and asserts an unauthenticated call is rejected with `Code::Unauthenticated`. ## Deviations from the issue's literal proto sketch (flagged for PO review) 1. **Added `CreateContact`** (not listed in the issue's message bullets). The acceptance criteria explicitly requires `contact add` to persist across CLI invocations, but there is no per-account CardDAV credential to write back to a real server (same gap Calendar has). `CreateContact` only ever writes to the daemon's own local SQLite store — never a fabricated CardDAV write-back — which is the same honest-surface spirit as Calendar's "no create/delete" note (that note is about never pretending to write to the *remote* server, not about refusing to persist locally-authored data at all). Without this RPC, `contact add` could not honestly satisfy the acceptance criteria. 2. **`account_id` is required** on `ListContactsRequest`/`CreateContactRequest` (the issue sketched a bare `ListContactsRequest {}`), because the real `DatabaseEngine::list_contacts`/`save_contact` are account-scoped. `ContactSubcommand::List`/`Search`/`Add`/`Sync` all gained a `--account` flag to match (mirroring `CalSubcommand`). 3. **No server-side search RPC.** `contact search` fetches the account's full `ListContacts` result and filters client-side (display name / organization / email, case-insensitive) — documented in `ContactSubcommand::Search`'s doc comment. 4. **Timestamps omitted.** `Contact`'s `created_at`/`updated_at`/`last_interacted_at` are not on the wire — no real client use case identified for them yet; can be added additively later. ## Gate evidence ``` cargo fmt --all -- --check # clean cargo check-all # clippy --all-targets --workspace -D warnings: clean cargo test-all # all suites green, 0 ignored (see below) ``` Representative counts from the full run: `nuncio-proto` 23 unit tests (incl. `connect_contacts_*` and the contract-stability golden check), `nunciod` lib 100 unit tests (incl. `contacts_rpcs_reject_missing_bearer_token`, `contacts_sync_without_override_reports_honest_error`, `contacts_sync_with_injected_backend_persists_and_is_immediately_visible`, `create_contact_persists_and_is_immediately_visible`, `list_contacts_returns_only_genuinely_persisted_contacts`, `get_contact_reports_not_found_for_unknown_contact_id`), `nunciod/tests/contacts_e2e_test.rs` 1 full-daemon E2E, `nuncio-cli` lib 29 unit tests (incl. `contacts_rpcs_round_trip_over_grpc_to_a_stub_daemon`). Local pre-commit hook (fmt + clippy + full test-all) passed cleanly on the commit. ## Test plan - [x] `cargo fmt --all -- --check` - [x] `cargo check-all` - [x] `cargo test -p nuncio-proto` (golden regenerated and passing) - [x] `cargo test -p nunciod` (unit + `contacts_e2e_test`, `calendar_e2e_test`, `spine_e2e_test`, `grpc_auth_test`, `daemon_e2e_test`) - [x] `cargo test -p nuncio-cli` - [x] `cargo test-all` (full workspace, 0 ignored)
Sign in to join this conversation.