* fix(compat): accept YAML flow as a fallback for json_parse transform
Users who run `dws aitable table create --fields [{fieldName: 标题, ...}]`
hit a shell glob error (`zsh: bad pattern: [{fieldName:`) because the
unquoted brackets are interpreted as a zsh pattern. Even when the shell
is placated with outer quotes, the strict-JSON parser still rejected the
natural form that drops quotes around keys and string values.
transformJSONParse now tries strict JSON first (unchanged fast path) and,
on failure, falls back to yaml.Unmarshal. YAML's flow syntax is a
superset of JSON that accepts `{key: value}` without surrounding quotes,
so a single set of outer quotes is all the user needs:
dws aitable table create --fields '[{fieldName: 标题, type: text}]'
Already-valid strict JSON is unaffected. The @file syntax (`--fields
@path.json`) continues to work through the same transform.
The terminal error message, if both parsers fail, now points users at
the two working forms (quoted YAML flow or @file).
* fix(helpers): route chat send-by-bot to bot product
`chat message send-by-bot` previously stamped its invocation with
CanonicalProduct="chat", routing it through the chat product's auth /
server dispatch. The command is semantically a bot operation — switch
it to "bot" so auth client selection and downstream routing match the
actual MCP server. Extends chat_test.go with single-chat and group-chat
routing assertions.
126 lines
4.2 KiB
Go
126 lines
4.2 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package compat
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// TestJSONParse_StrictJSON covers the primary path: callers passing
|
|
// canonical JSON (as generated programmatically or by agents).
|
|
func TestJSONParse_StrictJSON(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
input := `[{"fieldName":"title","type":"text"},{"fieldName":"count","type":"number"}]`
|
|
got, err := ApplyTransform(input, "json_parse", nil)
|
|
if err != nil {
|
|
t.Fatalf("strict JSON should parse, got err: %v", err)
|
|
}
|
|
arr, ok := got.([]any)
|
|
if !ok || len(arr) != 2 {
|
|
t.Fatalf("expected []any of length 2, got %T %v", got, got)
|
|
}
|
|
}
|
|
|
|
// TestJSONParse_YAMLFlowFallback is the motivating case: a user types an
|
|
// ad-hoc JSON-shaped array without quoting every key and value. YAML flow
|
|
// syntax accepts it and the parsed output is indistinguishable from the
|
|
// strict-JSON equivalent.
|
|
func TestJSONParse_YAMLFlowFallback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Intentionally unquoted keys, unquoted string values, and Chinese
|
|
// identifiers — typical of what humans type at a shell.
|
|
input := `[{fieldName: 标题, type: text}, {fieldName: 数量, type: number, config: {formatter: INT}}, {fieldName: 状态, type: singleSelect, config: {options: [{name: 待办}, {name: 进行中}, {name: 已完成}]}}, {fieldName: 已确认, type: checkbox}]`
|
|
|
|
got, err := ApplyTransform(input, "json_parse", nil)
|
|
if err != nil {
|
|
t.Fatalf("YAML-flow input should parse, got err: %v", err)
|
|
}
|
|
arr, ok := got.([]any)
|
|
if !ok {
|
|
t.Fatalf("expected []any, got %T", got)
|
|
}
|
|
if len(arr) != 4 {
|
|
t.Fatalf("expected 4 field definitions, got %d", len(arr))
|
|
}
|
|
|
|
// Spot-check the third entry, which is the most deeply nested.
|
|
third, ok := arr[2].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("arr[2] expected map[string]any, got %T", arr[2])
|
|
}
|
|
if third["fieldName"] != "状态" {
|
|
t.Errorf("arr[2].fieldName: want 状态, got %v", third["fieldName"])
|
|
}
|
|
config, ok := third["config"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("arr[2].config expected map, got %T", third["config"])
|
|
}
|
|
options, ok := config["options"].([]any)
|
|
if !ok || len(options) != 3 {
|
|
t.Fatalf("arr[2].config.options: want 3 items, got %v", config["options"])
|
|
}
|
|
}
|
|
|
|
// TestJSONParse_EmptyString preserves the legacy behaviour of returning the
|
|
// original value untouched when the caller passes an empty / whitespace-only
|
|
// string, matching how other transforms treat empty input.
|
|
func TestJSONParse_EmptyString(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []string{"", " ", "\n\t"}
|
|
for _, in := range cases {
|
|
got, err := ApplyTransform(in, "json_parse", nil)
|
|
if err != nil {
|
|
t.Errorf("empty input %q should not error: %v", in, err)
|
|
continue
|
|
}
|
|
if !reflect.DeepEqual(got, in) {
|
|
t.Errorf("empty input %q should pass through, got %v", in, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestJSONParse_NonString passes through non-string inputs (already-parsed
|
|
// values flowing through the pipeline).
|
|
func TestJSONParse_NonString(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
preParsed := []any{map[string]any{"k": "v"}}
|
|
got, err := ApplyTransform(preParsed, "json_parse", nil)
|
|
if err != nil {
|
|
t.Fatalf("non-string should pass through: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, preParsed) {
|
|
t.Errorf("non-string should pass through unchanged, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestJSONParse_InvalidInput verifies that genuine garbage is still rejected
|
|
// with a user-facing validation error that nudges towards `@file` syntax.
|
|
func TestJSONParse_InvalidInput(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Unterminated bracket — neither valid JSON nor valid YAML flow.
|
|
_, err := ApplyTransform("[{fieldName:", "json_parse", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for malformed input")
|
|
}
|
|
if msg := err.Error(); msg == "" {
|
|
t.Fatal("error message should be non-empty")
|
|
}
|
|
}
|