Files
dingtalk-workspace-cli/internal/helpers/devapp_declaration_completeness_test.go

213 lines
8.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 (
"reflect"
"sort"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal"
"github.com/spf13/cobra"
)
// collectDevLeafCommands 递归收集命令树中所有叶子命令(无子命令者)。
func collectDevLeafCommands(cmd *cobra.Command, out *[]*cobra.Command) {
if cmd == nil {
return
}
if len(cmd.Commands()) == 0 {
*out = append(*out, cmd)
return
}
for _, sub := range cmd.Commands() {
collectDevLeafCommands(sub, out)
}
}
func TestDevRepresentativeResultContractsReachContractFinal(t *testing.T) {
root := newDevAppTestRoot(&captureRunner{})
var leaves []*cobra.Command
collectDevLeafCommands(root, &leaves)
finals := make(map[string]contract.ContractFinalPayload)
for _, leaf := range leaves {
final, ok := contractfinal.RuntimeContractFinal(leaf)
if ok && final.Identity != nil {
finals[final.Identity.CanonicalPath] = final
}
}
paginationCheck := func(t *testing.T, final contract.ContractFinalPayload) {
t.Helper()
if final.Pagination == nil || final.Pagination.Kind != contract.PaginationKindCursor ||
final.Pagination.CursorParameter != "cursor" || final.Pagination.MetaPath != contract.PaginationMetaPath {
t.Fatalf("paginated list contract = %#v", final)
}
}
tests := []struct {
canonical string
outcomes []contract.ResultOutcome
check func(*testing.T, contract.ContractFinalPayload)
}{
{
canonical: "dev.list_dev_app",
outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
check: paginationCheck,
},
{canonical: "dev.list_dev_app_permissions", outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure}, check: paginationCheck},
{canonical: "dev.list_dev_app_events", outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure}, check: paginationCheck},
{canonical: "dev.list_dev_app_versions", outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure}, check: paginationCheck},
{canonical: "dev.get_dev_app", outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure}},
{
canonical: "dev.get_dev_app_credentials",
outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
check: func(t *testing.T, final contract.ContractFinalPayload) {
result := final.Result
if want := []string{"appSecret", "clientSecret"}; !reflect.DeepEqual(result.SensitivePaths, want) {
t.Fatalf("sensitive paths = %#v, want %#v", result.SensitivePaths, want)
}
},
},
{canonical: "dev.get_dev_app_version_status", outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomePending, contract.ResultOutcomeFailure}},
{canonical: "dev.connect_status", outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure}},
{canonical: "dev.connect_list", outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure}},
}
for _, test := range tests {
t.Run(test.canonical, func(t *testing.T) {
final, ok := finals[test.canonical]
if !ok || final.Result == nil {
t.Fatalf("representative leaf %s has no final Result", test.canonical)
}
if !reflect.DeepEqual(final.Result.Outcomes, test.outcomes) {
t.Fatalf("outcomes = %#v, want %#v", final.Result.Outcomes, test.outcomes)
}
if len(final.Result.DataSchema) == 0 {
t.Fatal("data_schema is empty")
}
if test.check != nil {
test.check(t, final)
}
})
}
}
// TestDevLeafDeclarationCompleteness 是队列 B190 的 dev 叶子声明完整性自查:
// 每一个**注册了 ContractFinal** 的 dev 叶子都必须满足 Identity+Safety 最小集
// (无半壳:有注册但字段缺失)。对未注册 ContractFinal 的叶子(裸 Cobra 迁移态
// 或 NewLeafCommand 漏声明)不硬失败,而是汇总为 findings 清单打印,供人工判断
// 是否属「空壳入册」。
func TestDevLeafDeclarationCompleteness(t *testing.T) {
root := newDevAppTestRoot(&captureRunner{})
var leaves []*cobra.Command
collectDevLeafCommands(root, &leaves)
if len(leaves) == 0 {
t.Fatalf("no dev leaves collected under test root")
}
t.Logf("dev domain leaves under test: %d", len(leaves))
var noFinal []string
for _, leaf := range leaves {
path := leaf.CommandPath()
final, ok := contractfinal.RuntimeContractFinal(leaf)
if !ok {
noFinal = append(noFinal, path)
continue
}
t.Run(path, func(t *testing.T) {
if final.Identity == nil {
t.Fatalf("leaf %q ContractFinal.Identity is nil", path)
}
if final.Identity.ProductID == "" {
t.Fatalf("leaf %q Identity.ProductID empty", path)
}
if final.Identity.Name == "" {
t.Fatalf("leaf %q Identity.Name empty", path)
}
if final.Identity.CanonicalPath == "" {
t.Fatalf("leaf %q Identity.CanonicalPath empty", path)
}
if final.Safety == nil {
t.Fatalf("leaf %q ContractFinal.Safety is nil (Safety minimal set missing)", path)
}
if final.Description == "" {
t.Fatalf("leaf %q ContractFinal.Description empty (no description declared)", path)
}
})
}
if len(noFinal) > 0 {
sort.Strings(noFinal)
t.Logf("B190 findings: %d dev leaves without ContractFinal (may be intentional bare-Cobra migration or empty-shell NewLeafCommand):", len(noFinal))
for _, p := range noFinal {
t.Logf(" - %s", p)
}
}
}
// TestDevLeafDeclarationNoEmptyShellNewLeaf 是队列 B190 的核心断言:NewLeafCommand
// 完全托管的叶子必须注册 ContractFinal——声明了 LeafSpec 却无 ContractFinal 就是
// 「空壳入册」。本测试通过 Verify 一个已知的既存漏洞(version check-approval 用
// NewLeafCommand 但 LeafSpec 未声明 Contract)来固化当前现状,并断言这类「声明了
// Shell 却无最终契约」的问题在受控已知集中,而非蔓延到其它托管叶子。
func TestDevLeafDeclarationNoEmptyShellNewLeaf(t *testing.T) {
root := newDevAppTestRoot(&captureRunner{})
var leaves []*cobra.Command
collectDevLeafCommands(root, &leaves)
knownEmptyShell := map[string]bool{}
var unexpected []string
for _, leaf := range leaves {
path := leaf.CommandPath()
if _, ok := contractfinal.RuntimeContractFinal(leaf); ok {
continue // 正常托管叶子已有 ContractFinal。
}
// 无 ContractFinal:裸 Cobra 迁移态(connect 系列)或已知空壳。
if knownEmptyShell[path] {
continue
}
// 裸 Cobra 命令(不经 NewLeafCommand)不要求 ContractFinal,排除。
if isBareDevCobraLeaf(leaf) {
continue
}
unexpected = append(unexpected, path)
}
if len(unexpected) > 0 {
t.Fatalf("unexpected empty-shell NewLeafCommand leaves without ContractFinal: %v", unexpected)
}
}
// isBareDevCobraLeaf 判断某 dev 叶子是否为裸 Cobra 命令(不经 NewLeafCommand,
// 不注册 ContractFinal)。通过多次构造命令并检查其是否有本地 flags 来间接识别
// 不可靠,改用命令名白名单:connect 本地运维子命令为已知裸 Cobra 迁移态。
func isBareDevCobraLeaf(cmd *cobra.Command) bool {
switch cmd.Name() {
case "list", "status", "restart", "stop":
// 仅当命令位于 dev connect 子树下才算裸 Cobra(避免误伤 dev app 的
// version list 等托管叶子)。
if strings.Contains(cmd.CommandPath(), "dev connect") {
return true
}
}
return false
}
var (
_ = contractfinal.HasRuntimeContractFinal
_ = sort.Strings
)