Skip to content

Sheet Management Tools — QA Test Cases

Source: src/mcp_gee_sweet/tools/sheets.py

Fixtures: see docs/qa/setup.md. Substitute your {SPREADSHEET_ID} from fixtures.local.md.

Tests marked ⚠️ destructive rename or delete sheets — reset fixtures afterward.


list_sheets

TC-S01: Happy path

Prompt

"List all the sheets in {SPREADSHEET_ID}"

Checks - Returns 3 tabs: Sales, Empty, Notes & Misc - Each entry includes the sheet name - No error field


TC-S02: Cache hit on second call

Prompt (run twice in the same session)

"List the sheets in {SPREADSHEET_ID} again"

Checks - Second call returns the same list - Server logs show cache hit for the second call (make logs)


TC-S03: Cache invalidated after rename

Prompt

"Rename the 'Empty' sheet in {SPREADSHEET_ID} to 'WasEmpty', then immediately list all sheets"

Checks - list_sheets response includes 'WasEmpty', not 'Empty' - Confirms cache.mark_dirty fires after rename and the next list re-fetches


copy_sheet

TC-S04: Copy within same spreadsheet

Prompt

"Copy the Sales sheet within {SPREADSHEET_ID} and name the copy 'Sales Copy'"

Checks - New sheet 'Sales Copy' appears in {SPREADSHEET_ID} - Data in 'Sales Copy' matches Sales (6 rows, 4 columns) - Original Sales sheet unchanged


TC-S05: Copy to a different spreadsheet

Prompt

"Copy the Sales sheet from {SPREADSHEET_ID} into a different spreadsheet — use {SPREADSHEET_ID} as both source and destination for this test, but note the tool supports different IDs"

Checks - Tool accepts src_spreadsheet and dst_spreadsheet as separate params - Copy operation completes without error - 🔍 Note: requires a second spreadsheet ID to fully verify cross-spreadsheet copy


TC-S06: Name differs from Google's auto-assigned name — rename triggered

Prompt

"Copy the Sales sheet in {SPREADSHEET_ID} and name the copy 'My Custom Name'"

Checks - Sheet is named 'My Custom Name', not Google's default 'Copy of Sales' - Rename was triggered automatically after the copy


TC-S07: Name matches Google's auto-assigned name — rename skipped

Prompt

"Copy the Sales sheet in {SPREADSHEET_ID} and name the copy 'Copy of Sales'"

Checks - Sheet is named 'Copy of Sales' - No unnecessary rename API call (names already match)


TC-S08: Source sheet not found

Prompt

"Copy a sheet called 'DoesNotExist' from {SPREADSHEET_ID}"

Checks - Returns {"error": ...} before calling the copy API - Error references the missing sheet name


TC-S09: Destination spreadsheet not writable

Prompt

"Copy the Sales sheet from {SPREADSHEET_ID} to spreadsheet 'invalidid123xyz'"

Checks - Returns a clear API error — destination not accessible - Source spreadsheet unaffected


TC-S10: Cache invalidated after copy ⚠️ destructive

Prompt

"Copy the Sales sheet in {SPREADSHEET_ID} as 'PostCopyCache', then immediately list all sheets"

Checks - list_sheets includes 'PostCopyCache' - Confirms cache.mark_dirty(dst_spreadsheet) fires after copy


rename_sheet

TC-S11: Rename to a new name ⚠️ destructive

Prompt

"Rename the 'Empty' sheet in {SPREADSHEET_ID} to 'Renamed'"

Checks - Sheet formerly called 'Empty' is now 'Renamed' - list_sheets reflects the new name - No error field


TC-S12: Rename to the same name

Prompt

"Rename the 'Sales' sheet in {SPREADSHEET_ID} to 'Sales' (same name)"

Checks - API succeeds or no-ops — no error - Sheet still exists with the same name - 🔍 Product decision: is a same-name rename a no-op or does it round-trip to the API?


TC-S13: Sheet not found

Prompt

"Rename a sheet called 'NoSuchSheet' in {SPREADSHEET_ID} to 'Anything'"

Checks - Returns {"error": ...} — sheet not found - Does not call the Sheets API


