From 63b6112fa2b40b16777fe5155f0c8030720b9c41 Mon Sep 17 00:00:00 2001 From: qinze Date: Tue, 16 Jun 2026 17:09:18 +0800 Subject: [PATCH] =?UTF-8?q?test(dev):=20dev=20=E5=91=BD=E4=BB=A4=E9=9B=86?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=20+=20=E5=BB=BA=E8=81=94/=E7=AB=AF=E7=82=B9?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit helpers 单测、connect/card/stream 测试,app runner 与 direct_runtime devapp 端点路由。 Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/app/direct_runtime_test.go | 12 +- internal/app/runner.go | 2 +- .../helpers/connect_agent_options_test.go | 6 +- internal/helpers/connect_card_test.go | 2 +- internal/helpers/connect_stream.go | 2 +- internal/helpers/devapp_pretty_test.go | 51 +++ internal/helpers/devapp_robot_connect_test.go | 14 +- internal/helpers/devapp_test.go | 322 ++++++++++-------- internal/helpers/devdoc_test.go | 10 +- 9 files changed, 261 insertions(+), 160 deletions(-) create mode 100644 internal/helpers/devapp_pretty_test.go diff --git a/internal/app/direct_runtime_test.go b/internal/app/direct_runtime_test.go index 8363143a..fd0b3fe4 100644 --- a/internal/app/direct_runtime_test.go +++ b/internal/app/direct_runtime_test.go @@ -52,14 +52,14 @@ func TestDirectRuntimeEndpoint_DevappEnvOverrideWithoutRegistry(t *testing.T) { withCleanDynamicRegistry(t) t.Setenv("DINGTALK_DEVAPP_MCP_URL", "https://example.test/server/devapp") - assertEndpoint(t, "devapp", "list_open_dev_apps_by_condition", "https://example.test/server/devapp") + assertEndpoint(t, "devapp", "list_dev_app", "https://example.test/server/devapp") } func TestDirectRuntimeEndpoint_DevappEnvOverridePreservesQuery(t *testing.T) { withCleanDynamicRegistry(t) t.Setenv("DINGTALK_DEVAPP_MCP_URL", "https://example.test/server/devapp?key=secret") - assertEndpoint(t, "devapp", "list_open_dev_apps_by_condition", "https://example.test/server/devapp?key=secret") + assertEndpoint(t, "devapp", "list_dev_app", "https://example.test/server/devapp?key=secret") } func TestDirectRuntimeEndpoint_DevappDynamicServerDoesNotOverrideHardcoded(t *testing.T) { @@ -74,7 +74,7 @@ func TestDirectRuntimeEndpoint_DevappDynamicServerDoesNotOverrideHardcoded(t *te }, }) - assertEndpoint(t, "devapp", "list_open_dev_apps_by_condition", devappEndpoint) + assertEndpoint(t, "devapp", "list_dev_app", devappEndpoint) } func TestDirectRuntimeEndpoint_DevappEditionSupplementDoesNotOverrideHardcoded(t *testing.T) { @@ -95,7 +95,7 @@ func TestDirectRuntimeEndpoint_DevappEditionSupplementDoesNotOverrideHardcoded(t }) t.Cleanup(func() { edition.Override(prev) }) - assertEndpoint(t, "devapp", "list_open_dev_apps_by_condition", devappEndpoint) + assertEndpoint(t, "devapp", "list_dev_app", devappEndpoint) } func TestDirectRuntimeEndpoint_DevappEditionStaticDoesNotOverrideHardcoded(t *testing.T) { @@ -116,7 +116,7 @@ func TestDirectRuntimeEndpoint_DevappEditionStaticDoesNotOverrideHardcoded(t *te }) t.Cleanup(func() { edition.Override(prev) }) - assertEndpoint(t, "devapp", "list_open_dev_apps_by_condition", devappEndpoint) + assertEndpoint(t, "devapp", "list_dev_app", devappEndpoint) } func TestDirectRuntimeEndpoint_DevappEnvOverrideWinsOverEditionSupplement(t *testing.T) { @@ -137,7 +137,7 @@ func TestDirectRuntimeEndpoint_DevappEnvOverrideWinsOverEditionSupplement(t *tes }) t.Cleanup(func() { edition.Override(prev) }) - assertEndpoint(t, "devapp", "list_open_dev_apps_by_condition", "https://example.test/server/devapp-env") + assertEndpoint(t, "devapp", "list_dev_app", "https://example.test/server/devapp-env") } func TestDirectRuntimeEndpoint_DefaultPATFallbackWhenRegistryMissing(t *testing.T) { diff --git a/internal/app/runner.go b/internal/app/runner.go index a15901c6..119f0c7c 100644 --- a/internal/app/runner.go +++ b/internal/app/runner.go @@ -262,7 +262,7 @@ func (r *runtimeRunner) handleCatalogMiss(ctx context.Context, invocation execut hint := "产品 envelope 可能未下发到 discovery,或已经被 serverDeps fail-fast 丢弃;可执行 'dws cache refresh' 强制重新 discovery,仍失败请向 Portal 确认 envelope 状态。" actions := []string{"dws cache refresh"} if strings.TrimSpace(invocation.CanonicalProduct) == devappProductID { - hint = "devapp 是 helper-only 产品,命令树不依赖 discovery;真实调用需要内部版通过 SupplementServers/StaticServers 注入 MCP endpoint,或本地调试临时设置 DINGTALK_DEVAPP_MCP_URL。" + hint = "dev app(product id: devapp)是 helper-only 产品,命令树不依赖 discovery;真实调用需要内部版通过 SupplementServers/StaticServers 注入 MCP endpoint,或本地调试临时设置 DINGTALK_DEVAPP_MCP_URL。" actions = []string{ "检查内部版 SupplementServers/StaticServers 是否包含 devapp endpoint", "本地调试可临时设置 DINGTALK_DEVAPP_MCP_URL 后重试", diff --git a/internal/helpers/connect_agent_options_test.go b/internal/helpers/connect_agent_options_test.go index a8ada37a..88162747 100644 --- a/internal/helpers/connect_agent_options_test.go +++ b/internal/helpers/connect_agent_options_test.go @@ -132,7 +132,7 @@ func TestRobotConnectAgentFlagsInDryRun(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs([]string{"devapp", "robot", "connect", + root.SetArgs([]string{"dev", "connect", "--channel", "claudecode", "--robot-client-id", "id1", "--robot-client-secret", "sec1", "--agent-model", "claude-sonnet-4-6", "--agent-workdir", "/tmp/kb", @@ -151,7 +151,7 @@ func TestRobotConnectAgentFlagsInDryRun(t *testing.T) { root = newDevAppTestRoot(&captureRunner{}) root.SetOut(&out) root.SetErr(&out) - root.SetArgs([]string{"devapp", "robot", "connect", + root.SetArgs([]string{"dev", "connect", "--channel", "claudecode", "--robot-client-id", "id1", "--robot-client-secret", "sec1", "--agent-memory=false", "--dry-run"}) @@ -179,7 +179,7 @@ func TestRobotConnectDryRunShowsCliStatus(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs([]string{"devapp", "robot", "connect", "--channel", channel, + root.SetArgs([]string{"dev", "connect", "--channel", channel, "--robot-client-id", "a", "--robot-client-secret", "b", "--dry-run"}) if err := root.Execute(); err != nil { t.Fatalf("Execute(%s): %v\n%s", channel, err, out.String()) diff --git a/internal/helpers/connect_card_test.go b/internal/helpers/connect_card_test.go index 03f1f96b..c8671034 100644 --- a/internal/helpers/connect_card_test.go +++ b/internal/helpers/connect_card_test.go @@ -230,7 +230,7 @@ func TestRobotConnectReplyCardFlag(t *testing.T) { var out strings.Builder root.SetOut(&out) root.SetErr(&out) - args := append([]string{"devapp", "robot", "connect", + args := append([]string{"dev", "connect", "--channel", "claudecode", "--robot-client-id", "a", "--robot-client-secret", "b", "--dry-run"}, tc.args...) root.SetArgs(args) diff --git a/internal/helpers/connect_stream.go b/internal/helpers/connect_stream.go index e73cddd5..960322bb 100644 --- a/internal/helpers/connect_stream.go +++ b/internal/helpers/connect_stream.go @@ -66,7 +66,7 @@ type forwarder interface { } // connectAgentOptions carries the user-facing agent tuning exposed on -// `devapp robot connect` (and mirrored env vars). Zero value = defaults: +// `dev connect` (and mirrored env vars). Zero value = defaults: // channel's built-in model, per-conversation memory on (where the CLI supports // it), empty scratch workdir. type connectAgentOptions struct { diff --git a/internal/helpers/devapp_pretty_test.go b/internal/helpers/devapp_pretty_test.go new file mode 100644 index 00000000..c236cc81 --- /dev/null +++ b/internal/helpers/devapp_pretty_test.go @@ -0,0 +1,51 @@ +// 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 helpers + +import "testing" + +func TestDevAppPrettyAnnotateAppStatus(t *testing.T) { + payload := map[string]any{ + "result": map[string]any{ + "items": []any{ + map[string]any{"status": float64(1)}, + map[string]any{"status": float64(0)}, + }, + }, + } + devAppPrettyAnnotate("list_dev_app", payload) + items := payload["result"].(map[string]any)["items"].([]any) + if got := items[0].(map[string]any)["statusText"]; got != "ACTIVE 已激活" { + t.Fatalf("statusText = %v, want ACTIVE 已激活", got) + } + if got := items[1].(map[string]any)["statusText"]; got != "IN_ACTIVE 已停用" { + t.Fatalf("statusText = %v, want IN_ACTIVE 已停用", got) + } +} + +func TestDevAppPrettyAnnotateVersionStatusKindSeparation(t *testing.T) { + // 版本工具的字符串枚举要注解 + version := map[string]any{"status": "INIT", "versionStatus": "RELEASE"} + devAppPrettyAnnotate("get_dev_app_version", version) + if version["statusText"] != "未发布(有待发布变更)" || version["versionStatusText"] != "已发布生效" { + t.Fatalf("version annotations wrong: %#v", version) + } + + // 应用工具的数字枚举不能套用版本语义;未登记的工具不动 + unknown := map[string]any{"status": float64(1)} + devAppPrettyAnnotate("some_unknown_tool", unknown) + if _, exists := unknown["statusText"]; exists { + t.Fatalf("unknown tool should not be annotated: %#v", unknown) + } +} diff --git a/internal/helpers/devapp_robot_connect_test.go b/internal/helpers/devapp_robot_connect_test.go index 90b2b752..d0508625 100644 --- a/internal/helpers/devapp_robot_connect_test.go +++ b/internal/helpers/devapp_robot_connect_test.go @@ -20,12 +20,12 @@ import ( "testing" ) -// TestDevAppRobotConnectRegistered confirms `devapp robot connect` is wired into +// TestDevAppRobotConnectRegistered confirms `dev connect` is wired into // the robot subtree alongside the (reused) provisioning commands. func TestDevAppRobotConnectRegistered(t *testing.T) { root := newDevAppCommand(&captureRunner{}) if _, _, err := root.Find([]string{"robot", "connect"}); err != nil { - t.Fatalf("devapp robot connect not registered: %v", err) + t.Fatalf("dev connect not registered: %v", err) } // Provisioning stays under robot (reused, not duplicated by connect). for _, name := range []string{"create", "submit", "result"} { @@ -72,7 +72,7 @@ func TestDevAppRobotConnectValidation(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs(append([]string{"devapp", "robot", "connect"}, tc.args...)) + root.SetArgs(append([]string{"dev", "connect"}, tc.args...)) err := root.Execute() if tc.wantErr != "" { @@ -94,7 +94,7 @@ func TestDevAppRobotConnectValidation(t *testing.T) { } // TestDevAppFetchCredentials checks that --unified-app-id credential resolution -// reuses get_open_dev_app_credentials and extracts clientId/clientSecret from the +// reuses get_dev_app_credentials and extracts clientId/clientSecret from the // MCP envelope. func TestDevAppFetchCredentials(t *testing.T) { runner := &devAppResponseRunner{response: map[string]any{ @@ -106,7 +106,7 @@ func TestDevAppFetchCredentials(t *testing.T) { }, }} root := newDevAppTestRoot(runner) - connectCmd, _, err := root.Find([]string{"devapp", "robot", "connect"}) + connectCmd, _, err := root.Find([]string{"dev", "connect"}) if err != nil { t.Fatalf("find connect: %v", err) } @@ -119,8 +119,8 @@ func TestDevAppFetchCredentials(t *testing.T) { if id != "ck-123" || secret != "cs-456" { t.Fatalf("got id=%q secret=%q, want ck-123/cs-456", id, secret) } - if runner.last.Tool != "get_open_dev_app_credentials" { - t.Fatalf("Tool = %q, want get_open_dev_app_credentials", runner.last.Tool) + if runner.last.Tool != "get_dev_app_credentials" { + t.Fatalf("Tool = %q, want get_dev_app_credentials", runner.last.Tool) } if runner.last.CanonicalProduct != devAppProduct { t.Fatalf("CanonicalProduct = %q, want %q", runner.last.CanonicalProduct, devAppProduct) diff --git a/internal/helpers/devapp_test.go b/internal/helpers/devapp_test.go index b672d2e3..030481d4 100644 --- a/internal/helpers/devapp_test.go +++ b/internal/helpers/devapp_test.go @@ -18,7 +18,7 @@ func newDevAppTestRoot(runner executor.Runner) *cobra.Command { } root.PersistentFlags().Bool("dry-run", false, "dry run") root.PersistentFlags().Bool("yes", false, "yes") - root.AddCommand(newDevAppCommand(runner)) + root.AddCommand(devHandler{}.Command(runner)) return root } @@ -44,8 +44,8 @@ func TestDevAppMemberCommandsBuildToolParams(t *testing.T) { { name: "list", cmd: "list", - args: []string{"--app-id", "app-001"}, - wantTool: "list_open_dev_app_members", + args: []string{"--unified-app-id", "app-001"}, + wantTool: "list_dev_app_members", wantParams: map[string]any{ "unifiedAppId": "app-001", }, @@ -53,8 +53,8 @@ func TestDevAppMemberCommandsBuildToolParams(t *testing.T) { { name: "add multiple users", cmd: "add", - args: []string{"--app-id", "app-001", "--users", "userId1,userId2,userId3,userId4", "--member-type", "DEVELOPER", "--yes"}, - wantTool: "add_open_dev_app_members", + args: []string{"--unified-app-id", "app-001", "--users", "userId1,userId2,userId3,userId4", "--member-type", "DEVELOPER", "--yes"}, + wantTool: "add_dev_app_members", wantParams: map[string]any{ "unifiedAppId": "app-001", "memberUserIds": []string{"userId1", "userId2", "userId3", "userId4"}, @@ -64,8 +64,8 @@ func TestDevAppMemberCommandsBuildToolParams(t *testing.T) { { name: "remove trims users", cmd: "remove", - args: []string{"--app-id", "app-001", "--users", " userId1 , userId2 ", "--member-type", "DEVELOPER", "--yes"}, - wantTool: "remove_open_dev_app_members", + args: []string{"--unified-app-id", "app-001", "--users", " userId1 , userId2 ", "--member-type", "DEVELOPER", "--yes"}, + wantTool: "remove_dev_app_members", wantParams: map[string]any{ "unifiedAppId": "app-001", "memberUserIds": []string{"userId1", "userId2"}, @@ -81,7 +81,7 @@ func TestDevAppMemberCommandsBuildToolParams(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs(append([]string{"devapp", "member", tc.cmd}, tc.args...)) + root.SetArgs(append([]string{"dev", "app", "member", tc.cmd}, tc.args...)) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) @@ -100,23 +100,37 @@ func TestDevAppMemberCommandsBuildToolParams(t *testing.T) { } } -func TestDevAppCommandHasAppAliasAndCoreCommands(t *testing.T) { - root := newDevAppCommand(&captureRunner{}) - if root.Name() != "devapp" { - t.Fatalf("Name() = %q, want devapp", root.Name()) +func TestDevCommandTree(t *testing.T) { + dev := devHandler{}.Command(&captureRunner{}) + if dev.Name() != "dev" { + t.Fatalf("Name() = %q, want dev", dev.Name()) } - hasAlias := false - for _, alias := range root.Aliases { - if alias == "app" { - hasAlias = true + if len(dev.Aliases) != 0 { + t.Fatalf("Aliases = %v, want none (clean switch from devapp)", dev.Aliases) + } + // 三支柱:app / connect / doc + for _, name := range []string{"app", "connect", "doc"} { + if _, _, err := dev.Find([]string{name}); err != nil { + t.Fatalf("missing subtree %q: %v", name, err) } } - if !hasAlias { - t.Fatalf("Aliases = %v, want app", root.Aliases) + app, _, err := dev.Find([]string{"app"}) + if err != nil { + t.Fatalf("find app: %v", err) } - for _, name := range []string{"list", "get", "create", "update", "delete", "inactive", "active", "credentials", "webapp", "permission", "member", "security", "robot", "version"} { - if _, _, err := root.Find([]string{name}); err != nil { - t.Fatalf("missing command %q: %v", name, err) + for _, name := range []string{"list", "get", "create", "update", "delete", "disable", "enable", "credentials", "webapp", "permission", "member", "security", "robot", "version", "event"} { + if _, _, err := app.Find([]string{name}); err != nil { + t.Fatalf("missing app command %q: %v", name, err) + } + } + // connect 已从 robot 子树提升为 dev connect,robot 下不应再有 + robot, _, err := app.Find([]string{"robot"}) + if err != nil { + t.Fatalf("find robot: %v", err) + } + for _, sub := range robot.Commands() { + if sub.Name() == "connect" { + t.Fatalf("robot subtree still has connect; it moved to dev connect") } } } @@ -128,16 +142,6 @@ func TestDevAppRobotCommandsBuildToolParams(t *testing.T) { wantTool string wantParams map[string]any }{ - { - name: "create sync", - args: []string{"robot", "create", "--app-name", "智能体", "--robot-name", "小助手", "--desc", "审批问答", "--yes"}, - wantTool: "create_dingtalk_robot", - wantParams: map[string]any{ - "appName": "智能体", - "robotName": "小助手", - "desc": "审批问答", - }, - }, { name: "submit async fills media placeholders", args: []string{"robot", "submit", "--app-name", "智能体", "--robot-name", "小助手", "--desc", "审批问答", "--task-id", "t-1", "--yes"}, @@ -146,7 +150,7 @@ func TestDevAppRobotCommandsBuildToolParams(t *testing.T) { "appName": "智能体", "robotName": "小助手", "desc": "审批问答", - "robotMediaId": "", + "icon": "", "previewMediaId": "", "taskId": "t-1", }, @@ -160,13 +164,13 @@ func TestDevAppRobotCommandsBuildToolParams(t *testing.T) { { name: "get config", args: []string{"robot", "get", "--unified-app-id", "u-1"}, - wantTool: "get_open_dev_app_robot_config", + wantTool: "get_extension_robot_config", wantParams: map[string]any{"unifiedAppId": "u-1"}, }, { name: "config create with skills and mode", args: []string{"robot", "config", "--unified-app-id", "u-1", "--name", "小助手", "--brief", "审批助手", "--mode", "2", "--skills", "qa,approval", "--add-scope", "--yes"}, - wantTool: "create_open_dev_app_robot_config", + wantTool: "set_extension_robot_config", wantParams: map[string]any{ "unifiedAppId": "u-1", "name": "小助手", @@ -177,9 +181,9 @@ func TestDevAppRobotCommandsBuildToolParams(t *testing.T) { }, }, { - name: "offline", - args: []string{"robot", "offline", "--unified-app-id", "u-1", "--yes"}, - wantTool: "offline_open_dev_app_robot", + name: "robot disable", + args: []string{"robot", "disable", "--unified-app-id", "u-1", "--yes"}, + wantTool: "disable_dev_app_robot", wantParams: map[string]any{"unifiedAppId": "u-1"}, }, } @@ -191,7 +195,7 @@ func TestDevAppRobotCommandsBuildToolParams(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs(append([]string{"devapp"}, tc.args...)) + root.SetArgs(append([]string{"dev", "app"}, tc.args...)) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) @@ -219,43 +223,43 @@ func TestDevAppVersionCommandsBuildToolParams(t *testing.T) { { name: "create", args: []string{"version", "create", "--unified-app-id", "u-1", "--version", "1.0.1", "--desc", "新增机器人", "--yes"}, - wantTool: "create_open_dev_app_version", + wantTool: "create_dev_app_version", wantParams: map[string]any{ "unifiedAppId": "u-1", "version": "1.0.1", - "description": "新增机器人", + "desc": "新增机器人", }, }, { name: "list", - args: []string{"version", "list", "--unified-app-id", "u-1", "--page", "2", "--page-size", "5"}, - wantTool: "list_open_dev_app_versions", + args: []string{"version", "list", "--unified-app-id", "u-1", "--cursor", "tok-1", "--page-size", "5"}, + wantTool: "list_dev_app_versions", wantParams: map[string]any{ "unifiedAppId": "u-1", - "currentPage": 2, + "cursor": "tok-1", "pageSize": 5, }, }, { name: "get detail", args: []string{"version", "get", "--unified-app-id", "u-1", "--version-id", "v-1"}, - wantTool: "get_open_dev_app_version_detail", + wantTool: "get_dev_app_version_detail", wantParams: map[string]any{"unifiedAppId": "u-1", "versionId": "v-1"}, }, { - name: "check-approval forces dryRun", + name: "check-approval forces precheckOnly", args: []string{"version", "check-approval", "--unified-app-id", "u-1", "--version-id", "v-1"}, - wantTool: "publish_open_dev_app_version", - wantParams: map[string]any{"unifiedAppId": "u-1", "versionId": "v-1", "dryRun": true}, + wantTool: "publish_dev_app_version", + wantParams: map[string]any{"unifiedAppId": "u-1", "versionId": "v-1", "precheckOnly": true}, }, { - name: "publish sets dryRun false and sensitive", + name: "publish sets precheckOnly false and sensitive", args: []string{"version", "publish", "--unified-app-id", "u-1", "--version-id", "v-1", "--confirm-sensitive", "--approver", "user-1", "--yes"}, - wantTool: "publish_open_dev_app_version", + wantTool: "publish_dev_app_version", wantParams: map[string]any{ "unifiedAppId": "u-1", "versionId": "v-1", - "dryRun": false, + "precheckOnly": false, "confirmedSensitive": true, "approverUserId": "user-1", }, @@ -263,7 +267,7 @@ func TestDevAppVersionCommandsBuildToolParams(t *testing.T) { { name: "status", args: []string{"version", "status", "--unified-app-id", "u-1", "--version-id", "v-1"}, - wantTool: "get_open_dev_app_version_status", + wantTool: "get_dev_app_version_status", wantParams: map[string]any{"unifiedAppId": "u-1", "versionId": "v-1"}, }, } @@ -275,7 +279,7 @@ func TestDevAppVersionCommandsBuildToolParams(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs(append([]string{"devapp"}, tc.args...)) + root.SetArgs(append([]string{"dev", "app"}, tc.args...)) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) @@ -295,11 +299,10 @@ func TestDevAppRobotAndVersionWritesRequireGuard(t *testing.T) { name string args []string }{ - {name: "robot create", args: []string{"devapp", "robot", "create", "--app-name", "智能体", "--robot-name", "小助手", "--desc", "审批"}}, - {name: "robot config", args: []string{"devapp", "robot", "config", "--unified-app-id", "u-1", "--name", "小助手"}}, - {name: "robot offline", args: []string{"devapp", "robot", "offline", "--unified-app-id", "u-1"}}, - {name: "version create", args: []string{"devapp", "version", "create", "--unified-app-id", "u-1", "--version", "1.0.1"}}, - {name: "version publish", args: []string{"devapp", "version", "publish", "--unified-app-id", "u-1", "--version-id", "v-1"}}, + {name: "robot config", args: []string{"dev", "app", "robot", "config", "--unified-app-id", "u-1", "--name", "小助手"}}, + {name: "robot disable", args: []string{"dev", "app", "robot", "disable", "--unified-app-id", "u-1"}}, + {name: "version create", args: []string{"dev", "app", "version", "create", "--unified-app-id", "u-1", "--version", "1.0.1"}}, + {name: "version publish", args: []string{"dev", "app", "version", "publish", "--unified-app-id", "u-1", "--version-id", "v-1"}}, } for _, tc := range cases { @@ -315,7 +318,7 @@ func TestDevAppRobotAndVersionWritesRequireGuard(t *testing.T) { if err == nil { t.Fatal("Execute() error = nil, want write guard") } - if !strings.Contains(err.Error(), "write operation") { + if !strings.Contains(err.Error(), "写操作") { t.Fatalf("error = %q, want write guard", err.Error()) } if runner.last.Tool != "" { @@ -331,20 +334,20 @@ func TestDevAppListBuildsListByConditionParams(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs([]string{"list", "--name", "Waker", "--page", "2", "--page-size", "5", "--sort", "gmt_modified", "--order", "desc"}) + root.SetArgs([]string{"list", "--name", "Waker", "--cursor", "tok-2", "--page-size", "5", "--sort", "gmt_modified", "--order", "desc"}) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) } - if got := runner.last.Tool; got != "list_open_dev_apps_by_condition" { - t.Fatalf("Tool = %q, want list_open_dev_apps_by_condition", got) + if got := runner.last.Tool; got != "list_dev_app" { + t.Fatalf("Tool = %q, want list_dev_app", got) } want := map[string]any{ - "currentPage": 2, - "pageSize": 5, - "appName": "Waker", - "sortType": "gmt_modified", - "sortOrder": "desc", + "cursor": "tok-2", + "pageSize": 5, + "appName": "Waker", + "sortType": "gmt_modified", + "sortOrder": "desc", } if !reflect.DeepEqual(runner.last.Params, want) { t.Fatalf("Params = %#v, want %#v", runner.last.Params, want) @@ -362,8 +365,8 @@ func TestDevAppGetBuildsDetailParams(t *testing.T) { if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) } - if got := runner.last.Tool; got != "get_open_dev_app_detail" { - t.Fatalf("Tool = %q, want get_open_dev_app_detail", got) + if got := runner.last.Tool; got != "get_dev_app" { + t.Fatalf("Tool = %q, want get_dev_app", got) } want := map[string]any{"unifiedAppId": "u-1"} if !reflect.DeepEqual(runner.last.Params, want) { @@ -388,15 +391,15 @@ func TestDevAppCreateUsesCurrentInnerToolAndWriteGuard(t *testing.T) { root = newDevAppTestRoot(runner) root.SetOut(&out) root.SetErr(&out) - root.SetArgs([]string{"devapp", "create", "--name", "Demo", "--desc", "internal app", "--yes"}) + root.SetArgs([]string{"dev", "app", "create", "--name", "Demo", "--desc", "internal app", "--yes"}) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) } - if got := runner.last.Tool; got != "create_inner_app" { - t.Fatalf("Tool = %q, want create_inner_app", got) + if got := runner.last.Tool; got != "create_dev_app" { + t.Fatalf("Tool = %q, want create_dev_app", got) } - want := map[string]any{"appName": "Demo", "appDesc": "internal app"} + want := map[string]any{"appName": "Demo", "desc": "internal app"} if !reflect.DeepEqual(runner.last.Params, want) { t.Fatalf("Params = %#v, want %#v", runner.last.Params, want) } @@ -408,15 +411,15 @@ func TestDevAppUpdateUsesCurrentInnerTool(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs([]string{"devapp", "update", "--agent-id", "123", "--desc", "new desc", "--yes"}) + root.SetArgs([]string{"dev", "app", "update", "--unified-app-id", "u-123", "--desc", "new desc", "--yes"}) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) } - if got := runner.last.Tool; got != "update_inner_app" { - t.Fatalf("Tool = %q, want update_inner_app", got) + if got := runner.last.Tool; got != "update_dev_app" { + t.Fatalf("Tool = %q, want update_dev_app", got) } - want := map[string]any{"agentId": 123, "appDesc": "new desc"} + want := map[string]any{"unifiedAppId": "u-123", "desc": "new desc"} if !reflect.DeepEqual(runner.last.Params, want) { t.Fatalf("Params = %#v, want %#v", runner.last.Params, want) } @@ -429,23 +432,17 @@ func TestDevAppLifecycleBuildsLocatorParams(t *testing.T) { wantTool string wantParams map[string]any }{ - { - name: "delete by agent id", - args: []string{"delete", "--agent-id", "123", "--yes"}, - wantTool: "delete_inner_app", - wantParams: map[string]any{"agentId": 123}, - }, { name: "inactive by unified app id", - args: []string{"inactive", "--unified-app-id", "u-1", "--yes"}, - wantTool: "inactive_inner_app", + args: []string{"disable", "--unified-app-id", "u-1", "--yes"}, + wantTool: "disable_dev_app", wantParams: map[string]any{"unifiedAppId": "u-1"}, }, { - name: "active by app key", - args: []string{"active", "--app-key", "dingxxx", "--yes"}, - wantTool: "active_inner_app", - wantParams: map[string]any{"appKey": "dingxxx"}, + name: "active by unified id", + args: []string{"enable", "--unified-app-id", "u-2", "--yes"}, + wantTool: "enable_dev_app", + wantParams: map[string]any{"unifiedAppId": "u-2"}, }, } @@ -456,7 +453,7 @@ func TestDevAppLifecycleBuildsLocatorParams(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs(append([]string{"devapp"}, tc.args...)) + root.SetArgs(append([]string{"dev", "app"}, tc.args...)) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) @@ -471,6 +468,58 @@ func TestDevAppLifecycleBuildsLocatorParams(t *testing.T) { } } +// TestDevAppDeleteConfirmName covers the danger-tier delete: --confirm-name +// must match the located app's real name, and a name that can't be read is +// fail-closed (abort), never a silent proceed. +func TestDevAppDeleteConfirmName(t *testing.T) { + t.Run("matching name proceeds", func(t *testing.T) { + runner := &devAppResponseRunner{response: map[string]any{"appName": "DemoApp"}} + root := newDevAppTestRoot(runner) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"dev", "app", "delete", "--unified-app-id", "u-123", "--confirm-name", "DemoApp", "--yes"}) + if err := root.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + if runner.last.Tool != "delete_dev_app" { + t.Fatalf("Tool = %q, want delete_dev_app", runner.last.Tool) + } + }) + + t.Run("mismatched name aborts", func(t *testing.T) { + runner := &devAppResponseRunner{response: map[string]any{"appName": "RealName"}} + root := newDevAppTestRoot(runner) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"dev", "app", "delete", "--unified-app-id", "u-123", "--confirm-name", "WrongName", "--yes"}) + err := root.Execute() + if err == nil || !strings.Contains(err.Error(), "名称不匹配") { + t.Fatalf("error = %v, want 名称不匹配", err) + } + if runner.last.Tool == "delete_dev_app" { + t.Fatal("delete must not run on name mismatch") + } + }) + + t.Run("unreadable name is fail-closed", func(t *testing.T) { + runner := &devAppResponseRunner{response: map[string]any{}} // no appName + root := newDevAppTestRoot(runner) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"dev", "app", "delete", "--unified-app-id", "u-123", "--confirm-name", "DemoApp", "--yes"}) + err := root.Execute() + if err == nil || !strings.Contains(err.Error(), "无法读取应用名") { + t.Fatalf("error = %v, want 无法读取应用名 (fail-closed)", err) + } + if runner.last.Tool == "delete_dev_app" { + t.Fatal("delete must not run when name is unreadable") + } + }) +} + func TestDevAppWebappCommandsBuildParams(t *testing.T) { cases := []struct { name string @@ -481,17 +530,17 @@ func TestDevAppWebappCommandsBuildParams(t *testing.T) { { name: "get", args: []string{"webapp", "get", "--unified-app-id", "u-1"}, - wantTool: "get_webapp_config", + wantTool: "get_extension_webapp_config", wantParams: map[string]any{"unifiedAppId": "u-1"}, }, { name: "config", - args: []string{"webapp", "config", "--unified-app-id", "u-1", "--homepage-link", "https://example.com", "--pc-homepage-link", "https://pc.example.com", "--yes"}, - wantTool: "set_webapp_config", + args: []string{"webapp", "config", "--unified-app-id", "u-1", "--homepage-url", "https://example.com", "--pc-homepage-url", "https://pc.example.com", "--yes"}, + wantTool: "set_extension_webapp_config", wantParams: map[string]any{ - "unifiedAppId": "u-1", - "homepageLink": "https://example.com", - "pcHomepageLink": "https://pc.example.com", + "unifiedAppId": "u-1", + "homepageUrl": "https://example.com", + "pcHomepageUrl": "https://pc.example.com", }, }, } @@ -503,7 +552,7 @@ func TestDevAppWebappCommandsBuildParams(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs(append([]string{"devapp"}, tc.args...)) + root.SetArgs(append([]string{"dev", "app"}, tc.args...)) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) @@ -527,31 +576,32 @@ func TestDevAppPermissionCommandsBuildParams(t *testing.T) { }{ { name: "list", - args: []string{"permission", "list", "--agent-id", "123", "--keyword", "手机号", "--status", "all", "--limit", "5"}, - wantTool: "list_open_dev_app_permissions", + args: []string{"permission", "list", "--unified-app-id", "u-123", "--keyword", "手机号", "--status", "all", "--cursor", "tok-3", "--page-size", "5"}, + wantTool: "list_dev_app_permissions", wantParams: map[string]any{ - "agentId": 123, - "keyword": "手机号", - "authStatus": "ALL", - "limit": 5, + "unifiedAppId": "u-123", + "keyword": "手机号", + "authStatus": "ALL", + "cursor": "tok-3", + "pageSize": 5, }, }, { name: "add", - args: []string{"permission", "add", "--agent-id", "123", "--permissions", "Contact.User.mobile,qyapi_robot_sendmsg", "--yes"}, - wantTool: "apply_open_dev_app_permissions", + args: []string{"permission", "add", "--unified-app-id", "u-123", "--permissions", "Contact.User.mobile,qyapi_robot_sendmsg", "--yes"}, + wantTool: "apply_dev_app_permissions", wantParams: map[string]any{ - "agentId": 123, - "scopeValues": []string{"Contact.User.mobile", "qyapi_robot_sendmsg"}, + "unifiedAppId": "u-123", + "scopeValues": []string{"Contact.User.mobile", "qyapi_robot_sendmsg"}, }, }, { name: "remove", - args: []string{"permission", "remove", "--agent-id", "123", "--permission", "Contact.User.mobile", "--yes"}, - wantTool: "remove_open_dev_app_permission", + args: []string{"permission", "remove", "--unified-app-id", "u-123", "--permission", "Contact.User.mobile", "--yes"}, + wantTool: "remove_dev_app_permissions", wantParams: map[string]any{ - "agentId": 123, - "scopeValue": "Contact.User.mobile", + "unifiedAppId": "u-123", + "scopeValue": "Contact.User.mobile", }, }, } @@ -563,7 +613,7 @@ func TestDevAppPermissionCommandsBuildParams(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs(append([]string{"devapp"}, tc.args...)) + root.SetArgs(append([]string{"dev", "app"}, tc.args...)) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) @@ -584,15 +634,15 @@ func TestDevAppCredentialsGetBuildsParams(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs([]string{"devapp", "credentials", "get", "--agent-id", "123"}) + root.SetArgs([]string{"dev", "app", "credentials", "get", "--unified-app-id", "u-123"}) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) } - if got := runner.last.Tool; got != "get_open_dev_app_credentials" { - t.Fatalf("Tool = %q, want get_open_dev_app_credentials", got) + if got := runner.last.Tool; got != "get_dev_app_credentials" { + t.Fatalf("Tool = %q, want get_dev_app_credentials", got) } - want := map[string]any{"agentId": 123} + want := map[string]any{"unifiedAppId": "u-123"} if !reflect.DeepEqual(runner.last.Params, want) { t.Fatalf("Params = %#v, want %#v", runner.last.Params, want) } @@ -613,7 +663,7 @@ func TestDevAppCredentialsGetKeepsSecretFields(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs([]string{"devapp", "credentials", "get", "--agent-id", "123"}) + root.SetArgs([]string{"dev", "app", "credentials", "get", "--unified-app-id", "u-123"}) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) @@ -633,10 +683,10 @@ func TestDevAppMemberCommandsValidateRequiredFlags(t *testing.T) { args []string wantErr string }{ - {name: "list requires app", cmd: "list", args: nil, wantErr: "--app-id is required"}, - {name: "add requires users", cmd: "add", args: []string{"--app-id", "app-001", "--member-type", "DEVELOPER", "--dry-run"}, wantErr: "--users is required"}, - {name: "add rejects empty users", cmd: "add", args: []string{"--app-id", "app-001", "--users", " , ", "--member-type", "DEVELOPER", "--dry-run"}, wantErr: "--users must contain at least one userId"}, - {name: "remove requires member type", cmd: "remove", args: []string{"--app-id", "app-001", "--users", "userId1", "--dry-run"}, wantErr: "--member-type is required"}, + {name: "list requires app", cmd: "list", args: nil, wantErr: "--unified-app-id 为必填"}, + {name: "add requires users", cmd: "add", args: []string{"--unified-app-id", "app-001", "--member-type", "DEVELOPER", "--dry-run"}, wantErr: "--users 为必填"}, + {name: "add rejects empty users", cmd: "add", args: []string{"--unified-app-id", "app-001", "--users", " , ", "--member-type", "DEVELOPER", "--dry-run"}, wantErr: "--users 至少包含一个 userId"}, + {name: "remove requires member type", cmd: "remove", args: []string{"--unified-app-id", "app-001", "--users", "userId1", "--dry-run"}, wantErr: "--member-type 为必填"}, } for _, tc := range cases { @@ -646,7 +696,7 @@ func TestDevAppMemberCommandsValidateRequiredFlags(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs(append([]string{"devapp", "member", tc.cmd}, tc.args...)) + root.SetArgs(append([]string{"dev", "app", "member", tc.cmd}, tc.args...)) err := root.Execute() if err == nil { @@ -669,8 +719,8 @@ func TestDevAppSecurityConfigBuildsOnlyProvidedLists(t *testing.T) { root.SetOut(&out) root.SetErr(&out) root.SetArgs([]string{ - "devapp", "security", "config", - "--app-id", "app-001", + "dev", "app", "security", "config", + "--unified-app-id", "app-001", "--ip-whitelist", "192.0.2.10,192.0.2.11", "--redirect-url", "https://callback.example.invalid/callback", "--sso-url", "https://sso.example.invalid/sso", @@ -684,14 +734,14 @@ func TestDevAppSecurityConfigBuildsOnlyProvidedLists(t *testing.T) { if got := runner.last.CanonicalProduct; got != "devapp" { t.Fatalf("CanonicalProduct = %q, want devapp", got) } - if got := runner.last.Tool; got != "update_app_security_config" { - t.Fatalf("Tool = %q, want update_app_security_config", got) + if got := runner.last.Tool; got != "update_dev_app_security_config" { + t.Fatalf("Tool = %q, want update_dev_app_security_config", got) } want := map[string]any{ - "unifiedAppId": "app-001", - "ipWhiteList": []string{"192.0.2.10", "192.0.2.11"}, - "redirectUrls": []string{"https://callback.example.invalid/callback"}, - "otherAuthUrls": []string{"https://sso.example.invalid/sso"}, + "unifiedAppId": "app-001", + "ipWhitelist": []string{"192.0.2.10", "192.0.2.11"}, + "redirectUrls": []string{"https://callback.example.invalid/callback"}, + "ssoUrls": []string{"https://sso.example.invalid/sso"}, } if !reflect.DeepEqual(runner.last.Params, want) { t.Fatalf("Params = %#v, want %#v", runner.last.Params, want) @@ -704,7 +754,7 @@ func TestDevAppSecurityConfigOmitsAbsentOptionalLists(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs([]string{"devapp", "security", "config", "--app-id", "app-001", "--redirect-url", "https://callback.example.invalid/callback", "--dry-run"}) + root.SetArgs([]string{"dev", "app", "security", "config", "--unified-app-id", "app-001", "--redirect-url", "https://callback.example.invalid/callback", "--dry-run"}) if err := root.Execute(); err != nil { t.Fatalf("Execute() error = %v\noutput:\n%s", err, out.String()) @@ -725,13 +775,13 @@ func TestDevAppSecurityConfigRequiresAtLeastOneConfig(t *testing.T) { var out bytes.Buffer root.SetOut(&out) root.SetErr(&out) - root.SetArgs([]string{"devapp", "security", "config", "--app-id", "app-001", "--dry-run"}) + root.SetArgs([]string{"dev", "app", "security", "config", "--unified-app-id", "app-001", "--dry-run"}) err := root.Execute() if err == nil { t.Fatal("Execute() error = nil, want validation error") } - if !strings.Contains(err.Error(), "one of --ip-whitelist, --redirect-url, or --sso-url is required") { + if !strings.Contains(err.Error(), "至少提供一项安全配置:--ip-whitelist、--redirect-url 或 --sso-url") { t.Fatalf("error = %q", err.Error()) } if runner.last.Tool != "" { @@ -746,15 +796,15 @@ func TestDevAppMemberAndSecurityRequireWriteGuard(t *testing.T) { }{ { name: "member add", - args: []string{"devapp", "member", "add", "--app-id", "app-001", "--users", "userId1", "--member-type", "DEVELOPER"}, + args: []string{"dev", "app", "member", "add", "--unified-app-id", "app-001", "--users", "userId1", "--member-type", "DEVELOPER"}, }, { name: "member remove", - args: []string{"devapp", "member", "remove", "--app-id", "app-001", "--users", "userId1", "--member-type", "DEVELOPER"}, + args: []string{"dev", "app", "member", "remove", "--unified-app-id", "app-001", "--users", "userId1", "--member-type", "DEVELOPER"}, }, { name: "security config", - args: []string{"devapp", "security", "config", "--app-id", "app-001", "--redirect-url", "https://callback.example.invalid/callback"}, + args: []string{"dev", "app", "security", "config", "--unified-app-id", "app-001", "--redirect-url", "https://callback.example.invalid/callback"}, }, } @@ -771,7 +821,7 @@ func TestDevAppMemberAndSecurityRequireWriteGuard(t *testing.T) { if err == nil { t.Fatal("Execute() error = nil, want write guard") } - if !strings.Contains(err.Error(), "write operation") { + if !strings.Contains(err.Error(), "写操作") { t.Fatalf("error = %q, want write guard", err.Error()) } if runner.last.Tool != "" { diff --git a/internal/helpers/devdoc_test.go b/internal/helpers/devdoc_test.go index 96c042f7..41096199 100644 --- a/internal/helpers/devdoc_test.go +++ b/internal/helpers/devdoc_test.go @@ -38,7 +38,7 @@ func TestDevdocArticleSearchAcceptsWukongKeywordAlias(t *testing.T) { var out, errOut bytes.Buffer cmd.SetOut(&out) cmd.SetErr(&errOut) - cmd.SetArgs([]string{"article", "search", "--keyword", "openConversationId", "--page", "2", "--size", "5"}) + cmd.SetArgs([]string{"article", "search", "--keyword", "openConversationId", "--cursor", "tok-d", "--page-size", "5"}) if err := cmd.Execute(); err != nil { t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String()) @@ -49,11 +49,11 @@ func TestDevdocArticleSearchAcceptsWukongKeywordAlias(t *testing.T) { if got := runner.last.Params["keyword"]; got != "openConversationId" { t.Fatalf("keyword = %#v, want openConversationId", got) } - if got := runner.last.Params["page"]; got != 2 { - t.Fatalf("page = %#v, want 2", got) + if got := runner.last.Params["cursor"]; got != "tok-d" { + t.Fatalf("cursor = %#v, want tok-d", got) } - if got := runner.last.Params["size"]; got != 5 { - t.Fatalf("size = %#v, want 5", got) + if got := runner.last.Params["pageSize"]; got != 5 { + t.Fatalf("pageSize = %#v, want 5", got) } }