Write Tools — QA Test Cases¶
Source: src/mcp_gee_sweet/tools/write.py
Fixtures: see docs/qa/setup.md. Substitute your {SPREADSHEET_ID} from fixtures.local.md.
Many write tests mutate the fixture spreadsheet. Tests marked ⚠️ destructive should be run last or followed by a fixture reset.
update_cells¶
TC-W01: Write simple values¶
Prompt
"Write the values 'Test', 'A', 'B', 'C' into row 7 of the Sales sheet in {SPREADSHEET_ID}, starting at column A"
Checks
- Row 7 in Sales now contains: Test, A, B, C
- Existing rows 1–6 unchanged
- No error field
Result (2026-09-04) ✅ PASS Row 7 A–D = Test/A/B/C; 4 cells; rows 1–6 unchanged; no error
TC-W02: Write a formula via USER_ENTERED ⚠️ destructive¶
Prompt
"Write the formula =A2&\" \"&A3 into cell E2 of the Sales sheet in {SPREADSHEET_ID}"
Note (v0.9.0, TC-W02): the prompt previously used single-quote string delimiters (=A2&' '&A3), which Google Sheets rejects (#ERROR! — Sheets formulas require double quotes for string literals). Fixed to the correct double-quote form; not a tool defect — USER_ENTERED mode itself was independently confirmed correct.
Checks
- E2 shows computed value "Widget Gadget" (formula evaluated, not stored as string)
- Confirms USER_ENTERED input mode
Result (2026-09-04) ❌ FAIL
Prompt formula =A2&' '&A3 (single-quote string delims) → E2 = #ERROR! "Formula parse error"; expected computed "Widget Gadget". Root cause = test-case bug: Google Sheets requires DOUBLE quotes for string literals. USER_ENTERED mode itself IS confirmed (userEnteredValue.formulaValue stored, not stringValue); re-ran with corrected =A2&" "&A3 → E2 = "Widget Gadget". Recommend fixing the test prompt to use double quotes
TC-W03: Range smaller than data provided¶
Prompt
"Write these four values — Alpha, Beta, Gamma, Delta — into just cells A8:A9 of the Sales sheet in {SPREADSHEET_ID}"
Checks - Returns an error — the API rejects an oversized array with a 400 "tried writing to row [N]" error - No values are written - 🔍 Product decision: should the tool pre-truncate the data to fit the range instead of letting the API error?
Result (2026-09-04) ✅ PASS 4 values into A8:A9 → HttpError 400 "tried writing to row [10]"; no partial write. Tool passes to API rather than pre-truncating
TC-W04: Cache invalidated after write¶
Prompt (run TC-R16 or TC-R17 first to warm the cache, then run this)
"Update cell A7 in the Sales sheet of {SPREADSHEET_ID} to 'CacheTest', then immediately summarize that spreadsheet"
Checks
- Summary reflects the new value in A7 — not stale cached data
- Confirms sheet_data_cache.mark_dirty fired after the write
Result (2026-09-04) ✅ PASS A7→"CacheTest" then summary shows "CacheTest" in first_rows — cache invalidated
TC-W05: Non-existent sheet name¶
Prompt
"Write 'Hello' into cell A1 of a sheet called 'NoSuchSheet' in {SPREADSHEET_ID}"
Checks - Returns a clear API error - Does not silently succeed or create the sheet
Result (2026-09-04) ✅ PASS Sheet "NoSuchSheet" → HttpError 400 "Unable to parse range: NoSuchSheet!A1"; no silent success, no sheet created
TC-W33: Partial (rich-text) hyperlink in a single cell ⚠️ destructive¶
Prompt Playwright: required
"In the Sales sheet of {SPREADSHEET_ID}, write cell F2 using
update_cellsso it reads 'See the docs' where only the 'the docs' part is a hyperlink to https://example.com — pass F2's value as a two-run list: {\"text\": \"See \"} then {\"text\": \"the docs\", \"hyperlink\": \"https://example.com\"}"
Checks
- get_sheet_data with include_grid_data=True on F2 shows userEnteredValue.stringValue = "See the docs"
- textFormatRuns has two entries: first with no startIndex (implicit 0) and an empty format, second with startIndex: 4 and format.link.uri = "https://example.com"
- userEnteredFormat.hyperlinkDisplayType = "LINKED"
- 🔍 Visual check: only "the docs" renders underlined/blue and is clickable; "See " renders as plain text
Result (2026-07-19) ✅ — get_sheet_data(include_grid_data=True) on F2 confirmed userEnteredValue.stringValue = "See the docs", textFormatRuns = [{"format":{}}, {"startIndex":4,"format":{"link":{"uri":"https://example.com"},...}}], userEnteredFormat.hyperlinkDisplayType = "LINKED". 🔍 Visual check blocked by the documented "Chart-covered grid (Sales sheet)" limitation (docs/qa/run.md) — a floating chart from earlier add_chart runs covers rows 1–22 including F2; API response used as the confirmation source per that doc's guidance instead.
Result (2026-09-04) ✅ PASS F2 stringValue "See the docs"; textFormatRuns [{format:{}}, {startIndex:4, link.uri "https://example.com"}]; hyperlinkDisplayType LINKED. Visual check blocked by documented Chart-covered-grid limitation → API used as confirmation source
TC-W34: Mixed plain and rich-text cells in the same call ⚠️ destructive¶
Prompt Playwright: required
"In the Sales sheet of {SPREADSHEET_ID}, use
update_cellson range F3:G3 to write 'PlainValue' into F3 and, into G3, a hyperlinked cell reading 'Link' that links to https://example.com/g3"
Checks
- F3 = "PlainValue" (written via a per-cell values.batchUpdate scoped to just F3, not a whole-range values.update — the whole-range write was retired because it required blanking G3 to "" first and relying on the rich-text pass to overwrite it back, losing G3's content for good if that second call failed)
- G3 shows "Link" with userEnteredFormat.hyperlinkDisplayType = "LINKED" and a textFormatRuns entry linking to https://example.com/g3
- Both cells are correct in a single tool call — confirms the plain-cell pass and the rich-text batchUpdate pass compose correctly over the same range
- The tool's own return value is {"values_update": {...}, "rich_text_update": {...}} — both results present, not just the plain-cell one (previously the rich-text result was silently dropped whenever plain cells were also present in the same call)
- 🔍 Visual check: G3 renders underlined/blue and clickable; F3 renders as plain text
Result (2026-07-19) ✅ (write) / ⚠️ tool-response gap — get_sheet_data(include_grid_data=True) on F3:G3 confirmed both cells correct: F3 userEnteredValue.stringValue = "PlainValue" (hyperlinkDisplayType: PLAIN_TEXT); G3 userEnteredValue.stringValue = "Link", hyperlinkDisplayType: LINKED, textFormatRuns[0].format.link.uri = "https://example.com/g3" — the two passes do compose correctly on the sheet. However the tool's own return value was {"updatedRange":"Sales!F3:G3","updatedRows":1,"updatedColumns":2,"updatedCells":2} — only the plain values().update() response; the batchUpdate reply for G3's rich-text write is silently dropped from what the caller sees, even though the write itself succeeded. Matches code-review finding (data.py:687, result = batch_result only fires if not has_plain_cells) — real, live-confirmed, not just a code-reading inference. 🔍 Visual check blocked by the same chart-pollution limitation as TC-W33.
Note (2026-07-20): the tool-response gap above was addressed post-review — mixed writes now return {"values_update": ..., "rich_text_update": ...} (both results present) instead of only the plain-cell response, and the plain-cell write is now a per-cell values().batchUpdate() rather than a whole-range values().update(). The Result entry above reflects pre-fix behavior and is left as-is for history; needs a fresh live pass to confirm the fixed return shape.
Result (2026-07-20, post-fix re-verification) ✅ — Re-ran against c368ce1. Return value was {"values_update": {"spreadsheetId":"...","totalUpdatedRows":1,"totalUpdatedColumns":1,"totalUpdatedCells":1,"totalUpdatedSheets":1,"responses":[{"updatedRange":"Sales!F3",...}]}, "rich_text_update": {"spreadsheetId":"...","replies":[{}]}} — both results now present, confirming the fix. get_sheet_data(include_grid_data=True) on F3:G3 confirmed F3 userEnteredValue.stringValue = "PlainValue" and G3 userEnteredValue.stringValue = "Link" with hyperlinkDisplayType: LINKED and hyperlink = "https://example.com/g3" — both cells still correct with the new per-cell values.batchUpdate write path. 🔍 Visual check still blocked by the chart-pollution limitation.
Result (2026-09-04) ✅ PASS Return = {values_update:{...responses:[{updatedRange:"Sales!F3"}]}, rich_text_update:{...}} — both present; F3 "PlainValue"/PLAIN_TEXT via per-cell values.batchUpdate; G3 "Link"/LINKED, link.uri "https://example.com/g3". Visual blocked by chart pollution
TC-W35: Rich-text run missing "text" key returns an error¶
Prompt
"Call
update_cellson {SPREADSHEET_ID}'s Sales sheet, range F4, with a malformed rich-text cell: a run list containing only{\"hyperlink\": \"https://example.com\"}with notextkey"
Checks
- Returns {"error": ...} naming the missing text key
- No write occurs — F4 is unchanged (neither the plain-value pass nor the batchUpdate pass fires)
Result (2026-07-19) ✅ — Returned {"error": "Rich-text cell runs must be dicts with a 'text' key, e.g. {'text': ..., 'hyperlink': ...}"}. Follow-up get_sheet_data on F4 confirmed no write occurred (empty). Note: this test only covers a missing text key — it does not cover a present-but-wrong-type text value (e.g. {"text": None, ...} or {"text": 123, ...}), which code review found crashes with an unhandled TypeError instead of returning this same graceful error (see PR comment).
Result (2026-09-04) ✅ PASS
Run {"hyperlink":...} no text → {"error":"Rich-text cell runs must be dicts with a string 'text' key..."}; F4 not written (came back as empty cell entry)
TC-W36: Rich-text run offsets after an astral-plane character (emoji) ⚠️ destructive¶
Background: Sheets API TextFormatRun.startIndex counts UTF-16 code units, not Python characters — an emoji outside the Basic Multilingual Plane is 2 units. A run offset computed with plain len() would land the hyperlink one character early.
Prompt Playwright: required
"In the Sales sheet of {SPREADSHEET_ID}, write cell F5 using
update_cellswith a two-run rich-text cell: first run text is a single 🚀 emoji with no hyperlink, second run text is 'link' hyperlinked to https://example.com"
Checks
- textFormatRuns's second entry has startIndex: 2 (the emoji occupies 2 UTF-16 units), not 1
- 🔍 Visual check: the hyperlink underline/color starts exactly at "link", not one character into the emoji
Result (2026-07-19) ✅ — get_sheet_data(include_grid_data=True) on F5 confirmed userEnteredValue.stringValue = "🚀link", textFormatRuns[1].startIndex = 2 (not 1) with format.link.uri = "https://example.com". _utf16_len correctly accounts for the emoji's UTF-16 surrogate pair. 🔍 Visual check blocked by the same chart-pollution limitation as TC-W33.
Result (2026-09-04) ✅ PASS F5 stringValue "🚀link"; textFormatRuns[1].startIndex = 2 (emoji = 2 UTF-16 units), not 1; link.uri present. Visual blocked by chart pollution
TC-W37: Rich-text run with a wrong-typed "text" value returns an error, not a crash¶
Prompt
"Call
update_cellson {SPREADSHEET_ID}'s Sales sheet, range F6, with a malformed rich-text cell: a run list containing{\"text\": null, \"hyperlink\": \"https://example.com\"}"
Checks
- Returns {"error": ...} — does not raise/crash (a prior version only checked "text" was present, not that it was a string, and crashed with an unhandled TypeError on a non-string value)
- No write occurs — F6 is unchanged
Result (2026-07-20) ✅ — Returned {"error": "Rich-text cell runs must be dicts with a string 'text' key, e.g. {'text': ..., 'hyperlink': ...}"}, no crash. Follow-up get_sheet_data on F6 confirmed no write occurred (absent from the response entirely, same as an untouched row).
Result (2026-09-04) ✅ PASS
{"text": null,...} → {"error":"...string 'text' key..."}; no crash; F6 untouched
TC-W38: Empty rich-text run list returns an error, not a silent blank¶
Prompt
"Call
update_cellson {SPREADSHEET_ID}'s Sales sheet, range F7, passing[](an empty list) as F7's cell value"
Checks
- Returns {"error": ...} — an empty run list is rejected up front
- No write occurs — F7 is unchanged (a prior version treated [] as a valid zero-run rich-text cell and silently blanked it)
Result (2026-07-20) ✅ — Returned {"error": "Rich-text cell runs list cannot be empty"}. Follow-up get_sheet_data on F7 confirmed no write occurred.
Result (2026-09-04) ✅ PASS
[] as cell value → {"error":"Rich-text cell runs list cannot be empty"}; F7 untouched
TC-W39: Empty data returns an error, not a silent no-op¶
Prompt
"Call
update_cellson {SPREADSHEET_ID}'s Sales sheet, range F8, withdataset to an empty list[]"
Checks
- Returns {"error": "data cannot be empty"}
- No API call is made and the spreadsheet is unchanged
Result (2026-07-20) ✅ — Returned {"error": "data cannot be empty"}. Follow-up get_sheet_data on F8 confirmed no write occurred.
Result (2026-09-04) ✅ PASS data=[] → {"error":"data cannot be empty"}; no write
batch_update_cells¶
TC-W06: Multiple ranges in one call¶
Prompt
"In {SPREADSHEET_ID}, update two ranges at once: write 'Batch1' into Sales!A8 and 'Batch2' into Sales!A9"
Checks - Both cells updated in a single operation - A8 = Batch1, A9 = Batch2
Result (2026-09-04) ✅ PASS batch_update_cells A8=Batch1, A9=Batch2 in one op
TC-W07: Ranges on the same sheet¶
Prompt
"Update Sales!B8 to 999 and Sales!C8 to 888 in {SPREADSHEET_ID} in one batch call"
Checks - Both cells updated correctly - No conflict from writing multiple ranges to the same sheet
Result (2026-09-04) ✅ PASS B8=999, C8=888 in one batch; no same-sheet conflict
TC-W08: Empty ranges dict¶
Prompt
"Do a batch cell update on {SPREADSHEET_ID} with an empty set of ranges — no ranges to update"
Checks - Returns success or a clear no-op response — not a server error - 🔍 Product decision: should an empty ranges dict be an error or a no-op?
Result (2026-09-04) ✅ PASS Empty ranges {} → {"spreadsheetId":"..."} bare, no error, no values write (no-op)
TC-W09: Cache invalidated after batch write¶
Prompt
"Batch update Sales!A8 to 'dirty' in {SPREADSHEET_ID}, then immediately summarize the spreadsheet"
Checks
- Summary reflects A8 = 'dirty'
- Confirms sheet_data_cache.mark_dirty is called for batch_update_cells
Result (2026-09-04) ✅ PASS batch A8="dirty" then summary reflects "dirty" — mark_dirty fired
TC-W10: Trailing empty-array rows are normalized¶
Prompt
"In {SPREADSHEET_ID}, batch update Sales!A8:B10 with these values: row 1 is ['hello', 'world'], rows 2 and 3 are empty (no data). Check that updatedRange in the response covers all three rows (A8:B10), not just the first."
Checks
- updatedRange in the response is Sales!A8:B10, not Sales!A8:B8
- Rows A9 and A10 are cleared (blank) in the sheet
- No duplicate row appended after the write
add_rows¶
TC-W10: Add row at beginning (no position specified)¶
Prompt
"Add 1 row at the very beginning of the Sales sheet in {SPREADSHEET_ID}"
Checks
- New blank row appears above the current row 1 (headers pushed to row 2)
- inheritFromBefore=False (inserting at index 0 — no row above to inherit from)
TC-W11: Add row at explicit position ⚠️ destructive¶
Prompt
"Add 1 row after row 3 of the Sales sheet in {SPREADSHEET_ID}"
Checks - New blank row at position 4 - Existing rows 4–6 shifted down
Result (2026-09-04) ✅ PASS add 1 row start_row=3 → inserted, data shifted, formulas auto-adjusted; rowCount +1
TC-W12: start_row=0 — inheritFromBefore=False¶
Prompt
"Add 1 row at position 0 (the very first row) of the Sales sheet in {SPREADSHEET_ID}"
Checks
- Row inserted at the top
- inheritFromBefore is False (0 is not > 0)
- 🔍 Product decision: is start_row=0 meaningfully different from start_row=None?
Result (2026-09-04) ✅ PASS add 1 row start_row=0 → inserted at top, same as no-position. inheritFromBefore request shape unit-level
TC-W13: start_row=1 — inheritFromBefore=True¶
Prompt
"Add 1 row at position 1 in the Sales sheet of {SPREADSHEET_ID}"
Checks
- Row inserted at index 1 (after the first row)
- inheritFromBefore=True — new row inherits formatting from row above
Result (2026-09-04) ✅ PASS add 1 row start_row=1 → inserted at index 1; inheritFromBefore=True request shape unit-level (reply is {replies:[{}]})
TC-W14: Add multiple rows at once¶
Prompt
"Add 5 blank rows at the end of the Sales sheet in {SPREADSHEET_ID}"
Checks - 5 new rows appear - Existing data unchanged
Result (2026-09-04) ✅ PASS add 5 rows start_row=1004 (end) → rowCount 1004→1009; existing data unshifted
TC-W15: Invalid sheet name¶
Prompt
"Add 1 row to a sheet called 'NoSuchSheet' in {SPREADSHEET_ID}"
Checks
- Returns {"error": ...} without calling the Sheets API
- Does not throw an unhandled exception
Result (2026-09-04) ✅ PASS add row to "NoSuchSheet" → {"error":"Sheet 'NoSuchSheet' not found"}, no API call, no exception
TC-W16: Large count value¶
Prompt
"Add 1000 rows to the Sales sheet in {SPREADSHEET_ID}"
Checks
- Succeeds or returns a clear API limit error
- 🔍 Product decision: should the tool cap count to prevent accidental large inserts?
Result (2026-09-04) ✅ PASS add 1000 rows → succeeds, no cap enforced, no API-limit error. Tool does not cap count
add_columns¶
TC-W17: Add column at beginning (no position specified)¶
Prompt
"Add 1 column at the very beginning of the Sales sheet in {SPREADSHEET_ID}"
Checks
- New blank column A; existing columns shift right (Product moves to B)
- inheritFromBefore=False
Result (2026-09-04) ✅ PASS add 1 col, no position → blank col A; Product shifted to B
TC-W18: start_column=0 — inheritFromBefore=False¶
Prompt
"Add 1 column at position 0 of the Sales sheet in {SPREADSHEET_ID}"
Checks
- Column inserted at the leftmost position
- inheritFromBefore is False
Result (2026-09-04) ✅ PASS add 1 col start_column=0 → inserted leftmost
TC-W19: start_column=1 — inheritFromBefore=True¶
Prompt
"Add 1 column at position 1 in the Sales sheet of {SPREADSHEET_ID}"
Checks
- Column inserted after column A
- inheritFromBefore=True
Result (2026-09-04) ✅ PASS add 1 col start_column=1 → inserted after A; inheritFromBefore request shape unit-level
TC-W20: Add multiple columns¶
Prompt
"Add 3 columns at position 2 in the Sales sheet of {SPREADSHEET_ID}"
Checks - 3 new blank columns inserted at the correct position - Existing columns shifted right
Result (2026-09-04) ✅ PASS add 3 cols start_column=2 → 3 blank cols inserted, data shifted right (Product ended at col G after W17–W20)
batch_update (raw passthrough)¶
TC-W21: Add a sheet via raw request¶
Prompt
"Use a raw batch update on {SPREADSHEET_ID} to add a new sheet called 'RawAdded'"
Checks - New sheet 'RawAdded' appears in the spreadsheet - Response includes the new sheet's properties
Result (2026-09-04) ✅ PASS raw addSheet "RawAdded" → created; reply has full properties (sheetId 914226533, title, index)
TC-W22: Rename a sheet via raw request¶
Prompt
"Use a raw batch update to rename the 'Empty' sheet to 'RawRenamed' in {SPREADSHEET_ID}"
Checks
- Sheet formerly called 'Empty' is now 'RawRenamed'
- list_sheets reflects the new name
Result (2026-09-04) ✅ PASS raw updateSheetProperties rename Empty(sheetId 1225372232)→RawRenamed; list_sheets reflects it
TC-W23: Insert dimension via raw request¶
Prompt
"Use a raw batch update on {SPREADSHEET_ID} to insert 2 rows at index 1 in the Sales sheet"
Checks - 2 new rows appear at index 1 - Existing data shifts down
Result (2026-09-04) ✅ PASS raw insertDimension 2 ROWS at index 1 in Sales → replies [{}]; data shifted down
TC-W24: Delete dimension — structure cache invalidated ⚠️ destructive¶
Prompt
"Use a raw batch update to delete row 7 from the Sales sheet in {SPREADSHEET_ID}, then list the sheets"
Checks
- Row 7 deleted
- list_sheets call after reflects current state — not stale cache
- Confirms the structure cache bug fix: both cache.mark_dirty and sheet_data_cache.mark_dirty are called
Result (2026-09-04) ✅ PASS raw deleteDimension row 7 (ROWS 6:7) → replies [{}]; subsequent list_sheets returns current state (no stale cache)
TC-W25: Empty requests list¶
Prompt
"Send a raw batch update to {SPREADSHEET_ID} with an empty requests list"
Checks
- Returns {"error": "requests list cannot be empty"} or similar
- Does not call the Sheets API
Result (2026-09-04) ✅ PASS requests=[] → {"error":"requests list cannot be empty"}, no API call
TC-W26: Non-dict item in requests¶
Prompt
"Send a raw batch update to {SPREADSHEET_ID} with a requests list containing the string 'notadict'"
Checks
- Returns an error field
- Does not forward the malformed request to the API
Result (2026-09-04) ✅ PASS requests=["notadict"] → MCP pydantic schema validation error ("Input should be a valid dictionary"); not forwarded to API. Caught at schema layer, not tool body
TC-W27: Invalid request structure¶
Prompt
"Send a raw batch update to {SPREADSHEET_ID} with requests: [{\"unknownKey\": {}}]"
Checks - API error propagates back — not a server crash - Error message is from the Sheets API (unknown request type)
Result (2026-09-04) ✅ PASS requests=[{"unknownKey":{}}] → HttpError 400 from Sheets API "Unknown name 'unknownKey'"; propagates, no server crash
TC-W28: Both caches marked dirty¶
Prompt
"Use a raw batch update to add a sheet called 'CacheCheck' to {SPREADSHEET_ID}, then immediately list all sheets and summarize the spreadsheet"
Checks
- list_sheets includes 'CacheCheck' — structure cache was invalidated
- Summary reflects the new sheet — data cache was invalidated
- Confirms both cache.mark_dirty and sheet_data_cache.mark_dirty are called
Result (2026-09-04) ✅ PASS raw addSheet "CacheCheck" then list_sheets includes it AND summary reflects it — both structure + data caches invalidated
clear_values¶
TC-W29: Clear a specific range ⚠️ destructive¶
Prompt
"Clear the values in cells A1:C5 of the Sales sheet in {SPREADSHEET_ID}, leaving formatting intact"
Setup: Confirm A1:C5 has values and formatting before clearing.
Checks
- Cells A1:C5 are empty (no values)
- Cell formatting (background color, borders, number format) is unchanged
- No error field
Result (2026-06-21) ✅ A1:C5 cleared. get_sheet_data confirmed columns A–C rows 1–5 empty; column D (Q3) and row 6 (Totals) untouched. SUM formulas recalculated to 0 for cleared columns.
Result (2026-09-04) ✅ PASS clear A1:C5 of Sales → A1:C5 empty; col D + row 6 untouched; B6/C6 SUM formulas recalc to 0, D6 still 705
TC-W30: Clear entire sheet ⚠️ destructive¶
Prompt
"Clear all values from the Notes sheet in {SPREADSHEET_ID}"
Checks
- Sheet is empty (all cells blank)
- No error field
- Formatting preserved (not a full delete)
Result (2026-06-21) ✅ clear_values called with no range on Notes & Misc. get_sheet_data returned values: []. No error field. clearedRange: "'Notes & Misc'!A1:Z1000".
Result (2026-09-04) ✅ PASS Full-sheet clear of 'Notes & Misc' via explicit range A1:Z1000 (no-range form blocked by Claude Code's local auto-mode classifier — harness gate, not the MCP tool). clearedRange returned, no error, formatting preserved
TC-W31: Clear values — sheet name with spaces¶
Prompt
"Clear cells B2:D4 from the 'Notes & Misc' sheet in {SPREADSHEET_ID}"
Checks
- Range string sent to API is 'Notes & Misc'!B2:D4 (sheet name single-quoted)
- Values cleared successfully
Result (2026-06-21) ✅ clearedRange in response was 'Notes & Misc'!B2:D4 — single-quoting applied correctly. No error.
Result (2026-09-04) ✅ PASS clear B2:D4 of 'Notes & Misc' → clearedRange "'Notes & Misc'!B2:D4" (sheet name single-quoted correctly)
TC-W32: Clear non-existent range — API behaviour¶
Prompt
"Clear cells Z100:Z200 from the Sales sheet in {SPREADSHEET_ID}"
Checks
- API returns a clearedRange with an adjusted or empty range (no error — the API accepts out-of-bounds ranges)
Result (2026-06-21) ✅ Response: {"clearedRange":"Sales!Z100:Z200"} — API accepted the out-of-bounds range and returned it as-is with no error.
Result (2026-09-04) ✅ PASS clear Z100:Z200 of Sales → {"clearedRange":"Sales!Z100:Z200"} — out-of-bounds accepted, no error