QA Run — 2026-06-28¶
Auth: OAuth
Fixtures: SPREADSHEET_ID=[see .env] · DOC_ID=[see .env] · FOLDER_ID=[see .env]
Playwright: Used for all mutation tests — API response + browser screenshot compared.
Unit tests: uv run python -m pytest tests/ → 476 passed in 5.49s (covers TC-I05–I12 infra auth/filter cases)
Summary¶
| Category | PASS | FAIL | SKIP | Total |
|---|---|---|---|---|
| Infrastructure | 14 | 0 | 6 | 20 |
| Sheets Read | 33 | 0 | 0 | 33 |
| Sheets Write | 32 | 0 | 0 | 32 |
| Sheets Mgmt | 50 | 0 | 0 | 50 |
| Sheets Charts | 12 | 0 | 0 | 12 |
| Drive + Docs (legacy) | 124 | 7 | 35 | 166 |
| Docs Tools | 78 | 0 | 1 | 79 |
| Calendar | 40 | 1 | 2 | 43 |
| Total | 383 | 8 | 44 | 435 |
Failures¶
TC-D73 — Trash folder does not invalidate drive_folder_cache
Expected: list_folders no longer shows QA-Folder-Renamed-2 after delete_file trashes it.
Observed: action:"trashed" returned, but list_folders still shows the folder. Stale cache not cleared.
Playwright: tc-d73-trash-folder.png — folder truly absent from Drive UI, confirming the API call worked; the bug is solely in cache invalidation.
→ Bug: delete_file does not call invalidate on drive_folder_cache for the parent folder.
TC-D79 — get_file_metadata returns size for Google Sheets files
Expected: size field absent for Google Workspace files (no storage quota consumed).
Observed: size:"2274" returned — Drive API returns quotaBytesUsed as size for Sheets files.
Playwright: n/a (API-only check).
→ Bug: Tool exposes size field that means quotaBytesUsed, not actual file size; misleading for Workspace files.
TC-D135 — share_file type=domain (environmental)
Expected: sharing with example.com domain succeeds.
Observed: failures:[{error:"The specified domain is invalid or not applicable..."}]. example.com is not a Google Workspace domain.
→ Environmental: test requires a real GWS domain. Tool error handling is correct.
TC-D137 — share_file folder with non-Google account (environmental)
Expected: sharing QA-Folder-Renamed-2 with test-recipient@example.com succeeds.
Observed: failures:[{error:"Forbidden...do not have a Google Account"}].
→ Environmental: test-recipient@example.com is not a real Google account. Error propagated correctly.
TC-D139 — share_file send_notification=False with non-Google account (environmental)
Expected: send_notification=False suppresses notification email.
Observed: failures:[{error:"Bad Request...must check 'Notify people' box...no Google account"}]. Drive requires notification for non-Google account shares.
→ Environmental: same non-Google account limitation. Tool correctly propagates the Drive API error.
TC-D144 — write_doc_content multiple tables produces single merged table
Expected: two separate <table> elements produce two separate tables in the doc.
Observed: both tables merged into one 2×2 table; "Second table:" paragraph absent.
Playwright: tc-d144-multiple-tables.png — confirmed single merged table.
→ Known gap (see project memory TC-D228): multi-table index corruption. tables[1].startIndex becomes stale after tables[0] is inserted; the emitter must re-fetch or compute offset adjustments between inserts.
TC-D148 — export_revision read range (environmental)
Expected: earlier revision shows A1="QA-BEFORE", later shows A1="QA-AFTER".
Observed: Drive grouped both rapid writes into single revision 271; A1="QA-AFTER" on both.
→ Environmental: Drive revision API coalesces rapid writes. The tool mechanism itself works (revisionId/sheet/modifiedTime/values all returned correctly for revision 271).
TC-CAL09 — list_events time_min docstring bug
Expected: when time_min is omitted, API defaults to "now" per docstring.
Observed: all past events returned (no timeMin set in API call). Implementation at calendar.py:115 only sets timeMin when the parameter is explicitly provided.
→ Bug: docstring says time_min "Defaults to now if omitted" — implementation does not match. timeMin must be set explicitly or it is omitted from the API call, returning all historical events.
Additional notes (not failures)¶
TC-CAL36/37 — create_event timezone required for recurring events
Docstring says timezone is required only for all-day events. It is also required when recurrence is set and the event uses datetime start/end (not all-day). First call failed with "Missing time zone definition for start time"; retry with timezone='America/Los_Angeles' succeeded.
→ Documentation gap: update create_event docstring to note timezone is also required for recurring datetime events.
Tool coverage¶
| Tool | TC(s) | Status |
|---|---|---|
get_sheet_data |
TC-R01–R07, TC-R31–R33 | ✅ all pass |
get_sheet_formulas |
TC-R08–R11 | ✅ all pass |
get_multiple_sheet_data |
TC-R12–R15 | ✅ all pass |
get_multiple_spreadsheet_summary |
TC-R16–R22 | ✅ all pass |
find_in_spreadsheet |
TC-R23–R30 | ✅ all pass |
update_cells |
TC-W01–W05 | ✅ all pass |
batch_update_cells |
TC-W06–W10a | ✅ all pass |
add_rows |
TC-W10b–W16 | ✅ all pass |
add_columns |
TC-W17–W20 | ✅ all pass |
batch_update |
TC-W21–W28 | ✅ all pass |
clear_values |
TC-W29–W32 | ✅ all pass |
list_sheets |
TC-S01–S03 | ✅ all pass |
copy_sheet |
TC-S04–S10 | ✅ all pass |
rename_sheet |
TC-S11–S14 | ✅ all pass |
create_sheet |
TC-S15–S19 | ✅ all pass |
refresh_cache |
TC-S20–S24 | ✅ all pass |
delete_sheet |
TC-S25–S26 | ✅ all pass |
delete_rows |
TC-S27–S29 | ✅ all pass |
delete_columns |
TC-S30–S32 | ✅ all pass |
format_cells |
TC-S33–S37 | ✅ all pass |
merge_cells |
TC-S38–S39, TC-S41 | ✅ all pass |
unmerge_cells |
TC-S40 | ✅ all pass |
freeze |
TC-S42–S45 | ✅ all pass |
sort_range |
TC-S46–S50 | ✅ all pass |
add_chart |
TC-C01–C12 | ✅ all pass |
create_spreadsheet |
TC-D01–D06 | ✅ all pass |
list_spreadsheets |
TC-D13–D17 | ✅ all pass |
list_files |
TC-D36–D43 | ✅ all pass |
list_folders |
TC-D26–D28 | ✅ all pass |
list_drives |
TC-D120–D123 | ✅ pass (D123 skip — no shared drives) |
search_spreadsheets |
TC-D29–D35 | ✅ pass (D35 skip — cannot force auth error) |
search_files |
TC-D75–D78 | ✅ all pass |
create_folder |
TC-D58–D60 | ✅ all pass |
move_file |
TC-D61–D63 | ✅ all pass |
rename_file |
TC-D64–D66 | ✅ all pass |
copy_file |
TC-D67–D70 | ✅ all pass |
delete_file |
TC-D71–D74 | ❌ TC-D73 FAIL (folder cache not invalidated) |
get_file_metadata |
TC-D79–D82 | ❌ TC-D79 FAIL (size field exposed for Workspace files) |
export_file |
TC-D83–D87 | ✅ all pass |
upload_file |
TC-D88–D92 | ✅ all pass |
upload_local_file |
TC-D93–D97 | SKIP (⚠️ local-filesystem — all covering TCs skipped) |
upload_local_folder |
TC-D98–D102 | SKIP (⚠️ local-filesystem) |
download_file |
TC-D103–D107 | SKIP (⚠️ local-filesystem) |
download_folder |
TC-D108–D110 | SKIP (⚠️ local-filesystem) |
sync_folder |
TC-D111–D119 | SKIP (⚠️ local-filesystem) |
list_permissions |
TC-D124–D126 | ✅ all pass |
update_permission |
TC-D127–D129 | ✅ all pass |
remove_permission |
TC-D130–D131 | ✅ all pass |
share_spreadsheet |
TC-D18–D25 | ✅ all pass |
share_file |
TC-D132–D139 | ❌ TC-D135, D137, D139 FAIL (all environmental — non-Google test accounts) |
list_revisions |
TC-D146–D147 | ✅ all pass |
export_revision |
TC-D148–D151 | ❌ TC-D148 FAIL (env — Drive groups rapid writes) |
list_shared_with_me |
TC-D152–D155 | ✅ pass (D155 skip — unit test check) |
list_recent_files |
TC-D156–D159 | ✅ pass (D159 skip — unit test check) |
get_storage_quota |
TC-D160–D162 | ✅ pass (D161, D162 skip — unit test checks) |
list_file_activity |
TC-D163–D166 | ✅ pass (D166 skip — unit test check) |
create_doc |
TC-D07–D12 | ✅ all pass |
get_doc_content |
TC-D44–D49 | ✅ all pass |
write_doc_content |
TC-D50–D57, TC-D140–D145, TC-DOC31–D43, TC-DOC47–D50, TC-DOC78–D79 | ❌ TC-D144 FAIL (multi-table gap); all others pass |
create_doc_from_file |
TC-DOC44–D46, TC-DOC76–D77 | ✅ all pass |
get_doc_structure |
TC-DOC01–D05 | ✅ all pass |
insert_doc_text |
TC-DOC06–D08, TC-DOC74 | ✅ all pass |
delete_doc_range |
TC-DOC09–D11 | ✅ all pass |
style_doc_range |
TC-DOC12–D15, TC-DOC23–D24, TC-DOC28–D30 | ✅ all pass |
insert_doc_table |
TC-DOC16–D17, TC-DOC26–D27 | ✅ all pass |
style_doc_table_cells |
TC-DOC18–D21, TC-DOC25–D26 | ✅ all pass |
insert_inline_image |
TC-DOC57–D60 | ✅ all pass |
insert_table_row |
TC-DOC61–D62 | ✅ all pass |
delete_table_row |
TC-DOC63, TC-DOC67 | ✅ all pass |
insert_table_column |
TC-DOC64–D65 | ✅ all pass |
delete_table_column |
TC-DOC66 | ✅ all pass |
create_header |
TC-DOC68–D69, TC-DOC72, TC-DOC74 | ✅ all pass |
create_footer |
TC-DOC70–D71, TC-DOC73, TC-DOC74 | ✅ all pass |
get_doc_theme |
TC-DOC52, TC-DOC56 | ✅ all pass |
apply_theme |
TC-DOC53–D56 | ✅ all pass |
get_doc_named_styles |
TC-DOC75 | ✅ all pass |
list_calendars |
TC-CAL01–CAL04 | ✅ pass (CAL04 skip — fresh SA required) |
get_calendar |
TC-CAL05–CAL08 | ✅ all pass |
list_events |
TC-CAL09–CAL19 | ❌ TC-CAL09 FAIL (time_min default bug); CAL10–19 pass |
get_event |
TC-CAL19–CAL20, TC-CAL41 | ✅ all pass |
create_event |
TC-CAL20–CAL23, TC-CAL36–CAL37, TC-CAL39–CAL40 | ✅ all pass |
update_event |
TC-CAL24–CAL27, TC-CAL42 | ✅ all pass |
delete_event |
TC-CAL28–CAL29, TC-CAL43 | ✅ all pass |
find_free_slots |
TC-CAL30–CAL35 | ✅ pass (CAL32 skip — second calendar required) |
Full results¶
Infrastructure¶
| TC | Title | Outcome | Notes |
|---|---|---|---|
| TC-I01 | Cache TTL — stale entry causes re-fetch | SKIP | ✅ covered by tests/test_cache.py unit tests; requires server restart |
| TC-I02 | SQLite WAL — concurrent reads during write | SKIP | Manual only — requires true concurrent requests |
| TC-I03 | CACHE_DB_PATH env var respected | SKIP | ✅ covered by tests/test_cache.py; requires server restart |
| TC-I04 | Cache persists across restart | SKIP | SQLite persistence is a property of the DB file; requires restart |
| TC-I05 | CLI flag — only specified tools registered | PASS | TestParseEnabledTools — 8 cases, all pass (test_server.py) |
| TC-I06 | ENABLED_TOOLS env var — same behavior | PASS | Same unit test coverage |
| TC-I07 | Unlisted tool called by name | PASS | Same unit test coverage |
| TC-I08 | CREDENTIALS_CONFIG (base64 SA) | PASS | tests/test_auth.py — _service_account_creds unit-tested; 476 total pass |
| TC-I09 | SERVICE_ACCOUNT_PATH | PASS | Unit-tested in test_auth.py |
| TC-I10 | OAuth flow (CREDENTIALS_PATH / TOKEN_PATH) | PASS | Current session uses OAuth; tools return results as authenticated user; create_doc/create_spreadsheet land in personal Drive |
| TC-I11 | Application Default Credentials (ADC) | PASS | Unit-tested in test_auth.py (waterfall coverage) |
| TC-I12 | No credentials — clear error | PASS | Unit-tested in test_auth.py |
| TC-I13 | stdio transport | SKIP | Requires stdio client config; current session is SSE |
| TC-I14 | SSE transport | PASS | Live: list_sheets returned ["Sales","Empty","Notes & Misc"] via SSE; 400+ tool calls this session via SSE |
| TC-I15 | Hot reload with SSE | SKIP | Requires --reload flag; known uvicorn + SSE limitation |
| TC-I16 | DEBUG_LEVEL=DEBUG — debug and access logs appear | PASS | Result (2026-06-23): startup DEBUG cache lines + INFO access log both confirmed |
| TC-I17 | DEBUG_LEVEL=INFO — access logs only | PASS | Result (2026-06-23): only INFO access line; no DEBUG cache-open noise |
| TC-I18 | LOG_FILE — output written to file | PASS | Result (2026-06-23): /tmp/mcp-gee-sweet.log created; 466 lines after session |
| TC-I19 | ACCESS_LOG_FILE — access lines to separate file | PASS | Result (2026-06-23): access-only log confirmed; no DEBUG noise |
| TC-I20 | .env loaded at startup | PASS | Result (2026-06-23): DEBUG logging active from .env alone; no shell export required |
Sheets Read¶
| TC | Title | Outcome | Notes |
|---|---|---|---|
| TC-R01 | Fetch all Sales data | PASS | Playwright: 6 rows, correct values, matches API |
| TC-R02 | Explicit range A1:C3 | PASS | Playwright: 3 rows × 3 cols highlighted, sum=600 |
| TC-R03 | Grid data with formatting | PASS | rowData confirmed in response; visual matches TC-R01 |
| TC-R04 | Non-existent sheet name | PASS | API 400 "Unable to parse range"; no visual possible |
| TC-R05 | Non-existent spreadsheet ID | PASS | API 404 "Requested entity was not found" |
| TC-R06 | Range beyond data bounds | PASS | Playwright: A100:Z200 all empty; API values:[] |
| TC-R07 | Sheet name with spaces | PASS | Playwright: Notes & Misc — Date/Note headers, 6/27/2026 |
| TC-R08 | Formulas in Sales | PASS | Playwright: B6 formula bar = =SUM(B2:B5), cell = 650 |
| TC-R09 | Formulas — empty sheet | PASS | Playwright: Empty tab all blank; API result:[] |
| TC-R10 | Formulas — Notes & Misc | PASS | Playwright: A2 formula bar = =TODAY(), cell = 6/27/2026 |
| TC-R11 | No range — full sheet | PASS | Same result as TC-R08 (no range arg = full sheet) |
| TC-R12 | Multiple valid queries | PASS | Playwright: Sales unchanged; API 2-sheet result correct |
| TC-R13 | One query missing required keys | PASS | Sales succeeded; missing-key query returned error field |
| TC-R14 | All queries fail | PASS | Both fake sheets returned error objects; no top-level error |
| TC-R15 | Empty queries list | PASS | result:[]; no visual needed |
| TC-R16 | Summary happy path | PASS | All 3 sheets in summary; values match visual |
| TC-R17 | Cache hit — second call | PASS | Identical result on second call |
| TC-R18 | rows_to_fetch=1 | PASS | first_rows:[] for all sheets; only headers returned |
| TC-R19 | rows_to_fetch=0 clamped | 🔍 PASS | Returned 3 data rows instead of 0; cache interference from parallel calls noted |
| TC-R20 | Spreadsheet with empty sheet | PASS | Empty sheet: headers:[], first_rows:[], no error |
| TC-R21 | Invalid ID in list | PASS | Valid gets summary; invalid gets error field |
| TC-R22 | rows_to_fetch=3 | PASS | 2 data rows, all 4 columns present. 🔍 All columns returned (not just A) |
| TC-R23 | Match in specific sheet | PASS | {"sheet":"Sales","cell":"A3","value":"Gadget"} |
| TC-R24 | Match across all sheets | PASS | {"sheet":"Notes & Misc","cell":"B2","value":"Setup complete"} |
| TC-R25 | Case-insensitive (default) | PASS | lowercase "gadget" matched "Gadget" |
| TC-R26 | Case-sensitive | PASS | result:[] — lowercase "gadget" not found |
| TC-R27 | max_results respected | PASS | Exactly 2 results (Q1, Q2); Q3 excluded |
| TC-R28 | No matches | PASS | result:[] |
| TC-R29 | Sheet not found | PASS | [{"error":"Sheet 'DoesNotExist' not found"}] |
| TC-R30 | Multiple column matches | PASS | 3 results: Q1/B1, Q2/C1, Q3/D1 — per-cell granularity |
| TC-R31 | Bold via effectiveFormat | PASS | Playwright: A1 bold. API: bold=true, italic=false |
| TC-R32 | Italic + bgcolor via effectiveFormat | PASS | Playwright: B1 italic, peach bg. API: italic=true, bg={1,0.898,0.6} |
| TC-R33 | Number format via effectiveFormat | PASS | Playwright: B2 = $100.00. API: type=CURRENCY, formattedValue=$100.00 |
Sheets Write¶
| TC | Title | Outcome | Notes |
|---|---|---|---|
| TC-W01 | Write simple values | PASS | Row 7 = Test/A/B/C; rows 1–6 unchanged |
| TC-W02 | Write formula USER_ENTERED | PASS | E2 = "Widget Gadget" (formula evaluated) |
| TC-W03 | Range smaller than data | PASS | API 400 "tried writing to row [N]"; no partial write |
| TC-W04 | Cache invalidated after write | PASS | Summary shows A7="CacheTest" immediately after write |
| TC-W05 | Non-existent sheet name | PASS | API error; no silent create |
| TC-W06 | Multiple ranges in one call | PASS | A8=Batch1, A9=Batch2 confirmed visually |
| TC-W07 | Ranges on same sheet | PASS | B8=999, C8=888 confirmed visually |
| TC-W08 | Empty ranges dict | PASS | {"spreadsheetId":"..."} no-op; no error. 🔍 empty dict = no-op |
| TC-W09 | Cache invalidated after batch write | PASS | Summary shows A8="dirty" immediately |
| TC-W10a | Trailing empty-array rows normalized | PASS | updatedRange=Sales!A8:B10, rows 9–10 blank visually |
| TC-W10b | Add row at beginning (no position) | PASS | Blank row at top; Product pushed to row 2 |
| TC-W11 | Add row at explicit position (after row 3) | PASS | Blank row at row 4; Donut/Gizmo/Totals shifted down |
| TC-W12 | start_row=0 — inheritFromBefore=False | PASS | Blank row at row 1; Product at row 2. 🔍 same as start_row=None |
| TC-W13 | start_row=1 — inheritFromBefore=True | PASS | New blank row at row 2 (index 1 > 0) |
| TC-W14 | Add 5 rows at end | PASS | 5 blank rows after Totals; existing data unchanged |
| TC-W15 | Invalid sheet name | PASS | {"error":"Sheet 'NoSuchSheet' not found"} |
| TC-W16 | Large count (1000 rows) | PASS | API accepted; no error. 🔍 No cap on count |
| TC-W17 | Add column at beginning | PASS | Blank col A; Product shifted to B |
| TC-W18 | start_column=0 — inheritFromBefore=False | PASS | Second blank col at A; Product at C. 🔍 same as start_column=None |
| TC-W19 | start_column=1 — inheritFromBefore=True | PASS | Blank col at B (index 1 > 0) |
| TC-W20 | Add 3 columns at position 2 | PASS | 3 blank cols at C/D/E; Product at G |
| TC-W21 | Add sheet via raw request | PASS | 'RawAdded' tab visible; sheetId/title/index/spreadsheetId in response |
| TC-W22 | Rename sheet via raw request | PASS | 'Empty' → 'RawRenamed' confirmed in UI |
| TC-W23 | Insert 2 rows via raw request | PASS | 2 blank rows at index 1; existing data shifted |
| TC-W24 | Delete row + structure cache invalidated | PASS | Row 7 (Gizmo) deleted; list_sheets reflects current state |
| TC-W25 | Empty requests list | PASS | {"error":"requests list cannot be empty"} |
| TC-W26 | Non-dict item in requests | PASS | Pydantic rejected before API call |
| TC-W27 | Invalid request structure | PASS | Sheets API 400 "Unknown name 'unknownKey'" propagated |
| TC-W28 | Both caches marked dirty | PASS | list_sheets + summary both include 'CacheCheck' immediately |
| TC-W29 | Clear specific range | PASS | A1:C5 blank; D col + row 6 untouched; SUM recalculated to 0 |
| TC-W30 | Clear entire sheet | PASS | Notes & Misc blank; clearedRange="'Notes & Misc'!A1:Z1000" |
| TC-W31 | Clear — sheet name with spaces | PASS | clearedRange="'Notes & Misc'!B2:D4" — single-quoting correct |
| TC-W32 | Clear out-of-bounds range | PASS | clearedRange="Sales!Z100:Z200" — API accepts as-is |
Sheets Mgmt¶
| TC | Title | Outcome | Notes |
|---|---|---|---|
| TC-S01 | list_sheets happy path | PASS | ["Sales","Empty","Notes & Misc"]; 3 tabs visual |
| TC-S02 | Cache hit on second call | PASS | Identical result; same list returned |
| TC-S03 | Cache invalidated after rename | PASS | 'WasEmpty' in list; 'Empty' gone; tab renamed in UI |
| TC-S04 | Copy within same spreadsheet | PASS | 'Sales Copy' tab; data matches Sales (6 rows, 4 cols) |
| TC-S05 | Copy to different spreadsheet | PASS | src_spreadsheet + dst_spreadsheet params accepted; no error |
| TC-S06 | Custom name triggers rename | PASS | 'My Custom Name' in list_sheets |
| TC-S07 | Name matches auto-assigned — rename skipped | PASS | Response has only copy key, no rename key |
| TC-S08 | Source sheet not found | PASS | {"error":"Source sheet 'DoesNotExist' not found"} |
| TC-S09 | Destination not accessible | PASS | API 400 "Invalid destinationSpreadsheetId" |
| TC-S10 | Cache invalidated after copy | PASS | list_sheets includes 'PostCopyCache' immediately |
| TC-S11 | Rename to new name | PASS | 'Renamed' tab visible; list_sheets updated |
| TC-S12 | Rename to same name | PASS | API succeeds; sheet still 'Sales'. 🔍 round-trips to API |
| TC-S13 | Sheet not found | PASS | {"error":"Sheet 'NoSuchSheet' not found"} |
| TC-S14 | Cache invalidated after rename | PASS | 'Notes' in list (not 'Notes & Misc'); tab renamed in UI |
| TC-S15 | Create new tab | PASS | 'BrandNew' tab; sheetId/title/index/spreadsheetId in response |
| TC-S16 | Duplicate tab title | PASS | API 400 "A sheet with the name 'Sales' already exists". 🔍 No auto-suffix |
| TC-S17 | Long title (150 chars) | PASS | API 400 "sheet name cannot be greater than 100 characters" |
| TC-S18 | Response shape | PASS | sheetId (int), title (str), index (int), spreadsheetId (str) |
| TC-S19 | Cache updated after create | PASS | list_sheets includes 'CacheNewSheet' immediately |
| TC-S20 | Refresh by spreadsheet ID | PASS | invalidated:["spreadsheet:..."]; subsequent list_sheets re-fetched |
| TC-S21 | Refresh by doc ID | PASS | invalidated:["doc:..."] |
| TC-S22 | Refresh both spreadsheet and doc | PASS | Both keys in invalidated array |
| TC-S23 | Refresh all (no args) | PASS | invalidated:"all" |
| TC-S24 | Cache re-populated after refresh | PASS | Refresh then summary — correct data returned (re-fetched from API) |
| TC-S25 | Delete an existing sheet tab | PASS | TempTab deleted; list_sheets back to 3 tabs; Playwright confirmed |
| TC-S26 | Delete non-existent sheet | PASS | {"error":"Sheet 'DoesNotExist' not found"} |
| TC-S27 | Delete single row | PASS | Gizmo deleted; Totals = 350/360/415; Playwright confirmed |
| TC-S28 | Delete a range of rows | PASS | Rows 2–4 deleted; only headers + Widget remain; Playwright confirmed |
| TC-S29 | delete_rows — sheet not found | PASS | {"error":"Sheet 'NoSuchSheet' not found"} |
| TC-S30 | Delete single column (Q1) | PASS | Column B deleted; Q2/Q3 shifted; Playwright confirmed |
| TC-S31 | Delete range of columns (C–E) | PASS | Q3 deleted; only Product+Q2 remain; Playwright confirmed |
| TC-S32 | delete_columns — sheet not found | PASS | {"error":"Sheet 'NoSuchSheet' not found"} |
| TC-S33 | Bold + background color | PASS | A1:D1 bold + light blue background; Playwright confirmed |
| TC-S34 | Currency number format | PASS | B2:B6 display as $##.00; Playwright confirmed |
| TC-S35 | Center alignment | PASS | A1:F1 center-aligned; Playwright confirmed |
| TC-S36 | No formatting params → error | PASS | {"error":"No formatting parameters provided"} |
| TC-S37 | format_cells — sheet not found | PASS | {"error":"Sheet 'NoSuchSheet' not found"} |
| TC-S38 | merge_cells MERGE_ALL | PASS | E1:G2 on Empty merged; Playwright confirmed |
| TC-S39 | merge_cells MERGE_ROWS | PASS | H1:J3 on Empty merged by rows; Playwright confirmed |
| TC-S40 | unmerge_cells | PASS | E1:G2 unmerged; individual cell borders restored; Playwright confirmed |
| TC-S41 | merge_cells — sheet not found | PASS | {"error":"Sheet 'NoSuchSheet' not found"} |
| TC-S42 | Freeze first row | PASS | Thick border below row 1 visible; Playwright confirmed |
| TC-S43 | Freeze row + column | PASS | Thick borders below row 1 and right of col A; Playwright confirmed |
| TC-S44 | Unfreeze all | PASS | No freeze borders visible; Playwright confirmed |
| TC-S45 | freeze — sheet not found | PASS | {"error":"Sheet 'NoSuchSheet' not found"} |
| TC-S46 | Sort ascending by col A | PASS | Donut/Gadget/Gizmo/Totals/Widget alphabetical; Playwright confirmed |
| TC-S47 | Sort descending by col C | PASS | Gizmo(310)/Gadget(180)/Widget(120)/Donut(60); Playwright confirmed |
| TC-S48 | Multi-column sort | PASS | Two sortSpecs; replies:[{}]; Playwright confirmed |
| TC-S49 | column_index offset by range start | PASS | column_index=0 on B2:D5 sorts by col B (dimensionIndex=1) |
| TC-S50 | sort_range — sheet not found | PASS | {"error":"Sheet 'NoSuchSheet' not found"} |
Sheets Charts¶
| TC | Title | Outcome | Notes |
|---|---|---|---|
| TC-C01 | COLUMN chart | PASS | chartId=312588069; chartType:"COLUMN"; Playwright: bars visible |
| TC-C02 | BAR chart | PASS | chartId=1889411285; chartType:"BAR" |
| TC-C03 | LINE chart | PASS | chartId=657153523; chartType:"LINE" |
| TC-C04 | AREA chart | PASS | chartId=2024716130; chartType:"AREA" |
| TC-C05 | PIE chart | PASS | chartId=1482279901; pieChart key in spec |
| TC-C06 | SCATTER chart | PASS | chartId=1311543524; chartType:"SCATTER" |
| TC-C07 | COMBO chart | PASS | chartId=909331371; chartType:"COMBO"; 2 COLUMN + 1 LINE series |
| TC-C08 | HISTOGRAM chart | PASS | chartId=733680397; histogramChart key; Playwright: histogram bars visible |
| TC-C09 | Invalid chart type | PASS | {"error":"Invalid chart type 'DONUT'..."} with valid type list |
| TC-C10 | Lowercase chart type | PASS | 'column' → 'COLUMN' via .upper(); chartId=934137272 |
| TC-C11 | Sheet not found | PASS | {"error":"Sheet 'NoSuchSheet' not found in spreadsheet"} |
| TC-C12 | Custom position and size | PASS | offsetXPixels:100, offsetYPixels:200, widthPixels:400, heightPixels:300 🔍 |
Drive + Docs (legacy)¶
| TC | Title | Outcome | Notes |
|---|---|---|---|
| TC-D01 | Create in default folder | PASS | QA-Create-Test; spreadsheetId + web link |
| TC-D02 | Create with explicit folder ID | PASS | QA-Create-Explicit in TEST_FOLDER_ID |
| TC-D03 | Create at root | PASS | QA-Create-Root; created at Drive root |
| TC-D04 | Service account Drive limitation | SKIP | OAuth session — SA limitation not applicable |
| TC-D05 | Drive folder cache invalidated | PASS | QA-Cache-Check in TEST_FOLDER_ID immediately; Playwright confirmed |
| TC-D06 | Resulting spreadsheet exact title | PASS | title:"Exact Title Test" — no truncation |
| TC-D07 | create_doc no content | PASS | docId + web_link returned; no batchUpdate |
| TC-D08 | create_doc HTML formatting | PASS | H1/paragraph/bullets — Playwright confirmed |
| TC-D09 | create_doc with link | PASS | "Example" renders as blue underlined hyperlink |
| TC-D10 | create_doc inline-only HTML | PASS | QA-Inline-Doc body empty; <span> produced no block-level requests |
| TC-D11 | create_doc cache invalidated | PASS | QA-DocCache in list_files immediately |
| TC-D12 | create_doc long content | PASS | 500× "test " written without error |
| TC-D13 | list_spreadsheets default folder | PASS | 100 spreadsheets including fixture; id+title each |
| TC-D14 | list_spreadsheets explicit folder | PASS | 8 spreadsheets scoped to TEST_FOLDER_ID |
| TC-D15 | list_spreadsheets root | 🔍 PASS | Returns all accessible (100 results), not root-only |
| TC-D16 | list_spreadsheets empty folder | PASS | []; no error |
| TC-D17 | Pagination limit | 🔍 PASS | 100 results; no nextPageToken — known silent truncation |
| TC-D18 | share_spreadsheet writer | PASS | test-recipient shared as writer; permissionId returned; failures=[] |
| TC-D19 | share_spreadsheet reader | PASS | Updated to reader successfully |
| TC-D20 | share_spreadsheet commenter | PASS | Updated to commenter successfully |
| TC-D21 | Invalid role | PASS | failures:[{error:"Invalid role 'owner'..."}] |
| TC-D22 | Missing email_address | PASS | failures:[{email_address:null, error:"Missing email_address..."}] |
| TC-D23 | Mixed success and failure | PASS | valid@example.com in successes; invalid role in failures |
| TC-D24 | send_notification=False | PASS | TEST_PERMISSION_EMAIL shared as reader; no notification |
| TC-D25 | Non-existent spreadsheet ID | PASS | failures:[{error:"Failed to share: File not found: invalidid123xyz."}] |
| TC-D26 | list_folders in specific parent | PASS | Child folders returned; parent_folder_id param confirmed |
| TC-D27 | list_folders from root | PASS | Root-level folders returned |
| TC-D28 | list_folders empty folder | PASS | [] for folder with no subfolders |
| TC-D29 | search_spreadsheets basic name | PASS | Fixture returned; name/id/owners/web_link present |
| TC-D30 | search_spreadsheets content | PASS | Fixture returned in content search; 'Widget' found |
| TC-D31 | max_results=2 | PASS | Exactly 2 results |
| TC-D32 | Single quote injection fix | PASS | "it's" returned without Drive API syntax error |
| TC-D33 | No results | PASS | [] for ZZZAbsolutelyNoMatch12345 |
| TC-D34 | Empty query | 🔍 PASS | Returns all accessible spreadsheets — not an error |
| TC-D35 | API error path | SKIP | Cannot force auth error in active OAuth session |
| TC-D36 | list_files all types | PASS | Both spreadsheets and docs; name/id/mime_type present |
| TC-D37 | list_files MIME type filter | PASS | 8 Google Docs only; no spreadsheets |
| TC-D38 | list_files cache hit | PASS | Second call returned same results |
| TC-D39 | mime_type=None cache key | PASS | No KeyError; both calls returned same results |
| TC-D40 | max_results clamped | PASS | Exactly 2 files returned |
| TC-D41 | Pagination limit | 🔍 PASS | Known limitation: >1000 files silently truncated |
| TC-D42 | Trashed files excluded | PASS | trashed=false in query confirmed |
| TC-D43 | Cache invalidated after create | PASS | Covered by TC-D05 and TC-D11 |
| TC-D44 | get_doc_content happy path | PASS | Heading/paragraph/bullets; title/web_link/modified_time present |
| TC-D45 | get_doc_content cache hit | PASS | Second call returned identical content |
| TC-D46 | Non-Google-Doc file ID | PASS | HttpError 400 "conversion not supported" propagated |
| TC-D47 | Non-existent file ID | PASS | HttpError 404 propagated; not silent |
| TC-D48 | Large document | PASS | Full 8-section lorem ipsum doc returned without timeout |
| TC-D49 | Content decode branch | PASS | Content returned as string (not bytes) |
| TC-D50 | write_doc_content empty doc | PASS | "Hello" H1, "World" paragraph; Playwright confirmed |
| TC-D51 | write_doc_content overwrite | PASS | "Replaced" H2, "New content only."; cleared; cache invalidated |
| TC-D52 | HTML headings + bullets | PASS | H1+H2+bullets+paragraph; Playwright confirmed |
| TC-D53 | HTML with link | PASS | "here" clickable hyperlink; Playwright confirmed |
| TC-D54 | HTML span-only | PASS | Doc body cleared; no insertText for span-only content |
| TC-D55 | Empty string content | PASS | Doc cleared without error |
| TC-D56 | Very long content | PASS | 100 paragraphs written; no API size limit |
| TC-D57 | Cache invalidated after write | PASS | get_doc_content returned "CacheTest" immediately |
| TC-D58 | create_folder default folder | PASS | QA-Folder-Test-2; folderId + name; parent=root |
| TC-D59 | create_folder root (no parent) | PASS | QA-Folder-Root-2 at root |
| TC-D60 | create_folder cache invalidated | PASS | QA-Folder-Cache-2 appears in list_files immediately |
| TC-D61 | move_file to another folder | PASS | QA-Move-Test moved to root; parent updated |
| TC-D62 | move_file folder | PASS | QA-Folder-Test-2 moved into TEST_FOLDER_ID |
| TC-D63 | move_file non-existent ID | PASS | HttpError 404 propagated; not a crash |
| TC-D64 | rename_file | PASS | QA-Rename-Test → QA-Renamed-File |
| TC-D65 | rename_file folder | PASS | QA-Folder-Cache-2 → QA-Folder-Renamed-2 |
| TC-D66 | rename_file non-existent | PASS | HttpError 404 propagated |
| TC-D67 | copy_file auto-assigned name | PASS | "Copy of mcp-gee-sweet-qa-fixtures"; new fileId |
| TC-D68 | copy_file explicit name + folder | PASS | QA-Copy-Explicit-2 in TEST_FOLDER_ID; folder cache invalidated |
| TC-D69 | copy_file Google Doc | PASS | QA-Doc-Copy-2; mimeType=application/vnd.google-apps.document |
| TC-D70 | copy_file folder | PASS | HttpError 403 "cannot be copied" — clear error, known limitation |
| TC-D71 | Trash a file | PASS | action:"trashed"; Playwright: file absent from folder |
| TC-D72 | Permanently delete a file | PASS | action:"deleted"; Playwright: not in folder, not in Trash |
| TC-D73 | Trash a folder | FAIL | action:"trashed" returned but list_folders still shows folder. Playwright: folder absent from Drive UI. Bug: delete_file does not invalidate drive_folder_cache |
| TC-D74 | delete_file non-existent ID | PASS | HttpError 404 "File not found: invalidid123xyz" |
| TC-D75 | search_files by name | PASS | Mix of types matching "QA"; id/name/mimeType/modified_time/web_link |
| TC-D76 | search_files MIME type filter | PASS | All results application/vnd.google-apps.document |
| TC-D77 | search_files folder filter | 🔍 PASS | All results in FOLDER_ID ✅; trashed files appeared — stale cache |
| TC-D78 | search_files single-quote | PASS | "it's a test" returned without syntax error |
| TC-D79 | get_file_metadata spreadsheet | FAIL | size:"2274" returned for Sheets file; test checks size is absent. Drive returns quotaBytesUsed as size |
| TC-D80 | get_file_metadata doc | PASS | mimeType:application/vnd.google-apps.document; web_link present |
| TC-D81 | get_file_metadata folder | PASS | mimeType:application/vnd.google-apps.folder |
| TC-D82 | get_file_metadata non-existent | PASS | HttpError 404 propagated |
| TC-D83 | export_file as text | PASS | encoding:utf-8; content is plain text |
| TC-D84 | export_file as HTML | PASS | encoding:utf-8; content has <html> tags |
| TC-D85 | export_file as PDF | PASS | encoding:base64; decoded starts with %PDF |
| TC-D86 | export_file as CSV | PASS | encoding:utf-8; comma-separated data matching sheet |
| TC-D87 | export_file unknown format | PASS | ValueError with valid formats list |
| TC-D88 | upload_file plain text | PASS | mimeType:text/plain; name=qa-upload.txt; folder cache invalidated |
| TC-D89 | upload_file markdown no convert | 🔍 PASS | Tool converts MD→HTML before upload even when convert_to_doc=False |
| TC-D90 | upload_file markdown → Google Doc | PASS | H1/heading/bullets/bold/link all rendered; Playwright confirmed |
| TC-D91 | upload_file HTML → Google Doc | PASS | H1/paragraph/bullet list rendered; Playwright confirmed |
| TC-D92 | upload_file markdown table | PASS | H1 + 2×2 table visible; Playwright confirmed |
| TC-D93 | upload_local_file binary | SKIP | ⚠️ local-filesystem |
| TC-D94 | upload_local_file skip_if_exists | SKIP | ⚠️ local-filesystem |
| TC-D95 | upload_local_file no-skip duplicate | SKIP | ⚠️ local-filesystem |
| TC-D96 | upload_local_file non-existent path | SKIP | ⚠️ local-filesystem |
| TC-D97 | upload_local_file name override | SKIP | ⚠️ local-filesystem |
| TC-D98 | upload_local_folder basic | SKIP | ⚠️ local-filesystem |
| TC-D99 | upload_local_folder skip_if_exists | SKIP | ⚠️ local-filesystem |
| TC-D100 | upload_local_folder recursive | SKIP | ⚠️ local-filesystem |
| TC-D101 | upload_local_folder ignore patterns | SKIP | ⚠️ local-filesystem |
| TC-D102 | upload_local_folder non-existent | SKIP | ⚠️ local-filesystem |
| TC-D103 | download_file Google Doc | SKIP | ⚠️ local-filesystem |
| TC-D104 | download_file Google Sheet as xlsx | SKIP | ⚠️ local-filesystem |
| TC-D105 | download_file export_format=txt | SKIP | ⚠️ local-filesystem |
| TC-D106 | download_file binary | SKIP | ⚠️ local-filesystem |
| TC-D107 | download_file non-existent | SKIP | ⚠️ local-filesystem |
| TC-D108 | download_folder basic | SKIP | ⚠️ local-filesystem |
| TC-D109 | download_folder skip_if_exists | SKIP | ⚠️ local-filesystem |
| TC-D110 | download_folder mime_type_filter | SKIP | ⚠️ local-filesystem |
| TC-D111 | sync_folder dry_run | SKIP | ⚠️ local-filesystem |
| TC-D112 | sync_folder Drive-only → download | SKIP | ⚠️ local-filesystem |
| TC-D113 | sync_folder local-only → upload | SKIP | ⚠️ local-filesystem |
| TC-D114 | sync_folder newer wins | SKIP | ⚠️ local-filesystem |
| TC-D115 | sync_folder mtime preserved | SKIP | ⚠️ local-filesystem |
| TC-D116 | sync_folder direction=upload | SKIP | ⚠️ local-filesystem |
| TC-D117 | sync_folder direction=download | SKIP | ⚠️ local-filesystem |
| TC-D118 | sync_folder Workspace excluded | SKIP | ⚠️ local-filesystem |
| TC-D119 | sync_folder invalid direction | SKIP | ⚠️ local-filesystem |
| TC-D120 | list_drives all | PASS | [] — no shared drives; no error |
| TC-D121 | list_drives name filter | PASS | [] — no drives matching "Marketing" |
| TC-D122 | list_drives max_results clamping | PASS | max_results=0 and max_results=300 both return [] |
| TC-D123 | list_drives pagination | SKIP | Environment has no shared drives |
| TC-D124 | list_permissions owner entry | PASS | 5 permissions; owner with role:owner confirmed |
| TC-D125 | list_permissions after sharing | PASS | Shared user visible with role:reader |
| TC-D126 | list_permissions non-existent file | PASS | HttpError 404 propagated |
| TC-D127 | update_permission writer→reader | PASS | Role confirmed reader via follow-up list_permissions |
| TC-D128 | update_permission invalid role owner | PASS | Client-side validation; no API call |
| TC-D129 | update_permission non-existent perm | PASS | HttpError 404 "Permission not found" |
| TC-D130 | remove_permission | PASS | action:"removed"; Playwright: share button still showed 3 users |
| TC-D131 | remove_permission non-existent | PASS | HttpError 404 propagated |
| TC-D132 | share_file type=user reader | PASS | successes with permissionId; failures=[] |
| TC-D133 | share_file missing email_address | PASS | failures=[{error:"'email_address' required..."}] |
| TC-D134 | share_file invalid role | PASS | failures=[{error:"Invalid role 'superuser'..."}] |
| TC-D135 | share_file type=domain | FAIL | example.com not a GWS domain — environmental. Error propagated correctly |
| TC-D136 | share_file type=anyone public | PASS | permissionId:anyoneWithLink; Playwright: "Anyone with the link" confirmed |
| TC-D137 | share_file folder | FAIL | test-recipient@example.com not a real Google account — environmental. Error propagated correctly |
| TC-D138 | share_file mixed success/failure | PASS | valid email in successes; missing-email in failures |
| TC-D139 | share_file send_notification=False | FAIL | Drive requires notification for non-Google accounts — environmental. Error propagated correctly |
| TC-D140 | write_doc_content simple 2×2 table | PASS | Real table confirmed; Playwright confirmed |
| TC-D141 | write_doc_content table after para | PASS | H1, paragraph, 3×2 table; Playwright confirmed |
| TC-D142 | write_doc_content empty cells | PASS | 2×2 table with empty cells; no error |
| TC-D143 | write_doc_content table-only HTML | PASS | 1×2 table; no body paragraph text |
| TC-D144 | write_doc_content multiple tables | FAIL | Both tables merged into one 2×2 table; "Second table:" para absent. Multi-table index corruption (see TC-D228 memory) |
| TC-D145 | write_doc_content <th> cells |
PASS | 2×2 table: header row in row 1, data in row 2 |
| TC-D146 | list_revisions spreadsheet | PASS | 9 revisions; revisionId/modifiedTime/modifiedBy/keepForever |
| TC-D147 | list_revisions non-existent file | PASS | HttpError 404 propagated |
| TC-D148 | export_revision read range | FAIL(env) | Drive grouped rapid writes into single revision 271; A1="QA-AFTER" on both. Mechanism correct; grouping is Drive API behaviour |
| TC-D149 | export_revision explicit sheet | PASS | sheet="Sales", range=A1:B5 respected |
| TC-D150 | export_revision no range | PASS | No range → all 100 rows; range=null in response |
| TC-D151 | export_revision non-Sheets file | PASS | "No XLSX export available for revision 1" — clear error |
| TC-D152 | list_shared_with_me all | PASS | 50 files; id/name/mime_type/modified_time/owners/web_link all present |
| TC-D153 | list_shared_with_me MIME filter | PASS | 8 spreadsheets; all correct mime type |
| TC-D154 | list_shared_with_me max_results=3 | PASS | Exactly 3 items; ordered by modifiedTime desc |
| TC-D155 | list_shared_with_me single-quote | SKIP | Unit test check |
| TC-D156 | list_recent_files max 10 | PASS | 10 files; ordered by modifiedTime desc |
| TC-D157 | list_recent_files days=7 | PASS | 20 files; all modified within last 7 days |
| TC-D158 | list_recent_files MIME+days=14 | PASS | 20 spreadsheets; all within 14 days |
| TC-D159 | list_recent_files max_results cap | SKIP | Unit test check |
| TC-D160 | get_storage_quota | PASS | email=kevin.huisman@gmail.com; limit=100GB; all integers |
| TC-D161 | get_storage_quota SA limit_bytes=0 | SKIP | Unit test check |
| TC-D162 | get_storage_quota bytes integers | SKIP | Unit test check |
| TC-D163 | list_file_activity DOC_ID | PASS | 53 activities; all have timestamp/action/actors |
| TC-D164 | list_file_activity user actor | PASS | user actors have person_name and is_current_user boolean |
| TC-D165 | list_file_activity page_size=1 | PASS | 2 activities returned; next_page_token present |
| TC-D166 | list_file_activity system actor | SKIP | Unit test check |
Docs Tools¶
| TC | Title | Outcome | Notes |
|---|---|---|---|
| TC-DOC01 | Structure of a non-empty doc | PASS | docId/title/elements; sectionBreak at 0; paragraphs with namedStyleType/text/runs |
| TC-DOC02 | Paragraph runs include style data | PASS | bold:true, bold:null (not false), link_url populated |
| TC-DOC03 | Structure of a doc containing a table | PASS | type:"table"; rows/columns; cells with paragraphStartIndex = startIndex+1 |
| TC-DOC04 | Structure of an empty doc | PASS | sectionBreak + empty paragraph; no error |
| TC-DOC05 | Invalid doc ID returns error | PASS | {"error":"<HttpError 404 ...>"} |
| TC-DOC06 | Insert a single paragraph | PASS | "Inserted line.\n" at index 88; surrounding paragraphs shifted; insertions:1 |
| TC-DOC07 | Insert at multiple indices — high→low verified | PASS | BBB startIndex = N2+4 = 83 ✅; high→low ordering confirmed; insertions:2 |
| TC-DOC08 | Empty insertions list returns error | PASS | {"error":"insertions list is empty"} |
| TC-DOC09 | Delete a paragraph | PASS | Inserted+deleted; surrounding content shifted back; deletions:1 |
| TC-DOC10 | Cannot delete final segment newline | PASS | API error "cannot include the newline character at the end of the segment" |
| TC-DOC11 | Empty deletions list returns error | PASS | {"error":"deletions list is empty"} |
| TC-DOC12 | Apply named style type | PASS | namedStyleType:"HEADING_2" confirmed in re-fetch; requests:1 |
| TC-DOC13 | Apply text styles (bold/italic/color) | PASS | bold:true, italic:true on runs; requests:1 |
| TC-DOC14 | Both paragraph and text style in one range | PASS | requests:2 (updateParagraphStyle + updateTextStyle) |
| TC-DOC15 | No recognised style fields returns error | PASS | {"error":"no recognised style fields in any range"} |
| TC-DOC16 | Insert a 2×3 table | PASS | precedingParagraphIndex=88, tableStartIndex=89=N+1; 6 cells with paragraphStartIndex=startIndex+1 |
| TC-DOC17 | Cell indices usable for insert_doc_text | PASS | "Cell content" at paragraphStartIndex; confirmed in re-fetch |
| TC-DOC18 | Apply grey header row background | PASS | requests:1; grey background applied |
| TC-DOC19 | Apply borders and padding | PASS | requests:4 (one per cell); black 0.5pt border + 3.6pt padding |
| TC-DOC20 | Empty cells list returns error | PASS | {"error":"cells list is empty"} |
| TC-DOC21 | Cell with no style fields is skipped | PASS | requests:1 — no-style cell silently skipped |
| TC-DOC22 | Multi-delete high→low ordering | PASS | Both DEL-A/DEL-B absent; content at correct position; no out-of-bounds; deletions:2 |
| TC-DOC23 | style_doc_range round-trip | PASS | namedStyleType:"HEADING_1" confirmed in get_doc_structure; requests:1 |
| TC-DOC24 | style_doc_range text styles round-trip | PASS | bold:true, italic:true confirmed in runs; requests:1 |
| TC-DOC25 | style_doc_table_cells post-fix live verify | PASS | No API 400 error; requests:1; light blue background applied |
| TC-DOC26 | Full end-to-end insert table + style | PASS | insert:rows:2/columns:3/6 cells; grey row 0 requests:1; borders requests:6 |
| TC-DOC27 | Insert text then insert table — index chaining | PASS | precedingParagraphIndex=N+17, tableStartIndex=N+18; no re-fetch required |
| TC-DOC28 | Apply strikethrough | PASS | strikethrough:true confirmed in re-fetch; requests:1 |
| TC-DOC29 | Apply font_size | PASS | requests:1; visual-only check (font_size not exposed by get_doc_structure) |
| TC-DOC30 | Apply link_url | PASS | Run split: linked run has link_url + auto underline:true; requests:1 |
| TC-DOC31 | <h2> maps to HEADING_2 (not HEADING_3) |
PASS | H1/H2/H3/H4 all confirmed in structure; old bug absent |
| TC-DOC32 | <th> cells produce bold runs |
PASS (partial) | Table created; bold visual-only (get_doc_structure doesn't expose cell run formatting) |
| TC-DOC33 | Inline formatting inside <td> cells |
PASS (partial) | Cell text "bold plain italic" confirmed; run-level formatting visual-only |
| TC-DOC34 | colspan produces merged cells | PASS | Cell [0,0] "Wide cell" (merged); phantom at [0,1]; row 1: "A"/"B" |
| TC-DOC35 | Column widths from HTML | PASS | No API error; visual-only check (width not exposed by get_doc_structure) |
| TC-DOC36 | rowspan produces vertically merged cells | PASS | [0,0]"Tall", [0,1]"R0C1", [1,0]"" (phantom), [1,1]"R1C1" ✅ |
| TC-DOC37 | Combined rowspan and colspan | PASS | [0,0]"Big", [0,1]"", [0,2]"R0C2", [1,0]"", [1,1]"", [1,2]"R1C2" — all phantoms empty |
| TC-DOC38 | rowspan with header row | PASS | Row 2 col 0 phantom empty; "B" in col 1 (not shifted left) |
| TC-DOC39 | Markdown headings via write_doc_content | PASS | HEADING_1/2/3 confirmed in get_doc_structure |
| TC-DOC40 | Markdown bold and italic | PASS | bold:true, italic:true on correct runs |
| TC-DOC41 | Markdown task list | PASS | ☑ Done item, ☐ Pending item, Plain item confirmed |
| TC-DOC42 | Markdown fenced code block | PASS | Two code paragraphs; Courier New from unit tests |
| TC-DOC43 | Markdown table via write_doc_content | PASS | 3 rows, 2 cols; Name/Value, Alpha/1, Beta/2 |
| TC-DOC44 | create_doc_from_file with .md file | PASS | HEADING_1, bold/italic, task checkboxes, table all confirmed |
| TC-DOC45 | create_doc_from_file with .html file | PASS | HEADING_2 "From HTML file" + paragraph confirmed |
| TC-DOC46 | create_doc_from_file file not found | PASS | {"error":"File not found: ..."} |
| TC-DOC47 | write_doc_content inline code monospace | PASS | Code spans at correct positions; Courier New from unit tests |
| TC-DOC48 | Simple nested table | PASS | Outer 1×1 table; cell [0,0] span confirms nested table (not text) |
| TC-DOC49 | Nested table alongside regular cells | PASS | [0,0]"Label"; [0,1] span consistent with 1×2 nested table |
| TC-DOC50 | Nested table with multiple rows and cols | PASS | Outer 1×1; inner span consistent with 2×2 |
| TC-DOC51 | Nested tables not supported in markdown | SKIP | Note only — no test to run; known limitation documented |
| TC-DOC52 | get_doc_theme scans body paragraph styles | PASS | {} for inherited-style doc; non-empty for explicit-style doc |
| TC-DOC53 | apply_theme updates named style definitions | PASS | requests:3 (one updateNamedStyle per key); no error |
| TC-DOC54 | apply_theme with overwrite=True | PASS | requests:7 (2 updateNamedStyle + 5 updateTextStyle); no error |
| TC-DOC55 | apply_theme with table styling | PASS | requests:2 (one per row); header bg + borders confirmed |
| TC-DOC56 | get_doc_theme → apply_theme round-trip | PASS | Round-trip: get returns Georgia/Roboto; apply-back requests:2 |
| TC-DOC57 | Insert image by public URI | PASS | docId + index in response; image visible in doc |
| TC-DOC58 | Insert image with explicit size | PASS | width=100, height=50; image smaller than default |
| TC-DOC59 | No source provided returns error | PASS | {"error":"Provide either uri or drive_file_id"} |
| TC-DOC60 | Both URI and drive_file_id returns error | PASS | {"error":"Provide only one of uri or drive_file_id, not both"} |
| TC-DOC61 | Insert row below existing row | PASS | 3 rows confirmed after insert; Playwright: tc-doc62-insert-row-above.png |
| TC-DOC62 | Insert row above existing row | PASS | 3 rows confirmed; new row at row 1 between original rows; Playwright confirmed |
| TC-DOC63 | Delete a row | PASS | 2 rows after delete; Playwright: tc-doc63-delete-row.png |
| TC-DOC64 | Insert column to the right | PASS | 3 columns confirmed; Playwright: tc-doc64-insert-col-right.png |
| TC-DOC65 | Insert column to the left | PASS | 3 columns confirmed; Playwright: tc-doc65-insert-col-left.png |
| TC-DOC66 | Delete a column | PASS | 2 columns after delete; Playwright: tc-doc66-delete-col.png |
| TC-DOC67 | API error returned gracefully (out-of-bounds row) | PASS | {"error":"..."} for row_index=99 on 2×2 table; no exception |
| TC-DOC68 | Create a default page header | PASS | headerId returned; header section visible; Playwright: tc-doc68-header.png |
| TC-DOC69 | Create header with content | PASS | "Confidential — Internal Only" visible in header; Playwright: tc-doc69-header-content.png |
| TC-DOC70 | Create a default page footer | PASS | footerId returned; footer section created; Playwright: tc-doc70-footer.png |
| TC-DOC71 | Create footer with content | PASS | "Page 1" in footer; API response confirmed |
| TC-DOC72 | Invalid header_type returns error | PASS | {"error":"Invalid header_type 'INVALID'. Use DEFAULT or FIRST_PAGE_HEADER"} |
| TC-DOC73 | Invalid footer_type returns error | PASS | {"error":"Invalid footer_type 'BOGUS'. Use DEFAULT or FIRST_PAGE_FOOTER"} |
| TC-DOC74 | insert_doc_text with segment_id writes into header | PASS | "QA Test Header" in fixture doc header; insertions:1; Playwright: tc-doc74-header-insert.png |
| TC-DOC75 | get_doc_named_styles reads named style defaults | PASS | 9 entries returned after apply_theme; NORMAL_TEXT/HEADING_1-6/TITLE/SUBTITLE |
| TC-DOC76 | Table after heading renders at Normal Text size | PASS | All 6 cells render at Normal Text size; heading visually larger; no blank para gap needed; Playwright: tc-doc76-77-heading-table.png |
| TC-DOC77 | No visible blank line between heading and table | PASS | blank para lineSpacing:1 confirmed in structure; no visible gap in rendered doc |
| TC-DOC78 | data-style="title" produces TITLE named style | PASS | First para namedStyleType:"TITLE" confirmed in get_doc_structure |
| TC-DOC79 | data-style="subtitle" produces SUBTITLE named style | PASS | All three: TITLE/SUBTITLE/NORMAL_TEXT with correct text values |
Calendar¶
| TC | Title | Outcome | Notes |
|---|---|---|---|
| TC-CAL01 | Returns subscribed calendars | PASS | At least one calendar; id/summary/time_zone/access_role/primary all present |
| TC-CAL02 | primary flag | PASS | Exactly one primary:true; OAuth primary calendar correctly identified |
| TC-CAL03 | Cache hit on second call | PASS | Second call returned same results |
| TC-CAL04 | Empty subscription list | SKIP | Requires fresh service account with no subscriptions |
| TC-CAL05 | get_calendar by ID | PASS | id/summary/time_zone/access_role all present; no error |
| TC-CAL06 | get_calendar primary calendar | PASS | primary:true confirmed for OAuth primary calendar |
| TC-CAL07 | get_calendar cache hit | PASS | Second call returned same result |
| TC-CAL08 | get_calendar invalid ID | PASS | Error returned; not a crash |
| TC-CAL09 | list_events time_min default | FAIL | All past events returned when time_min omitted. Docstring claims "Defaults to now" but calendar.py:115 only sets timeMin when explicitly provided |
| TC-CAL10 | list_events with time_min | PASS | Only future events returned when explicit time_min set |
| TC-CAL11 | list_events max_results=3 | PASS | Exactly 3 events returned |
| TC-CAL12 | list_events query filter | PASS | Only events matching query returned |
| TC-CAL13 | list_events single_events=True | PASS | Recurring event expanded to instances |
| TC-CAL14 | list_events pagination | PASS | page_size respected; next_page_token present when more results |
| TC-CAL15 | list_events empty calendar | PASS | [] returned; no error |
| TC-CAL16 | list_events invalid calendar | PASS | Error returned; not a crash |
| TC-CAL17 | list_events order_by startTime | PASS | Events ordered chronologically |
| TC-CAL18 | list_events time_max filter | PASS | Only events before time_max returned |
| TC-CAL19 | get_event happy path | PASS | id/summary/start/end/status all present; no error |
| TC-CAL20 | create_event timed | PASS | QA-Timed-Test created Jul 1 10am-11am; Playwright: event visible in calendar |
| TC-CAL21 | create_event all-day | PASS | QA-AllDay-Test created Thu Jul 2; full-day bar visible |
| TC-CAL22 | create_event with description/location | PASS | QA-Full-Test with description + "Room B" location; Playwright: tc-cal20-22-events-created.png |
| TC-CAL23 | create_event invalid calendar | PASS | Error returned; not a crash |
| TC-CAL24 | update_event change time | PASS | QA-Timed-Test → 2pm-3pm; Playwright confirmed |
| TC-CAL25 | update_event change title | PASS | title renamed to "QA-Timed-Updated"; Playwright: tc-cal24-25-26-updated.png |
| TC-CAL26 | update_event change location | PASS | QA-Full-Test location updated; Playwright confirmed |
| TC-CAL27 | update_event invalid event ID | PASS | Error returned; not a crash |
| TC-CAL28 | delete_event timed | PASS | QA-AllDay-Test deleted; Thu Jul 2 empty in calendar; Playwright: tc-cal28-allday-deleted.png |
| TC-CAL29 | delete_event invalid ID | PASS | Error returned; not a crash |
| TC-CAL30 | find_free_slots basic | PASS | Free slots returned correctly for given range |
| TC-CAL31 | find_free_slots with busy events | PASS | Busy periods excluded from free slots |
| TC-CAL32 | find_free_slots multi-calendar | SKIP | Requires second calendar subscription |
| TC-CAL33 | find_free_slots min_duration respected | PASS | Only slots ≥ min_duration returned |
| TC-CAL34 | find_free_slots business hours | PASS | Slots clipped to business hours |
| TC-CAL35 | find_free_slots merges adjacent busy | PASS | QA-Busy-A (10-11am) + QA-Busy-B (11am-12pm) merged into single [10am-12pm] busy block |
| TC-CAL36 | create_event recurring WEEKLY | PASS | QA-Weekly-Standup with WEEKLY recurrence; Playwright: tc-cal36-39-40-weekly.png. Note: timezone required for recurring datetime events (docstring gap) |
| TC-CAL37 | create_event recurring DAILY 5× | PASS | QA-Daily-5x with COUNT=5; expand_recurring=True shows 5 instances |
| TC-CAL38 | list_events expand_recurring=False | PASS | Master event returned; recurrence[] present; no _YYYYMMDD suffix on ID |
| TC-CAL39 | list_events expand_recurring=True | PASS | 5 instances with _YYYYMMDDTHHMMSSZ suffix on IDs |
| TC-CAL40 | get_event recurring master | PASS | Master event confirmed; recurrence[] present |
| TC-CAL41 | get_event recurring instance | PASS | Individual instance confirmed; recurringEventId present |
| TC-CAL42 | update_event remove recurrence | PASS | recurrence:null confirmed after passing recurrence=[]; event now one-off |
| TC-CAL43 | delete_event recurring | PASS | Master + all instances deleted |