TC-S14: Cache invalidated after rename

Prompt

"Rename 'Notes & Misc' to 'Notes' in {SPREADSHEET_ID}, then list all sheets"

Checks - list_sheets shows 'Notes', not 'Notes & Misc' - Confirms cache.mark_dirty fires; next list re-fetches from API


create_sheet

TC-S15: Create a new tab

Prompt

"Add a new sheet called 'BrandNew' to {SPREADSHEET_ID}"

Checks - New tab 'BrandNew' appears - Response includes sheetId, title, index, spreadsheetId - cache.mark_dirty called — list_sheets reflects the new tab


TC-S16: Duplicate tab title

Prompt

"Add another sheet called 'Sales' to {SPREADSHEET_ID} — a tab with that name already exists"

Checks - 🔍 Product decision: does the API error, auto-suffix (e.g. "Sales2"), or succeed with a duplicate? - Note observed behavior


TC-S17: Long title

Prompt

"Add a sheet with a 150-character title to {SPREADSHEET_ID}"

Checks - API error with a clear message about title length limits, or succeeds if no limit enforced - Note the actual limit if an error is returned


TC-S18: Response shape

Prompt

"Create a sheet called 'ShapeTest' in {SPREADSHEET_ID} and show me the full response"

Checks - Response includes: sheetId (integer), title (string), index (integer), spreadsheetId (string) - No unexpected missing fields


TC-S19: Cache updated after create

Prompt

"Create a sheet called 'CacheNewSheet' in {SPREADSHEET_ID}, then list all sheets"

Checks - list_sheets includes 'CacheNewSheet' immediately - Confirms cache.mark_dirty fires after creation


refresh_cache

TC-S20: Refresh by spreadsheet ID only

Prompt

"Refresh the cache for {SPREADSHEET_ID}"

Checks - Returns success - Next list_sheets or summary call hits the API (visible in logs as a cache miss)


TC-S21: Refresh by doc ID only

Prompt

"Refresh the cache for doc {DOC_ID}"

Checks - Returns success - Next get_doc_content call re-fetches from API


TC-S22: Refresh both spreadsheet and doc

Prompt

"Refresh the cache for both {SPREADSHEET_ID} and doc {DOC_ID}"

Checks - Both caches marked dirty - Subsequent calls for both re-fetch from API


TC-S23: Refresh with no arguments — clears all caches

Prompt

"Clear all caches in mcp-gee-sweet"

Checks - All four caches marked dirty (structure, data, Drive folder, doc) - Next calls for any resource re-fetch from API


TC-S24: Cache re-populated after refresh

Prompt

"Refresh the cache for {SPREADSHEET_ID}, then immediately summarize it"

Checks - Summary returns correct data (re-fetched, not stale) - Logs show a cache miss followed by a cache store


delete_sheet

TC-S25: Delete an existing sheet tab ⚠️ destructive

Prompt

"Delete the sheet called 'TempTab' from {SPREADSHEET_ID}"

Setup: Create a throwaway tab called 'TempTab' first.

Checks - 'TempTab' no longer appears in list_sheets - No error field in response

Result (2026-06-21) ✅ TempTab created via create_sheet, then deleted. list_sheets returned ["Sales","Empty","Notes & Misc"] — TempTab absent. No error field.


TC-S26: Delete a non-existent sheet returns error

Prompt

"Delete a sheet called 'DoesNotExist' from {SPREADSHEET_ID}"

Checks - Response contains error field mentioning the sheet name - No API call made (no batchUpdate)

Result (2026-06-21) ✅ Response: {"error":"Sheet 'DoesNotExist' not found"}. No batchUpdate issued.


delete_rows

TC-S27: Delete a single row ⚠️ destructive

Prompt

"Delete row 5 (0-based index 4) from the Sales sheet in {SPREADSHEET_ID}"

Setup: Confirm rows 4 and 5 (0-based) have known values before deleting.

Checks - Former row 5 content is gone; row 5 now contains what was row 6 - Other rows unchanged

Result (2026-06-21) ✅ Row 4 (Gizmo/300/310/290) deleted. Former row 5 (Totals) shifted up. Totals recalculated to 350/360/415 reflecting the reduced data set.


