Decision: A Scoped, Opt-In pytest Harness for the Local-Filesystem QA Gap¶
Date: 2026-08-22 Issue: #50
Background¶
docs/qa/tests/drive_transfer.md carries ~27 test cases tagged ⚠️ local-filesystem — every case exercising upload_local_file/upload_local_folder, download_file/download_folder, and sync_folder. Per docs/qa/README.md, these "need file paths accessible to the MCP server process" and "cannot run in an AI-session QA run" — the human-led conductor flow (docs/qa/run.md) that decision-testing.md chose as the project's primary QA method has no guaranteed way to place a fixture file on the same host as the MCP server process it's driving (that flow is written for "a Claude session that has the mcp-gee-sweet MCP server connected" generically — nothing about it guarantees the client and server share a filesystem, e.g. a Docker-hosted SSE server driven from a separate machine). These cases are standing, permanent skips, not "pending."
Issue #50 asked whether a pytest-driven subprocess harness — start uv run mcp-gee-sweet, issue calls, check results — could cover them without manual intervention.
What this does not reopen¶
decision-testing.md rejected "Option A: automated integration tests against real Google APIs" as the project's primary QA method, for reasons that still hold at full-suite scale: dedicated CI secrets and fixture maintenance, quota/rate-limit flakiness gating every PR, and a real cleanup burden across four Google APIs. This decision does not revisit that verdict — human-led verification (now Playwright-assisted for visual checks) stays primary for the ~250+ other test cases that can run that way.
What's different here: the local-filesystem cases aren't a case where Option A's automation is merely nicer than the chosen Option C — Option C is structurally unable to reach them at all, for any human, regardless of diligence. That's a gap in the chosen method's own coverage, not a case for re-litigating the method.
Investigation findings¶
- The real constraint is broader than "local filesystem."
spreadsheet_lifespan(auth.py) authenticates eagerly on server startup, before any tool is called — so launching the server subprocess at all requires live, valid Google credentials, for literally any tool, not just the local-filesystem ones. A harness that can start the server can therefore also exercise the ~250 other tools; nothing scopes it to only the gap it was built for. - This repo already has every piece needed, confirmed live. The team-role
.mcp.jsonconfig (scripts/setup_team.sh) launches each role's own server exactly this way —uv run --directory <worktree> mcp-gee-sweetover stdio, withAUTH_METHOD/CREDENTIALS_PATH/TOKEN_PATH(orSERVICE_ACCOUNT_PATH) pointing at per-worktree credential files.scripts/oauth_setup.pyalready reconstructs a validtoken.jsonfromGOOGLE_OAUTH_REFRESH_TOKENwith no browser flow, for exactly the "no manual intervention" requirement this issue asks about. Building the harness meant reusing this plumbing, not inventing new credential machinery. - The
mcpSDK (v2.0.0, already a dependency post-#642) has a first-class stdio client —mcp.client.stdio.stdio_client+mcp.ClientSession— sufficient to drive the real subprocess and readCallToolResult.structured_contentdirectly. No custom transport or protocol code was needed. - CI has no live credentials and this stays true.
.github/workflows/ci.ymlrunsuv run python -m pytest --tb=shortwith no Google secrets configured. A subprocess harness fixes the filesystem barrier, not the credentials barrier — those are separate problems. Wiring live credentials into CI (viaGOOGLE_OAUTH_REFRESH_TOKENas a repo secret, the same mechanismoauth_setup.pyalready supports) is a distinct decision with its own cost/quota/secret-rotation tradeoffs, deliberately left open here rather than decided as a side effect of this ticket — see Scope below.
Decision¶
Add tests/integration/ — a pytest package, opt-in via MCP_GEE_SWEET_LIVE_TESTS=1, that:
- Spawns the real
mcp-gee-sweetserver as a subprocess over stdio usingmcp.client.stdio.stdio_client+ClientSession. - Auto-discovers credentials the same way
.mcp.jsonalready does (repo-rootcredentials.json+token.json, falling back toservice_account.json), or honors an already-exportedAUTH_METHODso a developer's own setup isn't second-guessed. - Reuses the existing
TEST_FOLDER_IDQA fixture (root.env,docs/qa/setup.md) and creates a throwaway child folder per test session, deleted permanently on teardown — same fixture-pollution disciplinedocs/qa/run.mdalready documents for the manual flow. - Skips cleanly (not a failure) whenever the opt-in env var or credentials are absent — verified live: the default
uv run python -m pytestrun (no env var set) collects and skips this package with zero live calls, so it's always safe to leave intests/and it never affects the existing CI matrix as configured today.
This module lives under tests/ (picked up by the existing testpaths) rather than a separate top-level directory, specifically because it self-skips safely — no pyproject.toml/CI wiring changes were needed to keep the default run green.
Scope of this PR¶
Three representative tests prove the mechanism rather than porting all ~27 cases:
test_upload_local_file_round_trip(covers the shape of TC-D93/94/97)test_download_file_round_trip(covers the shape of TC-D101/106)test_sync_folder_upload(covers the shape of TC-D113/117)
Each was run live against this repo's own QA fixture folder before this PR was opened (per this project's "verify a ticket's premise live" rule) — real subprocess, real Drive API calls, real cleanup.
Not in scope, follow-up ticket: upload_local_folder/download_folder, the convert/convert_markdown variants, use_checksum, recursive, and the various conflict/collision/error-path cases the remaining ~24 skipped QA cases cover. Each follows the identical pattern established here — no new mechanism needed, just more test functions.
Explicitly deferred, needs a separate decision: wiring GOOGLE_OAUTH_REFRESH_TOKEN into a GitHub Actions secret so this harness (or an expanded version of it) runs in CI rather than only ad hoc on a developer machine. That reopens exactly the secret-management and quota-flakiness tradeoffs decision-testing.md weighed for the full suite — worth deciding deliberately, not as a side effect of closing #50.
What this means for the ⚠️ local-filesystem QA tags¶
The tags stay. The human-led conductor flow in docs/qa/run.md still can't run these — that hasn't changed. docs/qa/README.md now cross-references this harness as the actual place this coverage lives, so a future reader doesn't read "always skipped" and stop looking.