test(aitable): close shortcut coverage gaps

This commit is contained in:
Dennis committed 2026-08-07 12:01:36 +08:00
1 parent 201949aec1
commit 07ce090eeb
14 files changed
+1529 -25

No files matched your search

@@ -0,0 +1,68 @@
// Copyright 2026 Alibaba Group
// SPDX-License-Identifier: Apache-2.0
package helpers
import (
"encoding/json"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/paging"
)
func TestCrossPlatformCoverageAITablePageParsingRemainingEdges(t *testing.T) {
for name, raw := range map[string]string{
"trailing invalid JSON": `{"records":[]} {`,
"data not object": `{"data":[]}`,
} {
t.Run(name, func(t *testing.T) {
if _, err := parseRecordQueryPage(raw); err == nil {
t.Fatalf("parseRecordQueryPage(%q) succeeded", raw)
}
})
}
}
func TestCrossPlatformCoverageAITableOptionalCountNumericTypes(t *testing.T) {
cases := []struct {
name string
value any
want int
wantErr bool
}{
{name: "bad number", value: json.Number("1.5"), wantErr: true},
{name: "fractional float", value: float64(1.5), wantErr: true},
{name: "whole float", value: float64(2), want: 2},
{name: "int", value: int(3), want: 3},
{name: "int64", value: int64(4), want: 4},
{name: "wrong type", value: "5", wantErr: true},
{name: "negative", value: int64(-1), wantErr: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := parseOptionalNonNegativeInt(tc.value)
if tc.wantErr {
if err == nil {
t.Fatalf("value %#v succeeded: %#v", tc.value, got)
}
return
}
if err != nil || got == nil || *got != tc.want {
t.Fatalf("value %#v = %#v, %v", tc.value, got, err)
}
})
}
}
func TestCrossPlatformCoverageAITableIncompleteResultMetadata(t *testing.T) {
total := 9
err := recordQueryIncompleteError(paging.Result{
Records: []any{map[string]any{"id": "r"}}, Pages: 1, Attempts: 1,
HasMore: true, LastCursor: "next", StopReason: paging.StopPageLimit,
TotalCount: &total,
}, 1)
if err == nil || !strings.Contains(err.Error(), "pagination incomplete") {
t.Fatalf("incomplete result error = %v", err)
}
}
@@ -136,10 +136,9 @@ func executeAttachmentPut(rt *shortcut.RuntimeContext) error {
result.Status = "unknown"
return compositeError(result, err, false)
}
request, err := http.NewRequestWithContext(rt.Command().Context(), http.MethodPut, uploadURL, file)
if err != nil {
return compositeError(result, err, false)
}
// validateAttachmentUploadURL has already rejected every URL shape for
// which NewRequestWithContext can fail; PUT is a fixed valid method.
request, _ := http.NewRequestWithContext(rt.Command().Context(), http.MethodPut, uploadURL, file)
request.Header.Set("Content-Type", mimeType)
request.ContentLength = info.Size()
response, err := attachmentHTTPDo(request)
@@ -14,6 +14,7 @@ import (
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
)
func writeAttachmentFixture(t *testing.T, content string) string {
@@ -157,3 +158,275 @@ func TestCrossPlatformCoverageAttachmentRemoveExplainsTokenBoundaryE2E(t *testin
t.Fatalf("token boundary = output:%q err:%v calls:%#v", out, err, caller.calls)
}
}
func TestCrossPlatformCoverageAttachmentPutRemainingOutcomesE2E(t *testing.T) {
filePath := writeAttachmentFixture(t, "bytes")
t.Run("file open failure", func(t *testing.T) {
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{}, "+attachment-put",
"--base-id", "base", "--table-id", "table", "--record-id", "record", "--field-id", "field", "--file", filepath.Join(t.TempDir(), "missing"), "--yes")
if err == nil || out != "" {
t.Fatalf("file open = output:%q err:%v", out, err)
}
})
t.Run("initial read failure", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{{err: errors.New("read failed")}}}
out, err := runAITableCompositeCLI(t, caller, "+attachment-put",
"--base-id", "base", "--table-id", "table", "--record-id", "record", "--field-id", "field", "--file", filePath, "--yes")
if err == nil || out != "" || !strings.Contains(err.Error(), "read failed") {
t.Fatalf("initial read = output:%q err:%v", out, err)
}
})
t.Run("append dry run preserves token", func(t *testing.T) {
caller := &upsertByKeyCaller{dryRun: true, steps: []upsertByKeyStep{{text: attachmentRecordJSON(t, "field", []any{
map[string]any{"file_token": "old-token", "filename": "old.txt"},
})}}}
out, err := runAITableCompositeCLI(t, caller, "+attachment-put",
"--base-id", "base", "--table-id", "table", "--record-id", "record", "--field-id", "field", "--file", filePath, "--dry-run", "--yes")
if err != nil || !strings.Contains(out, `"status": "planned"`) || len(caller.calls) != 1 {
t.Fatalf("append dry run = output:%q err:%v calls:%#v", out, err, caller.calls)
}
})
t.Run("prepare call failure", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: attachmentRecordJSON(t, "field", []any{})},
{err: errors.New("prepare failed")},
}}
out, err := runAITableCompositeCLI(t, caller, "+attachment-put",
"--base-id", "base", "--table-id", "table", "--record-id", "record", "--field-id", "field", "--file", filePath, "--mode", "replace", "--yes")
if err == nil || out != "" {
t.Fatalf("prepare failure = output:%q err:%v", out, err)
}
})
t.Run("invalid upload URL", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: attachmentRecordJSON(t, "field", []any{})},
{text: `{"uploadUrl":"http://example.com/upload","fileToken":"ft1"}`},
}}
out, err := runAITableCompositeCLI(t, caller, "+attachment-put",
"--base-id", "base", "--table-id", "table", "--record-id", "record", "--field-id", "field", "--file", filePath, "--mode", "replace", "--yes")
if err == nil || out != "" {
t.Fatalf("invalid upload URL = output:%q err:%v", out, err)
}
})
t.Run("HTTP transport failure", func(t *testing.T) {
testseam.Swap(t, &attachmentHTTPDo, func(*http.Request) (*http.Response, error) {
return nil, errors.New("transport failed")
})
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: attachmentRecordJSON(t, "field", []any{})},
{text: `{"uploadUrl":"https://uploads.example.test/put","fileToken":"ft1"}`},
}}
out, err := runAITableCompositeCLI(t, caller, "+attachment-put",
"--base-id", "base", "--table-id", "table", "--record-id", "record", "--field-id", "field", "--file", filePath, "--mode", "replace", "--yes")
if err == nil || out != "" {
t.Fatalf("transport failure = output:%q err:%v", out, err)
}
})
t.Run("write and read-back both fail", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }))
defer server.Close()
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: attachmentRecordJSON(t, "field", []any{})},
{text: mustJSONText(t, map[string]any{"uploadUrl": server.URL, "fileToken": "ft1"})},
{err: errors.New("write reply failed")},
{err: errors.New("verification read failed")},
}}
out, err := runAITableCompositeCLI(t, caller, "+attachment-put",
"--base-id", "base", "--table-id", "table", "--record-id", "record", "--field-id", "field", "--file", filePath, "--mode", "replace", "--yes")
if err == nil || out != "" {
t.Fatalf("verification failure = output:%q err:%v", out, err)
}
})
}
func TestCrossPlatformCoverageAttachmentRemoveRemainingOutcomesE2E(t *testing.T) {
run := func(t *testing.T, caller *upsertByKeyCaller, extra ...string) (string, error) {
args := []string{"--base-id", "base", "--table-id", "table", "--record-id", "record", "--field-id", "field"}
args = append(args, extra...)
args = append(args, "--yes")
return runAITableCompositeCLI(t, caller, "+attachment-remove", args...)
}
t.Run("initial read failure", func(t *testing.T) {
out, err := run(t, &upsertByKeyCaller{steps: []upsertByKeyStep{{err: errors.New("read failed")}}}, "--clear-all")
if err == nil || out != "" || !strings.Contains(err.Error(), "read failed") {
t.Fatalf("initial read = output:%q err:%v", out, err)
}
})
t.Run("unchanged selection", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{{text: attachmentRecordJSON(t, "field", []any{
map[string]any{"fileToken": "keep", "filename": "keep.pdf"},
})}}}
out, err := run(t, caller, "--remove-name", "missing.pdf")
if err != nil || !strings.Contains(out, `"status": "unchanged"`) || len(caller.calls) != 1 {
t.Fatalf("unchanged = output:%q err:%v", out, err)
}
})
t.Run("dry run", func(t *testing.T) {
caller := &upsertByKeyCaller{dryRun: true, steps: []upsertByKeyStep{{text: attachmentRecordJSON(t, "field", []any{
map[string]any{"fileToken": "remove", "filename": "remove.pdf"},
})}}}
out, err := run(t, caller, "--remove-name", "remove.pdf", "--dry-run")
if err != nil || !strings.Contains(out, `"status": "planned"`) || len(caller.calls) != 1 {
t.Fatalf("dry run = output:%q err:%v calls:%#v", out, err, caller.calls)
}
})
t.Run("read-back count mismatch", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: attachmentRecordJSON(t, "field", []any{map[string]any{"fileToken": "remove", "filename": "remove.pdf"}})},
{text: `{"updatedCount":1}`},
{text: attachmentRecordJSON(t, "field", []any{map[string]any{"filename": "other.pdf"}})},
}}
out, err := run(t, caller, "--clear-all")
if err == nil || out != "" {
t.Fatalf("count mismatch = output:%q err:%v", out, err)
}
})
t.Run("removed name remains", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: attachmentRecordJSON(t, "field", []any{
map[string]any{"fileToken": "remove", "filename": "remove.pdf"},
map[string]any{"fileToken": "keep", "filename": "keep.pdf"},
})},
{text: `{"updatedCount":1}`},
{text: attachmentRecordJSON(t, "field", []any{map[string]any{"filename": "remove.pdf"}})},
}}
out, err := run(t, caller, "--remove-name", "remove.pdf")
if err == nil || out != "" {
t.Fatalf("removed item remains = output:%q err:%v", out, err)
}
})
t.Run("write and verification fail", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: attachmentRecordJSON(t, "field", []any{map[string]any{"fileToken": "remove", "filename": "remove.pdf"}})},
{err: errors.New("write reply failed")},
{err: errors.New("verification failed")},
}}
out, err := run(t, caller, "--clear-all")
if err == nil || out != "" {
t.Fatalf("verification failure = output:%q err:%v", out, err)
}
})
}
func TestCrossPlatformCoverageAttachmentParsingAndValidation(t *testing.T) {
t.Run("file validation and MIME sniffing", func(t *testing.T) {
if file, _, _, err := openAttachmentFile(filepath.Join(t.TempDir(), "missing"), ""); err == nil || file != nil {
t.Fatalf("missing file must be rejected: file:%v err:%v", file, err)
}
if file, _, _, err := openAttachmentFile(t.TempDir(), ""); err == nil || file != nil {
t.Fatalf("directory must be rejected: file:%v err:%v", file, err)
}
empty := filepath.Join(t.TempDir(), "empty")
if err := os.WriteFile(empty, nil, 0o600); err != nil {
t.Fatal(err)
}
if file, _, _, err := openAttachmentFile(empty, ""); err == nil || file != nil {
t.Fatalf("empty file must be rejected: file:%v err:%v", file, err)
}
unknown := filepath.Join(t.TempDir(), "payload.unknown-extension")
if err := os.WriteFile(unknown, []byte("plain text"), 0o600); err != nil {
t.Fatal(err)
}
file, _, mimeType, err := openAttachmentFile(unknown, "")
if err != nil {
t.Fatal(err)
}
defer file.Close()
if mimeType == "" {
t.Fatal("sniffed MIME type is empty")
}
})
for name, raw := range map[string]string{
"missing host": "https:///upload",
"credentials": "https://user:pass@example.com/upload",
"wrong scheme": "ftp://example.com/upload",
} {
t.Run(name, func(t *testing.T) {
if err := validateAttachmentUploadURL(raw); err == nil {
t.Fatalf("%q must be rejected", raw)
}
})
}
for _, raw := range []string{"http://localhost/upload", "http://127.0.0.1/upload", "https://example.com/upload"} {
if err := validateAttachmentUploadURL(raw); err != nil {
t.Fatalf("%q = %v", raw, err)
}
}
if err := verifyAttachmentPut(nil, 1, "token", "name", 1); err == nil {
t.Fatal("count mismatch must fail")
}
if err := verifyAttachmentPut([]map[string]any{{"fileToken": "token"}}, 1, "token", "name", 1); err != nil {
t.Fatal(err)
}
if err := verifyAttachmentPut([]map[string]any{{"filename": "other", "size": 1}}, 1, "token", "name", 1); err == nil {
t.Fatal("identity mismatch must fail")
}
for value, want := range map[any]bool{int(1): true, int64(1): true, "1": false} {
_, ok := numericInt64(value)
if ok != want {
t.Fatalf("numericInt64(%T) = %v, want %v", value, ok, want)
}
}
}
func TestCrossPlatformCoverageAttachmentRemoveSelectionValidationE2E(t *testing.T) {
for name, extra := range map[string][]string{
"neither": nil,
"both": {"--remove-name", "a.pdf", "--clear-all"},
} {
t.Run(name, func(t *testing.T) {
args := []string{"--base-id", "base", "--table-id", "table", "--record-id", "record", "--field-id", "field"}
args = append(args, extra...)
args = append(args, "--yes")
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{}, "+attachment-remove", args...)
if err == nil || out != "" {
t.Fatalf("selection validation = output:%q err:%v", out, err)
}
})
}
}
func TestCrossPlatformCoverageAttachmentReadBackShapesE2E(t *testing.T) {
cases := []struct {
name string
step upsertByKeyStep
want string
}{
{name: "query error", step: upsertByKeyStep{err: errors.New("query failed")}, want: "query failed"},
{name: "wrong record", step: upsertByKeyStep{text: `{"records":[{"recordId":"other","cells":{}}]}`}, want: "exact records"},
{name: "missing cells", step: upsertByKeyStep{text: `{"records":[{"recordId":"record"}]}`}, want: "missing cells"},
{name: "non-array field", step: upsertByKeyStep{text: `{"records":[{"recordId":"record","cells":{"field":"bad"}}]}`}, want: "not an array"},
{name: "non-object item", step: upsertByKeyStep{text: `{"records":[{"recordId":"record","cells":{"field":["bad"]}}]}`}, want: "not an object"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{tc.step}}
out, err := runAITableCompositeCLI(t, caller, "+attachment-remove",
"--base-id", "base", "--table-id", "table", "--record-id", "record", "--field-id", "field", "--clear-all", "--yes")
if err == nil || out != "" || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("shape = output:%q err:%v", out, err)
}
})
}
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{{text: `{"records":[{"recordId":"record","cells":{}}]}`}}}
out, err := runAITableCompositeCLI(t, caller, "+attachment-remove",
"--base-id", "base", "--table-id", "table", "--record-id", "record", "--field-id", "field", "--clear-all", "--yes")
if err != nil || !strings.Contains(out, `"status": "unchanged"`) {
t.Fatalf("missing field = output:%q err:%v", out, err)
}
}
@@ -149,3 +149,135 @@ func TestCrossPlatformCoverageBaseBootstrapUnknownAndDryRunE2E(t *testing.T) {
}
})
}
func TestCrossPlatformCoverageBaseBootstrapInputValidation(t *testing.T) {
tooMany := make([]any, 101)
for index := range tooMany {
tooMany[index] = map[string]any{"name": fmt.Sprintf("T%d", index)}
}
tooManyJSON, err := json.Marshal(tooMany)
if err != nil {
t.Fatal(err)
}
cases := map[string]string{
"invalid JSON": `{`,
"not an array": `{}`,
"empty array": `[]`,
"too many tables": string(tooManyJSON),
"non-object table": `[1]`,
"empty name": `[{"name":" "}]`,
"duplicate name": `[{"name":"T"},{"name":"T"}]`,
"fields not array": `[{"name":"T","fields":{}}]`,
"field not object": `[{"name":"T","fields":[1]}]`,
"field missing type": `[{"name":"T","fields":[{"fieldName":"F"}]}]`,
}
for name, raw := range cases {
t.Run(name, func(t *testing.T) {
if tables, err := parseBootstrapTables(raw); err == nil || tables != nil {
t.Fatalf("parseBootstrapTables(%s) = %#v, %v", raw, tables, err)
}
})
}
}
func TestCrossPlatformCoverageBaseSnapshotFailureStagesE2E(t *testing.T) {
base := `{"baseId":"base","tables":[{"tableId":"t1","name":"T"}]}`
cases := []struct {
name string
steps []upsertByKeyStep
}{
{name: "get base error", steps: []upsertByKeyStep{{err: errors.New("get base failed")}}},
{name: "get base wrong id", steps: []upsertByKeyStep{{text: `{"baseId":"other","tables":[]}`}}},
{name: "table missing id", steps: []upsertByKeyStep{{text: `{"baseId":"base","tables":[{"name":"T"}]}`}}},
{name: "get tables error", steps: []upsertByKeyStep{{text: base}, {err: errors.New("get tables failed")}}},
{name: "get tables wrong id", steps: []upsertByKeyStep{{text: base}, {text: `{"tables":[{"tableId":"other"}]}`}}},
{name: "get fields error", steps: []upsertByKeyStep{{text: base}, {text: `{"tables":[{"tableId":"t1"}]}`}, {err: errors.New("get fields failed")}}},
{name: "get fields missing collection", steps: []upsertByKeyStep{{text: base}, {text: `{"tables":[{"tableId":"t1"}]}`}, {text: `{}`}}},
{name: "get views error", steps: []upsertByKeyStep{{text: base}, {text: `{"tables":[{"tableId":"t1"}]}`}, {text: `{"fields":[]}`}, {err: errors.New("get views failed")}}},
{name: "get views missing collection", steps: []upsertByKeyStep{{text: base}, {text: `{"tables":[{"tableId":"t1"}]}`}, {text: `{"fields":[]}`}, {text: `{}`}}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{steps: tc.steps}, "+base-schema-snapshot", "--base-id", "base")
if err == nil || out != "" {
t.Fatalf("snapshot failure = output:%q err:%v", out, err)
}
})
}
}
func TestCrossPlatformCoverageBaseBootstrapExecuteRejectsInvalidTablesE2E(t *testing.T) {
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{}, "+base-bootstrap", "--name", "Project", "--tables", `{`)
if err == nil || out != "" {
t.Fatalf("invalid bootstrap tables = output:%q err:%v", out, err)
}
}
func TestCrossPlatformCoverageBaseBootstrapFailureStagesE2E(t *testing.T) {
tables := marshalBootstrapTables(t, nil)
cases := []struct {
name string
steps []upsertByKeyStep
extra []string
withField bool
}{
{name: "missing base id", steps: []upsertByKeyStep{{text: `{}`}}},
{name: "base verification error", steps: []upsertByKeyStep{{text: `{"baseId":"b"}`}, {err: errors.New("verify base failed")}}},
{name: "base verification wrong id", steps: []upsertByKeyStep{{text: `{"baseId":"b"}`}, {text: `{"baseId":"other"}`}}},
{name: "create table error", steps: []upsertByKeyStep{{text: `{"baseId":"b"}`}, {text: `{"baseId":"b"}`}, {err: errors.New("create table failed")}}},
{name: "create table missing id", steps: []upsertByKeyStep{{text: `{"baseId":"b"}`}, {text: `{"baseId":"b"}`}, {text: `{}`}}},
{name: "verify table error", steps: []upsertByKeyStep{{text: `{"baseId":"b"}`}, {text: `{"baseId":"b"}`}, {text: `{"tableId":"t"}`}, {err: errors.New("verify table failed")}}},
{name: "verify table wrong id", steps: []upsertByKeyStep{{text: `{"baseId":"b"}`}, {text: `{"baseId":"b"}`}, {text: `{"tableId":"t"}`}, {text: `{"tables":[]}`}}},
{name: "verify fields error", steps: []upsertByKeyStep{{text: `{"baseId":"b"}`}, {text: `{"baseId":"b"}`}, {text: `{"tableId":"t"}`}, {text: `{"tables":[{"tableId":"t"}]}`}, {err: errors.New("verify fields failed")}}},
{name: "verify fields mismatch", steps: []upsertByKeyStep{{text: `{"baseId":"b"}`}, {text: `{"baseId":"b"}`}, {text: `{"tableId":"t"}`}, {text: `{"tables":[{"tableId":"t"}]}`}, {text: `{"fields":[]}`}}, extra: []string{"--folder-id", "folder", "--template-id", "template"}, withField: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
inputTables := tables
if tc.withField {
inputTables = marshalBootstrapTables(t, bootstrapFields(1))
}
args := []string{"--name", "Project", "--tables", inputTables}
args = append(args, tc.extra...)
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{steps: tc.steps}, "+base-bootstrap", args...)
if err == nil || out != "" {
t.Fatalf("bootstrap failure = output:%q err:%v", out, err)
}
})
}
}
func TestCrossPlatformCoverageBaseBootstrapRecoversFieldCallErrorE2E(t *testing.T) {
fields := bootstrapFields(16)
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: `{"baseId":"b"}`},
{text: `{"baseId":"b"}`},
{text: `{"tableId":"t"}`},
{err: errors.New("create fields reply failed")},
{text: `{"tables":[{"tableId":"t"}]}`},
{text: fieldReadBackJSON(t, fields)},
}}
out, err := runAITableCompositeCLI(t, caller, "+base-bootstrap",
"--name", "Project", "--tables", marshalBootstrapTables(t, fields), "--folder-id", "folder", "--template-id", "template")
if err != nil || !strings.Contains(out, `"status": "success"`) || !strings.Contains(out, "create_fields offset") {
t.Fatalf("field recovery = output:%q err:%v", out, err)
}
if caller.calls[0].args["folderId"] != "folder" || caller.calls[0].args["templateId"] != "template" {
t.Fatalf("create base args = %#v", caller.calls[0].args)
}
}
func TestCrossPlatformCoverageBaseCompositeShapeHelpers(t *testing.T) {
if _, ok := findNamedObjectList(map[string]any{"tables": "bad"}, "tables"); ok {
t.Fatal("non-array named child must fail")
}
if _, ok := findNamedObjectList(map[string]any{"tables": []any{"bad"}}, "tables"); ok {
t.Fatal("non-object list item must fail")
}
if containsAllFieldNames([]map[string]any{{"fieldName": "A"}}, []any{map[string]any{"fieldName": "B"}}) {
t.Fatal("missing field name must fail")
}
if got := findStringByKeys(map[string]any{"items": []any{map[string]any{"nested": " value "}}}, "nested"); got != "value" {
t.Fatalf("nested array string = %q", got)
}
}
+7 -4
View File
@@ -353,10 +353,13 @@ func createdRecordIDs(data map[string]any) []string {
case []any:
for _, child := range typed {
if createdContext {
if id, ok := child.(string); ok && strings.TrimSpace(id) != "" && !seen[id] {
seen[id] = true
out = append(out, strings.TrimSpace(id))
continue
if id, ok := child.(string); ok {
id = strings.TrimSpace(id)
if id != "" && !seen[id] {
seen[id] = true
out = append(out, id)
continue
}
}
}
walk(child, createdContext)
@@ -0,0 +1,336 @@
// Copyright 2026 Alibaba Group
// SPDX-License-Identifier: Apache-2.0
package aitable
import (
"errors"
"fmt"
"strconv"
"strings"
"testing"
)
func TestCrossPlatformCoverageRecordObjectAndIDValidation(t *testing.T) {
tooMany := make([]any, maxCompositeRecordRun+1)
for index := range tooMany {
tooMany[index] = map[string]any{"cells": map[string]any{"f": index}}
}
cases := []struct {
name string
raw string
requireID bool
}{
{name: "invalid JSON", raw: `{`},
{name: "not array", raw: `{}`},
{name: "empty array", raw: `[]`},
{name: "too many", raw: mustJSONText(t, tooMany)},
{name: "non-object", raw: `[1]`},
{name: "missing cells", raw: `[{"recordId":"r"}]`},
{name: "empty cells", raw: `[{"recordId":"r","cells":{}}]`},
{name: "missing record id", raw: `[{"cells":{"f":1}}]`, requireID: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if records, err := parseRecordObjects(tc.raw, tc.requireID); err == nil || records != nil {
t.Fatalf("parseRecordObjects = %#v, %v", records, err)
}
})
}
if ids, err := parseRecordIDs([]string{" ", "b", "a", "a"}); err != nil || strings.Join(ids, ",") != "a,b" {
t.Fatalf("parseRecordIDs = %#v, %v", ids, err)
}
tooManyIDs := make([]string, maxCompositeRecordRun+1)
for index := range tooManyIDs {
tooManyIDs[index] = strconv.Itoa(index)
}
if ids, err := parseRecordIDs(tooManyIDs); err == nil || ids != nil {
t.Fatalf("too many IDs = %#v, %v", ids, err)
}
}
func TestCrossPlatformCoverageRecordBatchEntryValidationE2E(t *testing.T) {
for _, command := range []string{"+record-update", "+record-upsert"} {
t.Run(command, func(t *testing.T) {
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{}, command,
"--base-id", "base", "--table-id", "table", "--records", `{`, "--yes")
if err == nil || out != "" {
t.Fatalf("invalid records = output:%q err:%v", out, err)
}
})
}
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{}, "+record-delete",
"--base-id", "base", "--table-id", "table", "--record-ids", ` `, "--yes")
if err == nil || out != "" {
t.Fatalf("invalid record IDs = output:%q err:%v", out, err)
}
}
func TestCrossPlatformCoverageRecordDeleteDryRunAndDualFailureE2E(t *testing.T) {
caller := &upsertByKeyCaller{dryRun: true}
out, err := runRecordDeleteCLI(t, caller, []string{"r1"}, "--dry-run")
if err != nil || len(caller.calls) != 0 || !strings.Contains(out, `"status": "planned"`) {
t.Fatalf("delete dry run = output:%q err:%v calls:%#v", out, err, caller.calls)
}
caller = &upsertByKeyCaller{steps: []upsertByKeyStep{{err: errors.New("write failed")}, {err: errors.New("verify failed")}}}
out, err = runRecordDeleteCLI(t, caller, []string{"r1"})
if err == nil || out != "" {
t.Fatalf("delete dual failure = output:%q err:%v", out, err)
}
out, err = runRecordDeleteCLI(t, &upsertByKeyCaller{}, []string{"", " "})
if err == nil || out != "" {
t.Fatalf("delete parsed empty IDs = output:%q err:%v", out, err)
}
out, err = runRecordDeleteCLI(t, &upsertByKeyCaller{}, recordIDFixtures(maxCompositeRecordRun+1))
if err == nil || out != "" {
t.Fatalf("delete excessive IDs = output:%q err:%v", out, err)
}
}
func TestCrossPlatformCoverageRecordBatchVerificationEdgesE2E(t *testing.T) {
record := []map[string]any{{"recordId": "r1", "cells": map[string]any{"f": "new"}}}
t.Run("read-back query error", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{{text: `{"updatedCount":1}`}, {err: errors.New("read failed")}}}
out, err := runRecordBatchCLI(t, caller, "+record-update", record)
if err == nil || out != "" {
t.Fatalf("query error = output:%q err:%v", out, err)
}
})
t.Run("read-back cells mismatch", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: `{"updatedCount":1}`},
{text: `{"records":[{"recordId":"r1","cells":{"f":"old"}}]}`},
}}
out, err := runRecordBatchCLI(t, caller, "+record-update", record)
if err == nil || out != "" {
t.Fatalf("cell mismatch = output:%q err:%v", out, err)
}
})
t.Run("upsert create read error", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: `{"createdRecords":[{"recordId":"new"}]}`},
{err: errors.New("created read failed")},
}}
out, err := runRecordBatchCLI(t, caller, "+record-upsert", []map[string]any{{"cells": map[string]any{"f": "new"}}})
if err == nil || out != "" {
t.Fatalf("create read error = output:%q err:%v", out, err)
}
})
t.Run("upsert create cells mismatch", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: `{"createdRecords":[{"recordId":"new"}]}`},
{text: `{"records":[{"recordId":"new","cells":{"f":"old"}}]}`},
}}
out, err := runRecordBatchCLI(t, caller, "+record-upsert", []map[string]any{{"cells": map[string]any{"f": "new"}}})
if err == nil || out != "" {
t.Fatalf("create mismatch = output:%q err:%v", out, err)
}
})
t.Run("upsert update read error", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: `{"createdRecords":[],"updatedCount":1}`},
{err: errors.New("updated read failed")},
}}
out, err := runRecordBatchCLI(t, caller, "+record-upsert", record)
if err == nil || out != "" {
t.Fatalf("update read error = output:%q err:%v", out, err)
}
})
t.Run("incomplete update read", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: `{"updatedCount":1}`},
{text: `{"records":[{"recordId":"r1","cells":{"f":"new"}}],"hasMore":true}`},
}}
out, err := runRecordBatchCLI(t, caller, "+record-update", record)
if err == nil || out != "" {
t.Fatalf("incomplete read = output:%q err:%v", out, err)
}
})
}
func TestCrossPlatformCoverageRecordBatchPureHelpers(t *testing.T) {
if err := matchCreatedCells([]map[string]any{{"cells": map[string]any{"f": 1}}}, nil); err == nil {
t.Fatal("created result count mismatch must fail")
}
if err := matchCreatedCells(
[]map[string]any{{"cells": map[string]any{"f": 1}}},
[]map[string]any{{"recordId": "r", "cells": map[string]any{"f": 2}}},
); err == nil {
t.Fatal("unmatched created cells must fail")
}
ids := createdRecordIDs(map[string]any{"createdIds": []any{" r1 ", "r1", map[string]any{"record_id": "r2"}}})
if strings.Join(ids, ",") != "r1,r2" {
t.Fatalf("created IDs = %#v", ids)
}
if got := stringSlice("bad"); got != nil {
t.Fatalf("stringSlice = %#v", got)
}
if minInt(1, 2) != 1 || minInt(2, 1) != 1 {
t.Fatal("minInt branch mismatch")
}
}
func TestCrossPlatformCoverageRecordBulkPatchValidationAndSelectorsE2E(t *testing.T) {
cases := []struct {
name string
args []string
}{
{name: "invalid patch", args: []string{"--all", "--patch", `{`}},
{name: "empty patch", args: []string{"--all", "--patch", `{}`}},
{name: "false all", args: []string{"--all=false", "--patch", `{"f":1}`}},
{name: "invalid max", args: []string{"--all", "--patch", `{"f":1}`, "--max-matches", "0"}},
{name: "invalid filters", args: []string{"--filters", `{`, "--patch", `{"f":1}`}},
{name: "empty record IDs", args: []string{"--record-ids", ` `, "--patch", `{"f":1}`}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
args := []string{"--base-id", "base", "--table-id", "table"}
args = append(args, tc.args...)
args = append(args, "--yes")
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{}, "+record-bulk-patch", args...)
if err == nil || out != "" {
t.Fatalf("validation = output:%q err:%v", out, err)
}
})
}
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{{text: `{"records":[]}`}}}
out, err := runAITableCompositeCLI(t, caller, "+record-bulk-patch",
"--base-id", "base", "--table-id", "table", "--filters", `{"operator":"and"}`, "--query", "text",
"--record-ids", "r2,r1", "--view-id", "view", "--patch", `{"f":1}`, "--yes")
if err != nil || !strings.Contains(out, `"matchedCount": 0`) {
t.Fatalf("selectors = output:%q err:%v", out, err)
}
for _, key := range []string{"filters", "keyword", "recordIds", "viewId"} {
if _, ok := caller.calls[0].args[key]; !ok {
t.Fatalf("query args missing %s: %#v", key, caller.calls[0].args)
}
}
caller = &upsertByKeyCaller{dryRun: true, steps: []upsertByKeyStep{{text: `{"records":[]}`}}}
out, err = runAITableCompositeCLI(t, caller, "+record-bulk-patch",
"--base-id", "base", "--table-id", "table", "--query", "none", "--patch", `{"f":1}`, "--dry-run", "--yes")
if err != nil || !strings.Contains(out, `"status": "planned"`) {
t.Fatalf("empty dry run = output:%q err:%v", out, err)
}
}
func TestCrossPlatformCoverageRecordBulkPatchQueryFailuresE2E(t *testing.T) {
cases := []struct {
name string
steps []upsertByKeyStep
}{
{name: "query error", steps: []upsertByKeyStep{{err: errors.New("query failed")}}},
{name: "missing records", steps: []upsertByKeyStep{{text: `{}`}}},
{name: "more without cursor", steps: []upsertByKeyStep{{text: `{"records":[],"hasMore":true}`}}},
{name: "cursor cycle", steps: []upsertByKeyStep{
{text: `{"records":[],"hasMore":true,"nextCursor":"same"}`},
{text: `{"records":[],"hasMore":true,"nextCursor":"same"}`},
}},
{name: "matched record missing id", steps: []upsertByKeyStep{{text: `{"records":[{"cells":{"f":1}}]}`}}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{steps: tc.steps}, "+record-bulk-patch",
"--base-id", "base", "--table-id", "table", "--all", "--patch", `{"f":1}`, "--yes")
if err == nil || out != "" {
t.Fatalf("query failure = output:%q err:%v", out, err)
}
})
}
}
func TestCrossPlatformCoverageRecordQueryPageSafetyBoundE2E(t *testing.T) {
caller := &upsertByKeyCaller{callFn: func(index int, _, tool string, _ map[string]any) (string, error) {
if tool != "query_records" {
return "", fmt.Errorf("unexpected tool %s", tool)
}
return fmt.Sprintf(`{"records":[],"hasMore":true,"nextCursor":"cursor-%d"}`, index), nil
}}
out, err := runAITableCompositeCLI(t, caller, "+record-bulk-patch",
"--base-id", "base", "--table-id", "table", "--all", "--patch", `{"f":1}`, "--yes")
if err == nil || out != "" || len(caller.calls) != 10000 {
t.Fatalf("page safety = output:%q err:%v calls:%d", out, err, len(caller.calls))
}
}
func TestCrossPlatformCoverageRecordUpsertInputAndQueryFailuresE2E(t *testing.T) {
baseArgs := []string{"--base-id", "base", "--table-id", "table", "--key-field-id", "key", "--cells", `{"value":1}`, "--yes"}
cases := []struct {
name string
key []string
cells string
steps []upsertByKeyStep
}{
{name: "empty string key", key: []string{"--key-value", " "}},
{name: "invalid JSON key", key: []string{"--key-value-json", `{`}},
{name: "null JSON key", key: []string{"--key-value-json", `null`}},
{name: "object JSON key", key: []string{"--key-value-json", `{}`}},
{name: "invalid cells", key: []string{"--key-value", "v"}, cells: `{`},
{name: "conflicting key", key: []string{"--key-value", "v"}, cells: `{"key":"other"}`},
{name: "query error", key: []string{"--key-value", "v"}, steps: []upsertByKeyStep{{err: errors.New("query failed")}}},
{name: "missing records", key: []string{"--key-value", "v"}, steps: []upsertByKeyStep{{text: `{}`}}},
{name: "incomplete unique query", key: []string{"--key-value", "v"}, steps: []upsertByKeyStep{{text: `{"records":[],"hasMore":true}`}}},
{name: "record missing id", key: []string{"--key-value", "v"}, steps: []upsertByKeyStep{{text: `{"records":[{"cells":{"key":"v"}}]}`}}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
args := append([]string{}, baseArgs...)
args = append(args, tc.key...)
if tc.cells != "" {
for index := range args {
if args[index] == "--cells" {
args[index+1] = tc.cells
}
}
}
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{steps: tc.steps}, "+record-upsert-by-key", args...)
if err == nil || out != "" {
t.Fatalf("upsert failure = output:%q err:%v", out, err)
}
})
}
}
func TestCrossPlatformCoverageRecordUpsertJSONScalarSuccessE2E(t *testing.T) {
caller := &upsertByKeyCaller{dryRun: true, steps: []upsertByKeyStep{{text: `{"records":[]}`}}}
out, err := runAITableCompositeCLI(t, caller, "+record-upsert-by-key",
"--base-id", "base", "--table-id", "table", "--key-field-id", "key", "--key-value-json", `true`,
"--cells", `{"value":1}`, "--dry-run", "--yes")
if err != nil || !strings.Contains(out, `"keyValue": true`) {
t.Fatalf("JSON scalar = output:%q err:%v", out, err)
}
}
func TestCrossPlatformCoverageRecordShapeHelpers(t *testing.T) {
if records, ok := findRecords(nil); ok || records != nil {
t.Fatalf("nil records = %#v, %v", records, ok)
}
if _, ok := findRecords(map[string]any{"records": "bad"}); ok {
t.Fatal("non-array records must fail")
}
if _, ok := findRecords(map[string]any{"records": []any{"bad"}}); ok {
t.Fatal("non-object record must fail")
}
if records, ok := findRecords(map[string]any{"result": map[string]any{"records": []any{map[string]any{"id": "r"}}}}); !ok || recordID(records[0]) != "r" {
t.Fatalf("nested records = %#v, %v", records, ok)
}
if responseHasMore(nil) || !responseHasMore(map[string]any{"cursor": "next"}) || !responseHasMore(map[string]any{"pagination": map[string]any{"hasMore": true}}) {
t.Fatal("responseHasMore shape mismatch")
}
if err := verifyRecordCells(map[string]any{"recordId": "r"}, map[string]any{"f": 1}); err == nil {
t.Fatal("missing cells must fail")
}
if responseCursor(nil) != "" || responseCursor(map[string]any{"data": map[string]any{"next_cursor": " next "}}) != "next" {
t.Fatal("responseCursor shape mismatch")
}
}
@@ -182,11 +182,8 @@ func executeRecordUpsertByKey(rt *shortcut.RuntimeContext) error {
func recordKeyValue(rt *shortcut.RuntimeContext) (any, error) {
if rt.Changed("key-value") {
value := rt.Str("key-value")
if value == "" {
return nil, apperrors.NewValidation("--key-value 不能为空")
}
return value, nil
// The command framework rejects an explicitly empty string before Execute.
return rt.Str("key-value"), nil
}
decoder := json.NewDecoder(strings.NewReader(rt.Str("key-value-json")))
decoder.UseNumber()
@@ -28,6 +28,7 @@ type upsertByKeyCaller struct {
steps []upsertByKeyStep
calls []aitableURLCall
dryRun bool
callFn func(index int, product, tool string, args map[string]any) (string, error)
}
func (c *upsertByKeyCaller) CallTool(_ context.Context, product, tool string, args map[string]any) (*edition.ToolResult, error) {
@@ -45,6 +46,13 @@ func (c *upsertByKeyCaller) call(product, tool string, args map[string]any) (*ed
}
c.calls = append(c.calls, aitableURLCall{product: product, tool: tool, args: cloned})
index := len(c.calls) - 1
if c.callFn != nil {
text, err := c.callFn(index, product, tool, args)
if err != nil {
return nil, err
}
return urlResult(text), nil
}
if index >= len(c.steps) {
return nil, errors.New("unexpected upsert-by-key call")
}
@@ -0,0 +1,367 @@
// Copyright 2026 Alibaba Group
// SPDX-License-Identifier: Apache-2.0
package aitable
import (
"errors"
"fmt"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
func TestCrossPlatformCoverageAITableProjectionShapeFailures(t *testing.T) {
if _, err := resolveNamedList("op", nil, []string{"data"}, []string{"items"}); err == nil {
t.Fatal("nil list response must fail")
}
if _, err := resolveNamedList("op", map[string]any{"data": "bad"}, []string{"data"}, []string{"items"}); err == nil {
t.Fatal("non-object envelope must fail")
}
if _, err := resolveNamedList("op", map[string]any{"data": map[string]any{"other": []any{}}}, []string{"data"}, []string{"items"}); err == nil {
t.Fatal("unrecognized envelope must fail")
}
for name, project := range map[string]func() error{
"base": func() error {
_, err := baseListProject("list_bases", map[string]any{"bases": []any{"bad"}})
return err
},
"template": func() error {
_, err := templateSearchProject(map[string]any{"templates": []any{"bad"}})
return err
},
"view": func() error {
_, err := viewGetProject(map[string]any{"views": []any{"bad"}})
return err
},
"form": func() error {
_, err := formListProject(map[string]any{"views": []any{"bad"}})
return err
},
"workflow": func() error {
_, err := workflowListProject(map[string]any{"workflows": []any{"bad"}})
return err
},
} {
t.Run(name, func(t *testing.T) {
if err := project(); err == nil {
t.Fatal("non-object projection item must fail")
}
})
}
}
func TestCrossPlatformCoverageAITableProjectionValidAndMissingIdentityShapes(t *testing.T) {
bases, err := baseListProject("list_bases", map[string]any{"bases": []any{map[string]any{"baseId": "b", "baseName": "Base"}}})
if err != nil || len(bases) != 1 {
t.Fatalf("base projection = %#v, %v", bases, err)
}
if _, err := templateSearchProject(map[string]any{"templates": []any{map[string]any{"name": "missing"}}}); err == nil {
t.Fatal("template missing ID succeeded")
}
templates, err := templateSearchProject(map[string]any{"templates": []any{map[string]any{"templateId": "t"}}})
if err != nil || len(templates) != 1 {
t.Fatalf("template projection = %#v, %v", templates, err)
}
views, err := viewGetProject(map[string]any{"views": []any{map[string]any{"viewId": "v"}}})
if err != nil || len(views) != 1 {
t.Fatalf("view projection = %#v, %v", views, err)
}
forms, err := formListProject(map[string]any{"views": []any{map[string]any{"viewId": "f"}}})
if err != nil || len(forms) != 1 {
t.Fatalf("form projection = %#v, %v", forms, err)
}
workflows, err := workflowListProject(map[string]any{"workflows": []any{map[string]any{"workflowId": "w"}}})
if err != nil || len(workflows) != 1 {
t.Fatalf("workflow projection = %#v, %v", workflows, err)
}
}
func TestCrossPlatformCoverageCompositeAndReviewedContractFallbacks(t *testing.T) {
err := compositeError(newCompositeResult("test"), errors.New("cause"), true)
if err == nil || !strings.Contains(err.Error(), "unknown") {
t.Fatalf("composite default status = %v", err)
}
unreviewed := shortcut.Shortcut{Command: "+not-reviewed"}
if got := withReviewedAITableShortcutContracts(unreviewed); len(got) != 1 || !got[0].Contract.Empty() {
t.Fatalf("unreviewed shortcut changed: %#v", got)
}
value := shortcut.Shortcut{
Command: "+base-list",
Description: "fallback description",
Tips: []string{"one", "two", "three"},
}
contract := reviewedAITableShortcutContract(value)
if contract.Description != "fallback description" || len(contract.Selection.Examples) != 2 {
t.Fatalf("reviewed contract fallback = %#v", contract)
}
}
func TestCrossPlatformCoverageURLResolveAllVerificationKindsE2E(t *testing.T) {
cases := []struct {
name string
url string
tool string
text string
}{
{name: "record", url: "https://alidocs.dingtalk.com/i/nodes/base?iframeQuery=sheetId%3Dtable%26recordId%3Drecord", tool: "query_records", text: `{"records":[{"record_id":"record"}]}`},
{name: "view", url: "https://alidocs.dingtalk.com/i/nodes/base?iframeQuery=sheetId%3Dtable%26viewId%3Dview", tool: "get_views", text: `{"views":[{"view_id":"view"}]}`},
{name: "base by id", url: "https://alidocs.dingtalk.com/i/nodes/base", tool: "get_base", text: `{"base_id":"base"}`},
{name: "base structural fallback", url: "https://alidocs.dingtalk.com/i/nodes/base", tool: "get_base", text: `{"data":{"tables":[]}}`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
caller := &urlResolveE2ECaller{results: []*edition.ToolResult{urlResult(tc.text)}}
out, err := runURLResolveCLI(t, caller, "--url", tc.url, "--verify")
if err != nil || !strings.Contains(out, `"verified": true`) || caller.calls[0].tool != tc.tool {
t.Fatalf("verification = output:%q err:%v calls:%#v", out, err, caller.calls)
}
})
}
t.Run("tool error", func(t *testing.T) {
caller := &urlResolveE2ECaller{errors: []error{errors.New("verification failed")}}
out, err := runURLResolveCLI(t, caller, "--url", "https://alidocs.dingtalk.com/i/nodes/base", "--verify")
if err == nil || out != "" {
t.Fatalf("tool error = output:%q err:%v", out, err)
}
})
t.Run("base unknown response", func(t *testing.T) {
caller := &urlResolveE2ECaller{results: []*edition.ToolResult{urlResult(`{"success":true}`)}}
out, err := runURLResolveCLI(t, caller, "--url", "https://alidocs.dingtalk.com/i/nodes/base", "--verify")
if err == nil || out != "" {
t.Fatalf("unknown base = output:%q err:%v", out, err)
}
})
}
func TestCrossPlatformCoverageURLResolveRecursiveShapeHelpers(t *testing.T) {
value := map[string]any{"data": []any{map[string]any{"id": "wanted"}}}
if !responseContainsID(value, "wanted", "id") || responseContainsID(value, "other", "id") {
t.Fatal("responseContainsID recursion mismatch")
}
if !responseHasAnyKey(map[string]any{"data": []any{map[string]any{"tables": []any{}}}}, "tables") {
t.Fatal("responseHasAnyKey recursion mismatch")
}
if responseHasAnyKey([]any{"value"}, "tables") {
t.Fatal("unrelated key must not match")
}
}
func TestCrossPlatformCoverageViewPresetValidationAndDryRunE2E(t *testing.T) {
cases := []struct {
name string
config string
steps []upsertByKeyStep
}{
{name: "invalid config", config: `{`},
{name: "empty config", config: `{}`},
{name: "preflight error", config: `{"f":1}`, steps: []upsertByKeyStep{{err: errors.New("views failed")}}},
{name: "preflight missing list", config: `{"f":1}`, steps: []upsertByKeyStep{{text: `{}`}}},
{name: "matched missing id", config: `{"f":1}`, steps: []upsertByKeyStep{{text: `{"views":[{"viewName":"X","viewType":"Grid"}]}`}}},
{name: "matched type conflict", config: `{"f":1}`, steps: []upsertByKeyStep{{text: `{"views":[{"viewId":"v","viewName":"X","viewType":"Kanban"}]}`}}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{steps: tc.steps}, "+view-preset-apply",
"--base-id", "base", "--table-id", "table", "--name", "X", "--view-type", "Grid", "--config", tc.config, "--yes")
if err == nil || out != "" {
t.Fatalf("validation = output:%q err:%v", out, err)
}
})
}
caller := &upsertByKeyCaller{dryRun: true, steps: []upsertByKeyStep{{text: `{"views":[]}`}}}
out, err := runAITableCompositeCLI(t, caller, "+view-preset-apply",
"--base-id", "base", "--table-id", "table", "--name", "X", "--view-type", "Grid", "--config", `{"f":1}`, "--dry-run", "--yes")
if err != nil || !strings.Contains(out, `"status": "planned"`) {
t.Fatalf("dry run = output:%q err:%v", out, err)
}
}
func TestCrossPlatformCoverageViewPresetReadBackFailuresAndUpdateE2E(t *testing.T) {
preflight := `{"views":[{"viewId":"v","viewName":"X","viewType":"Grid","config":{"f":0}}]}`
cases := []struct {
name string
write upsertByKeyStep
readBack upsertByKeyStep
}{
{name: "read error", write: upsertByKeyStep{text: `{"updated":1}`}, readBack: upsertByKeyStep{err: errors.New("read failed")}},
{name: "missing list", write: upsertByKeyStep{text: `{"updated":1}`}, readBack: upsertByKeyStep{text: `{}`}},
{name: "missing exact name", write: upsertByKeyStep{text: `{"updated":1}`}, readBack: upsertByKeyStep{text: `{"views":[]}`}},
{name: "identity mismatch", write: upsertByKeyStep{text: `{"updated":1}`}, readBack: upsertByKeyStep{text: `{"views":[{"viewId":"other","viewName":"X","viewType":"Grid","config":{"f":1}}]}`}},
{name: "dual failure", write: upsertByKeyStep{err: errors.New("write failed")}, readBack: upsertByKeyStep{err: errors.New("read failed")}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{{text: preflight}, tc.write, tc.readBack}}
out, err := runAITableCompositeCLI(t, caller, "+view-preset-apply",
"--base-id", "base", "--table-id", "table", "--name", "X", "--view-type", "Grid", "--config", `{"f":1}`, "--yes")
if err == nil || out != "" {
t.Fatalf("read-back failure = output:%q err:%v", out, err)
}
})
}
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: preflight}, {text: `{"updated":1}`}, {text: `{"views":[{"viewId":"v","viewName":"X","viewType":"Grid","config":{"f":1}}]}`},
}}
out, err := runAITableCompositeCLI(t, caller, "+view-preset-apply",
"--base-id", "base", "--table-id", "table", "--name", "X", "--view-type", "Grid", "--config", `{"f":1}`, "--yes")
if err != nil || !strings.Contains(out, `"action": "update"`) {
t.Fatalf("update success = output:%q err:%v", out, err)
}
}
func TestCrossPlatformCoverageViewPresetPureMatching(t *testing.T) {
if presetViewMatches(map[string]any{"viewType": "Kanban", "config": map[string]any{}}, "Grid", map[string]any{}) {
t.Fatal("type mismatch must fail")
}
if mapContains(map[string]any{}, map[string]any{"missing": 1}) {
t.Fatal("missing key must fail")
}
if mapContains(map[string]any{"nested": 1}, map[string]any{"nested": map[string]any{"x": 1}}) {
t.Fatal("nested type mismatch must fail")
}
if mapContains(map[string]any{"nested": map[string]any{}}, map[string]any{"nested": map[string]any{"x": 1}}) {
t.Fatal("nested value mismatch must fail")
}
if !mapContains(map[string]any{"nested": map[string]any{"x": 1, "extra": true}}, map[string]any{"nested": map[string]any{"x": 1}}) {
t.Fatal("nested subset must match")
}
if mapContains(map[string]any{"x": 1}, map[string]any{"x": 2}) {
t.Fatal("scalar mismatch must fail")
}
}
func TestCrossPlatformCoverageWorkflowDeployValidationAndFailureStagesE2E(t *testing.T) {
cases := []struct {
name string
dsl string
extra []string
steps []upsertByKeyStep
}{
{name: "invalid DSL", dsl: `{`},
{name: "wrong DSL version", dsl: `{"version":"v0"}`},
{name: "update preflight error", dsl: workflowDSLFixture, extra: []string{"--workflow-id", "w"}, steps: []upsertByKeyStep{{err: errors.New("preflight failed")}}},
{name: "update target not found", dsl: workflowDSLFixture, extra: []string{"--workflow-id", "w"}, steps: []upsertByKeyStep{{text: `{"flowId":"other"}`}}},
{name: "publish missing ID", dsl: workflowDSLFixture, steps: []upsertByKeyStep{{text: `{"valid":true}`}}},
{name: "publish ID wrong type", dsl: workflowDSLFixture, steps: []upsertByKeyStep{{text: `{"valid":true,"flowId":1}`}}},
{name: "read-back error", dsl: workflowDSLFixture, steps: []upsertByKeyStep{{text: `{"valid":true,"flowId":"w"}`}, {err: errors.New("read failed")}}},
{name: "read-back wrong ID", dsl: workflowDSLFixture, steps: []upsertByKeyStep{{text: `{"valid":true,"flowId":"w"}`}, {text: `{"flowId":"other"}`}}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
args := []string{"--base-id", "base", "--dsl", tc.dsl}
args = append(args, tc.extra...)
args = append(args, "--yes")
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{steps: tc.steps}, "+workflow-deploy", args...)
if err == nil || out != "" {
t.Fatalf("workflow failure = output:%q err:%v", out, err)
}
})
}
caller := &upsertByKeyCaller{dryRun: true}
out, err := runAITableCompositeCLI(t, caller, "+workflow-deploy", "--base-id", "base", "--dsl", workflowDSLFixture, "--enable", "--dry-run", "--yes")
if err != nil || !strings.Contains(out, `"status": "planned"`) || !strings.Contains(out, "enable and verify") {
t.Fatalf("workflow dry run = output:%q err:%v", out, err)
}
}
func TestCrossPlatformCoverageWorkflowEnableErrorCanStillBeVerifiedE2E(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: `{"valid":true,"flowId":"w"}`}, {text: `{"flowId":"w"}`},
{err: errors.New("enable reply failed")}, {text: `{"list":[{"flowId":"w","enabled":true}]}`},
}}
out, err := runAITableCompositeCLI(t, caller, "+workflow-deploy", "--base-id", "base", "--dsl", workflowDSLFixture, "--enable", "--yes")
if err != nil || !strings.Contains(out, "enable response error") {
t.Fatalf("enable recovery = output:%q err:%v", out, err)
}
}
func TestCrossPlatformCoverageWorkflowListFailureShapesE2E(t *testing.T) {
for name, step := range map[string]upsertByKeyStep{
"list error": {err: errors.New("list failed")},
"missing list": {text: `{}`},
"not present": {text: `{"list":[]}`},
} {
t.Run(name, func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: `{"valid":true,"flowId":"w"}`}, {text: `{"flowId":"w"}`}, {text: `{"enabled":true}`}, step,
}}
out, err := runAITableCompositeCLI(t, caller, "+workflow-deploy", "--base-id", "base", "--dsl", workflowDSLFixture, "--enable", "--yes")
if err == nil || out != "" {
t.Fatalf("list failure = output:%q err:%v", out, err)
}
})
}
}
func TestCrossPlatformCoverageWorkflowListPaginationAndBoundE2E(t *testing.T) {
page := make([]any, 100)
for index := range page {
page[index] = map[string]any{"flowId": fmt.Sprintf("other-%d", index)}
}
caller := &upsertByKeyCaller{callFn: func(index int, _, tool string, _ map[string]any) (string, error) {
switch index {
case 0:
return `{"valid":true,"flowId":"w"}`, nil
case 1:
return `{"flowId":"w"}`, nil
case 2:
return `{"enabled":true}`, nil
case 3:
return mustJSONText(t, map[string]any{"list": page}), nil
default:
return `{"list":[{"flowId":"w","status":"ACTIVE"}]}`, nil
}
}}
out, err := runAITableCompositeCLI(t, caller, "+workflow-deploy", "--base-id", "base", "--dsl", workflowDSLFixture, "--enable", "--yes")
if err != nil || !strings.Contains(out, `"running": true`) || len(caller.calls) != 5 {
t.Fatalf("paginated list = output:%q err:%v calls:%d", out, err, len(caller.calls))
}
fullPage := mustJSONText(t, map[string]any{"list": page})
caller = &upsertByKeyCaller{callFn: func(index int, _, _ string, _ map[string]any) (string, error) {
if index == 0 {
return `{"valid":true,"flowId":"w"}`, nil
}
if index == 1 {
return `{"flowId":"w"}`, nil
}
if index == 2 {
return `{"enabled":true}`, nil
}
return fullPage, nil
}}
out, err = runAITableCompositeCLI(t, caller, "+workflow-deploy", "--base-id", "base", "--dsl", workflowDSLFixture, "--enable", "--yes")
if err == nil || out != "" || len(caller.calls) != 103 {
t.Fatalf("workflow list bound = output:%q err:%v calls:%d", out, err, len(caller.calls))
}
}
func TestCrossPlatformCoverageWorkflowPureResponseHelpers(t *testing.T) {
if _, _, _, err := workflowPublishResult(nil); err == nil {
t.Fatal("nil publish result must fail")
}
if value, found := findBoolByKeys(map[string]any{"data": []any{map[string]any{"enabled": true}}}, "enabled"); !found || !value {
t.Fatal("nested bool not found")
}
if _, found := findBoolByKeys([]any{"value"}, "enabled"); found {
t.Fatal("unrelated bool unexpectedly found")
}
for _, workflow := range []map[string]any{
{"status": "enabled"}, {"state": "active"}, {"isEnabled": true},
} {
if !workflowIsRunning(workflow) {
t.Fatalf("workflow should be running: %#v", workflow)
}
}
if workflowIsRunning(map[string]any{"status": "STOP", "enabled": false}) {
t.Fatal("stopped workflow must not be running")
}
}
-3
View File
@@ -200,9 +200,6 @@ func executeTableCopy(rt *shortcut.RuntimeContext) error {
result.Checkpoint = map[string]any{"targetTableId": targetTable, "nextRecordOffset": offset}
return compositeError(result, verifyErr, false)
}
if writeErr != nil {
result.Warnings = append(result.Warnings, fmt.Sprintf("create_records offset %d returned an error, but returned IDs and read-back proved the copied state: %v", offset, writeErr))
}
createdCount = end
result.KnownEffects = append(result.KnownEffects, map[string]any{"tool": "create_records", "offset": offset, "recordIds": createdIDs})
}
@@ -0,0 +1,172 @@
// Copyright 2026 Alibaba Group
// SPDX-License-Identifier: Apache-2.0
package aitable
import (
"errors"
"fmt"
"strings"
"testing"
)
func copyFields(count int, prefix string) []any {
fields := make([]any, 0, count)
for index := 0; index < count; index++ {
fields = append(fields, map[string]any{
"fieldId": fmt.Sprintf("%s%d", prefix, index),
"fieldName": fmt.Sprintf("F%d", index),
"type": "text",
})
}
return fields
}
func TestCrossPlatformCoverageTableCopyValidationAndDryRunE2E(t *testing.T) {
t.Run("invalid maximum", func(t *testing.T) {
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{}, "+table-copy",
"--source-base-id", "b1", "--source-table-id", "t1", "--target-base-id", "b2", "--new-name", "copy", "--max-records", "0", "--yes")
if err == nil || out != "" {
t.Fatalf("invalid maximum = output:%q err:%v", out, err)
}
})
t.Run("source read error", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{{err: errors.New("source fields failed")}}}
out, err := runAITableCompositeCLI(t, caller, "+table-copy",
"--source-base-id", "b1", "--source-table-id", "t1", "--target-base-id", "b2", "--new-name", "copy", "--yes")
if err == nil || out != "" {
t.Fatalf("source read = output:%q err:%v", out, err)
}
})
t.Run("source fields collection missing", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{{text: `{}`}}}
out, err := runAITableCompositeCLI(t, caller, "+table-copy",
"--source-base-id", "b1", "--source-table-id", "t1", "--target-base-id", "b2", "--new-name", "copy", "--yes")
if err == nil || out != "" {
t.Fatalf("source fields collection = output:%q err:%v", out, err)
}
})
t.Run("invalid source field", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{{text: `{"fields":[{"fieldName":"F","type":"text"}]}`}}}
out, err := runAITableCompositeCLI(t, caller, "+table-copy",
"--source-base-id", "b1", "--source-table-id", "t1", "--target-base-id", "b2", "--new-name", "copy", "--yes")
if err == nil || out != "" {
t.Fatalf("invalid source field = output:%q err:%v", out, err)
}
})
t.Run("dry run skips unsupported and preserves config", func(t *testing.T) {
payload := map[string]any{"fields": []any{
map[string]any{"fieldId": "skip", "fieldName": "Formula", "type": "FORMULA"},
map[string]any{"fieldId": "keep", "fieldName": "Text", "type": "text", "config": map[string]any{"x": 1}, "aiConfig": map[string]any{"y": 2}},
}}
caller := &upsertByKeyCaller{dryRun: true, steps: []upsertByKeyStep{{text: mustJSONText(t, payload)}}}
out, err := runAITableCompositeCLI(t, caller, "+table-copy",
"--source-base-id", "b1", "--source-table-id", "t1", "--target-base-id", "b2", "--new-name", "copy", "--dry-run", "--yes")
if err != nil || !strings.Contains(out, `"status": "planned"`) || !strings.Contains(out, "skipped field") {
t.Fatalf("dry run = output:%q err:%v", out, err)
}
})
t.Run("source records query error", func(t *testing.T) {
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{{text: `{"fields":[]}`}, {err: errors.New("records failed")}}}
out, err := runAITableCompositeCLI(t, caller, "+table-copy",
"--source-base-id", "b1", "--source-table-id", "t1", "--target-base-id", "b2", "--new-name", "copy", "--include-records", "--yes")
if err == nil || out != "" {
t.Fatalf("source records = output:%q err:%v", out, err)
}
})
}
func TestCrossPlatformCoverageTableCopySchemaFailureStagesE2E(t *testing.T) {
source := mustJSONText(t, map[string]any{"fields": copyFields(1, "s")})
cases := []struct {
name string
steps []upsertByKeyStep
}{
{name: "target fields error", steps: []upsertByKeyStep{{text: source}, {text: `{"tableId":"target"}`}, {err: errors.New("target fields failed")}}},
{name: "target fields mismatch", steps: []upsertByKeyStep{{text: source}, {text: `{"tableId":"target"}`}, {text: `{"fields":[]}`}}},
{name: "target mapping duplicate", steps: []upsertByKeyStep{
{text: source}, {text: `{"tableId":"target"}`},
{text: `{"fields":[{"fieldId":"t1","fieldName":"F0","type":"text"},{"fieldId":"t2","fieldName":"F0","type":"text"}]}`},
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{steps: tc.steps}, "+table-copy",
"--source-base-id", "b1", "--source-table-id", "t1", "--target-base-id", "b2", "--new-name", "copy", "--yes")
if err == nil || out != "" {
t.Fatalf("schema failure = output:%q err:%v", out, err)
}
})
}
}
func TestCrossPlatformCoverageTableCopyFieldChunkErrorRecoveredE2E(t *testing.T) {
sourceFields := copyFields(16, "s")
targetFields := copyFields(16, "t")
caller := &upsertByKeyCaller{steps: []upsertByKeyStep{
{text: mustJSONText(t, map[string]any{"fields": sourceFields})},
{text: `{"tableId":"target"}`},
{err: errors.New("create fields reply failed")},
{text: mustJSONText(t, map[string]any{"fields": targetFields})},
}}
out, err := runAITableCompositeCLI(t, caller, "+table-copy",
"--source-base-id", "b1", "--source-table-id", "t1", "--target-base-id", "b2", "--new-name", "copy", "--yes")
if err != nil || !strings.Contains(out, "create_fields offset") || !strings.Contains(out, `"fieldCount": 16`) {
t.Fatalf("field chunk recovery = output:%q err:%v", out, err)
}
}
func TestCrossPlatformCoverageTableCopyRecordFailureStagesE2E(t *testing.T) {
source := mustJSONText(t, map[string]any{"fields": []any{sourceFieldFixture()}})
target := mustJSONText(t, map[string]any{"fields": []any{targetFieldFixture()}})
cases := []struct {
name string
record string
after []upsertByKeyStep
}{
{name: "source record missing cells", record: `{"records":[{"recordId":"source"}]}`},
{name: "created IDs read-back error", record: `{"records":[{"recordId":"source","cells":{"sf1":"v"}}]}`, after: []upsertByKeyStep{
{text: `{"createdRecords":[{"recordId":"target-record"}]}`}, {err: errors.New("target record read failed")},
}},
{name: "created records mismatch", record: `{"records":[{"recordId":"source","cells":{"sf1":"v"}}]}`, after: []upsertByKeyStep{
{text: `{"createdRecords":[{"recordId":"target-record"}]}`}, {text: `{"records":[{"recordId":"target-record","cells":{"tf1":"wrong"}}]}`},
}},
{name: "fallback records list", record: `{"records":[{"recordId":"source","cells":{"sf1":"v"}}]}`, after: []upsertByKeyStep{
{text: `{"records":[{"recordId":"target-record"}]}`}, {text: `{"records":[{"recordId":"target-record","cells":{"tf1":"v"}}]}`},
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
steps := []upsertByKeyStep{{text: source}, {text: tc.record}, {text: `{"tableId":"target"}`}, {text: target}}
steps = append(steps, tc.after...)
out, err := runAITableCompositeCLI(t, &upsertByKeyCaller{steps: steps}, "+table-copy",
"--source-base-id", "b1", "--source-table-id", "t1", "--target-base-id", "b2", "--new-name", "copy", "--include-records", "--yes")
if tc.name == "fallback records list" {
if err != nil || !strings.Contains(out, `"recordCount": 1`) {
t.Fatalf("fallback success = output:%q err:%v", out, err)
}
return
}
if err == nil || out != "" {
t.Fatalf("record failure = output:%q err:%v", out, err)
}
})
}
}
func TestCrossPlatformCoverageTableCopyPureHelpers(t *testing.T) {
if _, err := mapCopiedFieldIDs(nil, []map[string]any{{"fieldId": "a", "fieldName": "same"}, {"fieldId": "b", "fieldName": "same"}}); err == nil {
t.Fatal("duplicate target field must fail")
}
if _, err := mapCopiedFieldIDs([]map[string]any{{"fieldId": "s", "fieldName": "missing"}}, nil); err == nil {
t.Fatal("missing target field must fail")
}
if normalizedFieldType("FoRmUlA") != "formula" || normalizedFieldType("custom") != "custom" {
t.Fatal("field type normalization mismatch")
}
}
@@ -0,0 +1,111 @@
// Copyright 2026 Alibaba Group
// SPDX-License-Identifier: Apache-2.0
package aitabletarget
import (
"errors"
"fmt"
"net/url"
"strings"
"testing"
)
func TestCrossPlatformCoverageParseAITableURLRemainingEdges(t *testing.T) {
for _, raw := range []string{
"https://alidocs.dingtalk.com/i/nodes/base%2Fchild",
"https://alidocs.dingtalk.com/i/nodes/base?iframeQuery=%25",
"https://alidocs.dingtalk.com/i/nodes/base?viewId=one&viewId=two&sheetId=table",
"https://alidocs.dingtalk.com/i/nodes/base?recordId=one&recordId=two&sheetId=table",
} {
if _, err := ParseURL(raw); err == nil {
t.Fatalf("ParseURL(%q) succeeded", raw)
}
}
if _, err := parseIframeQuery("%"); err == nil {
t.Fatal("invalid iframe escape succeeded")
}
if values, err := parseIframeQuery("preview"); err != nil || !values.Has("preview") {
t.Fatalf("plain iframe query = %#v, %v", values, err)
}
if _, err := parseIframeQuery("key=%zz"); err == nil {
t.Fatal("invalid iframe query value succeeded")
}
values := url.Values{"id": {" ", "bad/id"}}
if _, err := uniqueQueryID(values, "id"); err == nil {
t.Fatal("invalid query ID succeeded")
}
if validID("ok\x7fno") {
t.Fatal("control character ID succeeded")
}
}
func TestCrossPlatformCoverageResolveBaseRemainingFailures(t *testing.T) {
if _, err := ResolveBaseName(&resolverReader{}, " ", false); err == nil {
t.Fatal("empty base name succeeded")
}
reader := &resolverReader{steps: []resolverStep{{err: errors.New("search failed")}}}
if _, err := ResolveBaseName(reader, "name", false); err == nil {
t.Fatal("search error was swallowed")
}
reader = &resolverReader{steps: []resolverStep{
{data: map[string]any{"bases": []any{}, "nextCursor": "same"}},
{data: map[string]any{"bases": []any{}, "nextCursor": "same"}},
}}
if _, err := ResolveBaseName(reader, "name", false); errorReason(err) != "target_incomplete" {
t.Fatalf("cursor cycle = %v", err)
}
steps := make([]resolverStep, maxResolutionPages)
for index := range steps {
steps[index] = resolverStep{data: map[string]any{"bases": []any{}, "nextCursor": fmt.Sprintf("cursor-%d", index)}}
}
reader = &resolverReader{steps: steps}
if _, err := ResolveBaseName(reader, "name", false); errorReason(err) != "target_incomplete" || len(reader.calls) != maxResolutionPages {
t.Fatalf("page bound = err:%v calls:%d", err, len(reader.calls))
}
}
func TestCrossPlatformCoverageResolveTableRemainingFailures(t *testing.T) {
for _, pair := range [][2]string{{"bad/id", "name"}, {"base", " "}} {
if _, err := ResolveTableName(&resolverReader{}, pair[0], pair[1], false); err == nil {
t.Fatalf("invalid table args %#v succeeded", pair)
}
}
reader := &resolverReader{steps: []resolverStep{{err: errors.New("tables failed")}}}
if _, err := ResolveTableName(reader, "base", "name", false); err == nil {
t.Fatal("table error was swallowed")
}
reader = &resolverReader{steps: []resolverStep{{data: map[string]any{"tables": "bad"}}}}
if _, err := ResolveTableName(reader, "base", "name", false); errorReason(err) != "target_invalid_response" {
t.Fatalf("invalid table list = %v", err)
}
reader = &resolverReader{steps: []resolverStep{{data: map[string]any{"success": true}}}}
if _, err := ResolveTableName(reader, "base", "name", false); errorReason(err) != "target_invalid_response" {
t.Fatalf("missing table list = %v", err)
}
}
func TestCrossPlatformCoverageResolverPureShapeHelpers(t *testing.T) {
if list, found, err := findObjectList(nil, "items"); err != nil || found || list != nil {
t.Fatalf("nil list = %#v, %v, %v", list, found, err)
}
if _, _, err := findObjectList(map[string]any{"items": "bad"}, "items"); err == nil {
t.Fatal("invalid list field succeeded")
}
if list, found, err := findObjectList(map[string]any{"items": map[string]any{"items": []any{}}}, "items"); err != nil || !found || len(list) != 0 {
t.Fatalf("nested keyed list = %#v, %v, %v", list, found, err)
}
cursor, more, known := pagination(nil)
if cursor != "" || more || known {
t.Fatalf("nil pagination = %q, %v, %v", cursor, more, known)
}
cursor, more, known = pagination(map[string]any{"cursor": "outer", "data": map[string]any{"nextCursor": "inner", "hasMore": true}})
if cursor != "outer" || !more || !known {
t.Fatalf("nested pagination = %q, %v, %v", cursor, more, known)
}
got := dedupe([]Candidate{{}, {ID: "one", Name: "first"}, {ID: "one", Name: "duplicate"}, {ID: "two", Name: "second"}})
if len(got) != 2 || strings.Join([]string{got[0].ID, got[1].ID}, ",") != "one,two" {
t.Fatalf("dedupe = %#v", got)
}
}
+16
View File
@@ -4,8 +4,10 @@
package shortcut
import (
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/spf13/cobra"
)
@@ -16,3 +18,17 @@ func TestCrossPlatformCoverageRuntimeContextForTest(t *testing.T) {
t.Fatalf("RuntimeContextForTest = %#v", rt)
}
}
func TestCrossPlatformCoverageRuntimeWriteDataRemainingBranches(t *testing.T) {
caller := &runtimeReadCoverageCaller{text: `not-json`}
old := helpers.GetCaller()
t.Cleanup(func() { helpers.InitDeps(old) })
helpers.InitDeps(caller)
rt := &RuntimeContext{}
if _, err := rt.callMCPWriteData("aitable", "update_records", nil); err == nil || !strings.Contains(err.Error(), "解析") {
t.Fatalf("invalid write JSON = %v", err)
}
if caller.args == nil {
t.Fatal("nil write parameters were not normalized")
}
}
+34 -9
View File
@@ -32,7 +32,7 @@ func (s *stubFetcher) Fetch(ctx context.Context, cursor string) (Page, error) {
return s.pages[idx], nil
}
func TestFetchAll_SinglePage(t *testing.T) {
func TestCrossPlatformCoverageFetchAllSinglePage(t *testing.T) {
s := &stubFetcher{
pages: []Page{
{Records: []any{"a", "b", "c"}, NextCursor: ""},
@@ -44,7 +44,7 @@ func TestFetchAll_SinglePage(t *testing.T) {
}
}
func TestFetchAll_MultiPage(t *testing.T) {
func TestCrossPlatformCoverageFetchAllMultiPage(t *testing.T) {
s := &stubFetcher{
pages: []Page{
{Records: []any{1, 2}, NextCursor: "c1"},
@@ -58,7 +58,7 @@ func TestFetchAll_MultiPage(t *testing.T) {
}
}
func TestFetchAll_PageLimit(t *testing.T) {
func TestCrossPlatformCoverageFetchAllPageLimit(t *testing.T) {
s := &stubFetcher{
pages: []Page{
{Records: []any{1}, NextCursor: "c1"},
@@ -85,7 +85,7 @@ func TestFetchAll_PageLimit(t *testing.T) {
}
}
func TestFetchAll_MidErrorPartial(t *testing.T) {
func TestCrossPlatformCoverageFetchAllMidErrorPartial(t *testing.T) {
upstreamErr := errors.New("502 bad gateway")
s := &stubFetcher{
pages: []Page{
@@ -108,7 +108,7 @@ func TestFetchAll_MidErrorPartial(t *testing.T) {
}
}
func TestFetchAll_InitialCursorResume(t *testing.T) {
func TestCrossPlatformCoverageFetchAllInitialCursorResume(t *testing.T) {
s := &stubFetcher{
pages: []Page{
{Records: []any{"resumed"}, NextCursor: ""},
@@ -123,7 +123,7 @@ func TestFetchAll_InitialCursorResume(t *testing.T) {
}
}
func TestFetchAll_ContextCancel(t *testing.T) {
func TestCrossPlatformCoverageFetchAllContextCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // 立即取消
s := &stubFetcher{pages: []Page{{Records: []any{1}, NextCursor: "c1"}}}
@@ -136,7 +136,7 @@ func TestFetchAll_ContextCancel(t *testing.T) {
}
}
func TestFetchAll_UnlimitedPageLimit(t *testing.T) {
func TestCrossPlatformCoverageFetchAllUnlimitedPageLimit(t *testing.T) {
// PageLimit=0 是公开契约中的“无限制”。用超过默认安全阀的页数防止
// 实现再次偷偷把 0 改写成 DefaultPageLimit。
pages := make([]Page, DefaultPageLimit+1)
@@ -157,7 +157,7 @@ func TestFetchAll_UnlimitedPageLimit(t *testing.T) {
}
}
func TestFetchAll_PreservesServerTotalCount(t *testing.T) {
func TestCrossPlatformCoverageFetchAllPreservesServerTotalCount(t *testing.T) {
total := 123
s := &stubFetcher{pages: []Page{{Records: []any{1, 2}, TotalCount: &total}}}
got := FetchAll(context.Background(), s.Fetch, Options{InterPageDelay: time.Millisecond})
@@ -166,7 +166,7 @@ func TestFetchAll_PreservesServerTotalCount(t *testing.T) {
}
}
func TestFetchAll_StopsOnCursorCycle(t *testing.T) {
func TestCrossPlatformCoverageFetchAllStopsOnCursorCycle(t *testing.T) {
s := &stubFetcher{pages: []Page{
{Records: []any{1}, NextCursor: "c1"},
{Records: []any{2}, NextCursor: "c1"},
@@ -179,3 +179,28 @@ func TestFetchAll_StopsOnCursorCycle(t *testing.T) {
t.Fatalf("cursor cycle progress: got=%+v", got)
}
}
func TestCrossPlatformCoverageFetchAllCancellationDuringDelay(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
calls := 0
got := FetchAll(ctx, func(context.Context, string) (Page, error) {
calls++
time.AfterFunc(10*time.Millisecond, cancel)
return Page{Records: []any{"first"}, NextCursor: "next"}, nil
}, Options{InterPageDelay: time.Second})
if calls != 1 || got.StopReason != StopCanceled || !errors.Is(got.Err, context.Canceled) || got.LastCursor != "next" {
t.Fatalf("delay cancellation = %+v calls:%d", got, calls)
}
}
func TestCrossPlatformCoverageFetchAllStopsOnPriorCursorCycle(t *testing.T) {
s := &stubFetcher{pages: []Page{
{Records: []any{1}, NextCursor: "c1"},
{Records: []any{2}, NextCursor: "c2"},
{Records: []any{3}, NextCursor: "c1"},
}}
got := FetchAll(context.Background(), s.Fetch, Options{InterPageDelay: time.Millisecond})
if got.StopReason != StopCursorCycle || got.Pages != 3 || got.LastCursor != "c1" || !errors.Is(got.Err, ErrCursorCycle) {
t.Fatalf("prior cursor cycle = %+v", got)
}
}