178 lines
8.7 KiB
Go
178 lines
8.7 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 helpers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"sort"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/spf13/cobra"
|
||
)
|
||
|
||
// devAppRegressionLeafPaths 遍历 dev app 命令树,收集全部叶子路径(无子命令
|
||
// 即叶子)。核销清单的「清单 ⇆ 命令树」双向绑定依赖它:新增叶子若不在清单
|
||
// 里,清单测试失败,强制信封覆盖随叶子同批落盘(AC-28/M1.8)。
|
||
func devAppRegressionLeafPaths(t *testing.T) []string {
|
||
t.Helper()
|
||
root := newDevAppTestRoot(devAppFamilyContentRunner(map[string]any{}))
|
||
appCmd, _, err := root.Find([]string{"dev", "app"})
|
||
if err != nil {
|
||
t.Fatalf("Find(dev app) error = %v", err)
|
||
}
|
||
var leaves []string
|
||
var traverse func(c *cobra.Command, prefix []string)
|
||
traverse = func(c *cobra.Command, prefix []string) {
|
||
if !c.HasSubCommands() {
|
||
leaves = append(leaves, strings.Join(prefix, " "))
|
||
return
|
||
}
|
||
for _, sub := range c.Commands() {
|
||
traverse(sub, append(append([]string{}, prefix...), sub.Name()))
|
||
}
|
||
}
|
||
traverse(appCmd, nil)
|
||
sort.Strings(leaves)
|
||
return leaves
|
||
}
|
||
|
||
// devAppEnvelopeRegressionChecklist 是 dev app 树 32 叶子的信封核销清单
|
||
// (队列 B138,AC-28/M1.8):每条给出可执行 argv。读叶子走成功路径
|
||
// (outcome=success),写叶子带 --dry-run 走预览路径(outcome=success +
|
||
// dry_run=true)。argv 与轮 11-B0 族验证测试同源(各叶子 flag 已逐一核对)。
|
||
func devAppEnvelopeRegressionChecklist() []struct {
|
||
path string
|
||
args []string
|
||
write bool
|
||
} {
|
||
return []struct {
|
||
path string
|
||
args []string
|
||
write bool
|
||
}{
|
||
{"list", []string{"dev", "app", "list", "--name", "DemoApp"}, false},
|
||
{"get", []string{"dev", "app", "get", "--unified-app-id", "u-1"}, false},
|
||
{"create", []string{"dev", "app", "create", "--name", "DemoApp", "--dry-run"}, true},
|
||
{"update", []string{"dev", "app", "update", "--unified-app-id", "u-1", "--name", "NewName", "--dry-run"}, true},
|
||
{"delete", []string{"dev", "app", "delete", "--unified-app-id", "u-1", "--dry-run"}, true},
|
||
{"disable", []string{"dev", "app", "disable", "--unified-app-id", "u-1", "--dry-run"}, true},
|
||
{"enable", []string{"dev", "app", "enable", "--unified-app-id", "u-1", "--dry-run"}, true},
|
||
{"credentials get", []string{"dev", "app", "credentials", "get", "--unified-app-id", "u-1"}, false},
|
||
{"webapp get", []string{"dev", "app", "webapp", "get", "--unified-app-id", "u-1"}, false},
|
||
{"webapp config", []string{"dev", "app", "webapp", "config", "--unified-app-id", "u-1", "--homepage-url", "https://example.com", "--dry-run"}, true},
|
||
{"permission list", []string{"dev", "app", "permission", "list", "--unified-app-id", "u-1"}, false},
|
||
{"permission add", []string{"dev", "app", "permission", "add", "--unified-app-id", "u-1", "--scope-values", "Contact.User.mobile", "--dry-run"}, true},
|
||
{"permission remove", []string{"dev", "app", "permission", "remove", "--unified-app-id", "u-1", "--scope-values", "Contact.User.mobile", "--dry-run"}, true},
|
||
{"member list", []string{"dev", "app", "member", "list", "--unified-app-id", "u-1"}, false},
|
||
{"member add", []string{"dev", "app", "member", "add", "--unified-app-id", "u-1", "--user-ids", "user-1", "--member-type", "DEVELOPER", "--dry-run"}, true},
|
||
{"member remove", []string{"dev", "app", "member", "remove", "--unified-app-id", "u-1", "--user-ids", "user-1", "--member-type", "DEVELOPER", "--dry-run"}, true},
|
||
{"security config", []string{"dev", "app", "security", "config", "--unified-app-id", "u-1", "--redirect-urls", "https://cb.example.invalid/cb", "--dry-run"}, true},
|
||
{"robot submit", []string{"dev", "app", "robot", "submit", "--name", "智能体", "--robot-name", "小助手", "--desc", "审批问答", "--dry-run"}, true},
|
||
{"robot result", []string{"dev", "app", "robot", "result", "--task-id", "t-1"}, false},
|
||
{"robot get", []string{"dev", "app", "robot", "get", "--unified-app-id", "u-1"}, false},
|
||
{"robot config", []string{"dev", "app", "robot", "config", "--unified-app-id", "u-1", "--name", "小助手", "--dry-run"}, true},
|
||
{"robot enable", []string{"dev", "app", "robot", "enable", "--unified-app-id", "u-1", "--dry-run"}, true},
|
||
{"robot disable", []string{"dev", "app", "robot", "disable", "--unified-app-id", "u-1", "--dry-run"}, true},
|
||
{"version create", []string{"dev", "app", "version", "create", "--unified-app-id", "u-1", "--desc", "新增机器人", "--dry-run"}, true},
|
||
{"version list", []string{"dev", "app", "version", "list", "--unified-app-id", "u-1"}, false},
|
||
{"version get", []string{"dev", "app", "version", "get", "--unified-app-id", "u-1", "--version-id", "v-1"}, false},
|
||
{"version check-approval", []string{"dev", "app", "version", "check-approval", "--unified-app-id", "u-1", "--version-id", "v-1"}, false},
|
||
{"version publish", []string{"dev", "app", "version", "publish", "--unified-app-id", "u-1", "--version-id", "v-1", "--dry-run"}, true},
|
||
{"version status", []string{"dev", "app", "version", "status", "--unified-app-id", "u-1", "--version-id", "v-1"}, false},
|
||
{"event list", []string{"dev", "app", "event", "list", "--unified-app-id", "u-1"}, false},
|
||
{"event subscribe", []string{"dev", "app", "event", "subscribe", "--unified-app-id", "u-1", "--event-codes", "a,b", "--dry-run"}, true},
|
||
{"event unsubscribe", []string{"dev", "app", "event", "unsubscribe", "--unified-app-id", "u-1", "--event-codes", "a,b", "--dry-run"}, true},
|
||
}
|
||
}
|
||
|
||
// TestDevAppEnvelopeRegressionLeafInventory 是核销清单的清单 ⇆ 命令树双向
|
||
// 绑定:dev app 树叶子集合必须与清单完全一致。新增叶子未补信封断言、或
|
||
// 清单残留已删除叶子,都会在此失败。
|
||
func TestDevAppEnvelopeRegressionLeafInventory(t *testing.T) {
|
||
tree := devAppRegressionLeafPaths(t)
|
||
|
||
want := make([]string, 0)
|
||
for _, entry := range devAppEnvelopeRegressionChecklist() {
|
||
want = append(want, entry.path)
|
||
}
|
||
sort.Strings(want)
|
||
|
||
if len(tree) != len(want) {
|
||
t.Fatalf("leaf count = %d, checklist = %d\ntree: %v\nchecklist: %v",
|
||
len(tree), len(want), tree, want)
|
||
}
|
||
for i := range tree {
|
||
if tree[i] != want[i] {
|
||
t.Fatalf("leaf[%d] = %q, checklist = %q\ntree: %v\nchecklist: %v",
|
||
i, tree[i], want[i], tree, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// regressionEnvelope 是核销断言的信封解码形状:ok 用 *bool——缺席(未走
|
||
// 统一出口)为 nil,`"ok":"true"` 字符串形态(违反 AC-02)直接解码失败。
|
||
type regressionEnvelope struct {
|
||
OK *bool `json:"ok"`
|
||
Outcome string `json:"outcome"`
|
||
DryRun bool `json:"dry_run"`
|
||
Data map[string]any `json:"data"`
|
||
}
|
||
|
||
// TestDevAppEnvelopeRegressionUnifiedExit 逐一执行 32 叶子并断言 stdout 恰为
|
||
// 一个统一信封(走 writeDevAppEnvelope 唯一出口):ok 为 JSON 布尔 true、
|
||
// outcome 与读/写形态一致、data 非空。任何绕过信封直写 stdout 的叶子
|
||
// (legacy 裸 JSON/人读文案)都会因缺 ok 键或 JSON 解析失败而暴露。
|
||
func TestDevAppEnvelopeRegressionUnifiedExit(t *testing.T) {
|
||
for _, entry := range devAppEnvelopeRegressionChecklist() {
|
||
t.Run(entry.path, func(t *testing.T) {
|
||
out, errBuf, err := runDevAppFamily(t,
|
||
devAppFamilyContentRunner(map[string]any{
|
||
"unifiedAppId": "u-1",
|
||
"name": "DemoApp",
|
||
"appStatus": "ENABLED",
|
||
}),
|
||
entry.args...)
|
||
if err != nil {
|
||
t.Fatalf("Execute() error = %v\nstdout:\n%s\nstderr:\n%s", err, out.String(), errBuf.String())
|
||
}
|
||
|
||
var env regressionEnvelope
|
||
if err := json.Unmarshal(out.Bytes(), &env); err != nil {
|
||
t.Fatalf("stdout is not a single JSON envelope (unified exit bypass?): %v\n%s", err, out.String())
|
||
}
|
||
if env.OK == nil {
|
||
t.Fatalf("envelope missing ok key (unified exit bypass?): %s", out.String())
|
||
}
|
||
if !*env.OK {
|
||
t.Fatalf("ok = false on success/pending path (I1 violated): %s", out.String())
|
||
}
|
||
if entry.write {
|
||
if env.Outcome != "success" || !env.DryRun {
|
||
t.Fatalf("write leaf dry-run outcome/dry_run = %q/%v, want success/true: %s",
|
||
env.Outcome, env.DryRun, out.String())
|
||
}
|
||
} else {
|
||
if env.Outcome != "success" || env.DryRun {
|
||
t.Fatalf("read leaf outcome/dry_run = %q/%v, want success/false: %s",
|
||
env.Outcome, env.DryRun, out.String())
|
||
}
|
||
}
|
||
if env.Data == nil {
|
||
t.Fatalf("envelope data is nil: %s", out.String())
|
||
}
|
||
})
|
||
}
|
||
}
|