mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Compare commits
11 Commits
feat/lark-
...
feat-undo-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f381aa439 | ||
|
|
251635ec1e | ||
|
|
4936a983bf | ||
|
|
886cca6032 | ||
|
|
b64018a672 | ||
|
|
1996b67451 | ||
|
|
c1ee8613e4 | ||
|
|
41e6acba11 | ||
|
|
a042942f7e | ||
|
|
66c16758ec | ||
|
|
b42db647ff |
@@ -1,4 +1,45 @@
|
||||
{
|
||||
"+undo": {
|
||||
"risk": "write",
|
||||
"flags": [
|
||||
{
|
||||
"name": "url",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Spreadsheet URL (XOR with `--spreadsheet-token`)"
|
||||
},
|
||||
{
|
||||
"name": "spreadsheet-token",
|
||||
"kind": "public",
|
||||
"type": "string",
|
||||
"required": "xor",
|
||||
"desc": "Spreadsheet token (XOR with `--url`)"
|
||||
},
|
||||
{
|
||||
"name": "steps",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "Undo the most recent N edits made through this CLI link (default 1); one step = one prior write call",
|
||||
"default": "1"
|
||||
},
|
||||
{
|
||||
"name": "rev",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "Undo anchor: the document revision returned by a prior write's response (`data.revision`). Omit to undo the latest edit. Doubles as an optimistic-concurrency check — rejected if the document has moved past this revision"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"+workbook-info": {
|
||||
"risk": "read",
|
||||
"flags": [
|
||||
|
||||
@@ -936,6 +936,16 @@ var flagDefs = map[string]commandDef{
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
"+undo": {
|
||||
Risk: "write",
|
||||
Flags: []flagDef{
|
||||
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL (XOR with `--spreadsheet-token`)"},
|
||||
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
|
||||
{Name: "steps", Kind: "own", Type: "int", Required: "optional", Desc: "Undo the most recent N edits made through this CLI link (default 1); one step = one prior write call", Default: "1"},
|
||||
{Name: "rev", Kind: "own", Type: "int", Required: "optional", Desc: "Undo anchor: the document revision returned by a prior write's response (`data.revision`). Omit to undo the latest edit. Doubles as an optimistic-concurrency check — rejected if the document has moved past this revision"},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
"+workbook-create": {
|
||||
Risk: "write",
|
||||
Flags: []flagDef{
|
||||
|
||||
@@ -688,7 +688,7 @@ func newFloatImageWriteShortcut(command, description, op string, withIDFlag, isH
|
||||
// With a local --image, Execute first uploads the file; surface that
|
||||
// extra step in the preview (mirrors +cells-set-image's dry-run).
|
||||
if img := strings.TrimSpace(runtime.Str("image")); img != "" {
|
||||
manageBody, _ := buildToolBody("manage_float_image_object", input)
|
||||
manageBody, _ := buildToolBody(ToolKindWrite, "manage_float_image_object", input)
|
||||
return common.NewDryRunAPI().
|
||||
POST("/open-apis/drive/v1/medias/upload_all").
|
||||
Desc("upload local image to drive (parent_type=sheet_image)").
|
||||
|
||||
@@ -740,7 +740,7 @@ func tablePutDryRun(runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
if s.AllowOverwrite != nil && !*s.AllowOverwrite {
|
||||
input["allow_overwrite"] = false
|
||||
}
|
||||
wireBody, _ := buildToolBody("set_cell_range", input)
|
||||
wireBody, _ := buildToolBody(ToolKindWrite, "set_cell_range", input)
|
||||
dry.POST(toolInvokePath(token, ToolKindWrite)).Desc(desc).Body(wireBody)
|
||||
}
|
||||
return dry
|
||||
@@ -777,14 +777,14 @@ var TableGet = common.Shortcut{
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
dry := common.NewDryRunAPI()
|
||||
if strings.TrimSpace(runtime.Str("sheet-id")) == "" && strings.TrimSpace(runtime.Str("sheet-name")) == "" {
|
||||
body, _ := buildToolBody("get_workbook_structure", map[string]interface{}{"excel_id": token})
|
||||
body, _ := buildToolBody(ToolKindRead, "get_workbook_structure", map[string]interface{}{"excel_id": token})
|
||||
dry.POST(toolInvokePath(token, ToolKindRead)).Desc("list sub-sheets via get_workbook_structure").Body(body)
|
||||
}
|
||||
rng := strings.TrimSpace(runtime.Str("range"))
|
||||
if rng == "" {
|
||||
rng = "<each sheet's current region>"
|
||||
}
|
||||
body, _ := buildToolBody("get_cell_ranges", map[string]interface{}{
|
||||
body, _ := buildToolBody(ToolKindRead, "get_cell_ranges", map[string]interface{}{
|
||||
"excel_id": token, "ranges": []string{rng},
|
||||
"include_styles": true, "value_render_option": "raw_value",
|
||||
})
|
||||
|
||||
108
shortcuts/sheets/lark_sheet_undo.go
Normal file
108
shortcuts/sheets/lark_sheet_undo.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// ─── lark_sheet_undo ──────────────────────────────────────────────────
|
||||
//
|
||||
// Wraps:
|
||||
// - undo_last (write) — powers +undo
|
||||
//
|
||||
// Reverses the most recent edits this CLI link made to a spreadsheet, addressed
|
||||
// by document revision. Every write response carries `data.revision`; that
|
||||
// number is the undo anchor. The backend records an inverse changeset for every
|
||||
// write and indexes it by the revision it produced (see the undo design doc,
|
||||
// "方案 A · rev 寻址"); +undo asks the backend executor to locate that inverse
|
||||
// data through the revision pointer, verify nobody else changed the document
|
||||
// since (tip / continuity / object-version / identity checks), re-apply it in
|
||||
// reverse order on the node Workbook, and push the result upstream as a
|
||||
// collaboration change. The CLI only triggers the tool — the read-back endpoint
|
||||
// is space-internal and not reachable through the /open-apis gateway, so all
|
||||
// the heavy lifting stays server-side.
|
||||
//
|
||||
// +undo carries no sheet selector: undo is scoped to the spreadsheet + this
|
||||
// link's edit history, not a single sub-sheet. Selection:
|
||||
// - (no flags) : undo the latest edit, if it was made by this caller
|
||||
// - --rev N : undo anchored at revision N (from a prior write response);
|
||||
// rejected when the document has moved past N
|
||||
// - --steps N : undo the last N edits in one atomic call (default 1)
|
||||
|
||||
// Undo wraps undo_last: reverse the most recent edits made through this CLI
|
||||
// link, anchored by the revision a prior write returned (--rev), defaulting
|
||||
// to the latest edit.
|
||||
var Undo = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+undo",
|
||||
Description: "Undo the most recent edits this CLI link made to a spreadsheet (anchored by a write's returned revision).",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: flagsFor("+undo"),
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
token, err := resolveSpreadsheetToken(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = undoInput(runtime, token)
|
||||
return err
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
input, _ := undoInput(runtime, token)
|
||||
return invokeToolDryRun(token, ToolKindWrite, "undo_last", input)
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
token, err := resolveSpreadsheetToken(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
input, err := undoInput(runtime, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := callTool(ctx, runtime, token, ToolKindWrite, "undo_last", input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
},
|
||||
Tips: []string{
|
||||
"Every write response carries data.revision — remember it; +undo --rev <that> undoes exactly that edit, and +recover --to-revision <that-1> is the full-rollback fallback.",
|
||||
"Without --rev, +undo targets the document's latest edit — it succeeds only when that edit was made through this CLI link by you.",
|
||||
"Repeated +undo steps back one edit at a time; --steps N undoes the last N edits in one atomic call. Already-undone edits are skipped automatically.",
|
||||
"If anyone else edited the document after (or between) the edits you want to undo, +undo refuses entirely and suggests +recover — it never partially undoes or overwrites others' changes.",
|
||||
"A success response with undone:0 plus warning_message means nothing was actually undone — the targeted revision wasn't produced by this caller, or was already undone.",
|
||||
"Use --dry-run to preview the request before running it.",
|
||||
},
|
||||
}
|
||||
|
||||
// undoInput builds the undo_last tool body. --rev anchors the undo at the
|
||||
// revision a prior write returned (omitted = latest); --steps selects how many
|
||||
// edits to reverse in one atomic call. Network-free; shared by Validate,
|
||||
// DryRun, and Execute.
|
||||
func undoInput(runtime flagView, token string) (map[string]interface{}, error) {
|
||||
input := map[string]interface{}{"excel_id": token}
|
||||
|
||||
if runtime.Changed("rev") {
|
||||
rev := runtime.Int("rev")
|
||||
if rev < 1 {
|
||||
return nil, common.FlagErrorf("--rev must be a positive revision number (from a prior write's data.revision)")
|
||||
}
|
||||
input["rev"] = rev
|
||||
}
|
||||
|
||||
steps := runtime.Int("steps")
|
||||
if steps < 1 {
|
||||
return nil, common.FlagErrorf("--steps must be >= 1")
|
||||
}
|
||||
input["steps"] = steps
|
||||
return input, nil
|
||||
}
|
||||
107
shortcuts/sheets/lark_sheet_undo_test.go
Normal file
107
shortcuts/sheets/lark_sheet_undo_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestUndo_DryRun asserts the undo_last body for the three selection shapes:
|
||||
// default (latest, steps=1), explicit --steps, and a --rev anchor. Numbers
|
||||
// round-trip through the wire JSON as float64, matching the other dry-run
|
||||
// body tests.
|
||||
func TestUndo_DryRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantInput map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "default undoes the latest edit",
|
||||
args: []string{"--url", testURL},
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
"steps": float64(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "explicit --steps",
|
||||
args: []string{"--url", testURL, "--steps", "3"},
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
"steps": float64(3),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "--rev anchors at a write's returned revision",
|
||||
args: []string{"--spreadsheet-token", testToken, "--rev", "123"},
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
"rev": float64(123),
|
||||
"steps": float64(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "--rev composes with --steps",
|
||||
args: []string{"--url", testURL, "--rev", "123", "--steps", "2"},
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
"rev": float64(123),
|
||||
"steps": float64(2),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, Undo, tt.args)
|
||||
got := decodeToolInput(t, body, "undo_last")
|
||||
assertInputEquals(t, got, tt.wantInput)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUndo_Validation covers the XOR token check, the --rev lower bound, and
|
||||
// the --steps lower bound.
|
||||
func TestUndo_Validation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantMsg string
|
||||
}{
|
||||
{
|
||||
name: "needs --url or --spreadsheet-token",
|
||||
args: []string{},
|
||||
wantMsg: "at least one of --url or --spreadsheet-token",
|
||||
},
|
||||
{
|
||||
name: "--rev must be positive",
|
||||
args: []string{"--url", testURL, "--rev", "0"},
|
||||
wantMsg: "--rev must be a positive revision number",
|
||||
},
|
||||
{
|
||||
name: "--steps must be >= 1",
|
||||
args: []string{"--url", testURL, "--steps", "0"},
|
||||
wantMsg: "--steps must be >= 1",
|
||||
},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
stdout, stderr, err := runShortcutCapturingErr(t, Undo, append(tt.args, "--dry-run"))
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error; got nil. stdout=%s stderr=%s", stdout, stderr)
|
||||
}
|
||||
combined := stdout + stderr + err.Error()
|
||||
if !strings.Contains(combined, tt.wantMsg) {
|
||||
t.Errorf("error message missing %q; got=%s", tt.wantMsg, combined)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -634,7 +634,7 @@ var WorkbookCreate = common.Shortcut{
|
||||
"range": tablePutFullRange(s, len(matrix)),
|
||||
"cells": matrix,
|
||||
}
|
||||
wireBody, _ := buildToolBody("set_cell_range", input)
|
||||
wireBody, _ := buildToolBody(ToolKindWrite, "set_cell_range", input)
|
||||
dry.POST("/open-apis/sheet_ai/v2/spreadsheets/<new-token>/tools/invoke_write").
|
||||
Desc(fmt.Sprintf("write typed sheet %q (%d data rows × %d cols) via set_cell_range", s.Name, len(s.Rows), len(s.Columns))).
|
||||
Body(wireBody)
|
||||
@@ -645,7 +645,7 @@ var WorkbookCreate = common.Shortcut{
|
||||
if fill, _ := buildInitialFillInput(runtime); fill != nil {
|
||||
fill["excel_id"] = "<new-token>"
|
||||
fill["sheet_id"] = "<first-sheet-id>" // resolved from the workbook at execute time
|
||||
wireBody, _ := buildToolBody("set_cell_range", fill)
|
||||
wireBody, _ := buildToolBody(ToolKindWrite, "set_cell_range", fill)
|
||||
dry.POST("/open-apis/sheet_ai/v2/spreadsheets/<new-token>/tools/invoke_write").
|
||||
Desc("fill headers + data via set_cell_range (sheet_id resolved after create)").
|
||||
Body(wireBody)
|
||||
|
||||
@@ -727,7 +727,7 @@ var CellsSetImage = common.Shortcut{
|
||||
if fileName == "" {
|
||||
fileName = filepath.Base(imgPath)
|
||||
}
|
||||
setCellBody, _ := buildToolBody("set_cell_range", map[string]interface{}{
|
||||
setCellBody, _ := buildToolBody(ToolKindWrite, "set_cell_range", map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"range": strings.TrimSpace(runtime.Str("range")),
|
||||
"sheet_id": sheetSelectorPlaceholder(sheetID, sheetName),
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/util"
|
||||
@@ -14,6 +16,26 @@ import (
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// sheetTxnIDEnv is the env var carrying a caller-provided, session-stable
|
||||
// transaction id for sheet tool calls.
|
||||
const sheetTxnIDEnv = "LARK_CLI_SHEET_TRANSACTION_ID"
|
||||
|
||||
// sheetTransactionID returns the optional per-session transaction id from the
|
||||
// environment, or "" when unset.
|
||||
//
|
||||
// NOTE: +undo does NOT use this id to locate edits — the server addresses undo
|
||||
// by document revision (the `rev` a write returns; see +undo --rev), not by
|
||||
// transaction id. This env var's only purpose is optional concurrency
|
||||
// isolation: write tools persist their reverse ("undo") changeset keyed by the
|
||||
// request's transaction id, and the server mints a fresh uuid per request when
|
||||
// none is supplied, so each invocation lands in its own undo stack by default.
|
||||
// Set a stable id across commands only to deliberately share one isolated undo
|
||||
// stack across a group of edits; empty preserves the per-request default and is
|
||||
// the norm.
|
||||
func sheetTransactionID() string {
|
||||
return strings.TrimSpace(os.Getenv(sheetTxnIDEnv))
|
||||
}
|
||||
|
||||
// ToolKind selects the One-OpenAPI endpoint and its rate-limit bucket.
|
||||
//
|
||||
// - ToolKindRead → POST .../tools/invoke_read (scope sheets:spreadsheet:read, 10 qps)
|
||||
@@ -39,15 +61,27 @@ func toolInvokePath(token string, kind ToolKind) string {
|
||||
// buildToolBody constructs the One-OpenAPI request body for a tool invocation.
|
||||
// `input` is serialized to a JSON string per the API contract; callers pass
|
||||
// a typed Go map and never need to handle JSON encoding themselves.
|
||||
func buildToolBody(toolName string, input map[string]interface{}) (map[string]interface{}, error) {
|
||||
func buildToolBody(kind ToolKind, toolName string, input map[string]interface{}) (map[string]interface{}, error) {
|
||||
inputJSON, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode tool input: %w", err)
|
||||
}
|
||||
return map[string]interface{}{
|
||||
body := map[string]interface{}{
|
||||
"tool_name": toolName,
|
||||
"input": string(inputJSON),
|
||||
}, nil
|
||||
}
|
||||
// Thread a session-stable transaction id (when provided) so a group of
|
||||
// edits and a later +undo share one undo stack. Omitted when unset, leaving
|
||||
// the server to mint a per-request id as before. Only write tools join the
|
||||
// undo transaction; reads must never carry it — a read scoped to a
|
||||
// transaction id resolves against that transaction's (often empty) snapshot
|
||||
// instead of the live document, so it would read back blank.
|
||||
if kind == ToolKindWrite {
|
||||
if txID := sheetTransactionID(); txID != "" {
|
||||
body["extra"] = map[string]interface{}{"transaction_id": txID}
|
||||
}
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// callTool invokes a sheet-ai tool via the One-OpenAPI endpoint and decodes
|
||||
@@ -65,7 +99,7 @@ func callTool(
|
||||
toolName string,
|
||||
input map[string]interface{},
|
||||
) (interface{}, error) {
|
||||
body, err := buildToolBody(toolName, input)
|
||||
body, err := buildToolBody(kind, toolName, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -109,7 +143,7 @@ func invokeToolDryRun(
|
||||
toolName string,
|
||||
input map[string]interface{},
|
||||
) *common.DryRunAPI {
|
||||
wireBody, _ := buildToolBody(toolName, input)
|
||||
wireBody, _ := buildToolBody(kind, toolName, input)
|
||||
return common.NewDryRunAPI().
|
||||
POST(toolInvokePath(token, kind)).
|
||||
Body(wireBody).
|
||||
|
||||
57
shortcuts/sheets/sheet_ai_api_test.go
Normal file
57
shortcuts/sheets/sheet_ai_api_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
import "testing"
|
||||
|
||||
// cellsSetArgs is a minimal valid +cells-set invocation used to inspect the
|
||||
// tool-call request body.
|
||||
func cellsSetArgs() []string {
|
||||
return []string{
|
||||
"--spreadsheet-token", testToken,
|
||||
"--sheet-id", testSheetID,
|
||||
"--range", "A1",
|
||||
"--cells", `[[{"value":"x"}]]`,
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildToolBody_ThreadsTransactionID verifies that a session-stable
|
||||
// transaction id from the environment is threaded into the request body's
|
||||
// extra.transaction_id, so a group of edits and a later +undo share one undo
|
||||
// stack.
|
||||
func TestBuildToolBody_ThreadsTransactionID(t *testing.T) {
|
||||
t.Setenv(sheetTxnIDEnv, "tx_test_123")
|
||||
body := parseDryRunBody(t, CellsSet, cellsSetArgs())
|
||||
extra, ok := body["extra"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("extra missing from body: %#v", body)
|
||||
}
|
||||
if extra["transaction_id"] != "tx_test_123" {
|
||||
t.Errorf("transaction_id = %#v, want tx_test_123", extra["transaction_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildToolBody_OmitsTransactionIDWhenUnset verifies the body carries no
|
||||
// extra when the env var is empty, preserving the per-request default.
|
||||
func TestBuildToolBody_OmitsTransactionIDWhenUnset(t *testing.T) {
|
||||
t.Setenv(sheetTxnIDEnv, "")
|
||||
body := parseDryRunBody(t, CellsSet, cellsSetArgs())
|
||||
if _, ok := body["extra"]; ok {
|
||||
t.Errorf("extra should be absent when %s is unset: %#v", sheetTxnIDEnv, body)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildToolBody_OmitsTransactionIDForReads verifies that read tools never
|
||||
// carry a transaction id even when one is set: a read scoped to a transaction
|
||||
// resolves against that transaction's snapshot (often empty) instead of the
|
||||
// live document, so threading it would make reads return blank cells.
|
||||
func TestBuildToolBody_OmitsTransactionIDForReads(t *testing.T) {
|
||||
t.Setenv(sheetTxnIDEnv, "tx_test_123")
|
||||
body := parseDryRunBody(t, CellsGet, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID, "--range", "A1",
|
||||
})
|
||||
if _, ok := body["extra"]; ok {
|
||||
t.Errorf("read tool must not carry extra.transaction_id: %#v", body)
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,9 @@ func shortcutList() []common.Shortcut {
|
||||
DropdownGet,
|
||||
TableGet,
|
||||
|
||||
// lark_sheet_undo
|
||||
Undo,
|
||||
|
||||
// lark_sheet_search_replace
|
||||
CellsSearch,
|
||||
CellsReplace,
|
||||
|
||||
Reference in New Issue
Block a user