TC-S28: Delete a range of rows ⚠️ destructive

Prompt

"Delete rows 3 through 5 (0-based indices 2–4) from the Sales sheet in {SPREADSHEET_ID}"

Checks - Three rows removed; subsequent rows shift up correctly - startIndex: 2, endIndex: 5 in the deleteDimension request

Result (2026-06-21) ✅ Rows 2–4 (Gadget/Donut/Gizmo) removed. Widget and Totals remain; Totals recalculated to 100/120/140 (Widget only).


TC-S29: Delete rows — sheet not found returns error

Prompt

"Delete row 0 from a sheet called 'NoSuchSheet' in {SPREADSHEET_ID}"

Checks - Response contains error field

Result (2026-06-21) ✅ Response: {"error":"Sheet 'NoSuchSheet' not found"}.


delete_columns

TC-S30: Delete a single column ⚠️ destructive

Prompt

"Delete column B (0-based index 1) from the Sales sheet in {SPREADSHEET_ID}"

Setup: Confirm column B has known content before deleting.

Checks - Column B content removed; former column C shifts left to become B - dimension: COLUMNS, startIndex: 1, endIndex: 2

Result (2026-06-21) ✅ Column index 1 (Q1) deleted. Q2 and Q3 shifted left. Totals recalculated to 670/705 (Q2+Q3 only).


TC-S31: Delete a range of columns ⚠️ destructive

Prompt

"Delete columns C through E (0-based indices 2–4) from the Sales sheet in {SPREADSHEET_ID}"

Checks - Three columns removed; columns to the right shift left - startIndex: 2, endIndex: 5

Result (2026-06-21) ✅ Column indices 2–3 (Q2 and Q3) deleted (only 4 cols exist so effective range was 2–3). Only Product and Q1 remained. Inclusive end index correctly translated to exclusive endIndex in API call.


TC-S32: Delete columns — sheet not found returns error

Prompt

"Delete column 0 from a sheet called 'NoSuchSheet' in {SPREADSHEET_ID}"

Checks - Response contains error field

Result (2026-06-21) ✅ Response: {"error":"Sheet 'NoSuchSheet' not found"}.


format_cells

TC-S33: Apply bold and background color to a range ⚠️ destructive

Prompt Playwright: required

"Make cells A1:D1 on the Sales sheet bold with a light blue background"

Checks - repeatCell request sent with textFormat.bold=true - backgroundColor set to the specified color - fields mask includes both userEnteredFormat.textFormat and userEnteredFormat.backgroundColor - No error in response

Result (2026-06-21) ✅ A1:D1 on Sales formatted bold with light blue background. replies: [{}] — no error.


TC-S34: Apply number format to a column ⚠️ destructive

Prompt Playwright: required

"Format column B (B2:B100) on Sales as currency with 2 decimal places"

Checks - numberFormat.type is "CURRENCY" (or "NUMBER" with pattern) - numberFormat.pattern applied - fields includes userEnteredFormat.numberFormat

Result (2026-06-21) ✅ B2:B6 on Sales formatted NUMBER with pattern #,##0.00. replies: [{}] — no error.


TC-S35: Set horizontal alignment ⚠️ destructive

Prompt Playwright: required

"Center-align cells A1:F1 on the Sales sheet"

Checks - horizontalAlignment is "CENTER" - fields includes userEnteredFormat.horizontalAlignment

Result (2026-06-21) ✅ A1:D1 on Sales center-aligned. replies: [{}] — no error.


TC-S36: No formatting params returns error

Checks (unit test) - Calling format_cells with no formatting params returns {"error": ...} - No batchUpdate API call made

Result (2026-06-21) ✅ Unit test confirms error returned and batchUpdate not called.


TC-S37: format_cells — sheet not found returns error

Checks (unit test) - Sheet name not in spreadsheet → {"error": "Sheet 'X' not found"}

Result (2026-06-21) ✅ Unit test confirms error.


merge_cells / unmerge_cells

TC-S38: Merge a header row range ⚠️ destructive

Prompt Playwright: required

