test(connectors/google_mail,google_calendar): phase 14 audit cluster D1 (integration gaps + rotation + scope ext)#313
Conversation
…D1 (integration gaps + rotation + scope ext) Closes Phase 14 audit cluster D1 (#308) integration test coverage gaps: - G-1 (light): adds `auth -> client -> cursor -> projection` round-trip pins for Gmail and Calendar via `httpx.MockTransport` hermetic handlers so the wire-shape contract is observed end-to-end (unit layer only mocked at the client class boundary). - G-2: adds 3-connector rotation continuation pin (`sync1 -> rotation -> sync2` x 3 for Drive + Gmail + Calendar) mirroring the Phase 13 lifecycle step 7 shape; asserts no re-bootstrap on any of the three connectors after a simulated refresh-token rotation. - G-10: adds scope-extension survival pin — runs the Phase 13 `GoogleWorkspaceConnector` end-to-end under the widened 3-scope `DEFAULT_SCOPES` and asserts the Drive cursor / projection round-trip is bit-for-bit unaffected. Design judgement: Option A (lifecycle 1-file consolidation) per the G5 closeout decision. `docs/phase-14-plan.md` §7.2 updated to reflect the consolidated structure (was originally listing 3 separate files). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
レビュー結果 (mode: quick, axes: correctness, security, conventions, testing, maintainability, documentation): correctness指摘事項なし。9/9 tests pass。アサーションは pre/post state を明示的に pin (rotation での security指摘事項なし。fixture の token / secret は全て合成値 ( conventions指摘事項なし。Conventional Commits 形式 ( testing指摘事項なし。
[Info] tests/integration/test_phase14_google_mail_calendar_lifecycle.py:638-645 documentation指摘事項なし。 maintainability指摘事項なし。helper 関数 ( サマリーCritical: 0 件 by_axis: 判定: drive_loop exit criteria 全観点満たす (Critical / Warning ゼロ)。merge-ready。 |
…D2 (mapper edge case + 401 insufficient scope + fixture activation) (#314) Closes #309. Phase 14 audit cluster D2 — unit-level coverage gaps surfaced by the read-only audit (4 perspectives, parallel). Cluster D1 (#313, integration) and clusters B1 / B2 / C (docs) are merged; this PR finishes the audit cycle. ## Summary ### Mapper edge cases (G-4 / G-5 / G-7) - **G-4 Calendar mapper**: `recurrence: tuple[str, ...]` may carry RRULE + RDATE + EXDATE + EXRULE per RFC 5545 — `test_master_event_body_includes_rdate_and_exdate` pins that every component lands in body verbatim, `test_master_event_body_preserves_complex_rrule_byday` pins that a complex RRULE (`BYDAY=MO,WE,FR;UNTIL=...;COUNT=52`) round-trips without normalisation. - **G-5 Calendar mapper**: `test_body_handles_single_attendee` pins the 1-attendee boundary (`Attendees:\nalice@example.com` — no special-case suppression vs the 0-attendee / multi-attendee paths). - **G-7 Both mappers**: kanji / emoji / control-char preservation. Gmail-side pins that `_decode_gmail_body`'s `errors="replace"` is on the UTF-8 boundary only (decoded control chars + U+FFFD round-trip intact). Calendar-side pins symmetric behaviour on description + location (text-only family — no sanitisation per ADR-0010 §Phase 14 改訂 (k)). ### Client edge cases (G-6 / G-9) - **G-6 Calendar client**: `start.timeZone` / `end.timeZone` are kept on `RawCalendarEvent.raw` only (no dedicated dataclass field — Phase 14 G4 deliberate design because the mapper consumes `dateTime` with its offset directly). Pinned so a future regression that drops or filters `raw` trips before silently losing forensic context for Phase 15+ projection work. - **G-9 Both clients**: 401 `insufficient_scope` detection added. Code change in both `client.py` files: a new `_is_insufficient_scope(response)` helper inspects the `WWW-Authenticate` header AND the JSON body (`{"error": "invalid_token", "error_subtype": "insufficient_scope"}` OR `error.status="PERMISSION_DENIED"` + `insufficient` in message). Matches → `GoogleAuthError` (subclass of `ConfigError`) with actionable message naming the recovery command (`opshub connector auth set google_workspace`). Non-`insufficient_scope` 401s still raise `ConnectorFailedError` (`test_non_rate_limit_4xx_fails_fast` / `test_other_4xx_fails_fast` continue to pin generic behaviour). This is the Phase 14 G2 OQ6 scenario: operator carrying a Phase 13 drive-only refresh token forward into Phase 14 G3 / G4 without re-running the paste-code flow. The actionable error short-circuits the retry loop the operator would otherwise enter. ### Fixture activation (F-1) - Calendar `test_client.py` now uses `_fixture(name)` helper mirroring the Gmail-side pattern. The three previously-unreferenced fixtures all back live tests: - `events_single.json` → `test_events_single_fixture_normalises_into_raw_calendar_event` + `test_normaliser_preserves_time_zone_field_on_raw_payload` (G-6) - `events_recurring_with_override.json` → `test_events_recurring_with_override_fixture_yields_master_plus_override` - `sync_token_gone.json` → `test_fetch_events_delta_410_raises_sync_token_expired` (refactored from inline JSON) - New `tests/fixtures/google_calendar/README.md` documents the fixture set, mirrors Gmail-side `README.md` structure, and explains the inline-JSON / fixture split: pagination / retry / sentinel tests stay inline because the fixture file shape would couple the pin to fixture content drift (the Gmail-side `history_page.json` activation made the same split). ### Phase 14 plan §7.5 alignment §7.5 lists `single / recurring + override / 410 GONE` for calendar fixtures. All three are now activated. No `events_all_day.json` fixture added — §7.5 does not list it and the all-day shape is adequately pinned by inline JSON in the existing `test_normaliser_handles_all_day_events` test. ### Existing 401 test vs new insufficient_scope test — boundary - `tests/unit/connectors/google_mail/test_client.py::test_non_rate_limit_4xx_fails_fast` — plain 401 (no `insufficient_scope` signal) → `ConnectorFailedError` (1 call, no retry) - `tests/unit/connectors/google_calendar/test_client.py::test_other_4xx_fails_fast` — generic 4xx (400 here) → `ConnectorFailedError` (1 call, no retry) - New `test_<gmail|calendar>_401_insufficient_scope_actionable_message` + `test_<gmail|calendar>_401_insufficient_scope_via_www_authenticate_header` — 401 with the OAuth signal → `GoogleAuthError` (1 call, no retry, actionable message) ## Test plan - [x] `uv run pytest tests/unit/connectors/google_calendar/ tests/unit/connectors/google_mail/` → 116 passed - [x] `uv run pytest tests/unit/connectors/test_mapper_symmetry.py tests/unit/connectors/google_auth/` → 37 passed (no regression) - [x] `uv run pytest tests/unit/` (with `--all-extras`) → 2384 passed - [x] `uv run pytest tests/integration/ -k "google_mail or google_calendar"` → 9 passed - [x] `uv run ruff check . && uv run ruff format --check .` → clean - [x] `uv run pyright src/opshub/connectors/google_calendar/ src/opshub/connectors/google_mail/ tests/unit/connectors/google_calendar/ tests/unit/connectors/google_mail/` → 0 errors - [x] `uv run mypy src/opshub/connectors/google_calendar/ src/opshub/connectors/google_mail/` → no issues Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Phase 14 audit cluster D1 (#308) — closes 3 integration test coverage gaps surfaced against the Phase 13 lifecycle baseline.
auth -> client -> cursor -> projectionwire round-trip for Gmail and Calendar viahttpx.MockTransporthermetic handlers (test_phase14_sync_round_trip_via_mock_httpx_gmail/_calendar). The unit layer only mocks at the client class boundary; this lifts the contract pin to the wire.test_phase14_rotation_propagates_to_all_three_connectors) —sync1 -> rotation -> sync2x 3 for Drive + Gmail + Calendar, mirroring the Phase 13 lifecycle step 7 shape. Asserts no re-bootstrap on any connector after a simulated refresh-token rotation; the Driveget_start_page_token/ Gmailget_profile_history_id/ Calendarfetch_events_windowcounters stay pinned at 1 across the 2-sync window.test_phase14_phase13_google_workspace_unaffected_by_scope_extension) — runsGoogleWorkspaceConnectorend-to-end under the widened 3-scopeDEFAULT_SCOPESand asserts the Drive cursor / projection round-trip is bit-for-bit unaffected. Includes a literalDEFAULT_SCOPES == [drive, gmail, calendar]pin alongside.Design judgement
Option A (chosen): lifecycle 1-file consolidation. The Phase 14 G5 closeout decision was to consolidate Phase 14 integration coverage into one lifecycle file (lifecycle = projection + MCP read surface + mapper symmetry + write-back guard); fragmenting it now would re-introduce the shape the G5 audit collapsed. The new test functions read naturally next to the existing projection / write-back guards.
Option B (3 separate files per the original
phase-14-plan.md§7.2 listing) was rejected — the contract layers (auth / cursor / projection / MCP read surface) reuse the Phase 13 shape verbatim, so per-connector round-trip pins read better when co-located with the projection / write-back guards.docs/phase-14-plan.md§7.2 updated to reflect the consolidated structure with explicit pointers to each new test function.Test plan
uv run pytest tests/integration/test_phase14_google_mail_calendar_lifecycle.py— 9/9 pass (4 new tests added on top of 5 pre-existing G5 lifecycle pins)uv run pytest tests/integration/— 166 pass, 2 skipped (real-Box / real-OneDrive contract tests)uv run ruff check+uv run ruff format --check+uv run pyrighton the touched test file — cleanmarkdownlint-cli2 docs/phase-14-plan.md— cleanhttpxcall routes throughhttpx.MockTransport; no real Google endpoint reachedCloses #308
🤖 Generated with Claude Code