528 lines
21 KiB
Go
528 lines
21 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
package devapp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
|
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type devAppCoverageCall struct {
|
|
tool string
|
|
params map[string]any
|
|
}
|
|
|
|
type devAppCoverageCaller struct {
|
|
responses map[string][]map[string]any
|
|
calls []devAppCoverageCall
|
|
}
|
|
|
|
func (c *devAppCoverageCaller) CallTool(_ context.Context, _, tool string, params map[string]any) (*edition.ToolResult, error) {
|
|
c.calls = append(c.calls, devAppCoverageCall{tool: tool, params: params})
|
|
queue := c.responses[tool]
|
|
if len(queue) == 0 {
|
|
return nil, errors.New("unexpected devapp coverage call: " + tool)
|
|
}
|
|
payload := queue[0]
|
|
c.responses[tool] = queue[1:]
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: string(encoded)}}}, nil
|
|
}
|
|
|
|
func (*devAppCoverageCaller) Format() string { return "json" }
|
|
func (*devAppCoverageCaller) DryRun() bool { return false }
|
|
func (*devAppCoverageCaller) Fields() string { return "" }
|
|
func (*devAppCoverageCaller) JQ() string { return "" }
|
|
|
|
func runDevAppCoverage(t *testing.T, declaration shortcut.Shortcut, caller *devAppCoverageCaller, args ...string) error {
|
|
t.Helper()
|
|
helpers.InitDeps(caller)
|
|
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
|
|
root.PersistentFlags().Bool("yes", false, "")
|
|
root.PersistentFlags().Bool("dry-run", false, "")
|
|
root.PersistentFlags().String("format", "json", "")
|
|
service := &cobra.Command{Use: "devapp"}
|
|
service.AddCommand(corecmd.New(shortcut.FromShortcut(declaration)))
|
|
root.AddCommand(service)
|
|
root.SetOut(io.Discard)
|
|
root.SetErr(io.Discard)
|
|
root.SetIn(strings.NewReader(""))
|
|
root.SetArgs(append([]string{"devapp", declaration.Command}, args...))
|
|
return root.Execute()
|
|
}
|
|
|
|
func runDevAppUnifiedCoverage(t *testing.T, declaration shortcut.Shortcut, caller *devAppCoverageCaller, args ...string) ([]byte, error) {
|
|
t.Helper()
|
|
helpers.InitDeps(caller)
|
|
ctx, _ := output.WithResultStore(context.Background())
|
|
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
|
|
root.SetContext(ctx)
|
|
root.PersistentFlags().Bool("yes", false, "")
|
|
root.PersistentFlags().Bool("dry-run", false, "")
|
|
root.PersistentFlags().String("format", "json", "")
|
|
service := &cobra.Command{Use: "devapp"}
|
|
service.AddCommand(corecmd.New(shortcut.FromShortcut(declaration)))
|
|
root.AddCommand(service)
|
|
var stdout bytes.Buffer
|
|
root.SetOut(&stdout)
|
|
root.SetErr(io.Discard)
|
|
root.SetIn(strings.NewReader(""))
|
|
root.PersistentPostRunE = func(executed *cobra.Command, _ []string) error {
|
|
_, _, err := output.EmitStoredResult(executed)
|
|
return err
|
|
}
|
|
root.SetArgs(append([]string{"devapp", declaration.Command}, args...))
|
|
if err := root.Execute(); err != nil {
|
|
return nil, err
|
|
}
|
|
return stdout.Bytes(), nil
|
|
}
|
|
|
|
func devAppRegistered(command string) (shortcut.Shortcut, bool) {
|
|
for _, item := range shortcut.All() {
|
|
if item.Service == "devapp" && item.Command == command {
|
|
return item, true
|
|
}
|
|
}
|
|
return shortcut.Shortcut{}, false
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDevAppStrictResponseValidation(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
data map[string]any
|
|
}{
|
|
{name: "empty", data: nil},
|
|
{name: "missing success", data: map[string]any{"list": []any{}}},
|
|
{name: "wrong success type", data: map[string]any{"success": "true", "list": []any{}}},
|
|
{name: "explicit failure", data: map[string]any{"success": false, "list": []any{}}},
|
|
{name: "conflicting error", data: map[string]any{"success": true, "errorCode": "DENIED", "list": []any{}}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := listAppProject(tc.data); err == nil {
|
|
t.Fatal("malformed response was accepted")
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
data map[string]any
|
|
}{
|
|
{name: "missing collection", data: map[string]any{"success": true, "hasMore": false}},
|
|
{name: "wrong collection type", data: map[string]any{"success": true, "list": map[string]any{}, "hasMore": false}},
|
|
{name: "bad element", data: map[string]any{"success": true, "list": []any{"bad"}, "hasMore": false}},
|
|
{name: "missing stable id", data: map[string]any{"success": true, "list": []any{map[string]any{"name": "app"}}, "hasMore": false}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := listAppProject(tc.data); err == nil {
|
|
t.Fatal("malformed collection was accepted")
|
|
}
|
|
})
|
|
}
|
|
|
|
valid := []map[string]any{{"unifiedAppId": "app"}}
|
|
for _, tc := range []struct {
|
|
name string
|
|
data map[string]any
|
|
}{
|
|
{name: "missing pagination", data: map[string]any{"success": true}},
|
|
{name: "wrong has more type", data: map[string]any{"success": true, "hasMore": "false"}},
|
|
{name: "cursor without has more", data: map[string]any{"success": true, "nextCursor": "next"}},
|
|
{name: "more without cursor", data: map[string]any{"success": true, "hasMore": true}},
|
|
{name: "wrong cursor type", data: map[string]any{"success": true, "hasMore": true, "nextCursor": 1}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := devAppListProjection(tc.data, "apps", valid, "devapp/test"); err == nil {
|
|
t.Fatal("malformed pagination was accepted")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDevAppConfirmationAndValidationPrecedeCalls(t *testing.T) {
|
|
writes := []struct {
|
|
declaration shortcut.Shortcut
|
|
args []string
|
|
}{
|
|
{CreateApp, []string{"--name", "app"}},
|
|
{UpdateApp, []string{"--unified-app-id", "app", "--name", "new"}},
|
|
{DeleteApp, []string{"--unified-app-id", "app"}},
|
|
{EnableApp, []string{"--unified-app-id", "app"}},
|
|
{DisableApp, []string{"--unified-app-id", "app"}},
|
|
{WebappConfig, []string{"--unified-app-id", "app", "--homepage-url", "https://example.invalid"}},
|
|
{RobotConfig, []string{"--unified-app-id", "app", "--name", "bot"}},
|
|
{RobotEnable, []string{"--unified-app-id", "app"}},
|
|
{RobotDisable, []string{"--unified-app-id", "app"}},
|
|
{EventSubscribe, []string{"--unified-app-id", "app", "--event-codes", "event.one"}},
|
|
{VersionCreate, []string{"--unified-app-id", "app", "--desc", "version"}},
|
|
}
|
|
for _, tc := range writes {
|
|
t.Run(tc.declaration.Command, func(t *testing.T) {
|
|
caller := &devAppCoverageCaller{}
|
|
err := runDevAppCoverage(t, tc.declaration, caller, tc.args...)
|
|
var typed *apperrors.Error
|
|
if !errors.As(err, &typed) || typed.Reason != "confirmation_required" {
|
|
t.Fatalf("unconfirmed error = %#v, want confirmation_required", err)
|
|
}
|
|
if len(caller.calls) != 0 {
|
|
t.Fatalf("unconfirmed command called MCP: %#v", caller.calls)
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
declaration shortcut.Shortcut
|
|
args []string
|
|
}{
|
|
{UpdateApp, []string{"--unified-app-id", "app", "--yes"}},
|
|
{WebappConfig, []string{"--unified-app-id", "app", "--yes"}},
|
|
{RobotConfig, []string{"--unified-app-id", "app", "--yes"}},
|
|
{EventSubscribe, []string{"--unified-app-id", "app", "--event-codes", "event.one,event.one", "--yes"}},
|
|
} {
|
|
caller := &devAppCoverageCaller{}
|
|
err := runDevAppCoverage(t, tc.declaration, caller, tc.args...)
|
|
var typed *apperrors.Error
|
|
if !errors.As(err, &typed) || typed.Reason == "confirmation_required" {
|
|
t.Fatalf("invalid input error = %#v", err)
|
|
}
|
|
if len(caller.calls) != 0 {
|
|
t.Fatalf("invalid input called MCP: %#v", caller.calls)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDevAppResidualAdapters(t *testing.T) {
|
|
t.Run("credentials require stable identity client and secret", func(t *testing.T) {
|
|
valid := &devAppCoverageCaller{responses: map[string][]map[string]any{
|
|
"get_dev_app_credentials": {{"success": true, "result": map[string]any{
|
|
"unifiedAppId": "app", "appKey": "client", "appSecret": "secret",
|
|
}}},
|
|
}}
|
|
if err := runDevAppCoverage(t, GetCredentials, valid, "--unified-app-id", "app"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
missingSecret := &devAppCoverageCaller{responses: map[string][]map[string]any{
|
|
"get_dev_app_credentials": {{"success": true, "result": map[string]any{
|
|
"unifiedAppId": "app", "appKey": "client",
|
|
}}},
|
|
}}
|
|
if err := runDevAppCoverage(t, GetCredentials, missingSecret, "--unified-app-id", "app"); err == nil {
|
|
t.Fatal("credential response without a secret was accepted")
|
|
}
|
|
})
|
|
|
|
t.Run("credentials unified data matches declared sensitive paths", func(t *testing.T) {
|
|
declaration, ok := devAppRegistered("+credentials-get")
|
|
if !ok || declaration.OutputRollout != output.RolloutUnifiedActive {
|
|
t.Fatalf("registered credentials shortcut is not unified: found=%t rollout=%q", ok, declaration.OutputRollout)
|
|
}
|
|
caller := &devAppCoverageCaller{responses: map[string][]map[string]any{
|
|
"get_dev_app_credentials": {{"success": true, "result": map[string]any{
|
|
"unifiedAppId": "app", "appKey": "client", "appSecret": "test-app-secret",
|
|
"clientSecret": "test-client-secret", "secret": "test-secret",
|
|
}}},
|
|
}}
|
|
encoded, err := runDevAppUnifiedCoverage(t, declaration, caller, "--unified-app-id", "app")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var envelope map[string]any
|
|
if err := json.Unmarshal(encoded, &envelope); err != nil {
|
|
t.Fatalf("unified output is not JSON: %v", err)
|
|
}
|
|
data, ok := envelope["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("unified output data type = %T, want object", envelope["data"])
|
|
}
|
|
if _, nested := data["result"]; nested {
|
|
t.Fatal("credential business object is nested under data.result; declared sensitive paths would miss it")
|
|
}
|
|
for _, path := range declaration.Contract.Result.SensitivePaths {
|
|
if value, present := data[path]; !present || strings.TrimSpace(fmt.Sprint(value)) == "" {
|
|
t.Fatalf("declared sensitive path %q does not resolve to a non-empty data field", path)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("member projector supports known and guaranteed zero", func(t *testing.T) {
|
|
members := []any{
|
|
map[string]any{"userId": "user-1", "memberType": "OWNER"},
|
|
map[string]any{"userId": "user-2", "memberType": "DEVELOPER"},
|
|
}
|
|
if got := projectDevAppMembers(members, "user-1"); len(got) != 1 || got[0]["userId"] != "user-1" {
|
|
t.Fatalf("known member projection = %#v", got)
|
|
}
|
|
if got := projectDevAppMembers(members, "guaranteed-missing"); len(got) != 0 {
|
|
t.Fatalf("zero member projection = %#v", got)
|
|
}
|
|
for _, bad := range []map[string]any{
|
|
{"success": true, "members": []any{"bad"}},
|
|
{"success": true, "members": []any{map[string]any{"name": "missing-id"}}},
|
|
} {
|
|
if _, _, err := requireDevAppCollection(bad, "devapp/list_dev_app_members", []string{"userId"}, "members"); err == nil {
|
|
t.Fatalf("malformed member response accepted: %#v", bad)
|
|
}
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
declaration shortcut.Shortcut
|
|
args []string
|
|
responses map[string][]map[string]any
|
|
wantTools []string
|
|
}{
|
|
{
|
|
name: "robot config", declaration: RobotConfig,
|
|
args: []string{"--unified-app-id", "app", "--name", "bot", "--mode", "STREAM", "--yes"},
|
|
responses: map[string][]map[string]any{
|
|
"set_extension_robot_config": {{"success": true}},
|
|
"get_extension_robot_config": {{"success": true, "result": map[string]any{"unifiedAppId": "app", "name": "bot", "mode": "STREAM"}}},
|
|
},
|
|
wantTools: []string{"set_extension_robot_config", "get_extension_robot_config"},
|
|
},
|
|
{
|
|
name: "robot enable", declaration: RobotEnable,
|
|
args: []string{"--unified-app-id", "app", "--yes"},
|
|
responses: map[string][]map[string]any{
|
|
"enable_dev_app_robot": {{"success": true}},
|
|
"get_extension_robot_config": {{"success": true, "result": map[string]any{"unifiedAppId": "app", "robotStatus": "ONLINE"}}},
|
|
},
|
|
wantTools: []string{"enable_dev_app_robot", "get_extension_robot_config"},
|
|
},
|
|
{
|
|
name: "robot disable", declaration: RobotDisable,
|
|
args: []string{"--unified-app-id", "app", "--yes"},
|
|
responses: map[string][]map[string]any{
|
|
"disable_dev_app_robot": {{"success": true}},
|
|
"get_extension_robot_config": {{"success": true, "result": map[string]any{"unifiedAppId": "app", "robotStatus": "UNCONFIGURED"}}},
|
|
},
|
|
wantTools: []string{"disable_dev_app_robot", "get_extension_robot_config"},
|
|
},
|
|
{
|
|
name: "event subscribe", declaration: EventSubscribe,
|
|
args: []string{"--unified-app-id", "app", "--event-codes", "event.one", "--yes"},
|
|
responses: map[string][]map[string]any{
|
|
"subscribe_dev_app_events": {{"success": true}},
|
|
"list_dev_app_events": {{"success": true, "list": []any{map[string]any{"eventCode": "event.one"}}, "hasMore": false}},
|
|
},
|
|
wantTools: []string{"subscribe_dev_app_events", "list_dev_app_events"},
|
|
},
|
|
{
|
|
name: "version create", declaration: VersionCreate,
|
|
args: []string{"--unified-app-id", "app", "--desc", "snapshot", "--yes"},
|
|
responses: map[string][]map[string]any{
|
|
"create_dev_app_version": {{"success": true, "result": map[string]any{"versionId": "version"}}},
|
|
"get_dev_app_version_detail": {{"success": true, "result": map[string]any{"unifiedAppId": "app", "versionId": "version", "desc": "snapshot"}}},
|
|
},
|
|
wantTools: []string{"create_dev_app_version", "get_dev_app_version_detail"},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
caller := &devAppCoverageCaller{responses: tc.responses}
|
|
if err := runDevAppCoverage(t, tc.declaration, caller, tc.args...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gotTools := make([]string, 0, len(caller.calls))
|
|
for _, call := range caller.calls {
|
|
gotTools = append(gotTools, call.tool)
|
|
}
|
|
if !reflect.DeepEqual(gotTools, tc.wantTools) {
|
|
t.Fatalf("tools = %#v, want %#v", gotTools, tc.wantTools)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("event false receipt is rejected by readback", func(t *testing.T) {
|
|
caller := &devAppCoverageCaller{responses: map[string][]map[string]any{
|
|
"subscribe_dev_app_events": {{"success": true}},
|
|
"list_dev_app_events": {{"success": true, "list": []any{}, "hasMore": false}},
|
|
}}
|
|
if err := runDevAppCoverage(t, EventSubscribe, caller, "--unified-app-id", "app", "--event-codes", "event.one", "--yes"); err == nil {
|
|
t.Fatal("event subscription without exact readback was accepted")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDevAppWritesUseExactReadback(t *testing.T) {
|
|
t.Run("create", func(t *testing.T) {
|
|
caller := &devAppCoverageCaller{responses: map[string][]map[string]any{
|
|
"create_dev_app": {{"success": true, "result": map[string]any{"unifiedAppId": "app"}}},
|
|
"get_dev_app": {{"success": true, "result": map[string]any{"unifiedAppId": "app", "name": "created"}}},
|
|
}}
|
|
if err := runDevAppCoverage(t, CreateApp, caller, "--name", "created", "--yes"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []devAppCoverageCall{
|
|
{tool: "create_dev_app", params: map[string]any{"name": "created"}},
|
|
{tool: "get_dev_app", params: map[string]any{"unifiedAppId": "app"}},
|
|
}
|
|
if !reflect.DeepEqual(caller.calls, want) {
|
|
t.Fatalf("calls = %#v, want %#v", caller.calls, want)
|
|
}
|
|
})
|
|
|
|
t.Run("update mismatch fails", func(t *testing.T) {
|
|
caller := &devAppCoverageCaller{responses: map[string][]map[string]any{
|
|
"update_dev_app": {{"success": true}},
|
|
"get_dev_app": {{"success": true, "result": map[string]any{"unifiedAppId": "app", "name": "old"}}},
|
|
}}
|
|
if err := runDevAppCoverage(t, UpdateApp, caller, "--unified-app-id", "app", "--name", "new", "--yes"); err == nil {
|
|
t.Fatal("write/readback mismatch was accepted")
|
|
}
|
|
if len(caller.calls) != 2 || caller.calls[0].tool != "update_dev_app" || caller.calls[1].tool != "get_dev_app" {
|
|
t.Fatalf("calls = %#v", caller.calls)
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
declaration shortcut.Shortcut
|
|
writeTool string
|
|
status string
|
|
}{
|
|
{name: "enable", declaration: EnableApp, writeTool: "enable_dev_app", status: "normal"},
|
|
{name: "disable", declaration: DisableApp, writeTool: "disable_dev_app", status: "disabled"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
caller := &devAppCoverageCaller{responses: map[string][]map[string]any{
|
|
tc.writeTool: {{"success": true}},
|
|
"get_dev_app": {{"success": true, "result": map[string]any{
|
|
"unifiedAppId": "app", "appStatus": tc.status,
|
|
}}},
|
|
}}
|
|
if err := runDevAppCoverage(t, tc.declaration, caller, "--unified-app-id", "app", "--yes"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(caller.calls) != 2 || caller.calls[0].tool != tc.writeTool || caller.calls[1].tool != "get_dev_app" {
|
|
t.Fatalf("calls = %#v", caller.calls)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("webapp config", func(t *testing.T) {
|
|
caller := &devAppCoverageCaller{responses: map[string][]map[string]any{
|
|
"set_extension_webapp_config": {{"success": true}},
|
|
"get_extension_webapp_config": {{"success": true, "result": map[string]any{
|
|
"unifiedAppId": "app", "homepageUrl": "https://example.invalid",
|
|
}}},
|
|
}}
|
|
if err := runDevAppCoverage(t, WebappConfig, caller, "--unified-app-id", "app", "--homepage-url", "https://example.invalid", "--yes"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(caller.calls) != 2 || caller.calls[0].tool != "set_extension_webapp_config" || caller.calls[1].tool != "get_extension_webapp_config" {
|
|
t.Fatalf("calls = %#v", caller.calls)
|
|
}
|
|
})
|
|
|
|
t.Run("delete pre-read and absence", func(t *testing.T) {
|
|
caller := &devAppCoverageCaller{responses: map[string][]map[string]any{
|
|
"get_dev_app": {{"success": true, "result": map[string]any{"unifiedAppId": "app", "appKey": "key"}}},
|
|
"delete_dev_app": {{"success": true}},
|
|
"list_dev_app": {{"success": true, "list": []any{}, "hasMore": false}},
|
|
}}
|
|
if err := runDevAppCoverage(t, DeleteApp, caller, "--unified-app-id", "app", "--yes"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []devAppCoverageCall{
|
|
{tool: "get_dev_app", params: map[string]any{"unifiedAppId": "app"}},
|
|
{tool: "delete_dev_app", params: map[string]any{"unifiedAppId": "app"}},
|
|
{tool: "list_dev_app", params: map[string]any{"appKey": "key", "pageSize": 20}},
|
|
}
|
|
if !reflect.DeepEqual(caller.calls, want) {
|
|
t.Fatalf("calls = %#v, want %#v", caller.calls, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDevAppReviewedInventory(t *testing.T) {
|
|
public := 0
|
|
unavailable := 0
|
|
total := 0
|
|
for _, item := range shortcut.All() {
|
|
if item.Service != "devapp" {
|
|
continue
|
|
}
|
|
total++
|
|
if item.Availability == shortcut.AvailabilityUnavailable {
|
|
unavailable++
|
|
if !item.Hidden || item.CompatibilityVisible || shortcut.InPublicCatalog(item.Service, item.Command) {
|
|
t.Errorf("unavailable %s visibility/public = hidden=%v compatibility=%v public=%v",
|
|
item.Command, item.Hidden, item.CompatibilityVisible, shortcut.InPublicCatalog(item.Service, item.Command))
|
|
}
|
|
if strings.TrimSpace(devAppUnavailableReasons[item.Command]) == "" {
|
|
t.Errorf("unavailable %s lacks concrete reason", item.Command)
|
|
}
|
|
if !item.Contract.Empty() {
|
|
t.Errorf("hidden unavailable %s must not publish an incomplete Contract", item.Command)
|
|
}
|
|
continue
|
|
}
|
|
if shortcut.InPublicCatalog(item.Service, item.Command) {
|
|
public++
|
|
if item.Hidden || item.Availability != shortcut.AvailabilityAvailable {
|
|
t.Errorf("public %s visibility/availability = %v/%q", item.Command, item.Hidden, item.Availability)
|
|
}
|
|
if item.OutputRollout != output.RolloutUnifiedActive {
|
|
t.Errorf("public %s rollout = %q", item.Command, item.OutputRollout)
|
|
}
|
|
if item.Contract.Result == nil || item.Contract.Interface == nil || item.Contract.Description == "" {
|
|
t.Errorf("public %s has incomplete Contract/Result", item.Command)
|
|
}
|
|
if item.Safety.Effect == "" || item.Safety.Risk == "" || item.Safety.Confirmation == "" || item.Safety.Idempotency == "" {
|
|
t.Errorf("public %s has incomplete Safety", item.Command)
|
|
}
|
|
}
|
|
}
|
|
if total != 30 || public != 25 || unavailable != 5 {
|
|
t.Fatalf("devapp inventory total/public/unavailable = %d/%d/%d, want 30/25/5", total, public, unavailable)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDevAppUnavailableCommandsMakeZeroCalls(t *testing.T) {
|
|
for command, reason := range devAppUnavailableReasons {
|
|
declaration, found := devAppRegistered(command)
|
|
if !found {
|
|
t.Fatalf("registered unavailable command missing: %s", command)
|
|
}
|
|
args := []string{}
|
|
for _, flag := range declaration.Flags {
|
|
if !flag.Required {
|
|
continue
|
|
}
|
|
args = append(args, "--"+flag.Name)
|
|
if flag.Type != shortcut.FlagBool {
|
|
args = append(args, "fixture")
|
|
}
|
|
}
|
|
caller := &devAppCoverageCaller{}
|
|
err := runDevAppCoverage(t, declaration, caller, args...)
|
|
if err == nil || !strings.Contains(err.Error(), reason) {
|
|
t.Errorf("%s unavailable error = %v", command, err)
|
|
}
|
|
if len(caller.calls) != 0 {
|
|
t.Errorf("%s unavailable command called MCP: %#v", command, caller.calls)
|
|
}
|
|
}
|
|
}
|