mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
* fix: unify dry-run output contract
* fix: address dry-run review feedback
* fix(dryrun): tighten preview contract and unify data shape
- transcribe HTTP method verbatim in previews (HEAD/OPTIONS were
reported as GET); reject an empty method in api with a typed error
- unify the dry-run data payload across api/service/shortcut paths:
{api, context?: {app_id, user_open_id}}; drop data.as — the envelope
top-level identity is the single identity source
- mark pretty dry-run stdout with '# dry-run: request not sent' so logs
that drop stderr still show it was a preview
- extract the shared preview builder, collapse PrintDryRunWithFile's
loose params into FileUploadMeta, and fail loudly on nil previews
- revert description-marker identity parsing: stale prose must not
override corrected accessTokens (blocks legal user calls on
images.create); identity gating keys off accessTokens only
- pin the new contracts with tests: verbatim method, three-way context
parity, nil-preview error, empty-context omission, marker line
* docs(agents): add typed-data, faithful-transcription, and contract-test conventions
- typed struct at the boundary over map[string]interface{} threading;
distinct types where values could swap silently (internal/meta.Token)
- transcribe input verbatim in previews/transformations; reject
unhonorable flag combinations with typed errors instead of silently
substituting behavior
- contract tests must fail when the implementation is reverted
* test: migrate dry-run tests grown on main to the envelope format
main gained raw-format dry-run readers while the PR was in flight
(wiki drive export #1802, drive list comments #1845, slash commands,
sheets history, docs fetch, mail draft-send/triage, vc meeting events).
Migrate them to the envelope accessors (clie2e.DryRunGet / data-wrapped
decoders) and drop the now-redundant DryRunData extractions in files
unified on DryRunGet.
---------
Co-authored-by: guokexin.02 <264159873+Tantanz20020918@users.noreply.github.com>
128 lines
4.6 KiB
Go
128 lines
4.6 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package apps
|
||
|
||
import (
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/larksuite/cli/internal/httpmock"
|
||
)
|
||
|
||
func TestAppsDBTableGet_DefaultJSONReturnsStructuredFields(t *testing.T) {
|
||
factory, stdout, reg := newAppsExecuteFactory(t)
|
||
reg.Register(&httpmock.Stub{
|
||
Method: "GET",
|
||
URL: "/open-apis/spark/v1/apps/app_x/tables/orders",
|
||
Body: map[string]interface{}{
|
||
"code": 0,
|
||
"data": map[string]interface{}{
|
||
"name": "orders",
|
||
"description": "订单表",
|
||
"columns": []interface{}{
|
||
map[string]interface{}{
|
||
"name": "id", "data_type": "int8",
|
||
"is_primary_key": true, "is_unique": true,
|
||
"is_allow_null": false, "default_value": "",
|
||
},
|
||
},
|
||
"indexes": []interface{}{
|
||
map[string]interface{}{"name": "orders_pkey", "type": "btree", "columns": []interface{}{"id"}, "definition": "..."},
|
||
},
|
||
"constraints": []interface{}{
|
||
map[string]interface{}{"type": "primary_key", "name": "orders_pkey", "columns": []interface{}{"id"}},
|
||
},
|
||
"estimated_row_count": 1200,
|
||
"size_bytes": 81920,
|
||
},
|
||
},
|
||
})
|
||
|
||
if err := runAppsShortcut(t, AppsDBTableGet,
|
||
[]string{"+db-table-get", "--app-id", "app_x", "--table", "orders", "--as", "user"},
|
||
factory, stdout); err != nil {
|
||
t.Fatalf("execute err=%v", err)
|
||
}
|
||
if got := stdout.String(); !strings.Contains(got, `"name": "orders"`) {
|
||
t.Fatalf("stdout missing schema name: %s", got)
|
||
}
|
||
}
|
||
|
||
// --format pretty 是触发 DDL 模式的唯一开关。
|
||
// 用 --format json + --dry-run 走 JSON envelope 路径方便 parse,但 query 形态由代码内部
|
||
// 根据 rctx.Format 决定 —— 这里我们直接传 --format pretty + --dry-run,pretty 模式下 dry-run
|
||
// 输出是 plain text 列表,用 substring 校验 format=ddl 出现在 URL query 中。
|
||
func TestAppsDBTableGet_PrettyFormatSendsFormatDDLQuery(t *testing.T) {
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
if err := runAppsShortcut(t, AppsDBTableGet,
|
||
[]string{"+db-table-get", "--app-id", "app_x", "--table", "orders", "--format", "pretty", "--dry-run", "--as", "user"},
|
||
factory, stdout); err != nil {
|
||
t.Fatalf("dry-run err=%v", err)
|
||
}
|
||
got := stdout.String()
|
||
if !strings.Contains(got, "/open-apis/spark/v1/apps/app_x/tables/orders") {
|
||
t.Fatalf("missing URL in dry-run output:\n%s", got)
|
||
}
|
||
if !strings.Contains(got, "format=ddl") {
|
||
t.Fatalf("--format=pretty should trigger ?format=ddl, got:\n%s", got)
|
||
}
|
||
}
|
||
|
||
func TestAppsDBTableGet_NonPrettyFormatsOmitFormatQuery(t *testing.T) {
|
||
// 默认 json / table / ndjson / csv 都走 schema 路径 —— CLI 不传 format query。
|
||
for _, format := range []string{"json", "table", "ndjson", "csv"} {
|
||
t.Run(format, func(t *testing.T) {
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
args := []string{"+db-table-get", "--app-id", "app_x", "--table", "orders", "--format", format, "--dry-run", "--as", "user"}
|
||
if err := runAppsShortcut(t, AppsDBTableGet, args, factory, stdout); err != nil {
|
||
t.Fatalf("dry-run err=%v", err)
|
||
}
|
||
var env dryRunAPIEnvelope
|
||
if err := json.Unmarshal([]byte(stdout.String()), &env); err != nil {
|
||
t.Fatalf("decode: %v", err)
|
||
}
|
||
if _, ok := env.API[0].Params["format"]; ok {
|
||
t.Fatalf("--format=%s should omit format query, got %v", format, env.API[0].Params)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestAppsDBTableGet_PrettyOutputIsDDLTextOnly(t *testing.T) {
|
||
// pretty 模式 stdout 直接打 ddl 字段文本,无 envelope / 表格包装。
|
||
factory, stdout, reg := newAppsExecuteFactory(t)
|
||
ddl := "CREATE TABLE orders (\n id bigint NOT NULL,\n PRIMARY KEY (id)\n);"
|
||
reg.Register(&httpmock.Stub{
|
||
Method: "GET",
|
||
URL: "/open-apis/spark/v1/apps/app_x/tables/orders",
|
||
Body: map[string]interface{}{
|
||
"code": 0,
|
||
"data": map[string]interface{}{"ddl": ddl},
|
||
},
|
||
})
|
||
|
||
if err := runAppsShortcut(t, AppsDBTableGet,
|
||
[]string{"+db-table-get", "--app-id", "app_x", "--table", "orders", "--format", "pretty", "--as", "user"},
|
||
factory, stdout); err != nil {
|
||
t.Fatalf("execute err=%v", err)
|
||
}
|
||
got := stdout.String()
|
||
if !strings.Contains(got, "CREATE TABLE orders") {
|
||
t.Fatalf("pretty output should contain raw DDL, got:\n%s", got)
|
||
}
|
||
if strings.Contains(got, `"data":`) || strings.Contains(got, `"ddl":`) {
|
||
t.Fatalf("pretty output should not be JSON envelope, got:\n%s", got)
|
||
}
|
||
}
|
||
|
||
func TestAppsDBTableGet_RequiresTable(t *testing.T) {
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
err := runAppsShortcut(t, AppsDBTableGet,
|
||
[]string{"+db-table-get", "--app-id", "app_x", "--as", "user"}, factory, stdout)
|
||
if err == nil || !strings.Contains(err.Error(), "table") {
|
||
t.Fatalf("expected table required error, got %v", err)
|
||
}
|
||
}
|