mcp-gee-sweet 0.9.2.dev215__tar.gz → 0.9.2.dev221__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/PKG-INFO +1 -1
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/drive_files.md +26 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/drive_transfer.md +50 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/roadmap.md +10 -33
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/drive/files.py +30 -2
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/drive/transfer.py +97 -17
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/drive/test_files.py +82 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/drive/test_transfer.py +372 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/cleanup-worktrees.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/merge-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/notify-kai.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/notify-partner.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/orchestrator.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/prep-for-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/qa-kickoff.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/release.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/retro.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/roadmap-status.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/start-worktree.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/team-member.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/commands/verify-pr.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/settings.json +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/team-roles/amy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/team-roles/aziz.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/team-roles/bob.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/team-roles/dev.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/team-roles/joy.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/team-roles/kai.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.claude/team-roles/qa.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.dockerignore +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.gitignore +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/.python-version +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/CLAUDE.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/Dockerfile +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/LICENSE +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/Makefile +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/assets/logo.png +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/configuration.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-async-tool-execution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-cache-invalidation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-comments-first-class.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-dev-team.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-gmail-domain.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-local-fs-test-harness.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-pillow-image-dependency.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-prompt-qa-role.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-release-docs-roles.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-repositioning.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-sync-folder-recursion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/blockquote-representation.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/borderless-table-columns.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/doc-to-markdown.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/heading-anchor-resolution.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/image-conversion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/module-history.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design/sse-consent-off-event-loop.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/design.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/index.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/gmail-test-plan.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/results/2026-09-04.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/retro-v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/runs/v0.9.0.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/docs_content.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/docs_layout.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/docs_style.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/docs_tables.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/drive_activity.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/drive_sharing.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/gmail.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/infra.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/sheets_read.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/style-guide.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/docs/tools.md +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/pyproject.toml +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/pyrightconfig.json +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/scripts/check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/scripts/gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/scripts/lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/scripts/qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/scripts/setup_team.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/scripts/write_team_mcp_configs.sh +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/server.json +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/auth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/http_transport.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/server.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/emitter.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/gmail.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/conftest.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/drive/test_activity.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/integration/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/integration/conftest.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/integration/test_local_fs_harness.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/sheets/test_data.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/sheets/test_structure.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_cache.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_calendar.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_check_supports_all_drives.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_concurrency.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_anchors.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_comments.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_content.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_core.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_editing.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_images.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_indices.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_markdown_export.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_named_ranges.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_docs_tables.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_gmail.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_lane_context_hook.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_qa_gmail_fixtures.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_response_limits.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_server_json.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/tests/test_team_mcp_configs.py +0 -0
- {mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-gee-sweet
|
|
3
|
-
Version: 0.9.2.
|
|
3
|
+
Version: 0.9.2.dev221
|
|
4
4
|
Summary: MCP server for Google Workspace — Sheets, Drive, Docs (HTML or Markdown), and Calendar.
|
|
5
5
|
Project-URL: Homepage, https://github.com/khuisman/mcp-gee-sweet
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/khuisman/mcp-gee-sweet/issues
|
|
@@ -887,6 +887,32 @@ delete_file('invalidid123xyz') → HttpError 404 propagates cleanly, no crash. N
|
|
|
887
887
|
|
|
888
888
|
---
|
|
889
889
|
|
|
890
|
+
### TC-D280: Permanent delete without permanent-delete permission — clear error, file kept (issue #876) ⚠️ destructive
|
|
891
|
+
|
|
892
|
+
**Requires:** an identity with the **Content manager** role (not Manager) on the `mcp-gee-sweet-shared` Shared Drive. The QA service account is added at exactly that role (`setup.md` Step 1), so run this on a service-account server (`mcp-gee-sweet-sa`, or the lane's own server relaunched with `AUTH_METHOD=service_account`). An OAuth identity that manages the Shared Drive gets `canDelete: true` and can't reproduce this.
|
|
893
|
+
|
|
894
|
+
**Setup:** `upload_file(name="QA-876-Delete-Denied.txt", content="x", folder_id={FOLDER_ID})` as the Content-manager identity. Note the returned file ID.
|
|
895
|
+
|
|
896
|
+
**Steps**
|
|
897
|
+
1. `delete_file(file_id=<id>, permanent=True)`
|
|
898
|
+
2. `get_file_metadata(file_id=<id>)`
|
|
899
|
+
3. `delete_file(file_id=<id>, permanent=False)`
|
|
900
|
+
|
|
901
|
+
**Checks**
|
|
902
|
+
- Step 1 fails with a `ValueError` saying the identity lacks permanent-delete permission, noting that on a Shared Drive this usually requires the Manager role, and suggesting `permanent=False`. It is **not** Drive's `HttpError 404 "File not found"`.
|
|
903
|
+
- Step 2 returns the file with `trashed: false`: nothing was deleted.
|
|
904
|
+
- Step 3 returns `{"fileId": <id>, "action": "trashed"}`.
|
|
905
|
+
|
|
906
|
+
**Cleanup:** step 3 leaves the file in the Shared Drive's trash. Purge it with `delete_file(file_id=<id>, permanent=True)` from an identity with the Manager role (e.g. the lane's OAuth server), or leave it for a later `empty_trash(drive_id={SHARED_DRIVE_ID})` run.
|
|
907
|
+
|
|
908
|
+
**Result (2026-10-04) ✅ PASS**
|
|
909
|
+
Run as the QA service account (Content manager) from a fresh interpreter in the kit worktree driving PR #888's `delete_file` (kit's server is OAuth/Manager). Pre-check: `capabilities={canDelete: false, canTrash: true}`, file on the Shared Drive. Step 1 raised `ValueError` naming missing permanent-delete permission + `permanent=False` hint; no `files().delete` sent. Step 2 `trashed: false`. Baseline: a raw `files().delete` as the same SA still returns Drive's `404 "File not found"`, so the pre-flight check is what fixes it. Step 3 `{"action": "trashed"}`, then `trashed: true`. Cleanup: `delete_file(permanent=True)` via kit's OAuth (Manager) server returned `{"action": "deleted"}`, which also covers the `canDelete: true` path live.
|
|
910
|
+
|
|
911
|
+
**Result (2026-10-04, round 2) ✅ PASS**
|
|
912
|
+
Re-run the same way against `255512b` (Shared Drive note scoped to `driveId`). Step 1's `ValueError` now reads "...lacks permanent-delete permission on it. On a Shared Drive this usually requires the Manager role. Use permanent=False to move it to the trash instead." Steps 2–3 and the raw-delete 404 baseline as in round 1. Cleanup via kit's reconnected OAuth server: `{"action": "deleted"}`. The My Drive (no `driveId`) wording isn't live-tested: it needs a file owned by someone else, where a wrong result would be a real permanent delete. Unit test `test_delete_file_permanent_without_can_delete_on_my_drive_omits_shared_drive_note` covers it.
|
|
913
|
+
|
|
914
|
+
---
|
|
915
|
+
|
|
890
916
|
## `restore_file`
|
|
891
917
|
|
|
892
918
|
### TC-D269: Restore a trashed file ⚠️ destructive
|
|
@@ -1787,6 +1787,56 @@ Unit tests: full suite 1883 passed / 3 skipped; ruff clean. Fast-path re-verific
|
|
|
1787
1787
|
|
|
1788
1788
|
---
|
|
1789
1789
|
|
|
1790
|
+
### TC-D279: downloads write to a temp file and rename it into place; a leftover temp file is never uploaded (issue #844) ⚠️ local-filesystem ⚠️ destructive
|
|
1791
|
+
|
|
1792
|
+
**Background:** `sync_folder`, `download_file`, and `download_folder` used to stream straight into the destination. A download that failed partway left a truncated file with a current mtime, so the next bidirectional sync read it as locally newer and uploaded it over the intact Drive copy. Every download now goes through `_write_atomically`: it writes a sibling `.gee-sweet-partial-<16 hex>` file, stamps `sync_folder`'s Drive mtime on it, and renames it onto the destination only when complete. A temp file survives only a hard kill mid-write, and `sync_folder` and `upload_local_folder` skip one. The failure path itself (a dropped connection mid-download) can't be triggered through the tool surface; it's unit-tested in `tests/drive/test_transfer.py::TestSyncFolderFailedDownloadLeavesNoPartial`, `TestDownloadToolsFailedDownloadLeavesNoPartial`, and `TestWriteAtomically`. This case checks the success path through the new write, the mode and mtime it leaves, and the leftover-temp-file skip.
|
|
1793
|
+
|
|
1794
|
+
**Setup**
|
|
1795
|
+
> 1. Call `create_folder(name="qa-279", parent_folder_id="{FOLDER_ID}")`; call the returned ID `{F}`.
|
|
1796
|
+
> 2. Run `rm -rf /tmp/qa-279 && mkdir -p /tmp/qa-279/src /tmp/qa-279/sync && head -c 3000000 /dev/urandom > /tmp/qa-279/src/big.bin`.
|
|
1797
|
+
> 3. Call `upload_local_file(local_path="/tmp/qa-279/src/big.bin", parent_folder_id="{F}")`; call the returned ID `{BIG}`. Call `get_file_metadata(file_id="{BIG}")` and note `modified_time` and `md5_checksum`.
|
|
1798
|
+
> 4. Plant a stale local copy and a leftover temp file: `printf 'stale' > /tmp/qa-279/sync/big.bin && chmod 640 /tmp/qa-279/sync/big.bin && touch -t 202001010000 /tmp/qa-279/sync/big.bin && printf 'half a file' > /tmp/qa-279/sync/.gee-sweet-partial-0123456789abcdef`.
|
|
1799
|
+
|
|
1800
|
+
**Steps**
|
|
1801
|
+
> 1. `sync_folder(folder_id="{F}", local_path="/tmp/qa-279/sync/")` (real run, default `bidirectional`).
|
|
1802
|
+
> 2. Run `ls -la /tmp/qa-279/sync/ && md5 -q /tmp/qa-279/sync/big.bin && stat -f '%Sp %Sm' -t '%Y-%m-%dT%H:%M:%S' /tmp/qa-279/sync/big.bin` (Linux: `md5sum`, `stat -c '%A %y'`).
|
|
1803
|
+
> 3. `list_files(folder_id="{F}")`.
|
|
1804
|
+
> 4. `sync_folder(folder_id="{F}", local_path="/tmp/qa-279/sync/")` again.
|
|
1805
|
+
> 5. `upload_local_folder(local_path="/tmp/qa-279/sync/", parent_folder_id="{F}", skip_if_exists=false)`, then `list_files(folder_id="{F}")`.
|
|
1806
|
+
> 6. `download_file(file_id="{BIG}", local_path="/tmp/qa-279/sync/big.bin")`, then `ls -la /tmp/qa-279/sync/`.
|
|
1807
|
+
> 7. `download_folder(folder_id="{F}", local_path="/tmp/qa-279/dl/", skip_if_exists=false)`, then `ls -la /tmp/qa-279/dl/`.
|
|
1808
|
+
> 8. Make a read-only local copy: `printf 'protected' > /tmp/qa-279/ro.bin && chmod 444 /tmp/qa-279/ro.bin && chmod 4644 /tmp/qa-279/dl/big.bin`. Call `download_file(file_id="{BIG}", local_path="/tmp/qa-279/ro.bin")`. Then call `download_file(file_id="{BIG}", local_path="/tmp/qa-279/dl/big.bin")` and run `ls -la /tmp/qa-279/ /tmp/qa-279/dl/ && cat /tmp/qa-279/ro.bin`. (Not as root: root can write a `0444` file.)
|
|
1809
|
+
|
|
1810
|
+
**Checks**
|
|
1811
|
+
- Step 1: `downloaded` is `["big.bin"]`. `uploaded` is empty, and `.gee-sweet-partial-0123456789abcdef` appears in no result list. `failed` is empty.
|
|
1812
|
+
- Step 2: `big.bin` is 3000000 bytes, its md5 matches setup step 3's `md5_checksum`, its mode is still `-rw-r-----` (the replaced file's mode is kept), and its mtime matches setup step 3's `modified_time` to the second (local time vs UTC aside). The only `.gee-sweet-partial-*` file in the directory is the planted `…0123456789abcdef`; no new one was left behind.
|
|
1813
|
+
- Step 3: `{F}` holds exactly one file, `big.bin`. No `.gee-sweet-partial-*` file was uploaded.
|
|
1814
|
+
- Step 4: `big.bin` in `skipped`; `uploaded` and `downloaded` empty (the restamped mtime round-trips, as in #346).
|
|
1815
|
+
- Step 5: `uploaded` lists only `big.bin` (a second copy, since `skip_if_exists=false`); the planted temp file is not uploaded and not in `failed`. `list_files` shows two `big.bin` and no `.gee-sweet-partial-*`.
|
|
1816
|
+
- Step 6: succeeds with `size_bytes` 3000000. The directory still holds only `big.bin` and the planted temp file.
|
|
1817
|
+
- Step 7: `downloaded` is `["big.bin"]` and `failed` has one duplicate-filename entry for the second `big.bin` (PR #351). `/tmp/qa-279/dl/` holds only `big.bin`, 3000000 bytes, and no `.gee-sweet-partial-*` file.
|
|
1818
|
+
- Step 8 (PR #884 QA round 1): the first `download_file` fails with a permission error. `ro.bin` still reads `protected`, still `-r--r--r--`, and no `.gee-sweet-partial-*` file is in `/tmp/qa-279/`. The second call succeeds, and `dl/big.bin` is `-rw-r--r--`: the setuid bit is not carried onto the new content.
|
|
1819
|
+
|
|
1820
|
+
**Teardown**
|
|
1821
|
+
`delete_file` on `{F}` (takes its contents with it). `rm -rf /tmp/qa-279`.
|
|
1822
|
+
|
|
1823
|
+
**Result (2026-10-04) ✅ PASS** — Sky, PR #884 round 1 (`b74ec2b`), OAuth, throwaway `qa-279` folder under `TEST_FOLDER_ID`. Drive `big.bin`: 3000000 B, md5 `f606776b…2c1a`, `modified_time` `17:42:20Z`.
|
|
1824
|
+
- **Step 1:** `downloaded: ["big.bin"]`; `uploaded`, `conflicts`, `failed` empty; the planted temp file appears in no list.
|
|
1825
|
+
- **Step 2:** `big.bin` 3000000 B, md5 matches, mode `-rw-r-----` kept, mtime `10:42:20-0700` (= `17:42:20Z`). Only the planted `…0123456789abcdef` temp file present.
|
|
1826
|
+
- **Step 3:** one file, `big.bin`.
|
|
1827
|
+
- **Step 4:** `skipped: ["big.bin"]`, nothing transferred.
|
|
1828
|
+
- **Step 5:** `uploaded: ["big.bin"]` only; `list_files` shows two `big.bin`, no temp file.
|
|
1829
|
+
- **Step 6:** `size_bytes` 3000000; directory still holds only `big.bin` + the planted temp file.
|
|
1830
|
+
- **Step 7:** `downloaded: ["big.bin"]`, one duplicate-filename `failed` entry; `dl/` holds only `big.bin`, 3000000 B.
|
|
1831
|
+
|
|
1832
|
+
The case passes, but the PR goes back for a regression this case doesn't cover: a read-only (`0444`) local destination is now silently replaced. Live, after this case: `printf protected > big.bin && chmod 444 big.bin`, then `download_file(file_id="{BIG}", local_path=".../big.bin")` returned success and left a 3000000 B `-r--r--r--` file. Before this PR the `open("wb")` raised `PermissionError` and left the file alone. See the PR comment.
|
|
1833
|
+
|
|
1834
|
+
**Result (2026-10-04) ✅ PASS** — Sky, PR #884 round 2 (`b4e7287`), step 8 only, after reconnecting to the fix. Steps 1–7 code paths are unchanged apart from the new up-front `os.access` check and the `0o777` mask. Fresh `qa-279` folder; setup plus step 7 (`download_folder` → `dl/big.bin`, 3000000 B) re-run first.
|
|
1835
|
+
- **Step 8, `ro.bin`:** `download_file` failed with `[Errno 13] Permission denied: '/tmp/qa-279/ro.bin'`. `ro.bin` still reads `protected`, still `-r--r--r--`, and there's no `.gee-sweet-partial-*` in `/tmp/qa-279/`.
|
|
1836
|
+
- **Step 8, `dl/big.bin` (`chmod 4644` first):** succeeded, `size_bytes` 3000000, and the file is now `-rw-r--r--`: setuid dropped.
|
|
1837
|
+
|
|
1838
|
+
---
|
|
1839
|
+
|
|
1790
1840
|
## `list_revisions`
|
|
1791
1841
|
|
|
1792
1842
|
### TC-D146: List revisions for a spreadsheet
|
|
@@ -192,10 +192,8 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
|
|
|
192
192
|
- [x] `create_paragraph_bullets` (`style.py`) has the same isolated-run collapse-to-`nestingLevel 0` bug #439 fixed in `emitter.py` only — port PR #711's anchor-paragraph approach to the sibling path (PR #723) ([#713](https://github.com/khuisman/mcp-gee-sweet/issues/713))
|
|
193
193
|
- [x] Lock in the isolated-run rendering rule — an isolated bullet run renders at its true depth (`depth N` → `nestingLevel N`) at *every* depth, never clamped (decided 2026-09-09: consistency over avoiding orphaned indent; the markup generator owns the depth it emits). Extend TC-DOC190 with a `min_depth >= 2` case; state the rule in `emitter.py`'s bullet-pass comment and the `docs/emitter.py` paragraph of `CLAUDE.md` (PR #731) ([#714](https://github.com/khuisman/mcp-gee-sweet/issues/714))
|
|
194
194
|
- [x] `create_doc` markdown: a fenced code block whose body is raw HTML tags renders as loose plain paragraphs (fence ignored, indentation flattened) instead of a monospaced code block — python-markdown's raw-HTML-block handling appears to pre-empt `fenced_code`. Surfaced during #714 triage (PR #730) ([#719](https://github.com/khuisman/mcp-gee-sweet/issues/719))
|
|
195
|
-
- [ ]
|
|
196
|
-
- [ ]
|
|
197
|
-
- [ ] `emitter.py` table-cell images have no cursor-advance handling — latent infinite loop if that code path is ever reached ([#509](https://github.com/khuisman/mcp-gee-sweet/issues/509))
|
|
198
|
-
- [ ] Calendar ACL tools use a bare `except Exception` instead of the `HttpError` friendly-error pattern used elsewhere in `calendar.py` ([#459](https://github.com/khuisman/mcp-gee-sweet/issues/459))
|
|
195
|
+
- [ ] **Bundle:** Docs write side (emitter, html_parser, images, content), one PR closing [#509](https://github.com/khuisman/mcp-gee-sweet/issues/509), [#567](https://github.com/khuisman/mcp-gee-sweet/issues/567), [#581](https://github.com/khuisman/mcp-gee-sweet/issues/581), [#638](https://github.com/khuisman/mcp-gee-sweet/issues/638), [#715](https://github.com/khuisman/mcp-gee-sweet/issues/715) — lane A ([#894](https://github.com/khuisman/mcp-gee-sweet/issues/894))
|
|
196
|
+
- [ ] **Bundle:** Calendar hardening, one PR closing [#459](https://github.com/khuisman/mcp-gee-sweet/issues/459), [#615](https://github.com/khuisman/mcp-gee-sweet/issues/615), [#626](https://github.com/khuisman/mcp-gee-sweet/issues/626), [#627](https://github.com/khuisman/mcp-gee-sweet/issues/627), [#726](https://github.com/khuisman/mcp-gee-sweet/issues/726) — lane B ([#890](https://github.com/khuisman/mcp-gee-sweet/issues/890))
|
|
199
197
|
- [ ] `delete_file(permanent=True)` surfaces a misleading 404 when the caller can trash but not permanently delete (Shared Drive Content manager) — an agent can read it as "already gone" and skip cleanup; check `capabilities.canDelete` first — lane B ([#876](https://github.com/khuisman/mcp-gee-sweet/issues/876))
|
|
200
198
|
- [ ] SSE: SIGTERM with an SSE stream open logs an "Exception in ASGI application" traceback; the server still exits cleanly, pre-existing before #833 — lane A ([#868](https://github.com/khuisman/mcp-gee-sweet/issues/868))
|
|
201
199
|
- [x] `sync_folder`'s `convert_markdown` restamp-failure handling: quota-message gap, duplicated create()+update() try/except, test fixture isolation — companion to #420 (PR #669) ([#650](https://github.com/khuisman/mcp-gee-sweet/issues/650))
|
|
@@ -238,20 +236,14 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
|
|
|
238
236
|
- [x] `content.py`: image-fetch-failure retry mechanism is fragile, resends the whole document per bad image ([#510](https://github.com/khuisman/mcp-gee-sweet/issues/510)) (PR #787)
|
|
239
237
|
- [x] `content.py`: duplicated image upload/share/revoke logic across `create_doc` and `insert_local_images`, plus an avoidable extra Drive round trip ([#511](https://github.com/khuisman/mcp-gee-sweet/issues/511)) (PR #842)
|
|
240
238
|
- [x] `docs/images.py` oversized-image handling: duplicated logic + unneeded full pixel decode (#400 follow-up) ([#560](https://github.com/khuisman/mcp-gee-sweet/issues/560)) (PR #801)
|
|
241
|
-
- [ ]
|
|
242
|
-
- [ ] Inline-image size validation: shrink-loop postcondition, check-order inconsistency, unguarded size parsing (#562 follow-up) ([#581](https://github.com/khuisman/mcp-gee-sweet/issues/581))
|
|
243
|
-
- [ ] `doc_to_ast.py` duplicates `style.py`'s glyph-inference and color-extraction logic ([#596](https://github.com/khuisman/mcp-gee-sweet/issues/596))
|
|
244
|
-
- [ ] `ast_to_markdown.py`: image-render duplication + falsy-based code-block buffer flush ([#600](https://github.com/khuisman/mcp-gee-sweet/issues/600))
|
|
245
|
-
- [ ] `html_parser.py` `handle_data`: a SIM114 lint merge conflates two structurally distinct buffering triggers ([#638](https://github.com/khuisman/mcp-gee-sweet/issues/638))
|
|
239
|
+
- [ ] **Bundle:** Docs read side (`doc_to_ast`/`ast_to_markdown`), one PR closing [#596](https://github.com/khuisman/mcp-gee-sweet/issues/596), [#600](https://github.com/khuisman/mcp-gee-sweet/issues/600), [#732](https://github.com/khuisman/mcp-gee-sweet/issues/732) — lane B ([#893](https://github.com/khuisman/mcp-gee-sweet/issues/893))
|
|
246
240
|
- [ ] Docs image-sharing failure paths: unrevocable `anyone:reader` grant + missing folder-cache invalidation in `_replace_doc_content` ([#666](https://github.com/khuisman/mcp-gee-sweet/issues/666))
|
|
247
241
|
- [ ] `write_doc_content`/`update_doc_from_file` leave the doc empty when the replacement fails after the clear — decided 2026-10-04: record the pre-clear revision ID and name it in the error so the caller can recover via `export_revision` ([#878](https://github.com/khuisman/mcp-gee-sweet/issues/878))
|
|
248
242
|
- [ ] `insert_local_images`: temporary `anyone:reader` image shares aren't revoked when the call is cancelled — apply PR #877's shielded-cleanup pattern — lane B ([#883](https://github.com/khuisman/mcp-gee-sweet/issues/883))
|
|
249
243
|
- [x] `_apply_doc_content`: temporary `anyone:reader` image shares aren't revoked when the doc edit raises — revoke pass should run in a `finally`. Same share/revoke surface as #666 and #511 ([#789](https://github.com/khuisman/mcp-gee-sweet/issues/789)) (PR #877)
|
|
250
244
|
- [ ] Docstrings: `create_paragraph_bullets` carries internal API-quirk mechanics plus residual bare issue refs in `style.py` tool docstrings ([#683](https://github.com/khuisman/mcp-gee-sweet/issues/683))
|
|
251
|
-
- [ ] `TestIsolatedDepthRunGlyph` asserts request shape, not rendered nesting outcome (PR #711) ([#715](https://github.com/khuisman/mcp-gee-sweet/issues/715))
|
|
252
245
|
- [ ] `style_doc_range` has no `font_family` parameter — can't apply monospace to an existing range ([#720](https://github.com/khuisman/mcp-gee-sweet/issues/720))
|
|
253
246
|
- [x] Extract a shared helper for the isolated-bullet-run zero-tab anchor mechanic (`emitter.py` ↔ `style.py`); harden its regression test (`lane-a`) ([#727](https://github.com/khuisman/mcp-gee-sweet/issues/727)) (PR #735)
|
|
254
|
-
- [ ] `doc_to_ast.py`'s Courier-New soft-break reversal is per-run, not per-paragraph — mismatched-style edge case can emit malformed inline code with an embedded newline (PR #730 follow-up) ([#732](https://github.com/khuisman/mcp-gee-sweet/issues/732))
|
|
255
247
|
|
|
256
248
|
**Drive hardening & dedup** _(triaged out of `backlog` 2026-09-11)_
|
|
257
249
|
- [x] `download_folder`/`sync_folder` progress notifications: report bytes transferred, not just file count ([#352](https://github.com/khuisman/mcp-gee-sweet/issues/352)) (PR #739)
|
|
@@ -273,37 +265,24 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
|
|
|
273
265
|
- [x] `sync_folder(convert_markdown=True)`: a `.md` name over ~95 bytes overflows Drive's 124-byte property cap — upload fails with `propertyLengthLimitExceeded` but Drive still creates an unmarked orphan Doc, repeated on every sync. Needs a design call on converted-Doc recognition (PR #800 follow-up) ([#805](https://github.com/khuisman/mcp-gee-sweet/issues/805)) (PR #812)
|
|
274
266
|
- [x] Deduplicate the `max_results` clamp expression, hand-copied 8x across `drive/files.py` with inconsistent caps (PR #736 follow-up) ([#737](https://github.com/khuisman/mcp-gee-sweet/issues/737)) (PR #781)
|
|
275
267
|
- [x] `download_file`: one pre-flight `local_path` check before any Drive call, for both trailing-slash and plain branches, naming the blocking path (#724/#827 follow-up; on-deck, `good first issue`) (PR #855) ([#836](https://github.com/khuisman/mcp-gee-sweet/issues/836))
|
|
276
|
-
- [
|
|
277
|
-
- [ ]
|
|
268
|
+
- [x] `sync_folder`: a failed mid-stream download leaves a truncated local file with a fresh mtime, which the next sync uploads over the intact Drive copy — download to a temp file and `os.replace` on success; check `download_file`/`download_folder` too. Found by code review, not yet observed live (lane-a, after #814 since both touch `_run_one`) ([#844](https://github.com/khuisman/mcp-gee-sweet/issues/844)) (PR #884)
|
|
269
|
+
- [ ] **Bundle:** Drive transfer follow-ups (`download_file` preflight + `_write_atomically`), one PR closing [#874](https://github.com/khuisman/mcp-gee-sweet/issues/874), [#887](https://github.com/khuisman/mcp-gee-sweet/issues/887) — lane A ([#895](https://github.com/khuisman/mcp-gee-sweet/issues/895))
|
|
278
270
|
|
|
279
271
|
**Calendar hardening** _(triaged out of `backlog` 2026-09-11)_
|
|
280
|
-
- [ ] `list_calendar_acl` pagination fix (#460) needs a response-size cap, loop safety bound, and partial-failure handling ([#615](https://github.com/khuisman/mcp-gee-sweet/issues/615))
|
|
281
|
-
- [ ] `list_all_events` concurrency cap: per-invocation only, no env override ([#626](https://github.com/khuisman/mcp-gee-sweet/issues/626))
|
|
282
|
-
- [ ] `_shape_event`: `get_event` still duplicates it, no null-guard, no kwarg-collision guard ([#627](https://github.com/khuisman/mcp-gee-sweet/issues/627))
|
|
283
|
-
- [ ] `find_free_slots`/`list_all_events`: unify duplicated id-to-summary lookup, align empty-string fallback (PR #725 follow-up) ([#726](https://github.com/khuisman/mcp-gee-sweet/issues/726))
|
|
284
272
|
|
|
285
273
|
**Testing** _(triaged out of `backlog` 2026-09-11)_
|
|
286
|
-
- [ ]
|
|
287
|
-
- [ ] Add unit test coverage for `sync_folder`'s SA-limitations caveat text ([#526](https://github.com/khuisman/mcp-gee-sweet/issues/526))
|
|
288
|
-
- [ ] `test`: strengthen `download_file` unit test assertions (metadata call signature, export str-fallback) ([#551](https://github.com/khuisman/mcp-gee-sweet/issues/551))
|
|
289
|
-
- [ ] `test(drive)`: dedupe `skip_if_exists` companion-test boilerplate in `test_transfer.py` ([#561](https://github.com/khuisman/mcp-gee-sweet/issues/561))
|
|
274
|
+
- [ ] **Bundle:** test-suite tightening, one PR closing [#392](https://github.com/khuisman/mcp-gee-sweet/issues/392), [#526](https://github.com/khuisman/mcp-gee-sweet/issues/526), [#551](https://github.com/khuisman/mcp-gee-sweet/issues/551), [#561](https://github.com/khuisman/mcp-gee-sweet/issues/561), [#574](https://github.com/khuisman/mcp-gee-sweet/issues/574) — lane B ([#891](https://github.com/khuisman/mcp-gee-sweet/issues/891))
|
|
290
275
|
- [ ] `qa`: `search_files` has no unit test coverage for the error-dict path ([#573](https://github.com/khuisman/mcp-gee-sweet/issues/573))
|
|
291
|
-
- [ ] `test`: brittle exact-equality assertion in `test_api_error_returns_error_dict_not_raised` (`test_files.py`) ([#574](https://github.com/khuisman/mcp-gee-sweet/issues/574))
|
|
292
276
|
|
|
293
277
|
**Infrastructure**
|
|
294
278
|
- [ ] Interaction-log middleware for tool calls — structured, append-only JSONL log per call (inputs, duration, cache hit, error), opt-in ID redaction, swappable backend ([#646](https://github.com/khuisman/mcp-gee-sweet/issues/646))
|
|
295
279
|
- [ ] Automated tooling for dependency-bump security review — today's manual skim can miss a well-disguised supply-chain compromise ([#520](https://github.com/khuisman/mcp-gee-sweet/issues/520))
|
|
296
|
-
- [ ] CI
|
|
297
|
-
- [ ] Consolidate `uv lock --check` + `uv sync --frozen` into a single `uv sync --locked` step ([#536](https://github.com/khuisman/mcp-gee-sweet/issues/536))
|
|
298
|
-
- [ ] Catch stale hardcoded tool counts in prose docs at commit time, instead of by hand each time drift is caught ([#308](https://github.com/khuisman/mcp-gee-sweet/issues/308))
|
|
280
|
+
- [ ] **Bundle:** CI tidy (on deck, no lane), one PR closing [#536](https://github.com/khuisman/mcp-gee-sweet/issues/536), [#619](https://github.com/khuisman/mcp-gee-sweet/issues/619), [#308](https://github.com/khuisman/mcp-gee-sweet/issues/308), [#708](https://github.com/khuisman/mcp-gee-sweet/issues/708) ([#897](https://github.com/khuisman/mcp-gee-sweet/issues/897))
|
|
299
281
|
- [ ] Automate the mechanical `release.md` steps as GitHub Actions ([#660](https://github.com/khuisman/mcp-gee-sweet/issues/660))
|
|
300
|
-
- [ ] Guard `README.md` against non-absolute image URLs — PyPI's `readme_renderer` silently strips them (shipped a logo-less v0.9.0 page, #706). CI guard test; release/prep-for-pr backstop routed to Bob (#709) ([#708](https://github.com/khuisman/mcp-gee-sweet/issues/708))
|
|
301
282
|
- [ ] `/release` + `prep-for-pr` backstop checks for README render breakage on PyPI — the human-facing companion to #708's CI guard, routed to Bob ([#709](https://github.com/khuisman/mcp-gee-sweet/issues/709))
|
|
302
283
|
- [x] `supportsAllDrives=True` is hand-inlined at ~58 Drive call sites — retrofitted twice now (#687 plus its own original miss); wants a shared wrapper or lint guard so a new call site can't omit it ([#696](https://github.com/khuisman/mcp-gee-sweet/issues/696)) (PR #784)
|
|
303
284
|
- [x] Extract a shared `_BaseCache` helper for the `rows_fetched` sufficiency check + epoch-guarded store — #688's fix copy-pasted the pattern into `DriveFolderCache` instead, and the epoch guard is still missing there ([#698](https://github.com/khuisman/mcp-gee-sweet/issues/698)) (PR #788)
|
|
304
|
-
- [ ]
|
|
305
|
-
- [ ] `server.json`/`test_server_json.py`: minor hardening findings from PR #603's review — triaged out of `backlog` 2026-09-11 ([#607](https://github.com/khuisman/mcp-gee-sweet/issues/607))
|
|
306
|
-
- [ ] Restore the explanatory comment for the `annotations=None` stub param in `gen_tool_docs.py` — triaged out of `backlog` 2026-09-11 ([#637](https://github.com/khuisman/mcp-gee-sweet/issues/637))
|
|
285
|
+
- [ ] **Bundle:** server/infra hygiene (start after #872), one PR closing [#517](https://github.com/khuisman/mcp-gee-sweet/issues/517), [#607](https://github.com/khuisman/mcp-gee-sweet/issues/607), [#614](https://github.com/khuisman/mcp-gee-sweet/issues/614), [#579](https://github.com/khuisman/mcp-gee-sweet/issues/579), [#637](https://github.com/khuisman/mcp-gee-sweet/issues/637) — lane B ([#892](https://github.com/khuisman/mcp-gee-sweet/issues/892))
|
|
307
286
|
- [x] Team setup: give each worktree only its own MCP server — other roles' tool names are an estimated ~15–20k of the ~80k lane bootstrap; measure first. Prioritized 2026-09-29, lane-b ahead of #789 ([#850](https://github.com/khuisman/mcp-gee-sweet/issues/850)) (PR #856)
|
|
308
287
|
- [x] Context-size backstop hook for lane sessions, plus a warning before a cold `--resume` — code half of #843; lane-b after #850 ([#847](https://github.com/khuisman/mcp-gee-sweet/issues/847)) (PR #866)
|
|
309
288
|
- [ ] Cut `CLAUDE.md` and the role files down to lasting rules + pointers, moving per-PR history to `docs/design/`/`docs/decisions/` — Amy writes it (user-assigned), Bob reviews each PR under the prompt-QA gate; can land as one PR per file ([#849](https://github.com/khuisman/mcp-gee-sweet/issues/849))
|
|
@@ -315,13 +294,11 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
|
|
|
315
294
|
- [x] `reply_to_message` should honor Reply-To and handle replies to your own sent mail ([#791](https://github.com/khuisman/mcp-gee-sweet/issues/791)) (PR #815)
|
|
316
295
|
- [x] Decode message bodies using the part's charset; tolerate missing base64 padding ([#792](https://github.com/khuisman/mcp-gee-sweet/issues/792)) (PR #816)
|
|
317
296
|
- [x] `get_message`/`get_thread` return no body when Gmail delivers a large (~MB) `text/plain`/`text/html` part by `attachmentId`. Found in #803's live QA ([#825](https://github.com/khuisman/mcp-gee-sweet/issues/825)) (PR #829)
|
|
318
|
-
- [
|
|
319
|
-
- [ ]
|
|
320
|
-
- [ ] Deduplicate `send_message`/`create_draft` compose and `list_messages`/`list_threads` kwargs/pagination helpers ([#795](https://github.com/khuisman/mcp-gee-sweet/issues/795))
|
|
297
|
+
- [x] `get_thread`/`get_message`: add a body-less option — a long thread trips the response-size cap and there's no other way to get its message IDs, so it can't be read at all ([#793](https://github.com/khuisman/mcp-gee-sweet/issues/793)) (PR #885)
|
|
298
|
+
- [ ] **Bundle:** Gmail cleanup, one PR closing [#795](https://github.com/khuisman/mcp-gee-sweet/issues/795), [#802](https://github.com/khuisman/mcp-gee-sweet/issues/802), [#823](https://github.com/khuisman/mcp-gee-sweet/issues/823) — lane B ([#896](https://github.com/khuisman/mcp-gee-sweet/issues/896))
|
|
321
299
|
- [ ] Local-file read policy across local-path tools — in a remote/SSE deployment these read the server's own filesystem (token, SA key), and Gmail attachments can now mail them out. One consistent policy (e.g. a config switch), not per-tool patches. Decision-needed; Joy scopes ([#796](https://github.com/khuisman/mcp-gee-sweet/issues/796))
|
|
322
300
|
- [ ] Release-pass Gmail QA (`docs/qa/tests/gmail.md`, added by #786). Many cases are destructive (send/draft/trash), so they need a dedicated mailbox fixture plan — Aziz ([#803](https://github.com/khuisman/mcp-gee-sweet/issues/803))
|
|
323
301
|
- [ ] Rewrite TC-GM01–22 prescriptively, add TC-GM26–44 and the Smoke rows, per `docs/qa/gmail-test-plan.md` §4. Part of #803, Aziz ([#822](https://github.com/khuisman/mcp-gee-sweet/issues/822))
|
|
324
|
-
- [ ] Gmail unit-test hardening: MIME build branches, label validation, subject normalization, `max_results` clamping (plan §5 P4–P9; nothing observed failing) ([#823](https://github.com/khuisman/mcp-gee-sweet/issues/823))
|
|
325
302
|
|
|
326
303
|
Tool surface (from the original v1.1+ plan):
|
|
327
304
|
|
{mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/drive/files.py
RENAMED
|
@@ -1260,6 +1260,9 @@ def register(tool):
|
|
|
1260
1260
|
file_id: The ID of the file or folder to remove.
|
|
1261
1261
|
permanent: If False (default), moves to trash (recoverable).
|
|
1262
1262
|
If True, permanently deletes — this cannot be undone.
|
|
1263
|
+
Raises ValueError, without deleting anything, if the
|
|
1264
|
+
caller can't permanently delete the file (e.g. a
|
|
1265
|
+
Content manager on a Shared Drive, who can only trash).
|
|
1263
1266
|
|
|
1264
1267
|
Returns:
|
|
1265
1268
|
Confirmation with fileId and action taken ('trashed' or 'deleted').
|
|
@@ -1267,13 +1270,38 @@ def register(tool):
|
|
|
1267
1270
|
lc = ctx.request_context.lifespan_context
|
|
1268
1271
|
drive_service = lc.drive_service
|
|
1269
1272
|
|
|
1270
|
-
# Fetch parents before deletion so we can invalidate the cache
|
|
1273
|
+
# Fetch parents before deletion so we can invalidate the cache, plus
|
|
1274
|
+
# capabilities: on a Shared Drive a Content manager can trash but not
|
|
1275
|
+
# permanently delete, and Drive reports that missing permission as a
|
|
1276
|
+
# misleading 404 "File not found" (#876).
|
|
1271
1277
|
existing = await execute_in_thread(
|
|
1272
1278
|
drive_service.files()
|
|
1273
|
-
.get(
|
|
1279
|
+
.get(
|
|
1280
|
+
fileId=file_id,
|
|
1281
|
+
fields="parents,driveId,capabilities(canDelete,canTrash)",
|
|
1282
|
+
supportsAllDrives=True,
|
|
1283
|
+
)
|
|
1274
1284
|
.execute,
|
|
1275
1285
|
drive_service,
|
|
1276
1286
|
)
|
|
1287
|
+
capabilities = existing.get("capabilities", {})
|
|
1288
|
+
if permanent and capabilities.get("canDelete") is False:
|
|
1289
|
+
# canDelete is also false for a non-owner editor of a My Drive
|
|
1290
|
+
# file, so only mention Shared Drive roles when the file is on one.
|
|
1291
|
+
shared_drive_note = (
|
|
1292
|
+
" On a Shared Drive this usually requires the Manager role."
|
|
1293
|
+
if existing.get("driveId")
|
|
1294
|
+
else ""
|
|
1295
|
+
)
|
|
1296
|
+
trash_hint = (
|
|
1297
|
+
" Use permanent=False to move it to the trash instead."
|
|
1298
|
+
if capabilities.get("canTrash")
|
|
1299
|
+
else ""
|
|
1300
|
+
)
|
|
1301
|
+
raise ValueError(
|
|
1302
|
+
f"Cannot permanently delete file {file_id}: the authenticated identity "
|
|
1303
|
+
f"lacks permanent-delete permission on it.{shared_drive_note}{trash_hint}"
|
|
1304
|
+
)
|
|
1277
1305
|
for parent in existing.get("parents", []):
|
|
1278
1306
|
lc.drive_folder_cache.mark_dirty(parent)
|
|
1279
1307
|
|
{mcp_gee_sweet-0.9.2.dev215 → mcp_gee_sweet-0.9.2.dev221}/src/mcp_gee_sweet/tools/drive/transfer.py
RENAMED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import base64
|
|
3
|
+
import contextlib
|
|
4
|
+
import errno
|
|
3
5
|
import hashlib
|
|
4
6
|
import io
|
|
5
7
|
import logging
|
|
6
8
|
import mimetypes
|
|
7
9
|
import os
|
|
8
|
-
|
|
10
|
+
import re
|
|
11
|
+
import secrets
|
|
12
|
+
from collections.abc import Callable, Iterable
|
|
9
13
|
from dataclasses import dataclass
|
|
10
14
|
from datetime import datetime, timezone
|
|
11
15
|
from pathlib import Path
|
|
12
|
-
from typing import Any, Literal
|
|
16
|
+
from typing import Any, BinaryIO, Literal
|
|
13
17
|
|
|
14
18
|
import markdown as _md
|
|
15
19
|
from googleapiclient.errors import HttpError
|
|
@@ -39,6 +43,15 @@ _EXPORT_MIME: dict[str, tuple[str, str]] = {
|
|
|
39
43
|
|
|
40
44
|
_SYSTEM_FILES = {".DS_Store", "Thumbs.db", "desktop.ini", ".localized"}
|
|
41
45
|
|
|
46
|
+
# Every download writes to a sibling temp file with this name and renames it
|
|
47
|
+
# onto the destination only once complete (_write_atomically, #844). A temp
|
|
48
|
+
# file only outlives its download when the process is killed mid-write;
|
|
49
|
+
# sync_folder and upload_local_folder skip one so a leftover partial is never
|
|
50
|
+
# uploaded. No part of the destination's name is embedded, so the temp name
|
|
51
|
+
# can't exceed the filesystem's name-length limit.
|
|
52
|
+
_PARTIAL_DOWNLOAD_PREFIX = ".gee-sweet-partial-"
|
|
53
|
+
_PARTIAL_DOWNLOAD_RE = re.compile(r"\.gee-sweet-partial-[0-9a-f]{16}")
|
|
54
|
+
|
|
42
55
|
_SYNC_MTIME_TOLERANCE = 5 # seconds — absorbs clock skew and upload-time drift
|
|
43
56
|
|
|
44
57
|
# use_checksum hashes every both-sides pair with a Drive md5Checksum (#716), so
|
|
@@ -375,6 +388,55 @@ def _drive_time_str(dt: datetime) -> str:
|
|
|
375
388
|
return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
376
389
|
|
|
377
390
|
|
|
391
|
+
def _is_partial_download(name: str) -> bool:
|
|
392
|
+
"""Whether `name` is a temp file _write_atomically left behind."""
|
|
393
|
+
return _PARTIAL_DOWNLOAD_RE.fullmatch(name) is not None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _write_atomically(
|
|
397
|
+
dest: Path, write: Callable[[BinaryIO], object], mtime: float | None = None
|
|
398
|
+
) -> None:
|
|
399
|
+
"""Run `write` against a sibling temp file, then rename it onto `dest`.
|
|
400
|
+
|
|
401
|
+
`dest` only ever holds its previous content or the complete new content.
|
|
402
|
+
Writing straight into `dest` left a truncated file behind when a download
|
|
403
|
+
failed partway, with a current mtime that made sync_folder treat it as the
|
|
404
|
+
newer copy and upload it over the intact Drive file (#844). `mtime`, when
|
|
405
|
+
given, is stamped on the temp file before the rename, so a failed restamp
|
|
406
|
+
also leaves `dest` untouched. The temp file is removed on any failure.
|
|
407
|
+
|
|
408
|
+
A symlink at `dest` is written through, as the old `open("wb")` did: the
|
|
409
|
+
temp file goes next to the link's target and replaces the target. The new
|
|
410
|
+
file gets the mode a fresh `open("wb")` would (0o666 less the umask), or the
|
|
411
|
+
replaced file's permission bits when there was one (never its
|
|
412
|
+
setuid/setgid/sticky bits, which an in-place write would have cleared). A
|
|
413
|
+
hard link to the old file keeps the old content, since the rename swaps in
|
|
414
|
+
a new inode.
|
|
415
|
+
|
|
416
|
+
A rename needs write permission on the directory, not the file, so an
|
|
417
|
+
existing file this process couldn't open for writing is refused up front
|
|
418
|
+
with the same PermissionError `open("wb")` raised; otherwise a file the
|
|
419
|
+
user made read-only would be silently replaced (PR #884 QA round 1).
|
|
420
|
+
"""
|
|
421
|
+
target = Path(os.path.realpath(dest)) if dest.is_symlink() else dest
|
|
422
|
+
if target.exists() and not os.access(target, os.W_OK):
|
|
423
|
+
raise PermissionError(errno.EACCES, os.strerror(errno.EACCES), str(target))
|
|
424
|
+
tmp = target.with_name(_PARTIAL_DOWNLOAD_PREFIX + secrets.token_hex(8))
|
|
425
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0), 0o666)
|
|
426
|
+
try:
|
|
427
|
+
with os.fdopen(fd, "wb") as fh:
|
|
428
|
+
write(fh)
|
|
429
|
+
with contextlib.suppress(FileNotFoundError):
|
|
430
|
+
os.chmod(tmp, target.stat().st_mode & 0o777)
|
|
431
|
+
if mtime is not None:
|
|
432
|
+
os.utime(tmp, (mtime, mtime))
|
|
433
|
+
os.replace(tmp, target)
|
|
434
|
+
except BaseException:
|
|
435
|
+
with contextlib.suppress(OSError):
|
|
436
|
+
os.unlink(tmp)
|
|
437
|
+
raise
|
|
438
|
+
|
|
439
|
+
|
|
378
440
|
def _local_mtime_str(path: Path) -> str:
|
|
379
441
|
"""_local_mtime_dt in the RFC 3339 form Drive's modifiedTime field takes."""
|
|
380
442
|
return _drive_time_str(_local_mtime_dt(path))
|
|
@@ -1328,6 +1390,8 @@ async def _sync_level(
|
|
|
1328
1390
|
continue
|
|
1329
1391
|
if skip_system_files and p.name in _SYSTEM_FILES:
|
|
1330
1392
|
continue
|
|
1393
|
+
if _is_partial_download(p.name):
|
|
1394
|
+
continue
|
|
1331
1395
|
# Only a backslash (a separator on Windows, legal in a POSIX name) can
|
|
1332
1396
|
# fail this for a name the OS listed. Such a file would upload under
|
|
1333
1397
|
# a name this check then refuses on the Drive side, so it'd never
|
|
@@ -1899,6 +1963,15 @@ async def _sync_level(
|
|
|
1899
1963
|
# The plan already routed unsafe names to 'unsafe_name'; this is
|
|
1900
1964
|
# the shared backstop, and a refusal lands as download_fail.
|
|
1901
1965
|
dest_file = _safe_local_dest(dest_dir, name, resolved_dest_dir)
|
|
1966
|
+
# Mirror what the upload branch above does in reverse: set the
|
|
1967
|
+
# local file's mtime to Drive's modifiedTime so the round trip
|
|
1968
|
+
# stays within _SYNC_MTIME_TOLERANCE on the next sync. Without
|
|
1969
|
+
# this the local mtime is "now" (write time), which is always
|
|
1970
|
+
# later than Drive's original timestamp — the next sync sees the
|
|
1971
|
+
# file as locally newer and re-uploads it, indefinitely (#346).
|
|
1972
|
+
# _write_atomically stamps it before the rename, so a failed
|
|
1973
|
+
# download never leaves a "newer" partial in place (#844).
|
|
1974
|
+
drive_ts = _drive_mtime(entry).timestamp()
|
|
1902
1975
|
if is_workspace:
|
|
1903
1976
|
target_mime = _EXPORT_MIME[export_format][0]
|
|
1904
1977
|
content = await execute_in_thread(
|
|
@@ -1907,7 +1980,9 @@ async def _sync_level(
|
|
|
1907
1980
|
)
|
|
1908
1981
|
if not isinstance(content, bytes):
|
|
1909
1982
|
content = content.encode("utf-8")
|
|
1910
|
-
await asyncio.to_thread(
|
|
1983
|
+
await asyncio.to_thread(
|
|
1984
|
+
_write_atomically, dest_file, lambda fh, c=content: fh.write(c), drive_ts
|
|
1985
|
+
)
|
|
1911
1986
|
else:
|
|
1912
1987
|
|
|
1913
1988
|
def _download_to_completion(fid=fid, dest_file=dest_file) -> None:
|
|
@@ -1915,21 +1990,16 @@ async def _sync_level(
|
|
|
1915
1990
|
fileId=fid, supportsAllDrives=True
|
|
1916
1991
|
)
|
|
1917
1992
|
request.http = thread_http(drive_service)
|
|
1918
|
-
|
|
1993
|
+
|
|
1994
|
+
def _stream(fh: BinaryIO) -> None:
|
|
1919
1995
|
downloader = MediaIoBaseDownload(fh, request)
|
|
1920
1996
|
done = False
|
|
1921
1997
|
while not done:
|
|
1922
1998
|
_, done = downloader.next_chunk()
|
|
1923
1999
|
|
|
2000
|
+
_write_atomically(dest_file, _stream, drive_ts)
|
|
2001
|
+
|
|
1924
2002
|
await asyncio.to_thread(_download_to_completion)
|
|
1925
|
-
# Mirror what the upload branch above does in reverse: set the
|
|
1926
|
-
# local file's mtime to Drive's modifiedTime so the round trip
|
|
1927
|
-
# stays within _SYNC_MTIME_TOLERANCE on the next sync. Without
|
|
1928
|
-
# this the local mtime is "now" (write time), which is always
|
|
1929
|
-
# later than Drive's original timestamp — the next sync sees the
|
|
1930
|
-
# file as locally newer and re-uploads it, indefinitely (#346).
|
|
1931
|
-
dtime = _drive_mtime(entry)
|
|
1932
|
-
os.utime(dest_file, (dtime.timestamp(), dtime.timestamp()))
|
|
1933
2003
|
size = dest_file.stat().st_size
|
|
1934
2004
|
logger.debug("Synced (download) Drive → %s%s (%d bytes)", rel_prefix, name, size)
|
|
1935
2005
|
return {"kind": "download_ok", "name": name, "bytes": size}
|
|
@@ -2628,7 +2698,9 @@ def register(tool):
|
|
|
2628
2698
|
if not folder.is_dir():
|
|
2629
2699
|
raise ValueError(f"No directory found at {local_path!r}")
|
|
2630
2700
|
|
|
2631
|
-
candidates = [
|
|
2701
|
+
candidates = [
|
|
2702
|
+
p for p in folder.iterdir() if p.is_file() and not _is_partial_download(p.name)
|
|
2703
|
+
]
|
|
2632
2704
|
if skip_system_files:
|
|
2633
2705
|
candidates = [p for p in candidates if p.name not in _SYSTEM_FILES]
|
|
2634
2706
|
|
|
@@ -2867,18 +2939,21 @@ def register(tool):
|
|
|
2867
2939
|
)
|
|
2868
2940
|
if not isinstance(content, bytes):
|
|
2869
2941
|
content = content.encode("utf-8")
|
|
2870
|
-
await asyncio.to_thread(dest
|
|
2942
|
+
await asyncio.to_thread(_write_atomically, dest, lambda fh: fh.write(content))
|
|
2871
2943
|
else:
|
|
2872
2944
|
|
|
2873
2945
|
def _download_to_completion() -> None:
|
|
2874
2946
|
request = drive_service.files().get_media(fileId=file_id, supportsAllDrives=True)
|
|
2875
2947
|
request.http = thread_http(drive_service)
|
|
2876
|
-
|
|
2948
|
+
|
|
2949
|
+
def _stream(fh: BinaryIO) -> None:
|
|
2877
2950
|
downloader = MediaIoBaseDownload(fh, request)
|
|
2878
2951
|
done = False
|
|
2879
2952
|
while not done:
|
|
2880
2953
|
_, done = downloader.next_chunk()
|
|
2881
2954
|
|
|
2955
|
+
_write_atomically(dest, _stream)
|
|
2956
|
+
|
|
2882
2957
|
await asyncio.to_thread(_download_to_completion)
|
|
2883
2958
|
|
|
2884
2959
|
size = dest.stat().st_size
|
|
@@ -3086,7 +3161,9 @@ def register(tool):
|
|
|
3086
3161
|
)
|
|
3087
3162
|
if not isinstance(content, bytes):
|
|
3088
3163
|
content = content.encode("utf-8")
|
|
3089
|
-
await asyncio.to_thread(
|
|
3164
|
+
await asyncio.to_thread(
|
|
3165
|
+
_write_atomically, candidate.dest_file, lambda fh: fh.write(content)
|
|
3166
|
+
)
|
|
3090
3167
|
else:
|
|
3091
3168
|
|
|
3092
3169
|
def _download_to_completion(
|
|
@@ -3096,12 +3173,15 @@ def register(tool):
|
|
|
3096
3173
|
fileId=fid, supportsAllDrives=True
|
|
3097
3174
|
)
|
|
3098
3175
|
request.http = thread_http(drive_service)
|
|
3099
|
-
|
|
3176
|
+
|
|
3177
|
+
def _stream(fh: BinaryIO) -> None:
|
|
3100
3178
|
downloader = MediaIoBaseDownload(fh, request)
|
|
3101
3179
|
done = False
|
|
3102
3180
|
while not done:
|
|
3103
3181
|
_, done = downloader.next_chunk()
|
|
3104
3182
|
|
|
3183
|
+
_write_atomically(dest_file, _stream)
|
|
3184
|
+
|
|
3105
3185
|
await asyncio.to_thread(_download_to_completion)
|
|
3106
3186
|
|
|
3107
3187
|
size = candidate.dest_file.stat().st_size
|
|
@@ -255,6 +255,88 @@ class TestFileMutations:
|
|
|
255
255
|
await _drive_tools["delete_file"](file_id="fid1", permanent=True, ctx=ctx)
|
|
256
256
|
folder_cache.mark_dirty.assert_called_once_with("par1")
|
|
257
257
|
|
|
258
|
+
async def test_delete_file_requests_capabilities_in_parents_lookup(self):
|
|
259
|
+
"""#876: the capability check rides on the existing get, no extra request."""
|
|
260
|
+
mock = MagicMock()
|
|
261
|
+
mock.files.return_value.get.return_value.execute.return_value = {"parents": ["par1"]}
|
|
262
|
+
ctx = _make_ctx(drive_service=mock, drive_folder_cache=MagicMock())
|
|
263
|
+
await _drive_tools["delete_file"](file_id="fid1", permanent=True, ctx=ctx)
|
|
264
|
+
mock.files.return_value.get.assert_called_once()
|
|
265
|
+
fields = mock.files.return_value.get.call_args.kwargs["fields"]
|
|
266
|
+
assert "canDelete" in fields
|
|
267
|
+
assert "canTrash" in fields
|
|
268
|
+
assert "driveId" in fields
|
|
269
|
+
|
|
270
|
+
async def test_delete_file_permanent_without_can_delete_raises_before_delete(self):
|
|
271
|
+
"""#876: Content manager on a Shared Drive gets a clear error, not Drive's 404."""
|
|
272
|
+
mock = MagicMock()
|
|
273
|
+
mock.files.return_value.get.return_value.execute.return_value = {
|
|
274
|
+
"parents": ["par1"],
|
|
275
|
+
"driveId": "drive1",
|
|
276
|
+
"capabilities": {"canDelete": False, "canTrash": True},
|
|
277
|
+
}
|
|
278
|
+
folder_cache = MagicMock()
|
|
279
|
+
ctx = _make_ctx(drive_service=mock, drive_folder_cache=folder_cache)
|
|
280
|
+
with pytest.raises(ValueError, match="permanent=False") as exc_info:
|
|
281
|
+
await _drive_tools["delete_file"](file_id="fid1", permanent=True, ctx=ctx)
|
|
282
|
+
assert "Shared Drive" in str(exc_info.value)
|
|
283
|
+
mock.files.return_value.delete.assert_not_called()
|
|
284
|
+
folder_cache.mark_dirty.assert_not_called()
|
|
285
|
+
|
|
286
|
+
async def test_delete_file_permanent_without_can_delete_on_my_drive_omits_shared_drive_note(
|
|
287
|
+
self,
|
|
288
|
+
):
|
|
289
|
+
"""A non-owner editor of a My Drive file also lacks canDelete; no Shared Drive aside."""
|
|
290
|
+
mock = MagicMock()
|
|
291
|
+
mock.files.return_value.get.return_value.execute.return_value = {
|
|
292
|
+
"parents": ["par1"],
|
|
293
|
+
"capabilities": {"canDelete": False, "canTrash": True},
|
|
294
|
+
}
|
|
295
|
+
ctx = _make_ctx(drive_service=mock, drive_folder_cache=MagicMock())
|
|
296
|
+
with pytest.raises(ValueError) as exc_info:
|
|
297
|
+
await _drive_tools["delete_file"](file_id="fid1", permanent=True, ctx=ctx)
|
|
298
|
+
assert "Shared Drive" not in str(exc_info.value)
|
|
299
|
+
assert "permanent=False" in str(exc_info.value)
|
|
300
|
+
mock.files.return_value.delete.assert_not_called()
|
|
301
|
+
|
|
302
|
+
async def test_delete_file_permanent_without_delete_or_trash_omits_trash_hint(self):
|
|
303
|
+
mock = MagicMock()
|
|
304
|
+
mock.files.return_value.get.return_value.execute.return_value = {
|
|
305
|
+
"parents": ["par1"],
|
|
306
|
+
"capabilities": {"canDelete": False, "canTrash": False},
|
|
307
|
+
}
|
|
308
|
+
ctx = _make_ctx(drive_service=mock, drive_folder_cache=MagicMock())
|
|
309
|
+
with pytest.raises(ValueError) as exc_info:
|
|
310
|
+
await _drive_tools["delete_file"](file_id="fid1", permanent=True, ctx=ctx)
|
|
311
|
+
assert "permanent=False" not in str(exc_info.value)
|
|
312
|
+
mock.files.return_value.delete.assert_not_called()
|
|
313
|
+
|
|
314
|
+
async def test_delete_file_permanent_with_can_delete_calls_delete(self):
|
|
315
|
+
mock = MagicMock()
|
|
316
|
+
mock.files.return_value.get.return_value.execute.return_value = {
|
|
317
|
+
"parents": ["par1"],
|
|
318
|
+
"capabilities": {"canDelete": True, "canTrash": True},
|
|
319
|
+
}
|
|
320
|
+
mock.files.return_value.delete.return_value.execute.return_value = None
|
|
321
|
+
ctx = _make_ctx(drive_service=mock, drive_folder_cache=MagicMock())
|
|
322
|
+
result = await _drive_tools["delete_file"](file_id="fid1", permanent=True, ctx=ctx)
|
|
323
|
+
mock.files.return_value.delete.assert_called_once_with(
|
|
324
|
+
fileId="fid1", supportsAllDrives=True
|
|
325
|
+
)
|
|
326
|
+
assert result == {"fileId": "fid1", "action": "deleted"}
|
|
327
|
+
|
|
328
|
+
async def test_delete_file_trash_ignores_can_delete_false(self):
|
|
329
|
+
"""Trashing only needs canTrash; a missing canDelete must not block it."""
|
|
330
|
+
mock = MagicMock()
|
|
331
|
+
mock.files.return_value.get.return_value.execute.return_value = {
|
|
332
|
+
"parents": ["par1"],
|
|
333
|
+
"capabilities": {"canDelete": False, "canTrash": True},
|
|
334
|
+
}
|
|
335
|
+
mock.files.return_value.update.return_value.execute.return_value = {"id": "fid1"}
|
|
336
|
+
ctx = _make_ctx(drive_service=mock, drive_folder_cache=MagicMock())
|
|
337
|
+
result = await _drive_tools["delete_file"](file_id="fid1", permanent=False, ctx=ctx)
|
|
338
|
+
assert result == {"fileId": "fid1", "action": "trashed"}
|
|
339
|
+
|
|
258
340
|
async def test_restore_file_marks_parent_dirty(self):
|
|
259
341
|
mock = MagicMock()
|
|
260
342
|
mock.files.return_value.update.return_value.execute.return_value = {
|