Files

384 lines
14 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package helpers
import (
"bytes"
"context"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/spf13/cobra"
)
func TestDevAppSharedResultMapperClassifiesServiceOutcomes(t *testing.T) {
t.Run("normalized success", func(t *testing.T) {
result := DevAppCommandResultFromPayload("", map[string]any{
"content": map[string]any{"success": true, "result": map[string]any{"id": "a"}},
}, false)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
data, _ := env.Data.(map[string]any)
if env.Outcome != output.OutcomeSuccess || data["id"] != "a" {
t.Fatalf("success envelope=%+v data=%#v", env, env.Data)
}
})
t.Run("non boolean success fails closed", func(t *testing.T) {
for _, invalid := range []any{"false", 0, map[string]any{"value": false}} {
result := DevAppCommandResultFromPayload("", map[string]any{"success": invalid}, false)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Outcome != output.OutcomeFailure || env.Error == nil ||
env.Error.Subtype != "invalid_success_type" || env.Error.Hint == "" {
t.Fatalf("invalid success=%#v envelope=%+v", invalid, env)
}
}
})
t.Run("pending approval", func(t *testing.T) {
result := DevAppCommandResultFromPayload("", map[string]any{
"versionStatus": "AUDIT", "versionId": "v1", "unifiedAppId": "u1",
}, false)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Outcome != output.OutcomePending || env.Meta == nil || env.Meta.Operation == nil || env.Meta.Operation.NextCommand == "" {
t.Fatalf("pending envelope=%+v", env)
}
})
t.Run("approval selection uses tool normalization", func(t *testing.T) {
result := DevAppCommandResultFromPayload(devAppVersionPublishTool, map[string]any{
"approvalMode": "SELECT_APPROVER",
"unifiedAppId": "u1",
"versionId": "v1",
"approvalCandidates": []any{
map[string]any{"userId": "user-1", "name": "Alice"},
},
}, false)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Outcome != output.OutcomePending || env.Meta == nil || env.Meta.Operation == nil {
t.Fatalf("approval envelope=%+v", env)
}
if env.Meta.Operation.State != "waiting_for_approver_selection" || env.Meta.Operation.NextCommand == "" {
t.Fatalf("approval operation=%+v", env.Meta.Operation)
}
if strings.Contains(env.Meta.Operation.NextCommand, "--yes") {
t.Fatalf("approval next_command bypasses confirmation: %q", env.Meta.Operation.NextCommand)
}
})
t.Run("pagination", func(t *testing.T) {
result := DevAppCommandResultFromPayload("", map[string]any{
"items": []any{}, "hasMore": true, "nextCursor": "next",
}, false)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Meta == nil || env.Meta.Pagination == nil || env.Meta.Pagination.EndpointExhausted || env.Meta.Pagination.NextToken != "next" {
t.Fatalf("pagination envelope=%+v", env)
}
data, ok := env.Data.(map[string]any)
if !ok {
t.Fatalf("pagination data type=%T, want map[string]any", env.Data)
}
if _, exists := data["hasMore"]; exists {
t.Fatalf("business data must not retain hasMore: %#v", data)
}
if _, exists := data["nextCursor"]; exists {
t.Fatalf("business data must not retain nextCursor: %#v", data)
}
if _, exists := data["items"]; !exists {
t.Fatalf("business data lost items while extracting pagination: %#v", data)
}
})
for name, payload := range map[string]map[string]any{
"missing cursor": {"items": []any{}, "hasMore": true},
"wrong flag type": {"items": []any{}, "hasMore": "false"},
} {
t.Run("pagination inconsistent "+name, func(t *testing.T) {
result := DevAppCommandResultFromPayload("", payload, false)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Outcome != output.OutcomeFailure || env.Error == nil || env.Error.Subtype != "pagination_inconsistent" {
t.Fatalf("pagination envelope=%+v", env)
}
})
}
t.Run("terminal cursor is not resumable", func(t *testing.T) {
result := DevAppCommandResultFromPayload("", map[string]any{
"items": []any{}, "hasMore": false, "nextCursor": "terminal-cursor",
}, false)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Outcome != output.OutcomeSuccess || env.Meta == nil || env.Meta.Pagination == nil ||
!env.Meta.Pagination.EndpointExhausted || env.Meta.Pagination.NextToken != "" {
t.Fatalf("terminal pagination envelope=%+v", env)
}
data, _ := env.Data.(map[string]any)
if _, exists := data["nextCursor"]; exists {
t.Fatalf("terminal data leaked cursor: %#v", data)
}
})
for _, tool := range []string{devAppListTool, devAppPermissionListTool, devAppEventListTool, devAppVersionListTool} {
t.Run("declared pagination missing "+tool, func(t *testing.T) {
result := DevAppCommandResultFromPayload(tool, map[string]any{"items": []any{}}, false)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Outcome != output.OutcomeFailure || env.Error == nil ||
env.Error.Subtype != "pagination_inconsistent" {
t.Fatalf("missing pagination envelope=%+v", env)
}
})
t.Run("dry run preview needs no server pagination "+tool, func(t *testing.T) {
result := DevAppCommandResultFromPayload(tool, map[string]any{
"invocation": map[string]any{"tool": tool},
}, true)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Outcome != output.OutcomeSuccess || !env.DryRun || env.Meta != nil {
t.Fatalf("dry-run preview envelope=%+v", env)
}
})
}
t.Run("partial", func(t *testing.T) {
started := true
result := DevAppCommandResultFromPayload("", map[string]any{
"multiProfile": true,
"profiles": []any{
map[string]any{"selector": "a", "ok": true, "result": map[string]any{"id": "r1"}},
map[string]any{"selector": "b", "ok": false, "error": map[string]any{
"category": "network", "reason": "timeout", "message": "bad",
"stage": "response", "execution_started": started,
}},
},
}, false)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Outcome != output.OutcomePartialFailure || result.ExitCode() != 7 {
t.Fatalf("partial outcome=%s rc=%d", env.Outcome, result.ExitCode())
}
partial, ok := env.Data.(*output.PartialData)
if !ok || len(partial.Failed) != 1 || partial.Failed[0].Error == nil ||
partial.Failed[0].Error.Type != "api" || partial.Failed[0].Error.Subtype != "timeout" ||
partial.Failed[0].Error.ExecutionStarted == nil || !*partial.Failed[0].Error.ExecutionStarted {
t.Fatalf("partial error projection=%#v", env.Data)
}
})
t.Run("dry run is completed preview", func(t *testing.T) {
result := DevAppCommandResultFromPayload("", map[string]any{"tool": "x"}, true)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Outcome != output.OutcomeSuccess || !env.DryRun {
t.Fatalf("dry-run envelope=%+v", env)
}
})
t.Run("dry run still enforces pagination contract", func(t *testing.T) {
result := DevAppCommandResultFromPayload(devAppListTool, map[string]any{
"items": []any{}, "hasMore": false,
}, true)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Outcome != output.OutcomeSuccess || !env.DryRun || env.Meta == nil ||
env.Meta.Pagination == nil || !env.Meta.Pagination.EndpointExhausted {
t.Fatalf("dry-run pagination envelope=%+v", env)
}
data, ok := env.Data.(map[string]any)
if !ok {
t.Fatalf("dry-run pagination data type=%T", env.Data)
}
if _, exists := data["hasMore"]; exists {
t.Fatalf("dry-run data leaked hasMore: %#v", data)
}
if _, exists := data["nextCursor"]; exists {
t.Fatalf("dry-run data leaked nextCursor: %#v", data)
}
})
t.Run("dry run still fails closed on invalid success type", func(t *testing.T) {
result := DevAppCommandResultFromPayload("", map[string]any{"success": "false"}, true)
env, err := output.EnvelopeFromResult(result)
if err != nil {
t.Fatal(err)
}
if env.Outcome != output.OutcomeFailure || env.Error == nil || env.Error.Subtype != "invalid_success_type" {
t.Fatalf("dry-run invalid success envelope=%+v", env)
}
})
}
func TestDevAppRecoveryCommandsDoNotBypassConfirmation(t *testing.T) {
steps := append(devAppRobotPublishSteps("u1"), devAppRobotRetryStep("task1", true))
approval := map[string]any{
"approvalMode": "SELECT_APPROVER",
"unifiedAppId": "u1",
"versionId": "v1",
"approvalCandidates": []any{
map[string]any{"userId": "user-1", "name": "Alice"},
},
}
normalizeDevAppVersionApproval(approval)
steps = append(steps, approval["nextSteps"].([]map[string]any)...)
for _, step := range steps {
for _, key := range []string{"command", "dryRunCommand"} {
command, _ := step[key].(string)
for _, field := range strings.Fields(command) {
if field == "--yes" || strings.HasPrefix(field, "--yes=") {
t.Fatalf("step %v publishes confirmation bypass in %s: %q", step["id"], key, command)
}
}
}
}
}
func TestFrameworkDevAppMapperBoundaryMatrix(t *testing.T) {
if got := devAppDataWithoutPagination("scalar"); got != "scalar" {
t.Fatalf("non-object data = %#v, want scalar preserved", got)
}
for name, payload := range map[string]any{
"scalar": "value",
"bad cursor": map[string]any{"nextCursor": 1},
"cursor only": map[string]any{"nextCursor": "next"},
"empty cursor": map[string]any{"nextCursor": ""},
"exhausted": map[string]any{"hasMore": false},
} {
t.Run(name, func(t *testing.T) { _, _ = devAppPaginationMeta(payload) })
}
if devAppMultiProfileResult(map[string]any{}) != nil || devAppMultiProfileResult(map[string]any{"multiProfile": true}) != nil {
t.Fatal("non-profile payload classified")
}
allSuccess := devAppMultiProfileResult(map[string]any{"multiProfile": true, "profiles": []any{map[string]any{"ok": true}}})
if allSuccess != nil {
t.Fatalf("all-success multi profile=%v", allSuccess)
}
allFailed := devAppMultiProfileResult(map[string]any{"multiProfile": true, "profiles": []any{
map[string]any{"ok": false, "error": map[string]any{
"type": "authorization", "message": "denied", "retryable": true, "execution_started": false,
"actions": []any{"retry", 1}, "details": map[string]any{"scope": "x"}, "requestId": "req", "traceId": "trace",
}},
"malformed",
}})
allFailedEnv, err := output.EnvelopeFromResult(allFailed)
if err != nil || allFailedEnv.Outcome != output.OutcomeFailure || allFailedEnv.Error == nil || allFailedEnv.Error.Details == nil {
t.Fatalf("all-failed=%+v err=%v", allFailedEnv, err)
}
mixed := devAppMultiProfileResult(map[string]any{"multiProfile": true, "profiles": []any{
map[string]any{"selector": "ok", "ok": true},
map[string]any{"selector": "bad", "ok": false, "error": map[string]any{"category": "forbidden", "actions": []string{"fix"}, "code": "E"}},
123,
}})
if env, err := output.EnvelopeFromResult(mixed); err != nil || env.Outcome != output.OutcomePartialFailure {
t.Fatalf("mixed=%+v err=%v", env, err)
}
duplicate := devAppMultiProfileResult(map[string]any{"multiProfile": true, "profiles": []any{
map[string]any{"selector": "same", "ok": true},
map[string]any{"selector": "same", "ok": false},
}})
if env, err := output.EnvelopeFromResult(duplicate); err != nil || env.Outcome != output.OutcomeFailure || env.Error.Type != "internal" {
t.Fatalf("duplicate=%+v err=%v", env, err)
}
for _, payload := range []map[string]any{
{"success": false, "message": "no", "errorCode": "E"},
{"status": "FAILED", "code": 1},
{"status": "OK"},
} {
_ = devAppFailureResult(payload)
}
for _, raw := range []string{"validation", "forbidden", "timeout"} {
_ = devAppWireErrorType(raw)
}
for _, content := range []map[string]any{
{},
{"mustAskUser": true},
{"status": "WAITING", "taskId": "task"},
{"status": "WAITING", "requestId": "req"},
{"status": "WAITING", "taskId": "task", "nextSteps": []map[string]any{{"command": "dws next"}}},
{"status": "WAITING", "requestId": "req", "nextSteps": []any{map[string]any{"dryRunCommand": "dws preview"}, "bad"}},
{"approvalSubmitted": true, "unifiedAppId": "app", "versionId": "v"},
} {
result := devAppPendingResult(content)
if result != nil {
_, _ = output.EnvelopeFromResult(result)
}
}
if got := devAppRecoveryCommand(map[string]any{"taskId": "t"}); !bytes.Contains([]byte(got), []byte("robot result")) {
t.Fatalf("task recovery=%q", got)
}
if got := devAppRecoveryCommand(map[string]any{}); got != "" {
t.Fatalf("empty recovery=%q", got)
}
if got := devAppEnvelopeData(executor.Result{}); got == nil {
t.Fatal("unimplemented result should be preserved")
}
for _, result := range []executor.Result{
{Invocation: executor.Invocation{Implemented: true, Kind: "helper_invocation", DryRun: true}, Response: map[string]any{"content": map[string]any{"preview": true}}},
{Invocation: executor.Invocation{Implemented: true, Kind: "helper_invocation"}, Response: map[string]any{"content": map[string]any{"items": []any{}, "hasMore": false}}},
} {
cmd := &cobra.Command{Use: "legacy-envelope"}
cmd.SetContext(context.Background())
cmd.SetOut(&bytes.Buffer{})
if err := writeDevAppEnvelope(cmd, result); err != nil {
t.Fatal(err)
}
}
legacy := &cobra.Command{Use: "legacy"}
legacy.SetContext(context.Background())
legacy.SetOut(&bytes.Buffer{})
if err := writeDevRolloutResult(legacy, output.Success(nil), output.NewSuccessEnvelope(nil), output.FormatJSON); err != nil {
t.Fatal(err)
}
dual := &cobra.Command{Use: "dual"}
dual.SetContext(context.Background())
dual.SetOut(&bytes.Buffer{})
output.SetCommandRollout(dual, output.RolloutDualValidate)
if err := writeDevRolloutResult(dual, output.Pending(nil, nil), output.NewSuccessEnvelope(nil), output.FormatJSON); err == nil {
t.Fatal("dual validate accepted malformed shadow result")
}
ctx, _ := output.WithResultStore(context.Background())
active := &cobra.Command{Use: "active"}
active.SetContext(ctx)
output.SetCommandRollout(active, output.RolloutUnifiedActive)
if err := writeDevRolloutResult(active, output.Success(nil), nil, output.FormatJSON); err != nil {
t.Fatal(err)
}
}