test: close unified framework contract coverage

This commit is contained in:
玉澜
2026-08-10 14:41:41 +08:00
parent 4da1e52b08
commit 03bda02e04
21 changed files with 1238 additions and 47 deletions
+228
View File
@@ -0,0 +1,228 @@
package app
import (
"bytes"
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
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/pipeline"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
"github.com/spf13/cobra"
)
func TestFrameworkErrorProjectionPreservesRecoveryMetadata(t *testing.T) {
next := time.Date(2026, 8, 10, 1, 2, 3, 0, time.FixedZone("test", 8*60*60))
retry := int64(4)
started := true
leaf := &helpers.CLIError{Code: "UPSTREAM_CODE", Suggestion: "retry with id", Operation: "create"}
call := &transport.CallError{Stage: transport.CallStage("decode"), HTTPStatus: 503, RPCCode: 91, TraceID: "call-trace", Cause: leaf}
typed := &apperrors.Error{
Category: apperrors.CategoryAPI, Message: "failed", Reason: "upstream_failed", Hint: "use status",
Actions: []string{"dws status"}, Retryable: true, RetryableSet: true, RetryAfterSeconds: &retry,
RPCCode: 92, RPCData: json.RawMessage(`{"task":"x"}`), Operation: "publish", ServerKey: "server",
Origin: "gateway", FailureStage: "response", ExecutionStarted: &started, NextRetryAt: &next,
AvailableFlags: []string{"--id"}, Snapshot: "/tmp/snapshot", Details: map[string]any{"id": "x"},
ServerDiag: apperrors.ServerDiagnostics{TraceID: "typed-trace", ServerErrorCode: "SERVER_CODE", TechnicalDetail: "detail", FriendlyHint: "friendly", ActionURL: "https://example.test"},
Cause: call,
}
info := errorInfoFromExecutionError(typed)
if info.Type != "api" || info.Subtype != "upstream_failed" || info.HTTPStatus != 503 || info.RPCCode != 92 || info.RequestID != "call-trace" || info.TraceID != "typed-trace" {
t.Fatalf("projection=%+v", info)
}
if info.UpstreamCode != "SERVER_CODE" || info.NextRetryAt == "" || info.Cause == "" || info.RPCData == nil || info.ExecutionStarted == nil || !*info.ExecutionStarted {
t.Fatalf("recovery metadata=%+v", info)
}
requestCall := &transport.CallError{Stage: transport.CallStage("request"), HTTPStatus: 429, RequestID: "request-id"}
requestInfo := errorInfoFromExecutionError(requestCall)
if requestInfo.RequestID != "request-id" || requestInfo.HTTPStatus != 429 {
t.Fatalf("request projection=%+v", requestInfo)
}
partial := errorInfoFromExecutionError(&apperrors.Error{Category: apperrors.CategoryPartial, Message: "partial"})
if partial.Type != "internal" {
t.Fatalf("partial error type=%s", partial.Type)
}
for code, want := range map[int]string{1: "api", 2: "auth", 3: "validation", 4: "permission", 6: "discovery", 99: "internal"} {
if got := errorTypeForExitCode(code); got != want {
t.Fatalf("errorTypeForExitCode(%d)=%q", code, got)
}
}
}
func TestFrameworkExecutePreparseUnifiedErrorAndEmissionFallback(t *testing.T) {
for _, failWriter := range []bool{false, true} {
t.Run(map[bool]string{false: "unified", true: "fallback"}[failWriter], func(t *testing.T) {
testseam.Protect(t, &os.Args)
os.Args = []string{"dws", "leaf"}
testseam.Swap(t, &rootNormalizeProcessProfileArgs, func() func() { return func() {} })
testseam.Swap(t, &rootStopAllStdioClients, func() {})
testseam.Swap(t, &rootRunPreParse, func(*cobra.Command, *pipeline.Engine) error { return errors.New("bad preparse") })
var stdout bytes.Buffer
testseam.Swap(t, &rootNewRootCommandWithEngine, func(ctx context.Context, _ *pipeline.Engine) *cobra.Command {
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
root.SetContext(ctx)
leaf := &cobra.Command{Use: "leaf"}
output.SetCommandRollout(leaf, output.RolloutUnifiedActive)
if failWriter {
leaf.SetOut(frameworkFailWriter{})
} else {
leaf.SetOut(&stdout)
}
leaf.SetErr(&bytes.Buffer{})
root.AddCommand(leaf)
return root
})
if code := Execute(); code != 3 {
t.Fatalf("Execute code=%d", code)
}
if !failWriter && !strings.Contains(stdout.String(), `"outcome": "failure"`) {
t.Fatalf("stdout=%q", stdout.String())
}
})
}
}
func TestFrameworkPublicRootRequiresResultFromActiveCommand(t *testing.T) {
root := NewRootCommand(context.Background())
leaf := &cobra.Command{Use: "active-no-result", RunE: func(*cobra.Command, []string) error { return nil }}
output.SetCommandRollout(leaf, output.RolloutUnifiedActive)
root.AddCommand(leaf)
root.SetArgs([]string{"active-no-result"})
if _, err := root.ExecuteC(); err == nil || !strings.Contains(err.Error(), "without a CommandResult") {
t.Fatalf("ExecuteC error=%v", err)
}
}
func TestFrameworkAbortOutputSinkRemoveFailure(t *testing.T) {
originalRemove := rootRemoveFile
t.Cleanup(func() { rootRemoveFile = originalRemove })
file, err := os.CreateTemp(t.TempDir(), "abort-*")
if err != nil {
t.Fatal(err)
}
rootRemoveFile = func(string) error { return errors.New("remove failed") }
cmd := &cobra.Command{Use: "abort"}
cmd.SetContext(context.WithValue(context.Background(), outputFileContextKey{}, &outputSinkState{file: file, tempPath: file.Name()}))
if err := abortOutputSink(cmd); err == nil || !strings.Contains(err.Error(), "remove temporary") {
t.Fatalf("abort error=%v", err)
}
}
func TestFrameworkOutputSinkHookWrappingAndCleanupEdges(t *testing.T) {
installOutputSinkErrorCleanup(nil)
plain := &cobra.Command{Use: "plain"}
plain.SetContext(context.Background())
installOutputSinkErrorCleanup(plain)
file, err := os.CreateTemp(t.TempDir(), "sink-*")
if err != nil {
t.Fatal(err)
}
state := &outputSinkState{file: file, tempPath: file.Name(), target: filepath.Join(t.TempDir(), "out")}
cmd := &cobra.Command{Use: "all"}
cmd.SetContext(context.WithValue(context.Background(), outputFileContextKey{}, state))
var calls int
cmd.PreRunE = func(*cobra.Command, []string) error { calls++; return nil }
cmd.PreRun = func(*cobra.Command, []string) { calls++ }
cmd.RunE = func(*cobra.Command, []string) error { calls++; return nil }
cmd.Run = func(*cobra.Command, []string) { calls++ }
cmd.PostRunE = func(*cobra.Command, []string) error { calls++; return nil }
cmd.PostRun = func(*cobra.Command, []string) { calls++ }
installOutputSinkErrorCleanup(cmd)
if err := cmd.PreRunE(cmd, nil); err != nil {
t.Fatal(err)
}
cmd.PreRun(cmd, nil)
if err := cmd.RunE(cmd, nil); err != nil {
t.Fatal(err)
}
cmd.Run(cmd, nil)
if err := cmd.PostRunE(cmd, nil); err != nil {
t.Fatal(err)
}
cmd.PostRun(cmd, nil)
if calls != 6 {
t.Fatalf("hook calls=%d", calls)
}
file2, err := os.CreateTemp(t.TempDir(), "sink-error-*")
if err != nil {
t.Fatal(err)
}
errorCmd := &cobra.Command{Use: "error"}
errorCmd.SetContext(context.WithValue(context.Background(), outputFileContextKey{}, &outputSinkState{file: file2, tempPath: file2.Name(), target: "unused"}))
if err := runWithOutputSinkErrorCleanup(errorCmd, func() error { return errors.New("boom") }); err == nil {
t.Fatal("run error swallowed")
}
file3, err := os.CreateTemp(t.TempDir(), "sink-panic-*")
if err != nil {
t.Fatal(err)
}
panicCmd := &cobra.Command{Use: "panic"}
panicCmd.SetContext(context.WithValue(context.Background(), outputFileContextKey{}, &outputSinkState{file: file3, tempPath: file3.Name(), target: "unused"}))
func() {
defer func() {
if recover() == nil {
t.Fatal("panic swallowed")
}
}()
_ = runWithOutputSinkErrorCleanup(panicCmd, func() error { panic("boom") })
}()
if closeOutputSink(nil) != nil || abortOutputSink(nil) != nil || outputSinkForCommand(nil) != nil {
t.Fatal("nil sink guards failed")
}
finished := &outputSinkState{finished: true, file: file}
finishedCmd := &cobra.Command{Use: "finished"}
finishedCmd.SetContext(context.WithValue(context.Background(), outputFileContextKey{}, finished))
if closeOutputSink(finishedCmd) != nil || abortOutputSink(finishedCmd) != nil {
t.Fatal("finished sink was processed twice")
}
}
type frameworkFailWriter struct{}
func (frameworkFailWriter) Write([]byte) (int, error) { return 0, errors.New("write failed") }
func TestFrameworkExecutePanicBeforeEmissionUsesUnifiedFailure(t *testing.T) {
for _, failWriter := range []bool{false, true} {
t.Run(map[bool]string{false: "emits", true: "fallback"}[failWriter], func(t *testing.T) {
testseam.Protect(t, &os.Args)
os.Args = []string{"dws"}
testseam.Swap(t, &rootNormalizeProcessProfileArgs, func() func() { return func() {} })
testseam.Swap(t, &rootRunPreParse, func(*cobra.Command, *pipeline.Engine) error { return nil })
testseam.Swap(t, &rootStopAllStdioClients, func() {})
var stdout bytes.Buffer
testseam.Swap(t, &rootNewRootCommandWithEngine, func(ctx context.Context, _ *pipeline.Engine) *cobra.Command {
cmd := &cobra.Command{Use: "dws"}
cmd.SetContext(ctx)
output.SetCommandRollout(cmd, output.RolloutUnifiedActive)
if failWriter {
cmd.SetOut(frameworkFailWriter{})
} else {
cmd.SetOut(&stdout)
}
cmd.SetErr(&bytes.Buffer{})
return cmd
})
testseam.Swap(t, &rootExecuteCommand, func(*cobra.Command) (*cobra.Command, error) { panic("before emission") })
if code := Execute(); code != 5 {
t.Fatalf("Execute code=%d", code)
}
if !failWriter && !strings.Contains(stdout.String(), `"outcome": "failure"`) {
t.Fatalf("stdout=%q", stdout.String())
}
})
}
}
-3
View File
@@ -610,9 +610,6 @@ func newRootCommandWithEngine(rootCtx context.Context, engine *pipeline.Engine,
return cmd.Help()
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := output.ValidateUnifiedFormat(cmd); err != nil {
return apperrors.NewValidation(err.Error(), apperrors.WithReason("unsupported_format"))
}
// Cobra performs these checks after persistent pre-run hooks. Run
// them before opening the transactional sink so validation errors
// cannot strand a temporary file during direct ExecuteC calls.
+7 -2
View File
@@ -16,17 +16,22 @@ var rootEscalateSignal = func(sig os.Signal) {
redeliverProcessSignal(sig)
}
var (
rootFindProcess = os.FindProcess
rootExitProcess = os.Exit
)
// redeliverProcessSignal asks the current process to handle the second signal
// with the platform's default semantics. Platforms that cannot deliver the
// requested signal through os.Process.Signal fall back to the conventional
// CLI exit status instead of leaving the process running after escalation.
func redeliverProcessSignal(sig os.Signal) {
process, err := os.FindProcess(os.Getpid())
process, err := rootFindProcess(os.Getpid())
if err == nil {
err = process.Signal(sig)
}
if err != nil {
os.Exit(interruptionExitCode(sig))
rootExitProcess(interruptionExitCode(sig))
}
}
+54
View File
@@ -10,6 +10,7 @@ import (
"io"
"os"
"os/exec"
"strings"
"syscall"
"testing"
@@ -30,6 +31,59 @@ func signalSelf(t *testing.T, sig syscall.Signal) {
}
}
func TestFrameworkSignalRedeliveryFallbackAndInterruptionMethods(t *testing.T) {
originalFind, originalExit := rootFindProcess, rootExitProcess
t.Cleanup(func() { rootFindProcess, rootExitProcess = originalFind, originalExit })
rootFindProcess = func(int) (*os.Process, error) { return nil, errors.New("find failed") }
exitCode := 0
rootExitProcess = func(code int) { exitCode = code }
rootEscalateSignal(syscall.SIGTERM)
if exitCode != 143 {
t.Fatalf("escalation exit=%d", exitCode)
}
exitCode = 0
redeliverProcessSignal(syscall.SIGTERM)
if exitCode != 143 {
t.Fatalf("fallback exit=%d", exitCode)
}
rootFindProcess = func(int) (*os.Process, error) { return os.FindProcess(99999999) }
exitCode = 0
redeliverProcessSignal(syscall.SIGINT)
if exitCode != 130 {
t.Fatalf("signal fallback exit=%d", exitCode)
}
interrupted := &processInterruption{signal: syscall.SIGINT}
if !errors.Is(interrupted, context.Canceled) || interrupted.ExitCode() != 130 || interrupted.Subtype() != "cancelled_by_user" || !strings.Contains(interrupted.Error(), "interrupt") {
t.Fatalf("interruption=%v", interrupted)
}
terminated := &processInterruption{signal: syscall.SIGTERM}
if terminated.ExitCode() != 143 || terminated.Subtype() != "terminated" {
t.Fatalf("termination=%v", terminated)
}
state := &processSignalState{}
if !state.record(syscall.SIGINT, nil) || state.record(syscall.SIGTERM, nil) {
t.Fatal("signal state did not reject a second interruption")
}
}
func TestFrameworkManageProcessSignalsNilAndEscalation(t *testing.T) {
signals := make(chan os.Signal, 3)
stopped, escalated := false, make(chan os.Signal, 1)
ctx, _, stop := manageProcessSignals(context.Background(), nil, signals, func() { stopped = true }, func(sig os.Signal) { escalated <- sig })
signals <- nil
signals <- syscall.SIGINT
<-ctx.Done()
signals <- syscall.SIGTERM
if got := <-escalated; got != syscall.SIGTERM {
t.Fatalf("escalated=%v", got)
}
stop()
stop()
if !stopped {
t.Fatal("signal notification was not stopped")
}
}
func installSignalExecuteSeams(t *testing.T, unified bool, stdout, stderr io.Writer) {
t.Helper()
testseam.Protect(t, &os.Args)
+1 -4
View File
@@ -206,10 +206,7 @@ func canonicalJSONObject(raw json.RawMessage) (json.RawMessage, error) {
if err := decoder.Decode(&struct{}{}); err != io.EOF {
return nil, fmt.Errorf("must be one JSON object")
}
canonical, err := json.Marshal(object)
if err != nil {
return nil, fmt.Errorf("cannot canonicalize JSON object: %w", err)
}
canonical, _ := json.Marshal(object) // decoded RawMessages are always marshalable
return json.RawMessage(canonical), nil
}
@@ -4,10 +4,53 @@
package contract
import (
"encoding/json"
"strings"
"testing"
)
func TestFrameworkResultSpecValidationEdges(t *testing.T) {
if got, err := NormalizeResultSpec(nil, ""); err != nil || got != nil {
t.Fatalf("NormalizeResultSpec(nil)=(%v,%v)", got, err)
}
base := func() *ResultSpec {
return &ResultSpec{Outcomes: []ResultOutcome{ResultOutcomeSuccess}, DataSchema: json.RawMessage(`{"type":"object"}`)}
}
if got, err := NormalizeResultSpec(base(), ""); err != nil || got == nil || got.SensitivePaths != nil {
t.Fatalf("valid default spec=(%#v,%v)", got, err)
}
cases := []struct {
name string
edit func(*ResultSpec)
}{
{"ndjson invalid path", func(s *ResultSpec) {
s.NDJSON = &ResultNDJSONSpec{RecordPath: ".items", RecordSchema: json.RawMessage(`{"type":"object"}`)}
}},
{"ndjson invalid schema", func(s *ResultSpec) {
s.NDJSON = &ResultNDJSONSpec{RecordPath: "items", RecordSchema: json.RawMessage(`[]`)}
}},
{"pagination invalid cursor", func(s *ResultSpec) { s.Pagination = &ResultPaginationSpec{CursorPath: ".next", ExhaustionPath: "done"} }},
{"pagination invalid exhausted", func(s *ResultSpec) {
s.Pagination = &ResultPaginationSpec{CursorPath: "next", ExhaustionPath: "done..bad"}
}},
{"sensitive invalid", func(s *ResultSpec) { s.SensitivePaths = []string{"bad..path"} }},
{"empty schema", func(s *ResultSpec) { s.DataSchema = nil }},
{"multiple schema", func(s *ResultSpec) { s.DataSchema = json.RawMessage(`{} {}`) }},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
spec := base()
tc.edit(spec)
if _, err := NormalizeResultSpec(spec, "sample"); err == nil {
t.Fatalf("invalid spec accepted: %#v", spec)
}
})
}
if err := validateResultPath("a.$"); err == nil {
t.Fatal("unsafe segment accepted")
}
}
func TestCrossPlatformCoverageDryRunSpecValidate(t *testing.T) {
for _, kind := range []string{DryRunPreviewInvocation, DryRunPreviewRequest, DryRunPreviewPlan, DryRunPreviewDiff} {
if err := (DryRunSpec{PreviewKind: kind}).Validate("sample.run"); err != nil {
+30
View File
@@ -42,6 +42,10 @@ func TestCrossPlatformCoverageNewCommandEmbedsFullContractDeclAsFinalSource(t *t
Description: "Create Desc",
Positionals: []contract.RuntimeSchemaPositional{{Name: "id", Required: true, Index: 0}},
DryRun: &contract.DryRunSpec{PreviewKind: "invocation", RemoteReads: true},
Result: &contract.ResultSpec{
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess},
DataSchema: []byte(`{"type":"object"}`),
},
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
@@ -79,6 +83,9 @@ func TestCrossPlatformCoverageNewCommandEmbedsFullContractDeclAsFinalSource(t *t
if final.DryRun == nil || final.DryRun.PreviewKind != "invocation" || !final.DryRun.RemoteReads {
t.Fatalf("dry_run = %#v", final.DryRun)
}
if final.Result == nil || len(final.Result.Outcomes) != 1 {
t.Fatalf("result = %#v", final.Result)
}
if final.Interface == nil || final.Interface.Mode != "mcp" || final.Interface.Ref == nil || final.Interface.Ref.RPCName != "create_thing" {
t.Fatalf("interface = %#v", final.Interface)
}
@@ -110,6 +117,29 @@ func TestCrossPlatformCoverageNewCommandEmbedsFullContractDeclAsFinalSource(t *t
}
}
func TestFrameworkContractDeclResultMarksNonEmptyAndRejectsInvalidSchema(t *testing.T) {
if (ContractDecl{Result: &contract.ResultSpec{}}).Empty() {
t.Fatal("Result declaration was treated as empty")
}
defer func() {
if recovered := recover(); recovered == nil || !strings.Contains(recovered.(string), "invalid Contract.Result") {
t.Fatalf("panic=%v", recovered)
}
}()
New(Spec{
Use: "bad-result",
Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"},
Contract: ContractDecl{
Title: "Bad", Description: "bad result",
Result: &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess}},
Interface: &contract.InterfaceSpec{Mode: "local", Availability: "available"},
Selection: contract.SelectionSpec{AgentSummary: "bad", UseWhen: []string{"bad"}, AvoidWhen: []string{"good"}, Examples: []string{"dws bad-result"}},
Identity: contract.ToolIdentitySpec{ProductID: "sample", Name: "bad", CanonicalPath: "sample.bad", CLIPath: "bad-result", PrimaryCLIPath: "bad-result"},
},
Invoke: func(*Ctx, map[string]any) error { return nil },
})
}
func TestNewCommandFallsBackToDeclaredDescriptionWithoutLong(t *testing.T) {
// Long wins when authored; without one the mandatory declaration supplies it.
cmd := New(Spec{
@@ -188,3 +188,63 @@ func TestResolveRuntimeSafetyUsesCanonicalOrCLIIdentityAndRejectsUnavailable(t *
t.Fatalf("legacy lookup = declared %v ok %v, want false false", declared, ok)
}
}
func boolPointer(value bool) *bool { return &value }
func intPointer(value int) *int { return &value }
func TestFrameworkContractFinalDeepCopyAndSafetyConflicts(t *testing.T) {
cmd := &cobra.Command{Use: "all"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(cmd) })
payload := contract.ContractFinalPayload{
Positionals: []contract.RuntimeSchemaPositional{{Name: "id"}},
Parameters: []contract.ParamDecl{{Name: "mode", Enum: []string{"a"}, Required: boolPointer(true)}},
Safety: &contract.SafetySpec{Effect: " read ", EffectSource: " source ", Risk: " low ", Confirmation: " not_required ", Idempotency: " idempotent "},
DryRun: &contract.DryRunSpec{PreviewKind: "plan"},
Result: &contract.ResultSpec{
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess},
DataSchema: []byte(`{"type":"object"}`), SensitivePaths: []string{"token"},
NDJSON: &contract.ResultNDJSONSpec{RecordPath: "items", RecordSchema: []byte(`{"type":"object"}`)},
Pagination: &contract.ResultPaginationSpec{CursorPath: "next", ExhaustionPath: "done"},
},
Interface: &contract.InterfaceSpec{Ref: &contract.InterfaceRefSpec{}},
Selection: &contract.SelectionSpec{
UseWhen: []string{"use"}, AvoidWhen: []string{"avoid"}, Prerequisites: []string{"pre"}, Tips: []string{"tip"},
WorkflowRefs: []string{"flow"}, Examples: []string{"example"}, SourceRefs: []string{"source"},
ExampleDispositions: []contract.ExampleDisposition{{Index: intPointer(1)}}, Reviewed: boolPointer(true),
},
Identity: &contract.ToolIdentitySpec{CanonicalPath: "sample.all", Aliases: []string{"alias"}},
}
RegisterRuntimeContractFinal(cmd, payload)
got, ok := RuntimeContractFinal(cmd)
if !ok || got.Result == payload.Result || got.Result.NDJSON == payload.Result.NDJSON || got.Result.Pagination == payload.Result.Pagination || got.Interface == payload.Interface || got.Selection == payload.Selection || got.Identity == payload.Identity {
t.Fatalf("payload not deeply cloned: %#v", got)
}
payload.Parameters[0].Enum[0] = "changed"
*payload.Parameters[0].Required = false
*payload.Selection.ExampleDispositions[0].Index = 9
*payload.Selection.Reviewed = false
again, _ := RuntimeContractFinal(cmd)
if again.Parameters[0].Enum[0] != "a" || !*again.Parameters[0].Required || *again.Selection.ExampleDispositions[0].Index != 1 || !*again.Selection.Reviewed {
t.Fatalf("stored payload aliased input: %#v", again)
}
matching := &cobra.Command{Use: "matching"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(matching) })
RegisterRuntimeContractFinal(matching, contract.ContractFinalPayload{Identity: &contract.ToolIdentitySpec{Path: "sample.all", CLIPath: "sample all"}, Safety: &contract.SafetySpec{Effect: "read", EffectSource: "source", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}})
if _, declared, valid := ResolveRuntimeSafety("sample.all", ""); !declared || !valid {
t.Fatalf("equivalent duplicate=(declared=%v valid=%v)", declared, valid)
}
conflict := &cobra.Command{Use: "conflict"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(conflict) })
RegisterRuntimeContractFinal(conflict, contract.ContractFinalPayload{Identity: &contract.ToolIdentitySpec{CanonicalPath: "sample.all"}, Safety: &contract.SafetySpec{Effect: "write"}})
if _, declared, valid := ResolveRuntimeSafety("sample.all", ""); !declared || valid {
t.Fatalf("conflict=(declared=%v valid=%v)", declared, valid)
}
if runtimeIdentityMatches(contract.ToolIdentitySpec{}, "", "") {
t.Fatal("empty identity matched")
}
if got := cloneSlice[string](nil); got != nil {
t.Fatalf("cloneSlice(nil)=%v", got)
}
}
+25
View File
@@ -3,6 +3,7 @@ package corecmd
import (
"bytes"
"context"
"errors"
"strings"
"testing"
@@ -49,3 +50,27 @@ func TestResultInvokeCarriesOneFrameworkResult(t *testing.T) {
t.Fatalf("rollout=%s", output.CommandRollout(cmd))
}
}
func TestFrameworkResultInvokeErrorLegacyAndStoreEdges(t *testing.T) {
wantErr := errors.New("invoke failed")
cases := []struct {
name string
rollout output.RolloutState
invoke func(*Ctx, map[string]any) (output.CommandResult, error)
want string
}{
{"invoke error", output.RolloutUnifiedActive, func(*Ctx, map[string]any) (output.CommandResult, error) { return nil, wantErr }, "invoke failed"},
{"legacy guard", output.RolloutLegacyOnly, func(*Ctx, map[string]any) (output.CommandResult, error) { return output.Success(nil), nil }, "without an active unified-result rollout"},
{"missing store", output.RolloutUnifiedActive, func(*Ctx, map[string]any) (output.CommandResult, error) { return output.Success(nil), nil }, "no result store"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cmd := New(Spec{Use: "result", OutputRollout: tc.rollout, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, ResultInvoke: tc.invoke})
cmd.SetArgs(nil)
err := cmd.Execute()
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("Execute error=%v, want %q", err, tc.want)
}
})
}
}
+4 -20
View File
@@ -335,26 +335,10 @@ func startDaemon(cmd *cobra.Command, dirKey, clientID, unifiedAppID, channel, no
// Release the child so the parent can exit without leaving a zombie.
pid := child.Process.Pid
_ = child.Process.Release()
if !output.UsesUnifiedResult(cmd) {
writeConnectDaemonStarted(cmd.OutOrStdout(), pid, logPath, clientID, dirKey)
return nil
}
// 统一输出 dev 域试点(队列 B110):人读提示行走 stderr,stdout 由
// 结果信封承载(pid/日志路径等机器事实进 data,ok:true + outcome:success)。
writeConnectDaemonStarted(cmd.ErrOrStderr(), pid, logPath, clientID, dirKey)
env := &output.Envelope{
OK: true,
Outcome: output.OutcomeSuccess,
Data: &connectDaemonStartedResult{
Status: "started",
Pid: pid,
LogPath: logPath,
DirKey: dirKey,
ClientID: clientID,
},
}
return writeDevRolloutResult(cmd, output.Success(env.Data), env, output.FormatJSON)
// The foreground/daemon root remains one streaming legacy command in this
// release. It cannot adopt a terminal result until the modes are split.
writeConnectDaemonStarted(cmd.OutOrStdout(), pid, logPath, clientID, dirKey)
return nil
}
// connectDaemonStartedResult 是 `dev connect --daemon` 父进程的结果 DTO
@@ -960,3 +960,50 @@ func TestCrossPlatformCoverageDaemonControlCommandEdges(t *testing.T) {
t.Fatal("failing restart subprocess succeeded")
}
}
func TestFrameworkConnectControlDryRunPlansAndLegacyListRollback(t *testing.T) {
preserveDaemonHooks(t)
connectDaemonDirOverride = t.TempDir()
stop := prepareUnifiedTestCommand(newDevAppRobotConnectStopCommand())
stop.SetArgs([]string{"--unified-app-id", "app", "--dry-run"})
var stopOut bytes.Buffer
stop.SetOut(&stopOut)
if err := stop.Execute(); err != nil || !strings.Contains(stopOut.String(), `"preview_kind": "plan"`) {
t.Fatalf("stop preview=%q err=%v", stopOut.String(), err)
}
restart := prepareUnifiedTestCommand(newDevAppRobotConnectRestartCommand())
restart.SetArgs([]string{"--unified-app-id", "app", "--dry-run"})
var restartOut bytes.Buffer
restart.SetOut(&restartOut)
if err := restart.Execute(); err != nil || !strings.Contains(restartOut.String(), `"preview_kind": "plan"`) {
t.Fatalf("restart preview=%q err=%v", restartOut.String(), err)
}
dir, err := connectDaemonDir("saved")
if err != nil {
t.Fatal(err)
}
if err := writeDaemonState(dir, daemonState{DirKey: "saved", UnifiedAppID: "saved-app", Channel: "custom"}); err != nil {
t.Fatal(err)
}
restart = prepareUnifiedTestCommand(newDevAppRobotConnectRestartCommand())
restart.SetArgs([]string{"--robot-client-id", "saved", "--dry-run"})
restartOut.Reset()
restart.SetOut(&restartOut)
if err := restart.Execute(); err != nil || !strings.Contains(restartOut.String(), "saved-app") {
t.Fatalf("saved restart preview=%q err=%v", restartOut.String(), err)
}
list := prepareUnifiedTestCommand(newDevAppRobotConnectListCommand(connectResponseRunner{response: map[string]any{"items": []any{}, "hasMore": false}}))
output.SetCommandRollout(list, output.RolloutLegacyOnly)
for _, format := range []string{"json", "table", "pretty"} {
var out bytes.Buffer
list.SetOut(&out)
list.SetArgs([]string{"--format", format})
if err := list.Execute(); err != nil {
t.Fatalf("legacy list %s: %v", format, err)
}
}
}
@@ -4,9 +4,13 @@
package helpers
import (
"bytes"
"context"
"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) {
@@ -126,3 +130,117 @@ func TestDevAppSharedResultMapperClassifiesServiceOutcomes(t *testing.T) {
}
})
}
func TestFrameworkDevAppMapperBoundaryMatrix(t *testing.T) {
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)
}
}
+2 -6
View File
@@ -106,9 +106,7 @@ func emitResult(cmd *cobra.Command, result CommandResult) (int, bool, error) {
message = env.Error.Message
}
var buf bytes.Buffer
if _, err := fmt.Fprintln(&buf, "Error: "+message); err != nil {
return exitCodeInternal, false, err
}
_, _ = fmt.Fprintln(&buf, "Error: "+message)
n, err := writeAllCount(stderr, buf.Bytes())
if err != nil {
return exitCodeInternal, n > 0, err
@@ -230,9 +228,7 @@ func renderEnvelopeInto(buf *bytes.Buffer, errW io.Writer, env *Envelope, format
return err
}
if summary := renderMetaSummary(env.Meta); summary != "" {
if _, err := fmt.Fprintf(buf, "\n%s\n", summary); err != nil {
return err
}
_, _ = fmt.Fprintf(buf, "\n%s\n", summary)
}
return nil
case FormatCSV, FormatNDJSON:
-3
View File
@@ -160,9 +160,6 @@ func jsonShorthandActive(cmd *cobra.Command) bool {
return false
}
for _, flags := range []*pflag.FlagSet{cmd.Flags(), cmd.InheritedFlags(), cmd.PersistentFlags()} {
if flags == nil {
continue
}
flag := flags.Lookup("json")
if flag == nil || !flag.Changed {
continue
+431
View File
@@ -0,0 +1,431 @@
package output
import (
"bytes"
"context"
"errors"
"io"
"reflect"
"strings"
"testing"
"github.com/spf13/cobra"
)
type forgedResult struct {
env Envelope
exit int
}
func (r forgedResult) Outcome() Outcome { return r.env.Outcome }
func (r forgedResult) ExitCode() int { return r.exit }
func (r forgedResult) envelope() *Envelope { copy := r.env; return &copy }
type cloneNode struct {
Name string
Next *cloneNode
Values []string
Hidden *string
}
func TestFrameworkResultCloneAndValidationEdges(t *testing.T) {
if cloneReflectValue(reflect.Value{}, nil).IsValid() {
t.Fatal("invalid reflect value became valid")
}
var nilAny any
if got := cloneResultData(nilAny); got != nil {
t.Fatalf("cloneResultData(nil)=%v", got)
}
node := &cloneNode{Name: "before", Values: []string{"one"}}
node.Next = node
payload := map[string]any{
"node": node,
"nil_ptr": (*cloneNode)(nil),
"nil_map": map[string]string(nil),
"nil_slice": []string(nil),
"array": [2]string{"a", "b"},
"interface": any([]string{"x"}),
}
cloned := cloneResultData(payload).(map[string]any)
gotNode := cloned["node"].(*cloneNode)
if gotNode == node || gotNode.Next != gotNode || gotNode.Values[0] != "one" {
t.Fatalf("cycle-safe clone=%#v", gotNode)
}
node.Name, node.Values[0] = "after", "two"
if gotNode.Name != "before" || gotNode.Values[0] != "one" {
t.Fatalf("clone was mutated: %#v", gotNode)
}
started := true
retry := int64(3)
info := &ErrorInfo{
Type: "api", UpstreamCode: map[string]any{"token": "kept"},
Params: []string{"p"}, Actions: []string{"a"}, AvailableFlags: []string{"f"},
ExecutionStarted: &started, RetryAfterSeconds: &retry,
Details: map[string]any{"nested": []any{"x"}}, RPCData: []any{"rpc"},
}
copyInfo := cloneErrorInfo(info)
if copyInfo == info || copyInfo.ExecutionStarted == info.ExecutionStarted || copyInfo.RetryAfterSeconds == info.RetryAfterSeconds {
t.Fatal("ErrorInfo pointer fields were not detached")
}
if cloneErrorInfo(nil) != nil {
t.Fatal("cloneErrorInfo(nil) must be nil")
}
cyclicMap := map[string]any{}
cyclicMap["self"] = cyclicMap
clonedMap := cloneResultData(cyclicMap).(map[string]any)
if reflect.ValueOf(clonedMap).Pointer() == reflect.ValueOf(cyclicMap).Pointer() {
t.Fatal("cyclic map was not cloned")
}
cyclicSlice := make([]any, 1)
cyclicSlice[0] = cyclicSlice
clonedSlice := cloneResultData(cyclicSlice).([]any)
if reflect.ValueOf(clonedSlice).Pointer() == reflect.ValueOf(cyclicSlice).Pointer() {
t.Fatal("cyclic slice was not cloned")
}
validFailure := Failure(&ErrorInfo{Type: "api", Message: "failed"})
if err := ValidateResult(validFailure); err != nil {
t.Fatal(err)
}
validPending := Pending(nil, &OperationInfo{ID: "task", State: "processing", NextCommand: "dws task get"})
if err := ValidateResult(validPending); err != nil {
t.Fatal(err)
}
decorated := Success(nil, WithIdentity("user"), WithDryRun(), ResultOption{})
if decorated.Outcome() != OutcomeSuccess {
t.Fatalf("Outcome=%s", decorated.Outcome())
}
decoratedEnv, err := EnvelopeFromResult(decorated)
if err != nil || decoratedEnv.Identity != "user" || !decoratedEnv.DryRun {
t.Fatalf("decorated=%+v err=%v", decoratedEnv, err)
}
badPending := []CommandResult{
Pending(nil, nil),
Pending(nil, &OperationInfo{State: "processing", NextCommand: "next"}),
Pending(nil, &OperationInfo{ID: "id", NextCommand: "next"}),
Pending(nil, &OperationInfo{ID: "id", State: "processing"}),
}
for _, result := range badPending {
if ValidateResult(result) == nil {
t.Fatalf("malformed pending accepted: %#v", result)
}
}
if ValidateResult(Partial(nil)) == nil {
t.Fatal("nil partial accepted")
}
if err := StoreResult(context.Background(), Pending(nil, nil)); err == nil {
t.Fatal("StoreResult accepted malformed result")
}
if ValidateResult(forgedResult{env: Envelope{OK: true, Outcome: OutcomeSuccess}, exit: 9}) == nil {
t.Fatal("forged exit code accepted")
}
badOverride := &commandResult{env: Envelope{OK: false, Outcome: OutcomeFailure, Error: &ErrorInfo{Type: "api"}}, exitCodeOverride: true}
if ValidateResult(badOverride) == nil {
t.Fatal("non-positive override accepted")
}
badWire := &commandResult{env: Envelope{OK: false, Outcome: OutcomeFailure, Error: &ErrorInfo{Type: "api", ExitCode: 5}}, exitCode: 1}
if ValidateResult(badWire) == nil {
t.Fatal("wire/process exit mismatch accepted")
}
}
type redactionEdge struct {
Password *string `json:"password"`
Plain string
Ignored string `json:"-"`
hidden string
}
type redactCycle struct{ Next *redactCycle }
func TestFrameworkRedactionReflectionEdges(t *testing.T) {
if redactEnvelope(nil) != nil {
t.Fatal("redactEnvelope(nil) must be nil")
}
seen := map[redactVisit]struct{}{}
redactReflectValue(reflect.Value{}, "", seen)
secret := "pointer-secret"
fixture := &redactionEdge{Password: &secret, Plain: "token=plain-secret", Ignored: "ignored-secret", hidden: "hidden-secret"}
redactReflectValue(reflect.ValueOf(fixture), "", seen) // not settable
redactReflectValue(reflect.ValueOf(&fixture).Elem(), "", map[redactVisit]struct{}{})
if fixture.Password == nil || *fixture.Password != redactedValue || !strings.Contains(fixture.Plain, redactedValue) {
t.Fatalf("fixture not redacted: %+v", fixture)
}
if fixture.Ignored != "ignored-secret" || fixture.hidden != "hidden-secret" {
t.Fatalf("ignored fields changed: %+v", fixture)
}
array := [2]string{"password=array-one", "token=array-two"}
redactReflectValue(reflect.ValueOf(&array).Elem(), "", map[redactVisit]struct{}{})
if strings.Contains(strings.Join(array[:], ","), "array-") {
t.Fatalf("array not redacted: %#v", array)
}
cyclicMap := map[string]any{}
cyclicMap["self"] = cyclicMap
cyclicMap["password"] = "map-secret"
redactReflectValue(reflect.ValueOf(cyclicMap), "", map[redactVisit]struct{}{})
if cyclicMap["password"] != redactedValue {
t.Fatalf("map secret=%v", cyclicMap["password"])
}
cyclicSlice := make([]any, 2)
cyclicSlice[0] = cyclicSlice
cyclicSlice[1] = "password=slice-secret"
redactReflectValue(reflect.ValueOf(cyclicSlice), "", map[redactVisit]struct{}{})
pointerCycle := &redactCycle{}
pointerCycle.Next = pointerCycle
redactReflectValue(reflect.ValueOf(pointerCycle), "", map[redactVisit]struct{}{})
unsettable := reflect.ValueOf("secret")
if setRedactedValue(unsettable) {
t.Fatal("unsettable value was redacted")
}
settableString := reflect.New(reflect.TypeOf("")).Elem()
if !setRedactedValue(settableString) || settableString.String() != redactedValue {
t.Fatal("settable string was not redacted")
}
settablePointer := reflect.New(reflect.TypeOf((*string)(nil))).Elem()
if !setRedactedValue(settablePointer) || settablePointer.Elem().String() != redactedValue {
t.Fatal("settable pointer was not redacted")
}
settableInt := reflect.New(reflect.TypeOf(0)).Elem()
if setRedactedValue(settableInt) {
t.Fatal("integer unexpectedly redacted")
}
text := redactRecognizableSecrets("?=unchanged https://u:p@example.test?normal=ok&signature=s")
if !strings.Contains(text, "?=unchanged") || strings.Contains(text, "u:p") || strings.Contains(text, "signature=s") {
t.Fatalf("recognized secret redaction=%q", text)
}
}
func TestFrameworkEnvelopeNilAndNestedValidationEdges(t *testing.T) {
if (&Pagination{}).Validate() == nil || (*Pagination)(nil).Validate() != nil {
t.Fatal("pagination nil/two-state validation mismatch")
}
if (*ErrorInfo)(nil).Validate() == nil {
t.Fatal("nil ErrorInfo accepted")
}
negative := int64(-1)
for _, info := range []*ErrorInfo{
{Type: "unknown"},
{Type: "api", ExitCode: -1},
{Type: "api", RetryAfterSeconds: &negative},
} {
if info.Validate() == nil {
t.Fatalf("invalid ErrorInfo accepted: %+v", info)
}
}
if (*PartialData)(nil).Validate() != nil {
t.Fatal("nil PartialData should be compatible")
}
if (*OperationInfo)(nil).ValidateNextCommand() != nil || (*OperationInfo)(nil).ValidateTimeoutState() != nil || (*OperationInfo)(nil).Validate() != nil {
t.Fatal("nil operation should validate")
}
env := NewSuccessEnvelope(nil)
env.Meta = &Meta{Operation: &OperationInfo{NextCommand: " bad "}, Pagination: &Pagination{}}
if env.Validate() == nil {
t.Fatal("nested invalid metadata accepted")
}
}
func TestFrameworkResultStoreLifecycleEdges(t *testing.T) {
if _, ok := resultStoreFromContext(nil); ok {
t.Fatal("nil context had a store")
}
ctx, store := WithResultStore(nil)
ctx2, same := WithResultStore(ctx)
if ctx2 != ctx || same != store {
t.Fatal("WithResultStore did not reuse existing store")
}
if err := StoreResult(context.Background(), Success(nil)); err == nil {
t.Fatal("StoreResult without root store succeeded")
}
if err := StoreResult(ctx, Success(map[string]any{"id": "x"})); err != nil {
t.Fatal(err)
}
if err := StoreResult(ctx, Success(nil)); err == nil {
t.Fatal("duplicate result accepted")
}
if _, _, err := EmitStoredResult(nil); err != nil {
t.Fatal(err)
}
plain := &cobra.Command{Use: "plain"}
if _, _, err := EmitStoredResult(plain); err != nil {
t.Fatal(err)
}
plain.SetContext(ctx)
if _, _, err := EmitStoredResult(plain); err == nil {
t.Fatal("legacy command emitted unified result")
}
active := &cobra.Command{Use: "active"}
active.SetContext(ctx)
SetCommandRollout(active, RolloutUnifiedActive)
var out bytes.Buffer
active.SetOut(&out)
code, emitted, err := EmitStoredResult(active)
if err != nil || code != 0 || !emitted {
t.Fatalf("EmitStoredResult=(%d,%v,%v)", code, emitted, err)
}
code, emitted, err = EmitStoredResult(active)
if err != nil || code != 0 || !emitted {
t.Fatalf("second EmitStoredResult=(%d,%v,%v)", code, emitted, err)
}
if code, attempted, emitted, risk := StoredEmissionState(store); code != 0 || !attempted || !emitted || !risk {
t.Fatalf("state=(%d,%v,%v,%v)", code, attempted, emitted, risk)
}
if code, ok := StoredExitCode(store); code != 0 || !ok {
t.Fatalf("StoredExitCode=(%d,%v)", code, ok)
}
}
func TestFrameworkResultStoreNilStateAndWriteFailure(t *testing.T) {
code, attempted, emitted, risk := StoredEmissionState(nil)
if code != 0 || attempted || emitted || risk {
t.Fatalf("nil state=(%d,%v,%v,%v)", code, attempted, emitted, risk)
}
if code, ok := StoredExitCode(nil); code != 0 || ok {
t.Fatalf("nil exit=(%d,%v)", code, ok)
}
ctx, store := WithResultStore(context.Background())
if err := StoreResult(ctx, Success(nil)); err != nil {
t.Fatal(err)
}
cmd := &cobra.Command{Use: "active"}
cmd.SetContext(ctx)
SetCommandRollout(cmd, RolloutUnifiedActive)
cmd.SetOut(errorWriter{})
if code, emitted, err := EmitStoredResult(cmd); code != exitCodeInternal || emitted || err == nil {
t.Fatalf("write failure=(%d,%v,%v)", code, emitted, err)
}
if code, attempted, emitted, risk := StoredEmissionState(store); code != exitCodeInternal || !attempted || emitted || risk {
t.Fatalf("failed state=(%d,%v,%v,%v)", code, attempted, emitted, risk)
}
if _, err := EnvelopeFromResult(nil); err == nil {
t.Fatal("EnvelopeFromResult(nil) succeeded")
}
}
type errorWriter struct{}
func (errorWriter) Write([]byte) (int, error) { return 0, errors.New("write failed") }
func TestFrameworkEmitterAndRolloutEdges(t *testing.T) {
if jsonShorthandActive(nil) {
t.Fatal("nil command activated --json")
}
if err := WriteEnvelope(nil, nil, FormatJSON); err != nil {
t.Fatal(err)
}
cmd := &cobra.Command{Use: "cmd"}
cmd.SetOut(io.Discard)
cmd.SetErr(errorWriter{})
cmd.Flags().String("format", "unknown", "")
if err := WriteEnvelope(cmd, NewSuccessEnvelope(nil), FormatJSON); err == nil {
t.Fatal("warning write error was ignored")
}
if _, _, err := emitResult(nil, nil); err == nil {
t.Fatal("nil result accepted")
}
broken := NewSuccessEnvelope(make(chan int))
if err := WriteEnvelope(&cobra.Command{Use: "broken"}, broken, FormatJSON); err == nil {
t.Fatal("WriteEnvelope accepted unrenderable data")
}
if _, _, err := emitResult(&cobra.Command{Use: "broken"}, Success(make(chan int))); err == nil {
t.Fatal("emitResult accepted unrenderable data")
}
human := &cobra.Command{Use: "human"}
human.Flags().String("format", "table", "")
human.SetErr(errorWriter{})
if _, risk, err := emitResult(human, Failure(&ErrorInfo{Type: "api", Message: "boom"})); err == nil || risk {
t.Fatalf("human failure write=(risk=%v, err=%v)", risk, err)
}
short := &shortWriter{}
human.SetErr(short)
if _, risk, err := emitResult(human, Failure(&ErrorInfo{Type: "api"})); !errors.Is(err, io.ErrShortWrite) || !risk {
t.Fatalf("human short write=(risk=%v, err=%v)", risk, err)
}
machine := &cobra.Command{Use: "machine"}
machine.SetOut(errorWriter{})
if _, risk, err := emitResult(machine, Success(nil)); err == nil || risk {
t.Fatalf("machine write=(risk=%v, err=%v)", risk, err)
}
machine.SetOut(&shortWriter{})
if _, risk, err := emitResult(machine, Success(nil)); !errors.Is(err, io.ErrShortWrite) || !risk {
t.Fatalf("machine short write=(risk=%v, err=%v)", risk, err)
}
if err := renderEnvelope(io.Discard, nil, nil, FormatJSON, "", ""); err != nil {
t.Fatal(err)
}
var fallback bytes.Buffer
if err := renderEnvelopeInto(&fallback, io.Discard, NewSuccessEnvelope(map[string]any{"id": "x"}), Format("unexpected"), "", ""); err != nil || !strings.Contains(fallback.String(), `"outcome"`) {
t.Fatalf("unknown format fallback=%q err=%v", fallback.String(), err)
}
if got := ExitCodeForEnvelope(&Envelope{Outcome: OutcomeFailure, Error: &ErrorInfo{Type: "permission"}}); got != exitCodePermission {
t.Fatalf("permission exit=%d", got)
}
var ndjson bytes.Buffer
if err := renderEnvelopeInto(&ndjson, io.Discard, Failure(&ErrorInfo{Type: "api"}).envelope(), FormatNDJSON, "", ""); err != nil || strings.Count(strings.TrimSpace(ndjson.String()), "\n") != 0 {
t.Fatalf("NDJSON failure=%q err=%v", ndjson.String(), err)
}
if _, err := writeAllCount(&shortWriter{}, []byte("payload")); !errors.Is(err, io.ErrShortWrite) {
t.Fatalf("short write err=%v", err)
}
if _, err := ParseRolloutState("bad"); err == nil {
t.Fatal("invalid rollout parsed")
}
if err := ValidateRolloutTransition("bad", RolloutLegacyOnly, false); err == nil {
t.Fatal("invalid from state accepted")
}
if err := ValidateRolloutTransition(RolloutLegacyOnly, "bad", false); err == nil {
t.Fatal("invalid to state accepted")
}
if rolloutRank("bad") != -1 {
t.Fatal("unknown rollout rank")
}
SetCommandRollout(nil, RolloutUnifiedActive)
if err := ValidateUnifiedFormat(nil); err != nil {
t.Fatal(err)
}
defer func() {
if recover() == nil {
t.Fatal("invalid rollout did not panic")
}
}()
SetCommandRollout(&cobra.Command{Use: "bad"}, "bad")
}
func TestFrameworkMCPAdapterErrors(t *testing.T) {
if _, err := AdaptMCP(nil); err == nil {
t.Fatal("AdaptMCP(nil) succeeded")
}
originalMarshal, originalUnmarshal := marshalMCPResult, unmarshalMCPResult
t.Cleanup(func() { marshalMCPResult, unmarshalMCPResult = originalMarshal, originalUnmarshal })
marshalMCPResult = func(any) ([]byte, error) { return nil, errors.New("marshal") }
if _, err := AdaptMCP(Success(nil)); err == nil {
t.Fatal("MCP marshal failure ignored")
}
marshalMCPResult = func(any) ([]byte, error) { return []byte("{"), nil }
unmarshalMCPResult = originalUnmarshal
if _, err := AdaptMCP(Success(nil)); err == nil {
t.Fatal("MCP decode failure ignored")
}
}
type shortWriter struct{}
func (*shortWriter) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
return len(p) - 1, nil
}
+7 -2
View File
@@ -8,6 +8,11 @@ import (
"fmt"
)
var (
marshalMCPResult = json.Marshal
unmarshalMCPResult = json.Unmarshal
)
// MCPResult is the protocol-neutral projection consumed by MCP transports.
// Both CLI and MCP adapters start from the same immutable CommandResult; MCP
// handlers must not rebuild a second business envelope.
@@ -25,12 +30,12 @@ func AdaptMCP(result CommandResult) (*MCPResult, error) {
return nil, err
}
env = redactEnvelope(env)
raw, err := json.Marshal(env)
raw, err := marshalMCPResult(env)
if err != nil {
return nil, fmt.Errorf("output: marshal MCP result: %w", err)
}
var structured map[string]any
if err := json.Unmarshal(raw, &structured); err != nil {
if err := unmarshalMCPResult(raw, &structured); err != nil {
return nil, fmt.Errorf("output: decode MCP structured result: %w", err)
}
return &MCPResult{
-3
View File
@@ -163,9 +163,6 @@ func redactRecognizableSecrets(text string) string {
text = urlCredentialsPattern.ReplaceAllString(text, `${1}`+redactedValue+`@`)
text = urlQueryValuePattern.ReplaceAllStringFunc(text, func(parameter string) string {
equals := strings.IndexByte(parameter, '=')
if equals < 2 {
return parameter
}
key, err := url.QueryUnescape(parameter[1:equals])
if err != nil || !isSensitiveQueryKey(key) {
return parameter
@@ -3,7 +3,26 @@
package devapp
import "testing"
import (
"bytes"
"context"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
type paginationCaller struct{ text string }
func (c *paginationCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: c.text}}}, nil
}
func (*paginationCaller) Format() string { return "json" }
func (*paginationCaller) DryRun() bool { return false }
func (*paginationCaller) Fields() string { return "" }
func (*paginationCaller) JQ() string { return "" }
func TestDevAppListProjectionPreservesPaginationEvidence(t *testing.T) {
items := []map[string]any{{"id": "one"}}
@@ -28,4 +47,32 @@ func TestDevAppListProjectionPreservesPaginationEvidence(t *testing.T) {
}
})
}
if got := devAppPaginationCandidates(nil); got != nil {
t.Fatalf("nil candidates=%#v", got)
}
deep := map[string]any{"content": map[string]any{"result": map[string]any{"hasMore": true, "nextCursor": "deep"}}}
if got := devAppListProjection(deep, "items", nil); got["nextCursor"] != "deep" {
t.Fatalf("deep projection=%#v", got)
}
}
func TestFrameworkPaginatedShortcutExecutionKeepsCursorEvidence(t *testing.T) {
caller := &paginationCaller{text: `{"content":{"result":{"hasMore":true,"nextCursor":"next","list":[]}}}`}
helpers.InitDepsForTest(t, caller)
helpers.GetFormatter().SetWriters(&bytes.Buffer{}, &bytes.Buffer{})
for _, tc := range []struct {
declaration shortcut.Shortcut
args []string
}{
{ListApp, nil},
{PermissionList, []string{"--unified-app-id", "app"}},
{EventList, []string{"--unified-app-id", "app"}},
{VersionList, []string{"--unified-app-id", "app"}},
} {
cmd := corecmd.New(shortcut.FromShortcut(tc.declaration))
cmd.SetArgs(tc.args)
if err := cmd.Execute(); err != nil {
t.Fatalf("%s: %v", tc.declaration.Command, err)
}
}
}
@@ -20,13 +20,51 @@ type dualValidateCaller struct {
dryRun bool
calls int
text string
err error
}
func (c *dualValidateCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
c.calls++
if c.err != nil {
return nil, c.err
}
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: c.text}}}, nil
}
func TestFrameworkDualValidateBackendAndShadowErrors(t *testing.T) {
caller := &dualValidateCaller{format: "json", text: `{"id":"x"}`, err: context.DeadlineExceeded}
helpers.InitDepsForTest(t, caller)
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
root.PersistentFlags().String("format", "json", "")
cmd := mount(Shortcut{Service: "sample", Command: "+compat", OutputRollout: output.RolloutDualValidate, Execute: func(rt *RuntimeContext) error { return rt.CallMCP("get", nil) }})
root.AddCommand(cmd)
root.SetArgs([]string{cmd.Name()})
if err := root.Execute(); err == nil {
t.Fatal("backend error swallowed")
}
original := validateShadowResult
t.Cleanup(func() { validateShadowResult = original })
validateShadowResult = func(output.CommandResult) error { return context.Canceled }
direct := &cobra.Command{Use: "+direct"}
direct.SetContext(context.Background())
output.SetCommandRollout(direct, output.RolloutDualValidate)
if err := RuntimeContextForTest(direct, Shortcut{Service: "sample", Command: "+direct"}).Output(map[string]any{"id": "x"}); err == nil {
t.Fatal("Output shadow validation error swallowed")
}
caller.err = nil
root.SetArgs([]string{cmd.Name()})
if err := root.Execute(); err == nil {
t.Fatal("shadow validation error swallowed")
}
caller.dryRun = true
root.SetArgs([]string{cmd.Name(), "--dry-run"})
root.PersistentFlags().Bool("dry-run", false, "")
if err := root.Execute(); err == nil {
t.Fatal("dry-run shadow validation error swallowed")
}
}
func (c *dualValidateCaller) Format() string { return c.format }
func (c *dualValidateCaller) DryRun() bool { return c.dryRun }
func (c *dualValidateCaller) Fields() string { return "" }
+5 -3
View File
@@ -27,6 +27,8 @@ import (
"github.com/spf13/pflag"
)
var validateShadowResult = output.ValidateResult
// RuntimeContext is handed to a Shortcut's Validate and Execute hooks. It wraps
// the cobra command and exposes typed flag accessors plus a single CallMCP entry
// point so shortcut authors never touch cobra/executor plumbing directly.
@@ -152,7 +154,7 @@ func (rt *RuntimeContext) CallMCP(tool string, params map[string]any) error {
"arguments": params,
})
if rt.DryRun() {
if err := output.ValidateResult(rt.resultForPayload(tool, preview)); err != nil {
if err := validateShadowResult(rt.resultForPayload(tool, preview)); err != nil {
return err
}
// The legacy caller owns dry-run presentation (including its human
@@ -165,7 +167,7 @@ func (rt *RuntimeContext) CallMCP(tool string, params map[string]any) error {
return err
}
data := legacyMCPPayload(text)
if err := output.ValidateResult(rt.resultForPayload(tool, data)); err != nil {
if err := validateShadowResult(rt.resultForPayload(tool, data)); err != nil {
return err
}
// dual_validate changes no external bytes: it renders the once-fetched
@@ -305,7 +307,7 @@ func (rt *RuntimeContext) Output(payload any) error {
return output.StoreResult(rt.cmd.Context(), rt.resultForPayload("", payload))
}
if output.CommandRollout(rt.cmd) == output.RolloutDualValidate {
if err := output.ValidateResult(rt.resultForPayload("", payload)); err != nil {
if err := validateShadowResult(rt.resultForPayload("", payload)); err != nil {
return err
}
}
+90
View File
@@ -4,10 +4,13 @@
package shortcut
import (
"bytes"
"context"
"errors"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
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"
@@ -84,3 +87,90 @@ func TestCrossPlatformCoverageRuntimeWriteDataRemainingBranches(t *testing.T) {
}
}
}
func TestFrameworkShortcutUnifiedOutputAndProjectionEdges(t *testing.T) {
oldCaller := helpers.GetCaller()
t.Cleanup(func() { helpers.InitDeps(oldCaller) })
ctx, _ := output.WithResultStore(context.Background())
cmd := &cobra.Command{Use: "+read"}
cmd.SetContext(ctx)
output.SetCommandRollout(cmd, output.RolloutUnifiedActive)
rt := RuntimeContextForTest(cmd, Shortcut{Service: "sample", Command: "+read", Safety: contract.SafetySpec{Effect: "read"}})
if err := rt.Output(map[string]any{"id": "x"}); err != nil {
t.Fatal(err)
}
caller := &runtimeReadCoverageCaller{text: `{"success":true,"id":"server"}`}
helpers.InitDeps(caller)
callCtx, _ := output.WithResultStore(context.Background())
callCmd := &cobra.Command{Use: "+call"}
callCmd.SetContext(callCtx)
output.SetCommandRollout(callCmd, output.RolloutUnifiedActive)
callRT := RuntimeContextForTest(callCmd, Shortcut{Service: "sample", Command: "+call", Safety: contract.SafetySpec{Effect: "read"}})
if err := callRT.CallMCP("get", nil); err != nil {
t.Fatal(err)
}
caller.err = errors.New("backend")
errorCtx, _ := output.WithResultStore(context.Background())
errorCmd := &cobra.Command{Use: "+error"}
errorCmd.SetContext(errorCtx)
output.SetCommandRollout(errorCmd, output.RolloutUnifiedActive)
errorRT := RuntimeContextForTest(errorCmd, Shortcut{Service: "sample", Command: "+error", Safety: contract.SafetySpec{Effect: "read"}})
if err := errorRT.CallMCP("get", nil); err == nil {
t.Fatal("backend error swallowed")
}
caller.err = nil
dryCtx, _ := output.WithResultStore(context.Background())
dryCmd := &cobra.Command{Use: "+dry"}
dryCmd.SetContext(dryCtx)
dryCmd.Flags().Bool("dry-run", true, "")
_ = dryCmd.Flags().Set("dry-run", "true")
output.SetCommandRollout(dryCmd, output.RolloutUnifiedActive)
dryRT := RuntimeContextForTest(dryCmd, Shortcut{Service: "sample", Command: "+dry", Safety: contract.SafetySpec{Effect: "read"}})
if err := dryRT.CallMCP("get", nil); err != nil {
t.Fatal(err)
}
dualCmd := &cobra.Command{Use: "+dual"}
dualCmd.SetContext(context.Background())
dualCmd.SetOut(&bytes.Buffer{})
output.SetCommandRollout(dualCmd, output.RolloutDualValidate)
dualRT := RuntimeContextForTest(dualCmd, Shortcut{Service: "sample", Command: "+dual", Safety: contract.SafetySpec{Effect: "read"}})
if err := dualRT.Output(map[string]any{"success": "false"}); err != nil {
t.Fatalf("dual validation should accept a typed failure result: %v", err)
}
for _, tc := range []struct {
name string
payload any
outcome output.Outcome
}{
{"scalar", "value", output.OutcomeSuccess},
{"nested success", map[string]any{"content": map[string]any{"success": true}}, output.OutcomeSuccess},
{"failure fallback", map[string]any{"success": false}, output.OutcomeFailure},
{"failure message", map[string]any{"content": map[string]any{"success": false, "errorMessage": "bad"}}, output.OutcomeFailure},
} {
t.Run(tc.name, func(t *testing.T) {
result := shortcutCommandResult(tc.payload, output.WithDryRun())
if result.Outcome() != tc.outcome {
t.Fatalf("outcome=%s", result.Outcome())
}
})
}
if hasExplicitShortcutSuccess("scalar") || hasExplicitShortcutSuccess(map[string]any{"success": false}) || !hasExplicitShortcutSuccess(map[string]any{"content": map[string]any{"success": true}}) {
t.Fatal("explicit success classification mismatch")
}
for _, shortcut := range []Shortcut{
{Safety: contract.SafetySpec{Effect: "write"}},
{Safety: contract.SafetySpec{Effect: "destructive"}},
{Risk: RiskHighWrite},
{Risk: RiskRead},
} {
probe := RuntimeContextForTest(&cobra.Command{Use: "probe"}, shortcut)
_ = probe.isWriteShortcut()
}
devRT := RuntimeContextForTest(&cobra.Command{Use: "dev"}, Shortcut{Service: "devapp", Command: "+get"})
if got := devRT.resultForPayload("get_dev_app", map[string]any{"success": true}); got.Outcome() != output.OutcomeSuccess {
t.Fatalf("devapp result=%s", got.Outcome())
}
}