Skip to content

feat(gmail): upgrade to SDK 2.0.0 with full unit + integration test coverage#325

Open
TheRealAgentK wants to merge 7 commits into
masterfrom
feat/324/gmail-sdk-2-upgrade-and-tests
Open

feat(gmail): upgrade to SDK 2.0.0 with full unit + integration test coverage#325
TheRealAgentK wants to merge 7 commits into
masterfrom
feat/324/gmail-sdk-2-upgrade-and-tests

Conversation

@TheRealAgentK
Copy link
Copy Markdown
Collaborator

Closes #324

Summary

Brings the freshly-migrated gmail/ integration into compliance with three skills:

  • upgrading-sdk-v2
  • writing-unit-tests
  • writing-integration-tests

Commits (logical)

  1. feat(gmail): upgrade to SDK 2.0.0 — bumps requirements.txt to ~=2.0.0 and config.json version to 2.0.0. Switches all error returns from ActionResult(data={"error": ...}) to ActionError(message=...). Drops the "result": True success flag and removes error / result properties from every output schema (Option A — rely on the SDK envelope's result.type instead). Hardens auth lookup with .get(...) chains.
  2. chore(gmail): replace tests scaffolding with conftest.py — drops legacy tests/context.py + placeholder test_gmail.py, adds tests/conftest.py with the mock_context fixture using the PlatformOauth2 auth envelope.
  3. test(gmail): add unit test suite covering all 21 actions — 56 unit tests. Patches gmail.gmail.build to inject a chained MagicMock Gmail service (gmail uses googleapiclient directly, not context.fetch).
  4. test(gmail): add integration test suite with destructive lifecycle coverage — 8 read-only + 5 destructive lifecycle tests (label CRUD, send/archive, draft CRUD, send-from-draft, reply-to-thread). Destructive tests are marked @pytest.mark.destructive with explicit docstrings describing exactly what they create/modify. Uses dynamic discovery for thread/message IDs with GMAIL_TEST_THREAD_ID / GMAIL_TEST_MESSAGE_ID env overrides.

Notes

  • Gmail does not use context.fetch() — it uses googleapiclient.discovery.build(). The SDK 2.0 FetchResponse breaking change therefore does not affect source code.
  • bleach pinning is intentionally left as follow-up.

Validation

✅ ruff check gmail            — clean
✅ ruff format gmail           — clean
✅ pytest test_gmail_unit.py   — 56 passed
✅ pytest test_gmail_integration.py --collect-only -m integration — 15 collected
✅ validate_integration.py gmail — passes (1 known false-positive scope warning on gmail.modify)
✅ check_code.py gmail          — passes

Running integration tests

export GMAIL_ACCESS_TOKEN=...   # token must include gmail.modify scope
# read-only:
pytest gmail/tests/test_gmail_integration.py -m "integration and not destructive"
# destructive (will create/delete labels, send emails to self, etc):
pytest gmail/tests/test_gmail_integration.py -m "integration and destructive"

…on bump)