"Merge cells A1:D1 on the Sales sheet to make a single header cell"

Checks - mergeCells request with mergeType=MERGE_ALL - Range covers A1:D1 - No error in response

Result (2026-06-21) ✅ E1:G2 on Empty merged with MERGE_ALL. replies: [{}] — no error.


TC-S39: Merge rows independently ⚠️ destructive

Prompt Playwright: required

"Merge each row independently in A1:C3 on Sales (merge_type=MERGE_ROWS)"

Checks - mergeCells request with mergeType=MERGE_ROWS

Result (2026-06-21) ✅ H1:J3 on Empty merged with MERGE_ROWS. replies: [{}] — no error.


TC-S40: Unmerge a previously merged range ⚠️ destructive

Prompt Playwright: required

"Unmerge cells A1:D1 on the Sales sheet"

Checks - unmergeCells request sent - No error in response

Result (2026-06-21) ✅ E1:G2 on Empty unmerged. replies: [{}] — no error.


TC-S41: merge_cells — sheet not found returns error

Checks (unit test) - Sheet not found → {"error": "Sheet 'X' not found"}

Result (2026-06-21) ✅ Unit test confirms error.


freeze

TC-S42: Freeze the header row ⚠️ destructive

Prompt Playwright: required

"Freeze the first row on the Sales sheet"

Checks - updateSheetProperties with frozenRowCount=1, frozenColumnCount=0 - fields covers both frozenRowCount and frozenColumnCount - No error in response

Result (2026-06-21) ✅ Row 1 frozen on Sales. replies: [{}] — no error.


TC-S43: Freeze first row and first column ⚠️ destructive

Prompt Playwright: required

"Freeze the first row and first column on Sales"

Checks - frozenRowCount=1, frozenColumnCount=1

Result (2026-06-21) ✅ Row 1 and column 1 frozen on Sales. replies: [{}] — no error.


TC-S44: Unfreeze all (rows=0, columns=0) ⚠️ destructive

Prompt

"Unfreeze all rows and columns on the Sales sheet"

Checks - frozenRowCount=0, frozenColumnCount=0

Result (2026-06-21) ✅ All rows and columns unfrozen on Sales. replies: [{}] — no error.


TC-S45: freeze — sheet not found returns error

Checks (unit test) - Sheet not found → {"error": "Sheet 'X' not found"}

Result (2026-06-21) ✅ Unit test confirms error.


sort_range

TC-S46: Sort a data range ascending by first column ⚠️ destructive

Prompt Playwright: required

"Sort the range A2:D50 on Sales by the first column ascending"

Checks - sortRange request with sortSpecs[0].dimensionIndex=0, sortOrder=ASCENDING - Range covers A2:D50

Result (2026-06-21) ✅ A2:D5 on Sales sorted by Product ascending (Donut, Gadget, Gizmo, Widget). replies: [{}] — no error.


TC-S47: Sort descending by a non-first column ⚠️ destructive

Prompt Playwright: required

"Sort A2:D50 on Sales by column C (index 2) descending"

Checks - sortSpecs[0].dimensionIndex=2, sortOrder=DESCENDING

Result (2026-06-21) ✅ A2:D5 sorted by Q2 descending (Gizmo 310, Widget 120, Gadget 180... descending). replies: [{}] — no error.


TC-S48: Multi-column sort ⚠️ destructive

Prompt Playwright: required

"Sort A2:D50 on Sales: primary key column A ascending, secondary key column C descending"

Checks - Two sort specs in sortSpecs - First spec: dimensionIndex=0, ASCENDING - Second spec: dimensionIndex=2, DESCENDING

Result (2026-06-21) ✅ A2:D5 sorted by Product ASC then Q2 DESC. Two sortSpecs emitted. replies: [{}] — no error.


TC-S49: column_index offset by range start column

Checks (unit test) - Range starting at column B (index 1) with column_index=0dimensionIndex=1

Result (2026-06-21) ✅ Unit test confirms offset applied correctly.


TC-S50: sort_range — sheet not found returns error

Checks (unit test) - Sheet not found → {"error": "Sheet 'X' not found"}

Result (2026-06-21) ✅ Unit test confirms error.