feat(doc): add docs +batch-update for multi-operation sequential updates#623
feat(doc): add docs +batch-update for multi-operation sequential updates#623herbertliu wants to merge 2 commits intomainfrom
Conversation
Agents editing a document often need to apply several related changes (rename a heading, update intro paragraph, insert a new section). Doing this today means N independent 'docs +update' invocations, each with its own MCP round-trip, partial-validation scope, and inconsistent reporting. Case 10 in the pitfall list tracks this as the 'no batch semantics' gap. Add a docs +batch-update shortcut that accepts a JSON array of update operations and applies them sequentially against a single document. The shape of each operation mirrors the flags of docs +update (mode, markdown, selection_with_ellipsis, selection_by_title, new_title), so converting an existing workflow is mechanical. Explicit non-goal: atomicity. There is no server-side transaction for update-doc, so a mid-batch failure leaves the document in a partial- apply state. The shortcut is honest about that in its description and supports --on-error=stop (default) to halt at the first failure plus --on-error=continue for best-effort runs. The response always reports per-op success/failure and how many ops landed before any halt, so callers can pair with docs +fetch to reconcile state. Validate runs the single-op rule set across every op before any MCP call, so a malformed entry fails the whole invocation up front rather than after N successful ops. Dry-run prints the update-doc argument map for each op. Coverage: 8-case parse test (object/scalar/empty/malformed rejections plus single and multi-op success), 9-case validate test (mode / markdown / selection combinations), and a buildBatchUpdateArgs test verifying optional fields are omitted when empty.
📝 WalkthroughWalkthroughIntroduces a new Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as "cli shortcut\nDocsBatchUpdate"
participant MCP as "MCP /update-doc"
participant DocStore as "Document store"
User->>CLI: run docs +batch-update --doc <id> --operations <JSON> --on-error=<mode>
CLI->>CLI: parse & validate operations array
CLI->>MCP: for each op -> call /update-doc (or build plan if DryRun)
MCP->>DocStore: apply update (internal)
DocStore-->>MCP: response (success/error + payload)
MCP-->>CLI: response (normalized result)
CLI-->>User: aggregated metadata + per-op results (stopped_early flag if applicable)
(Note: colored rectangles not used as diagram is simple sequence flow.) Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #623 +/- ##
==========================================
+ Coverage 60.20% 62.15% +1.95%
==========================================
Files 407 408 +1
Lines 43340 35912 -7428
==========================================
- Hits 26091 22320 -3771
+ Misses 15221 11560 -3661
- Partials 2028 2032 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
🚀 PR Preview Install Guide🧰 CLI updatenpm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@5d23894b0e6dd79e05023d89b486261b820116ed🧩 Skill updatenpx skills add larksuite/cli#feat/docs-batch-update -y -g |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
shortcuts/doc/docs_batch_update.go (2)
185-201: Prefix check rejects valid JSON arrays with UTF‑8 BOM / leading whitespace inside the payload.
strings.HasPrefix(trimmed, "[")runs afterTrimSpace, but if the input has a UTF‑8 BOM (\uFEFF) at the start — common when users--operations@file.json`` with an editor-saved file —TrimSpacedoes not strip the BOM and the check reports "must be a JSON array" even though `json.Unmarshal` would accept the body. This is a minor UX rough edge given the `File` input mode on line 61.Either strip a leading BOM before the prefix check, or detect the array-vs-object case by inspecting the first non-space, non-BOM rune. Not a blocker.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/doc/docs_batch_update.go` around lines 185 - 201, The prefix check in parseBatchUpdateOps wrongly rejects valid JSON when the input begins with a UTF‑8 BOM; before testing strings.HasPrefix(trimmed, "["), strip any leading BOM (U+FEFF) or determine the first non-space, non‑BOM rune and use that for the array-vs-object check so valid JSON files pass; adjust the logic that prepares trimmed (and the subsequent json.Unmarshal fallback) to remove a leading BOM character if present so that ops parsing and the existing error paths remain intact.
104-146:Executere-parses--operationsbut skips per-op validation.
ValidaterunsparseBatchUpdateOps+validateBatchUpdateOpfor every op, andExecutere-parses here (line 105) but does not re-runvalidateBatchUpdateOp. For the normal CLI path this is fine becauseValidategatesExecute, but:
- It's a latent footgun for any future caller that invokes
Executewithout going throughValidate(tests, programmatic reuse, or a refactor that rewires the pipeline) — malformed ops would reach MCP unchecked.- You already pay the JSON unmarshal cost three times (
Validate,DryRun,Execute).Consider parsing+validating once and stashing the result on the runtime, or at minimum re-running
validateBatchUpdateOpin theExecuteloop for defense-in-depth.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/doc/docs_batch_update.go` around lines 104 - 146, Execute currently re-parses --operations via parseBatchUpdateOps but skips per-op validation (validateBatchUpdateOp), risking malformed ops if Execute is called without Validate and wasting repeated unmarshalling; fix by parsing once and storing the parsed/validated ops on the runtime context (e.g., attach to runtime with a stable key) in Validate/DryRun and have Execute read that cached slice, or if you prefer a minimal change, call validateBatchUpdateOp for each op inside Execute's loop before using it (referencing Execute, parseBatchUpdateOps, validateBatchUpdateOp, Validate and DryRun to locate the related code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@shortcuts/doc/docs_batch_update.go`:
- Around line 185-201: The prefix check in parseBatchUpdateOps wrongly rejects
valid JSON when the input begins with a UTF‑8 BOM; before testing
strings.HasPrefix(trimmed, "["), strip any leading BOM (U+FEFF) or determine the
first non-space, non‑BOM rune and use that for the array-vs-object check so
valid JSON files pass; adjust the logic that prepares trimmed (and the
subsequent json.Unmarshal fallback) to remove a leading BOM character if present
so that ops parsing and the existing error paths remain intact.
- Around line 104-146: Execute currently re-parses --operations via
parseBatchUpdateOps but skips per-op validation (validateBatchUpdateOp), risking
malformed ops if Execute is called without Validate and wasting repeated
unmarshalling; fix by parsing once and storing the parsed/validated ops on the
runtime context (e.g., attach to runtime with a stable key) in Validate/DryRun
and have Execute read that cached slice, or if you prefer a minimal change, call
validateBatchUpdateOp for each op inside Execute's loop before using it
(referencing Execute, parseBatchUpdateOps, validateBatchUpdateOp, Validate and
DryRun to locate the related code).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6d7433de-4443-48b2-8ad6-cba6785fc7a3
📒 Files selected for processing (3)
shortcuts/doc/docs_batch_update.goshortcuts/doc/docs_batch_update_test.goshortcuts/doc/shortcuts.go
…isfy codecov 60% patch gate Prior unit tests covered parse / validate / build-args only, leaving DryRun and Execute at 0% which dropped the codecov/patch gate on #623 to 35.5%. Add mount-and-run tests that exercise the remaining paths: - TestDocsBatchUpdateDryRun — 3-op dry-run: asserts op_count, per-op step desc ("[1/3] replace_range" etc.) and overall batch description - TestDocsBatchUpdateValidateRejectsMalformedOp — validates the per-op Validate loop rejects a replace_range op missing its selection before any MCP call - TestDocsBatchUpdateValidateRejectsInvalidOnError — rejects a bogus --on-error enum value up front - TestDocsBatchUpdateExecuteAllSuccess — mocks two /mcp stubs and asserts stdout envelope: total=2, applied=2, stopped_early=false, per-op success=true - TestDocsBatchUpdateStopsOnFirstFailure — only one MCP stub for a 2-op batch, so the second call trips 'no stub' in httpmock; asserts applied=1, stopped_early=true, and the partial-result envelope still gets emitted before the error returns Introduces registerUpdateDocMCPStubs(count, payload) helper because httpmock stubs match exactly once, so batch paths need one stub per expected call. Coverage on docs_batch_update.go rises from ~35% to ~76%, above the codecov/patch 60% threshold.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@shortcuts/doc/docs_batch_update_test.go`:
- Around line 394-441: Add a new test (e.g.,
TestDocsBatchUpdateContinuesOnError) alongside
TestDocsBatchUpdateStopsOnFirstFailure that uses registerUpdateDocMCPStubs and
runBatchUpdateShortcut to exercise the new --on-error=continue behavior: arrange
stubs so one operation fails and a later one succeeds, invoke
runBatchUpdateShortcut with the "--on-error=continue" flag, assert the command
does not abort remaining ops (check err is nil or expected non-fatal behavior),
unmarshal stdout into the same envelope shape used in
TestDocsBatchUpdateStopsOnFirstFailure and assert Data.Total equals number of
ops, Data.Applied equals number of successful ops, Data.StoppedEarly is false,
and additionally inspect the per-operation result entries in the output to
verify the failed op contains an error and the later op shows success; reuse
helpers registerUpdateDocMCPStubs and runBatchUpdateShortcut to set up stubs and
run the shortcut.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 23d82d57-4524-43f3-bd0b-1c3d90190e78
📒 Files selected for processing (1)
shortcuts/doc/docs_batch_update_test.go
| // TestDocsBatchUpdateStopsOnFirstFailure registers only one successful MCP | ||
| // stub for a 2-op batch, so the second call trips "no stub" in httpmock and | ||
| // surfaces as an MCP error. With --on-error=stop (the default), the batch | ||
| // must halt, report applied=1, and set stopped_early=true. | ||
| func TestDocsBatchUpdateStopsOnFirstFailure(t *testing.T) { | ||
| // Explicitly no t.Parallel(): this test ends with an unmatched stub on | ||
| // the failure path, and the parent TestFactory registry.Verify() will | ||
| // flag unused stubs across siblings if the parallel schedule bleeds. | ||
| f, stdout, _, reg := cmdutil.TestFactory(t, batchUpdateTestConfig()) | ||
| registerUpdateDocMCPStubs(reg, 1, map[string]interface{}{ | ||
| "success": true, | ||
| }) | ||
|
|
||
| ops := `[ | ||
| {"mode":"append","markdown":"first"}, | ||
| {"mode":"append","markdown":"second"} | ||
| ]` | ||
| err := runBatchUpdateShortcut(t, f, stdout, []string{ | ||
| "+batch-update", | ||
| "--doc", "DOC123", | ||
| "--operations", ops, | ||
| "--as", "bot", | ||
| }) | ||
| if err == nil { | ||
| t.Fatalf("expected error from second op failing, got nil") | ||
| } | ||
|
|
||
| // Even on error the shortcut prints its partial result envelope first. | ||
| var envelope struct { | ||
| Data struct { | ||
| Total int `json:"total"` | ||
| Applied int `json:"applied"` | ||
| StoppedEarly bool `json:"stopped_early"` | ||
| } `json:"data"` | ||
| } | ||
| if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil { | ||
| t.Fatalf("failed to parse partial result: %v\nstdout:\n%s", err, stdout.String()) | ||
| } | ||
| if envelope.Data.Total != 2 { | ||
| t.Errorf("expected total=2, got %d", envelope.Data.Total) | ||
| } | ||
| if envelope.Data.Applied != 1 { | ||
| t.Errorf("expected applied=1 before stop, got %d", envelope.Data.Applied) | ||
| } | ||
| if !envelope.Data.StoppedEarly { | ||
| t.Errorf("expected stopped_early=true, got false") | ||
| } | ||
| } |
There was a problem hiding this comment.
Add an execution test for --on-error=continue.
The suite covers default stop-on-error, but not the new continue mode. Please add a batch execution test where one operation fails, a later operation still runs, and the envelope reports the expected applied, stopped_early=false, and per-op success/error entries. As per coding guidelines, “Every behavior change must have an accompanying test.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shortcuts/doc/docs_batch_update_test.go` around lines 394 - 441, Add a new
test (e.g., TestDocsBatchUpdateContinuesOnError) alongside
TestDocsBatchUpdateStopsOnFirstFailure that uses registerUpdateDocMCPStubs and
runBatchUpdateShortcut to exercise the new --on-error=continue behavior: arrange
stubs so one operation fails and a later one succeeds, invoke
runBatchUpdateShortcut with the "--on-error=continue" flag, assert the command
does not abort remaining ops (check err is nil or expected non-fatal behavior),
unmarshal stdout into the same envelope shape used in
TestDocsBatchUpdateStopsOnFirstFailure and assert Data.Total equals number of
ops, Data.Applied equals number of successful ops, Data.StoppedEarly is false,
and additionally inspect the per-operation result entries in the output to
verify the failed op contains an error and the later op shows success; reuse
helpers registerUpdateDocMCPStubs and runBatchUpdateShortcut to set up stubs and
run the shortcut.
Summary
Addresses Case 10 in the lark-cli pitfall list: Agents applying several related edits to one document currently pay N MCP round-trips and get N separate `{success:true}` responses, with no unified error/recovery story. This PR adds a `docs +batch-update` shortcut that accepts a JSON array of operations and runs them sequentially against a single document.
Changes
Explicit non-goal: atomicity
There is no server-side transaction for `update-doc`. A mid-batch failure leaves the document in a partial-apply state. The shortcut description states this up front. Callers choose how to handle it:
Pair with `docs +fetch` after a partial run to reconcile state manually.
Shape
```bash
lark-cli docs +batch-update --doc --operations '[
{"mode":"replace_range","markdown":"New intro","selection_by_title":"## Intro"},
{"mode":"insert_before","markdown":"> Note: revised","selection_with_ellipsis":"## Intro"},
{"mode":"delete_range","selection_with_ellipsis":"stale section...end"}
]'
```
Response:
```json
{
"doc": "",
"total": 3,
"applied": 3,
"stopped_early": false,
"results": [
{"index":0, "mode":"replace_range", "success":true, "result":{...}},
...
]
}
```
Validation semantics
Single-op validation runs against every op before any MCP call, so a malformed entry fails the whole invocation up front rather than after N successful ops. This matches what batch-users expect from `kubectl apply`-style tooling.
Test Plan
Summary by CodeRabbit
New Features
Bug Fixes / Behavior
Tests