Source-side upgrade per the upgrading-sdk-v2 skill:

  * Import ActionError alongside ActionResult
  * Convert all 21 error returns from
      `return ActionResult(data={"error": str(e)}, cost_usd=0.0)`
    to
      `return ActionError(message=str(e))`
  * Drop redundant `"result": True` keys from every success-path
    return — success/failure now lives on `result.type` (ACTION_SUCCESS
    vs ACTION_ERROR) instead of a duplicated payload field
  * Strip the matching `"error"` and `"result"` properties (and
    their entries in `required`) from every action's output_schema
    in config.json — 42 schema keys removed across 21 actions
  * Harden the auth lookup (skill gotcha #9) so a missing access_token
    surfaces as an upstream auth error rather than a KeyError:
      `context.auth.get("credentials", {}).get("access_token", "")`
  * Bump config.json version 0.1.0 → 2.0.0 (major bump for the
    SDK breaking change)
  * requirements.txt: autohive-integrations-sdk~=1.0.2 → ~=2.0.0

No `context.fetch()` work needed — gmail uses Google's
`googleapiclient.discovery.build()` directly, so the FetchResponse
breaking change has no impact on the source.

Validation:
  ✅ validate_integration.py — 0 errors, 1 warning (unused-scopes
     false positive on gmail.modify)
  ✅ check_code.py — passed
  ✅ ruff check / ruff format — clean

Refs #324
Removes the legacy `tests/context.py` shim and the placeholder
`test_gmail.py`, replaces them with a `tests/conftest.py` that
matches the current writing-unit-tests skill conventions:

  * sys.path setup so test files can use plain `from gmail import gmail`
  * mock_context fixture override pre-loaded with the
    PlatformOauth2 envelope Gmail expects, so every test in this
    directory inherits credentials of the right shape

Refs #324
Adds gmail/tests/test_gmail_unit.py with 56 tests covering the full
integration surface:

  * 7 helper tests for create_email_message:
    - plain text body, plain text + attachments
    - HTML body sanitisation (script tags stripped, javascript:
      protocol blocked, allowed tags preserved)
    - multipart/alternative structure with text+html parts
    - multipart/mixed structure for HTML + attachments

  * 1 service-build test (verifies hardened auth lookup does not
    KeyError on missing access_token)

  * 48 action tests across all 21 actions:
    - mark_emails_as_read, mark_emails_as_unread, archive_emails
    - get_user_info, read_email
    - read_inbox (default scope, unread scope -> Gmail q filter,
      pagination round-trip)
    - read_all_mail
    - send_email (text, HTML sanitised, exception path)
    - reply_to_thread (with original message header lookup)
    - list_labels, create_label
    - add_labels_to_emails, remove_labels_from_emails
    - list_emails_by_label
    - get_thread_emails
    - create_draft (new draft + reply-mode with thread/message id)
    - update_draft, list_drafts, get_draft, send_draft, delete_draft

Each action gets at minimum:
  * Happy path (verifies result.type == ResultType.ACTION + key data)
  * Exception path (verifies result.type == ResultType.ACTION_ERROR
    + the original error message is preserved on the ActionError)
  * Request-shape verification where it adds value (label add/remove
    body, query filters, threading IDs)

Mock pattern note: Gmail uses googleapiclient.discovery.build directly
rather than context.fetch, so the standard mock_context.fetch fixture
is not the right tool. Tests use `patch("gmail.gmail.build")` to
inject a MagicMock service that mimics the chained
service.users().messages().<verb>().execute() pattern.

Validation:
  ✅ 56 tests pass
  ✅ ruff check / ruff format (with tooling ruff.toml) — clean
  ✅ validate_integration.py — 0 errors
  ✅ check_code.py — passed

Refs #324
…verage

- 8 read-only tests (profile, labels, inbox/all-mail, drafts, threads)
- 5 destructive lifecycle tests (label CRUD, send/archive, draft CRUD,
  send-from-draft, reply-to-thread) — all marked @pytest.mark.destructive
  with explicit docstrings describing what is created/modified
- Uses live_context fixture (PlatformOauth2 envelope) — gmail uses
  googleapiclient directly, not context.fetch
- Supports GMAIL_TEST_THREAD_ID / GMAIL_TEST_MESSAGE_ID env overrides;
  otherwise picks most-recent inbox message dynamically
- .env.example documents GMAIL_ACCESS_TOKEN and optional overrides
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 13, 2026

🔍 Integration Validation Results

Commit: 7bd4fa335d48b81953e4d2bb45e35a7cb6fcab29 · refactor(gmail): extract shared build_raw_email helper and fix string-recipient bug
Updated: 2026-05-13T01:54:23Z

Changed directories: gmail

Check Result
Structure ⚠️ Passed with warnings
Code ⚠️ Passed with warnings
Tests ✅ Passed
README ✅ Passed
Version ✅ Passed
⚠️ Structure Check output
Validating 1 integration(s)...

============================================================
Integration: gmail
============================================================

Warnings (1):
  ⚠️ Potentially unused scopes (please verify): https://www.googleapis.com/auth/gmail.modify

============================================================
SUMMARY
============================================================
Integrations validated: 1
Total errors: 0
Total warnings: 1

⚠️ Validation passed with warnings - please review
⚠️ Code Check output

[notice] A new release of pip is available: 26.0.1 -> 26.1.1
[notice] To update, run: pip install --upgrade pip
----------------------------------------
Checking: gmail
----------------------------------------

📦 Installing dependencies...

🐍 Checking Python syntax...
   ✅ Syntax OK

📥 Checking imports...
   ✅ Imports OK

📄 Checking JSON files...
   ✅ JSON files OK

🔍 Linting with ruff...
   ✅ Lint OK

🎨 Checking formatting with ruff...
   ✅ Formatting OK

🔒 Scanning for security issues with bandit...
   ✅ Security OK

🛡️ Checking dependencies for vulnerabilities with pip-audit...
   ✅ Dependencies OK

🔗 Checking config-code sync...
   ⚠️  Action 'send_email': parameter 'body_format' defined in input_schema but never accessed in code
   ⚠️  Action 'send_email': parameter 'cc' defined in input_schema but never accessed in code
   ⚠️  Action 'send_email': parameter 'body' defined in input_schema but never accessed in code
   ⚠️  Action 'send_email': parameter 'to' defined in input_schema but never accessed in code
   ⚠️  Action 'send_email': parameter 'subject' defined in input_schema but never accessed in code
   ⚠️  Action 'send_email': parameter 'files' defined in input_schema but never accessed in code
   ⚠️  Action 'reply_to_thread': parameter 'body_format' defined in input_schema but never accessed in code
   ⚠️  Action 'reply_to_thread': parameter 'cc' defined in input_schema but never accessed in code
   ⚠️  Action 'reply_to_thread': parameter 'body' defined in input_schema but never accessed in code
   ⚠️  Action 'reply_to_thread': parameter 'to' defined in input_schema but never accessed in code
   ⚠️  Action 'reply_to_thread': parameter 'files' defined in input_schema but never accessed in code
   ⚠️  Action 'read_inbox': parameter 'scope' is required in schema but accessed with inputs.get() (safe for missing)
   ⚠️  Action 'read_inbox': parameter 'pageToken' is optional in schema but accessed with inputs["pageToken"] (will raise KeyError if not provided)
   ⚠️  Action 'read_all_mail': parameter 'scope' is required in schema but accessed with inputs.get() (safe for missing)
   ⚠️  Action 'read_all_mail': parameter 'pageToken' is optional in schema but accessed with inputs["pageToken"] (will raise KeyError if not provided)
   ⚠️  Action 'list_labels': parameter 'label_type' is optional in schema but accessed with inputs["label_type"] (will raise KeyError if not provided)
   ⚠️  Action 'list_emails_by_label': parameter 'pageToken' is optional in schema but accessed with inputs["pageToken"] (will raise KeyError if not provided)
   ⚠️  Action 'list_emails_by_label': parameter 'maxResults' is optional in schema but accessed with inputs["maxResults"] (will raise KeyError if not provided)
   ⚠️  Action 'create_label': parameter 'backgroundColor' is optional in schema but accessed with inputs["backgroundColor"] (will raise KeyError if not provided)
   ⚠️  Action 'create_label': parameter 'textColor' is optional in schema but accessed with inputs["textColor"] (will raise KeyError if not provided)
   ⚠️  Action 'create_draft': parameter 'from' accessed in code but not defined in input_schema
   ⚠️  Action 'create_draft': parameter 'body_format' defined in input_schema but never accessed in code
   ⚠️  Action 'create_draft': parameter 'bcc' defined in input_schema but never accessed in code
   ⚠️  Action 'create_draft': parameter 'cc' defined in input_schema but never accessed in code
   ⚠️  Action 'create_draft': parameter 'body' defined in input_schema but never accessed in code
   ⚠️  Action 'create_draft': parameter 'files' defined in input_schema but never accessed in code
   ⚠️  Action 'create_draft': parameter 'to' is optional in schema but accessed with inputs["to"] (will raise KeyError if not provided)
   ⚠️  Action 'create_draft': parameter 'subject' is optional in schema but accessed with inputs["subject"] (will raise KeyError if not provided)
   ⚠️  Action 'update_draft': parameter 'from' accessed in code but not defined in input_schema
   ⚠️  Action 'update_draft': parameter 'subject' defined in input_schema but never accessed in code
   ⚠️  Action 'update_draft': parameter 'body_format' defined in input_schema but never accessed in code
   ⚠️  Action 'update_draft': parameter 'bcc' defined in input_schema but never accessed in code
   ⚠️  Action 'update_draft': parameter 'cc' defined in input_schema but never accessed in code
   ⚠️  Action 'update_draft': parameter 'body' defined in input_schema but never accessed in code
   ⚠️  Action 'update_draft': parameter 'to' defined in input_schema but never accessed in code
   ⚠️  Action 'update_draft': parameter 'files' defined in input_schema but never accessed in code
   ⚠️  Action 'list_drafts': parameter 'pageToken' is optional in schema but accessed with inputs["pageToken"] (will raise KeyError if not provided)
   ⚠️  Action 'list_drafts': parameter 'maxResults' is optional in schema but accessed with inputs["maxResults"] (will raise KeyError if not provided)
   ⚠️  Action 'list_drafts': parameter 'includeSpamTrash' is optional in schema but accessed with inputs["includeSpamTrash"] (will raise KeyError if not provided)
   ⚠️  Action 'list_drafts': parameter 'q' is optional in schema but accessed with inputs["q"] (will raise KeyError if not provided)
   ✅ Config-code sync OK

🔄 Checking fetch patterns...
   ✅ Fetch patterns OK

========================================
✅ CODE CHECK PASSED
========================================
✅ Tests Check output

Integration   Tests  Coverage        Status
-------------------------------------------
gmail     63/63       88%      ✅ Passed
-------------------------------------------
Total     63/63            ✅ All passed

✅ Tests passed: gmail
✅ README Check output
========================================
✅ README CHECK PASSED
========================================
✅ Version Check output
✅ gmail: 0.1.0 → 2.0.0 (major bump)

========================================
✅ VERSION CHECK PASSED
========================================

Comment thread gmail/tests/test_gmail_unit.py Fixed
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cebb3f69c6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread gmail/tests/conftest.py Outdated
Addresses code-quality bot feedback on PR #325. Gmail uses
googleapiclient directly, so FetchResponse is never referenced.
Addresses Codex review on PR #325. Prepending the gmail/ integration
dir to sys.path could shadow the gmail package with the gmail.py
module file, breaking 'from gmail.gmail import ...'. The repo-root
conftest.py already puts the workspace root on sys.path and patches
Integration.load() from the caller frame, so no per-integration
sys.path tweaking is needed.
…-recipient bug

Addresses Codex review on PR #325.

- New module-level build_raw_email() consolidates raw RFC822 payload
  construction previously duplicated across SendEmail, ReplyToThread,
  CreateDraft, and UpdateDraft (3 near-identical _create_raw_email
  methods plus an inline block in ReplyToThread).
- Fixes a latent bug in ReplyToThread that assumed 'to' and 'cc' were
  always lists. With a string value, recipients.extend(inputs['to']) /
  ', '.join(inputs['cc']) silently character-split the address into
  per-letter recipients, producing malformed headers. The new helper
  routes both forms through _normalize_addresses so a string is always
  treated as a single recipient.
- Helper accepts extra_to (used by ReplyToThread to prepend the
  original sender), subject_override (for 'Re: ...'), and
  in_reply_to/references for threading. Callers compute the final
  References string so each existing behavior (append vs pass-through)
  is preserved verbatim.
- Adds a TestBuildRawEmail unit test class covering the regression
  (string vs list recipients), extra_to prepending, subject override,
  the 'me' From sentinel, and threading header behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(gmail): upgrade to SDK 2.0.0 and add unit + integration tests

1 participant