Skip to content

feat(doc): add docs +batch-update for multi-operation sequential updates#623

Open
herbertliu wants to merge 2 commits intomainfrom
feat/docs-batch-update
Open

feat(doc): add docs +batch-update for multi-operation sequential updates#623
herbertliu wants to merge 2 commits intomainfrom
feat/docs-batch-update

Conversation

@herbertliu
Copy link
Copy Markdown
Collaborator

@herbertliu herbertliu commented Apr 23, 2026

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

  • `shortcuts/doc/docs_batch_update.go` (new): `DocsBatchUpdate` shortcut; `batchUpdateOp` / `batchUpdateResult` types; `parseBatchUpdateOps` / `validateBatchUpdateOp` / `buildBatchUpdateArgs` helpers.
  • `shortcuts/doc/shortcuts.go`: register the new shortcut.
  • `shortcuts/doc/docs_batch_update_test.go` (new): parse (8 cases) + validate (9 cases) + build-args (3 subtests) coverage.

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:

  • `--on-error=stop` (default): halt at the first failure; the response still reports how many ops landed.
  • `--on-error=continue`: best-effort; every op runs, each entry in `results[]` has its own `success`/`error`.

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

  • `go test ./shortcuts/doc/...` passes
  • `go vet ./...` clean
  • `gofmt -l` clean
  • Coverage: parse/validate/build 90-100%
  • Manual smoke: run a 3-op batch against a test doc and verify `results[]` shape + partial-failure path

Summary by CodeRabbit

  • New Features

    • Added a batch document update command to apply multiple update operations to a single document in one request.
    • Supports dry-run and returns aggregated metadata (total, applied, stopped_early, results).
  • Bug Fixes / Behavior

    • Configurable error handling: stop-on-first-error or continue-on-error with partial results.
  • Tests

    • Added comprehensive tests covering parsing, validation, dry-run output, and execution behaviors.

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.
@github-actions github-actions Bot added domain/ccm PR touches the ccm domain size/M Single-domain feat or fix with limited business impact labels Apr 23, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Introduces a new docs +batch-update shortcut that processes multiple update operations on a single document. The shortcut validates operations upfront, executes them sequentially via the MCP update-doc tool, collects per-operation results, and supports error handling modes (stop/continue) plus dry-run.

Changes

Cohort / File(s) Summary
Batch-update Shortcut Implementation
shortcuts/doc/docs_batch_update.go
Adds DocsBatchUpdate shortcut with flags --doc, --operations, --on-error. Validates JSON array of operations, checks mode/field/selection constraints, emits per-op advisory warnings, executes each op via MCP update-doc, supports DryRun, aggregates results and metadata (doc, total, applied, stopped_early).
Batch-update Test Suite
shortcuts/doc/docs_batch_update_test.go
Adds comprehensive tests: parsing/validation of --operations JSON, per-operation semantic validation (mode and markdown requirements, mutually exclusive selections, heading prefix), argument building behavior, dry-run output, and mocked MCP execution tests for full success and stop-on-first-failure scenarios.
Shortcuts Registry
shortcuts/doc/shortcuts.go
Registers DocsBatchUpdate in Shortcuts() so the new shortcut is exposed by the CLI.

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)
Loading

(Note: colored rectangles not used as diagram is simple sequence flow.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • fangshuyu-768
  • zgz2048

Poem

🐇 Hoppity-hop, a JSON parade,
Ops lined up ready, no detail delayed.
Validate each one, then call out the change,
Aggregate results, in order and range.
Dry-run or commit — the rabbit applauds your batch brigade! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding a new docs +batch-update shortcut for multi-operation sequential updates, which is the primary change across all modified files.
Description check ✅ Passed The description includes all required sections from the template with substantial detail: a clear summary addressing the problem (Case 10), comprehensive change list, explicit test results, and related context about non-goals and validation semantics.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/docs-batch-update

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 91.15044% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.15%. Comparing base (81d22c6) to head (5d23894).

Files with missing lines Patch % Lines
shortcuts/doc/docs_batch_update.go 91.96% 5 Missing and 4 partials ⚠️
shortcuts/doc/shortcuts.go 0.00% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@5d23894b0e6dd79e05023d89b486261b820116ed

🧩 Skill update

npx skills add larksuite/cli#feat/docs-batch-update -y -g

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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 after TrimSpace, but if the input has a UTF‑8 BOM (\uFEFF) at the start — common when users --operations @file.json`` with an editor-saved file — TrimSpace does 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: Execute re-parses --operations but skips per-op validation.

Validate runs parseBatchUpdateOps + validateBatchUpdateOp for every op, and Execute re-parses here (line 105) but does not re-run validateBatchUpdateOp. For the normal CLI path this is fine because Validate gates Execute, but:

  • It's a latent footgun for any future caller that invokes Execute without going through Validate (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 validateBatchUpdateOp in the Execute loop 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

📥 Commits

Reviewing files that changed from the base of the PR and between 81d22c6 and 6e89797.

📒 Files selected for processing (3)
  • shortcuts/doc/docs_batch_update.go
  • shortcuts/doc/docs_batch_update_test.go
  • shortcuts/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.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6e89797 and 5d23894.

📒 Files selected for processing (1)
  • shortcuts/doc/docs_batch_update_test.go

Comment on lines +394 to +441
// 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")
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/ccm PR touches the ccm domain size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant