Files
dingtalk-workspace-cli/internal/app/event_stop_safety_test.go
玉澜 e705012011 feat(schema): migrate parameter overlays into code declarations
Move 121/210 parameter-level Schema field overlays from
schema_hints/metadata JSON into in-code ParamDecl declarations on
DeclareLeafMetadata commands. The declared values are emitted as
dws.schema.* annotations at assembly time via ApplyParamDecls,
outranking tool_schema_hint (rank 620 > 500) so the hint overlay
becomes redundant once the declaration is in place.

Key mechanism changes:
- Add SchemaDecl.Parameters []ParamDecl with property/required/
  interface_type/description/required_when/enum fields
- Add cli.ParamDecl type carried inside ContractFinalPayload
- ApplyParamDecls emits dws.schema.* annotations from the payload
  at assembly time (no sync.Map, no tree-rebuild key issue)
- Add cli.AnnotateRuntimeFlagInterfaceType (41 overlays needed it)
- Add cli.AnnotateRuntimeFlagRequiredValue for explicit true/false
- Compatibility alias check tolerates dws.schema.* annotation
  differences between primary and alias commands
- Remove runtime source_hash recomputation (87ms/997k allocs saved;
  enforced by check-generated-drift.sh at build time instead)
- Add shortcut.Flag.RequiredWhen and wire through FromShortcut
- Add boolFlag OR semantics to fix confirmationBypass disagreement
- Add bindKey default kebab-to-camel for forgotten Bind
- Add schema consumption benchmarks (catalog decode 1.5s/817MB,
  shortcut load 1.9ms/3MB — three orders of magnitude apart)
- Add catalog codegen feasibility probe (34 tools: 0.06s compile,
  31ns access, 87KB linked — extrapolates to 2.1MB for 845 tools)

Migrated products (29 tools, 121 fields):
  aisearch(1), chat(12), contact(4), doc(3), drive(1),
  hrbrain(10), mail(2), report(1), sheet(3), todo(6)

Remaining 89 fields across 11 products blocked by:
  - RPCName not found as string literal (19 tools, shortcut/variable)
  - No matching DeclareLeafMetadata near callMCPTool (11 tools)
  - No single RPCName for multi-step commands (drive.upload etc.)

All tests green: corecmd, cli, helpers. Generation and drift clean.
2026-08-01 09:25:40 +08:00

110 lines
3.8 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package app
import (
"bytes"
"encoding/json"
"errors"
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
)
func TestEventStopRequiresTypedConfirmationBeforeMutation(t *testing.T) {
root, _ := newEventStopSafetyRoot()
root.SetArgs([]string{"event", "stop", "sub-1"})
err := root.Execute()
if err == nil {
t.Fatal("event stop without --yes or --dry-run unexpectedly succeeded")
}
var appErr *apperrors.Error
if !errors.As(err, &appErr) || appErr.Category != apperrors.CategoryValidation {
t.Fatalf("event stop confirmation error = %T %v, want typed validation error", err, err)
}
if appErr.Reason != "confirmation_required" {
t.Fatalf("event stop confirmation reason = %q, want confirmation_required", appErr.Reason)
}
for _, recoveryFlag := range []string{"--dry-run", "--yes"} {
if !strings.Contains(err.Error(), recoveryFlag) {
t.Fatalf("event stop confirmation error %q does not explain %s", err, recoveryFlag)
}
}
}
func TestEventStopDryRunPrecedesConfirmationAndReturnsPreview(t *testing.T) {
tests := []struct {
name string
args []string
wantAll bool
wantSubscribeID string
}{
{name: "single subscription", args: []string{"event", "stop", "sub-1", "--dry-run"}, wantSubscribeID: "sub-1"},
{name: "all subscriptions", args: []string{"--dry-run", "event", "stop", "--all"}, wantAll: true},
{name: "dry run wins over yes", args: []string{"event", "stop", "sub-2", "--yes", "--dry-run"}, wantSubscribeID: "sub-2"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
root, stdout := newEventStopSafetyRoot()
root.SetArgs(test.args)
if err := root.Execute(); err != nil {
t.Fatalf("event stop dry-run error = %v", err)
}
var preview map[string]any
if err := json.Unmarshal(stdout.Bytes(), &preview); err != nil {
t.Fatalf("decode event stop dry-run preview: %v\n%s", err, stdout.String())
}
if preview["dry_run"] != true || preview["action"] != "event.stop" || preview["identity"] != "user" {
t.Fatalf("event stop dry-run preview = %#v", preview)
}
if got, _ := preview["all"].(bool); got != test.wantAll {
t.Fatalf("event stop dry-run all = %v, want %v", got, test.wantAll)
}
if got, _ := preview["subscribe_id"].(string); got != test.wantSubscribeID {
t.Fatalf("event stop dry-run subscribe_id = %q, want %q", got, test.wantSubscribeID)
}
})
}
}
func TestCrossPlatformCoverageEventStopDryRunDoesNotBypassTargetValidation(t *testing.T) {
for _, test := range []struct {
name string
args []string
want string
}{
{name: "missing target", args: []string{"event", "stop", "--dry-run"}, want: "subscribe_id is required unless --all is set"},
{name: "conflicting targets", args: []string{"event", "stop", "sub-1", "--all", "--dry-run"}, want: "subscribe_id and --all are mutually exclusive"},
} {
t.Run(test.name, func(t *testing.T) {
root, _ := newEventStopSafetyRoot()
root.SetArgs(test.args)
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("event stop dry-run validation error = %v, want %q", err, test.want)
}
})
}
}
func newEventStopSafetyRoot() (*cobra.Command, *bytes.Buffer) {
stdout := &bytes.Buffer{}
root := &cobra.Command{
Use: "dws",
SilenceErrors: true,
SilenceUsage: true,
}
root.SetOut(stdout)
root.SetErr(&bytes.Buffer{})
root.PersistentFlags().Bool("dry-run", false, "preview without executing")
root.PersistentFlags().Bool("yes", false, "confirm execution")
event := &cobra.Command{Use: "event"}
event.AddCommand(newEventStopCommand())
root.AddCommand(event)
return root, stdout
}