feat(calendar): add Calendar gRPC vertical (proto, daemon, CLI, E2E) #214

Merged
KofTwentyTwo merged 1 commits from story/m1-calendar-api into dev 2026-07-29 22:19:16 +00:00
KofTwentyTwo commented 2026-07-29 22:14:00 +00:00 (Migrated from github.com)

Summary

Closes #185. Wires the previously-orphaned nuncio-cal engine into nunciod as a real, authenticated gRPC vertical, mirroring the existing Mail vertical end to end (proto -> daemon -> CLI -> E2E).

  • Proto: added service Calendar (Sync / ListEvents / GetEvent — no create/delete) plus CalendarEvent, CalendarSyncRequest/Response, ListEventsRequest/Response, GetEventRequest/Response to nuncio.proto. Regenerated descriptor.bin; nuncio-proto's contract_stability::descriptor_matches_committed_golden test passes.
  • Client: connect_calendar / AuthenticatedCalendarClient in nuncio-proto/src/client.rs, with fail-closed / transport-error unit tests mirroring connect_mail.
  • Daemon: new nunciod::calendar_sync::sync_with_backend (pure fetch-and-persist against &dyn CalendarBackend, mirrors sync.rs's fetch_and_persist — deliberately no production entry point yet, since AccountConfig has no CalDAV fields to build a real backend from). CalendarEngineOverrides + CalendarGrpcService in grpc.rs, mounted behind its own BearerAuthInterceptor in serve_on_listener_with_overrides (new calendar_overrides parameter; serve_on_listener/serve pass CalendarEngineOverrides::default(), so production behavior is unchanged for every existing caller).
  • CLI: real nuncio cal sync / nuncio cal list gRPC clients in runner.rs, replacing the fabricated "Calendar Events: 0 events found" / "calendar_sync_started" strings. CalSubcommand::List/Sync gained --account/--calendar/--start/--end flags (account is required; the rest have sensible defaults) since the RPCs need an account/window to query — there's no persisted "current account" concept to default from yet.
  • Tests: unit tests for calendar_sync::sync_with_backend, grpc.rs unit tests (list/get honest-data + not-found, sync honest-error-with-no-backend, sync-with-injected-MockCalendarBackend persists and is immediately visible, and calendar_rpcs_reject_missing_bearer_token), a CLI stub-daemon round-trip test, and a new offline full-daemon E2E (crates/nunciod/tests/calendar_e2e_test.rs) that boots a real daemon with an injected MockCalendarBackend, drives Sync -> ListEvents/GetEvent over the authenticated API, and asserts an unauthenticated call is rejected with Code::Unauthenticated.

Key behavior / honesty invariants

  • ListEvents/GetEvent always read genuinely persisted data via DatabaseEngine::list_calendar_events/get_calendar_event — never fabricated.
  • Sync with no injected backend (the real production path today) returns Status::internal("no CalDAV configuration exists for account '...'; per-account CalDAV configuration is not yet implemented") — never a fabricated synced_count. Only the test-injection seam (CalendarEngineOverrides::calendar_backend, standing in a MockCalendarBackend) exercises a working Sync.
  • Calendar is mounted behind its own BearerAuthInterceptor, exactly like every other service — see serve_on_listener_with_overrides's security doc comment and the new unauth-reject tests.
  • No unsafe/unwrap/expect/panic/todo in non-test code; all comments are intent-only (no story/GH/phase refs in .rs/.proto).

Gate output (all green, run locally + via the pre-commit hook)

cargo fmt --all -- --check     -> clean, no diff
cargo check-all                -> clippy --all-targets --workspace -D warnings: 0 warnings
cargo test-all                 -> 0 failed, 0 ignored across the workspace
  nuncio-proto:  21 passed (incl. contract_stability::descriptor_matches_committed_golden)
  nunciod (lib): 85 passed (incl. calendar_sync::*, grpc::tests::calendar_*, list_events_*, get_event_*)
  nunciod (calendar_e2e_test.rs): 1 passed
  nunciod (spine_e2e_test.rs, daemon_e2e_test.rs, grpc_auth_test.rs): all passing, unaffected
  nuncio-cli:    28 passed (incl. calendar_rpcs_round_trip_over_grpc_to_a_stub_daemon)
  nuncio-cli (cli_system_test.rs): 4 passed
  nuncio-cal, nuncio-core, nuncio-store, nuncio-mail, nuncio-filter, nuncio-contacts: unaffected, all passing

Decisions for PO review

  1. CLI flags added to cal list/cal sync (--account required, --calendar default "default", --start/--end default 0/i64::MAX) rather than keeping them argument-free — the RPCs genuinely need an account/window to query and there's no "current account" concept to default from, so argument-free would have meant hardcoding a meaningless account id. Flagged in the issue as an acceptable choice.
  2. ListEventsRequest.calendar_id is accepted but not applied as a store filter — DatabaseEngine::list_calendar_events only filters by account_id + time window (no per-calendar-collection column filter exists in the store today). Documented inline in CalendarGrpcService::list_events; not a fabrication (still returns every genuinely persisted event for the account/window), but a known gap for a follow-up story if per-calendar filtering matters before per-account CalDAV config lands.
  3. No production run_account_sync-equivalent entry point was added for Calendar (explicitly out of scope per the issue) — Sync always errors in production until a follow-up story adds persisted per-account CalDAV configuration.

🤖 Generated with Claude Code

## Summary Closes #185. Wires the previously-orphaned `nuncio-cal` engine into `nunciod` as a real, authenticated gRPC vertical, mirroring the existing `Mail` vertical end to end (proto -> daemon -> CLI -> E2E). - **Proto**: added `service Calendar` (`Sync` / `ListEvents` / `GetEvent` — no create/delete) plus `CalendarEvent`, `CalendarSyncRequest/Response`, `ListEventsRequest/Response`, `GetEventRequest/Response` to `nuncio.proto`. Regenerated `descriptor.bin`; `nuncio-proto`'s `contract_stability::descriptor_matches_committed_golden` test passes. - **Client**: `connect_calendar` / `AuthenticatedCalendarClient` in `nuncio-proto/src/client.rs`, with fail-closed / transport-error unit tests mirroring `connect_mail`. - **Daemon**: new `nunciod::calendar_sync::sync_with_backend` (pure fetch-and-persist against `&dyn CalendarBackend`, mirrors `sync.rs`'s `fetch_and_persist` — deliberately **no** production entry point yet, since `AccountConfig` has no CalDAV fields to build a real backend from). `CalendarEngineOverrides` + `CalendarGrpcService` in `grpc.rs`, mounted behind its **own** `BearerAuthInterceptor` in `serve_on_listener_with_overrides` (new `calendar_overrides` parameter; `serve_on_listener`/`serve` pass `CalendarEngineOverrides::default()`, so production behavior is unchanged for every existing caller). - **CLI**: real `nuncio cal sync` / `nuncio cal list` gRPC clients in `runner.rs`, replacing the fabricated `"Calendar Events: 0 events found"` / `"calendar_sync_started"` strings. `CalSubcommand::List`/`Sync` gained `--account`/`--calendar`/`--start`/`--end` flags (account is required; the rest have sensible defaults) since the RPCs need an account/window to query — there's no persisted "current account" concept to default from yet. - **Tests**: unit tests for `calendar_sync::sync_with_backend`, `grpc.rs` unit tests (list/get honest-data + not-found, sync honest-error-with-no-backend, sync-with-injected-`MockCalendarBackend` persists and is immediately visible, and `calendar_rpcs_reject_missing_bearer_token`), a CLI stub-daemon round-trip test, and a new offline full-daemon E2E (`crates/nunciod/tests/calendar_e2e_test.rs`) that boots a real daemon with an injected `MockCalendarBackend`, drives `Sync` -> `ListEvents`/`GetEvent` over the authenticated API, and asserts an **unauthenticated** call is rejected with `Code::Unauthenticated`. ## Key behavior / honesty invariants - `ListEvents`/`GetEvent` **always** read genuinely persisted data via `DatabaseEngine::list_calendar_events`/`get_calendar_event` — never fabricated. - `Sync` with **no** injected backend (the real production path today) returns `Status::internal("no CalDAV configuration exists for account '...'; per-account CalDAV configuration is not yet implemented")` — never a fabricated `synced_count`. Only the test-injection seam (`CalendarEngineOverrides::calendar_backend`, standing in a `MockCalendarBackend`) exercises a working `Sync`. - `Calendar` is mounted behind its own `BearerAuthInterceptor`, exactly like every other service — see `serve_on_listener_with_overrides`'s security doc comment and the new unauth-reject tests. - No `unsafe`/`unwrap`/`expect`/`panic`/`todo` in non-test code; all comments are intent-only (no story/GH/phase refs in `.rs`/`.proto`). ## Gate output (all green, run locally + via the pre-commit hook) ``` cargo fmt --all -- --check -> clean, no diff cargo check-all -> clippy --all-targets --workspace -D warnings: 0 warnings cargo test-all -> 0 failed, 0 ignored across the workspace nuncio-proto: 21 passed (incl. contract_stability::descriptor_matches_committed_golden) nunciod (lib): 85 passed (incl. calendar_sync::*, grpc::tests::calendar_*, list_events_*, get_event_*) nunciod (calendar_e2e_test.rs): 1 passed nunciod (spine_e2e_test.rs, daemon_e2e_test.rs, grpc_auth_test.rs): all passing, unaffected nuncio-cli: 28 passed (incl. calendar_rpcs_round_trip_over_grpc_to_a_stub_daemon) nuncio-cli (cli_system_test.rs): 4 passed nuncio-cal, nuncio-core, nuncio-store, nuncio-mail, nuncio-filter, nuncio-contacts: unaffected, all passing ``` ## Decisions for PO review 1. **CLI flags added** to `cal list`/`cal sync` (`--account` required, `--calendar` default `"default"`, `--start`/`--end` default `0`/`i64::MAX`) rather than keeping them argument-free — the RPCs genuinely need an account/window to query and there's no "current account" concept to default from, so argument-free would have meant hardcoding a meaningless account id. Flagged in the issue as an acceptable choice. 2. **`ListEventsRequest.calendar_id` is accepted but not applied** as a store filter — `DatabaseEngine::list_calendar_events` only filters by `account_id` + time window (no per-calendar-collection column filter exists in the store today). Documented inline in `CalendarGrpcService::list_events`; not a fabrication (still returns every genuinely persisted event for the account/window), but a known gap for a follow-up story if per-calendar filtering matters before per-account CalDAV config lands. 3. No production `run_account_sync`-equivalent entry point was added for Calendar (explicitly out of scope per the issue) — `Sync` always errors in production until a follow-up story adds persisted per-account CalDAV configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.