Problem
There is no regression test that asserts structure preservation across the mutating-command surface. The existing live harness (scripts/live-tests/{docs,sheets,slides}.sh, gated by GOG_LIVE=1) exercises that commands run, but it does not seed one rich fixture and assert that each edit leaves untargeted structure intact. As a result, structure-loss bugs (e.g. #838 markdown block replacement dropping bullets/heading; #839 insert-image --at deleting the anchor) are found reactively in real use, not by CI.
A focused QA pass over the current surface (v0.28.1-dev, 21ca030d) found the surface is mostly sound, but the two known data-loss bugs are real, and nothing guards against regressions or the next instance of the class.
Proposal: a structure-preservation regression module
Add scripts/live-tests/structure.sh (sourced and run by live-test.sh, skip key structure) that:
- Seeds one rich fixture per surface, then snapshots
… raw --json as BEFORE.
- Docs: H1 + H2 headings; a paragraph with bold / italic / link runs; a bulleted list (3 items); a numbered list (3 items); a 2x2 table; an inline image; a Person chip.
- Slides: a slide with a title shape (multi-word, one styled sub-range + one linked sub-range) and a body shape with 3 bulleted paragraphs.
- Sheets: a 3x3 range with a bold + background-coloured header row.
- Runs each mutating op against one target, then re-reads
raw --json as AFTER.
- Asserts the invariants below via the existing Python-
assert idiom (paragraph bullet.listId, paragraphStyle.namedStyleType, run textStyle.{bold,italic,underline,link}, Person chip, inline image object, table cell text + cell style, sheet cell value + textFormat/backgroundColor), AND asserts adjacent/untargeted content + document order are unchanged (diff the BEFORE/AFTER element list outside the target range).
Each op gets a run_required/PASS-FAIL line. Known-bug ops (#838 block-replacement, #839 insert-image --at) seed run_optional (or xfail) entries so the suite documents them and flips to PASS when fixed.
Coverage matrix (command x invariant) — from the live QA pass
Legend: PASS = structure preserved; FAIL = data loss; — = n/a.
| Command |
bullet/listId |
heading namedStyle |
run textStyle |
Person chip |
inline image |
table cells/style |
adjacent/order |
Result |
| docs find-replace (plain) |
PASS |
PASS |
PASS |
— |
— |
— |
PASS |
PASS (native ReplaceAllText) |
| docs find-replace --format markdown, inline repl |
PASS |
PASS |
PASS |
— |
— |
— |
PASS |
PASS (guarded by inlineReplacement) |
| docs find-replace --format markdown, block/multi-para repl |
FAIL |
FAIL |
n/a |
— |
— |
— |
PASS |
FAIL → #838 |
| docs sed |
PASS |
PASS |
PASS |
— |
— |
— |
PASS |
PASS |
| docs format (bold/link/code) |
PASS |
PASS |
PASS |
— |
— |
— |
PASS |
PASS |
| docs format --heading-level on a list item |
keeps bullet |
sets heading |
PASS |
— |
— |
— |
PASS |
borderline (heading+bullet coexist) |
| docs insert / update --at |
PASS |
PASS |
PASS |
— |
— |
— |
PASS |
PASS (inserts at match start) |
| docs cell-update --format markdown |
— |
— |
PASS (cell) |
— |
— |
PASS (siblings intact) |
PASS |
PASS |
| docs insert-table / table-row insert |
— |
— |
— |
— |
— |
PASS (existing cells survive) |
PASS |
PASS |
| docs insert-image --at |
— |
— |
— |
— |
inserts image |
— |
deletes anchor text |
FAIL → #839 |
| docs insert-person --at |
— |
— |
— |
inserts chip |
— |
— |
deletes anchor (documented) |
by-design |
| slides style-text / link / bullets (range) |
PASS |
— |
PASS (neighbours intact) |
— |
— |
— |
PASS |
PASS |
| slides insert-text |
— |
— |
PASS |
— |
— |
— |
PASS |
PASS |
| slides replace-text (scoped) |
— |
— |
run-collapse on exact-match run* |
— |
— |
— |
PASS (other runs/links intact) |
caveat* |
| sheets update / find-replace |
— |
— |
header textFormat intact |
— |
— |
— |
PASS |
PASS |
| sheets delete-dimension (--force) |
— |
— |
header format intact |
— |
— |
preserves tables |
PASS |
PASS |
| sheets merge / unmerge |
— |
— |
— |
— |
— |
— |
PASS (A1 intact) |
PASS |
* slides replace-text uses the native Slides ReplaceAllText API; when the match exactly covers a styled sub-run the replacement inherits the surrounding run style (Google API behaviour, not gog deleting structure). Worth a doc note, not a gog data-loss defect.
Root-cause map
Why this is the right safety net
The harness slots into the existing GOG_LIVE integration structure (one new sourced module + one skip key), reuses the raw --json + Python-assert idiom already used in docs.sh, and turns "we discover data loss in production" into "CI fails the moment a mutating op drops a structural property." It is the smallest addition that covers the whole class rather than one instance.
A skeleton module is in a draft PR (linked below).
Problem
There is no regression test that asserts structure preservation across the mutating-command surface. The existing live harness (
scripts/live-tests/{docs,sheets,slides}.sh, gated byGOG_LIVE=1) exercises that commands run, but it does not seed one rich fixture and assert that each edit leaves untargeted structure intact. As a result, structure-loss bugs (e.g. #838 markdown block replacement dropping bullets/heading; #839insert-image --atdeleting the anchor) are found reactively in real use, not by CI.A focused QA pass over the current surface (
v0.28.1-dev,21ca030d) found the surface is mostly sound, but the two known data-loss bugs are real, and nothing guards against regressions or the next instance of the class.Proposal: a structure-preservation regression module
Add
scripts/live-tests/structure.sh(sourced and run bylive-test.sh, skip keystructure) that:… raw --jsonas BEFORE.raw --jsonas AFTER.assertidiom (paragraphbullet.listId,paragraphStyle.namedStyleType, runtextStyle.{bold,italic,underline,link}, Person chip, inline image object, table cell text + cell style, sheet cell value +textFormat/backgroundColor), AND asserts adjacent/untargeted content + document order are unchanged (diff the BEFORE/AFTER element list outside the target range).Each op gets a
run_required/PASS-FAIL line. Known-bug ops (#838 block-replacement, #839insert-image --at) seedrun_optional(or xfail) entries so the suite documents them and flips to PASS when fixed.Coverage matrix (command x invariant) — from the live QA pass
Legend: PASS = structure preserved; FAIL = data loss; — = n/a.
*
slides replace-textuses the native SlidesReplaceAllTextAPI; when the match exactly covers a styled sub-run the replacement inherits the surrounding run style (Google API behaviour, not gog deleting structure). Worth a doc note, not a gog data-loss defect.Root-cause map
replacePreparedDocsMarkdownRange(internal/cmd/docs_mutation.go): in the!inlineReplacementbranch it callsresetDocsParagraphRequests(DeleteParagraphBullets+ forceNORMAL_TEXT+ zero indent) andresetDocsTextStyleRequest, wiping the matched paragraph's bullet/heading/run style. TheinlineReplacementguard (fix(docs): preserve inline replacement paragraphs #740) already protects single inline replacements.buildInsertRequests(internal/cmd/docs_insert_image.go): unconditionalDeleteContentRangeover the resolved anchor beforeInsertInlineImage.Why this is the right safety net
The harness slots into the existing
GOG_LIVEintegration structure (one new sourced module + one skip key), reuses theraw --json+ Python-assertidiom already used indocs.sh, and turns "we discover data loss in production" into "CI fails the moment a mutating op drops a structural property." It is the smallest addition that covers the whole class rather than one instance.A skeleton module is in a draft PR (linked below).