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.
This commit is contained in:
玉澜 committed 2026-08-01 09:25:40 +08:00
1 parent 8791088027
commit e705012011
69 files changed
+4966 -2078

No files matched your search

+1 -1
View File
@@ -71,7 +71,7 @@ func TestEventStopDryRunPrecedesConfirmationAndReturnsPreview(t *testing.T) {
}
}
func TestEventStopDryRunDoesNotBypassTargetValidation(t *testing.T) {
func TestCrossPlatformCoverageEventStopDryRunDoesNotBypassTargetValidation(t *testing.T) {
for _, test := range []struct {
name string
args []string
@@ -0,0 +1,33 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package app
import "testing"
// BenchmarkNewRootCommand measures building the real Cobra tree — roughly 800
// leaves with their flags, Schema annotations and PostMount work.
//
// It is the other half of the cold-start attribution: `dws version` never
// decodes the Schema catalog, so whatever it spends beyond process start is
// mostly this. Without splitting the two, an optimization aimed at JSON parsing
// could target the smaller cost.
func BenchmarkNewRootCommand(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
root := NewRootCommand()
if root == nil {
b.Fatal("nil root")
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,63 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package catalogcodegen_test
import (
"reflect"
"strings"
"testing"
"unicode"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
)
// TestToolSpecIsExpressibleAsLiterals decides whether the catalog can be stored
// as compiled-in Go literals at all.
//
// Byte-identical `dws schema` output requires that a ToolSpec built by generated
// code in another package can hold exactly the state the JSON path produces. That
// is possible if and only if every field reachable from ToolSpec is exported —
// an unexported field cannot be set from outside internal/cli, so its value could
// never be reproduced. This walks the type graph and reports any that block it.
func TestToolSpecIsExpressibleAsLiterals(t *testing.T) {
var blockers []string
seen := map[reflect.Type]bool{}
var walk func(t reflect.Type, path string)
walk = func(rt reflect.Type, path string) {
for rt.Kind() == reflect.Ptr || rt.Kind() == reflect.Slice || rt.Kind() == reflect.Array {
rt = rt.Elem()
}
if rt.Kind() == reflect.Map {
walk(rt.Key(), path+"[key]")
rt = rt.Elem()
for rt.Kind() == reflect.Ptr || rt.Kind() == reflect.Slice {
rt = rt.Elem()
}
}
if rt.Kind() != reflect.Struct || seen[rt] {
return
}
seen[rt] = true
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
name := path + "." + field.Name
if !unicode.IsUpper(rune(field.Name[0])) {
// json.RawMessage and time internals are stdlib types a literal
// can still produce by value; only our own types matter.
if strings.HasPrefix(rt.PkgPath(), "github.com/DingTalk-Real-AI") {
blockers = append(blockers, name+" ("+rt.String()+")")
}
continue
}
walk(field.Type, name)
}
}
walk(reflect.TypeOf(cli.ToolSpec{}), "ToolSpec")
if len(blockers) != 0 {
t.Fatalf("unexported fields block literal construction: %v", blockers)
}
t.Logf("ToolSpec graph is fully exported across %d struct types", len(seen))
}
@@ -0,0 +1,50 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package catalogcodegen_test
import (
"encoding/json"
"os"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/catalogcodegen"
)
// BenchmarkGoLiteralAccess measures reaching a tool in the compiled-in map. The
// data is static, so this is a map lookup with no decode and no allocation —
// the cost model internal/shortcut already gets.
func BenchmarkGoLiteralAccess(b *testing.B) {
if len(catalogcodegen.Tools) == 0 {
b.Fatal("no generated tools")
}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
tool, ok := catalogcodegen.Tools["dev.delete_dev_app"]
if !ok || tool.CLIPath == "" {
b.Fatal("lookup missed")
}
}
}
// BenchmarkJSONDecodeSameShard measures decoding the same product's committed
// shard into untyped maps, i.e. what the current loader pays for this product.
func BenchmarkJSONDecodeSameShard(b *testing.B) {
data, err := os.ReadFile("../schema_catalog/tools/dev.json")
if err != nil {
b.Skip("shard unavailable")
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var shard struct {
Tools map[string]map[string]any `json:"tools"`
}
if err := json.Unmarshal(data, &shard); err != nil {
b.Fatal(err)
}
if len(shard.Tools) == 0 {
b.Fatal("empty shard")
}
}
}
@@ -0,0 +1,21 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// Command probemain exists only to measure the linked size contribution of the
// generated catalog literals; it keeps the data reachable so the linker cannot
// strip it.
package main
import (
"fmt"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/catalogcodegen"
)
func main() {
total := 0
for _, tool := range catalogcodegen.Tools {
total += len(tool.CLIPath) + len(tool.Description) + len(tool.Parameters)
}
fmt.Println(len(catalogcodegen.Tools), total)
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// Package catalogcodegen is a throwaway feasibility probe: it measures what it
// costs to store one product's Schema catalog as compiled-in Go literals rather
// than embedded JSON, which is how internal/shortcut stores its 376 definitions
// (measured at 1.9ms/3MB versus the catalog's 1400ms/740MB).
//
// The struct shape mirrors the runtime-consumed subset of schemaToolWire. It is
// structurally equivalent rather than shared, because the real type is
// package-private to internal/cli; the point is to measure how the Go compiler
// and linker handle this data volume.
package catalogcodegen
// Param is the runtime-consumed subset of a catalog parameter.
type Param struct {
Name string
Type string
Description string
Property string
Required bool
CLIRequired bool
RequiredWhen string
Format string
Enum []string
}
// Tool is the runtime-consumed subset of a catalog leaf ToolSpec.
type Tool struct {
Name string
CanonicalPath string
CLIPath string
PrimaryCLIPath string
ProductID string
Group string
Title string
Description string
Aliases []string
IsAlias bool
Effect string
Risk string
Confirmation string
Idempotency string
InterfaceMode string
Availability string
AgentSummary string
UseWhen []string
AvoidWhen []string
Examples []string
HasParameters bool
ParameterCount int
Parameters map[string]Param
}
+49
View File
@@ -14,6 +14,7 @@
package cli
import (
"strings"
"sync"
"github.com/spf13/cobra"
@@ -26,6 +27,7 @@ type ContractFinalPayload struct {
Title string
Description string
Positionals []RuntimeSchemaPositional
Parameters []ParamDecl
Safety *SafetySpec
DryRun *DryRunSpec
Interface *InterfaceSpec
@@ -35,6 +37,53 @@ type ContractFinalPayload struct {
var contractFinalByCommand sync.Map // *cobra.Command → *ContractFinalPayload
// ParamDecl is one parameter-level Schema fact declared on a command. It is
// stored at DeclareLeafMetadata time and applied as annotations at assembly
// time, when all flags are guaranteed to exist on the fully-built command tree.
type ParamDecl struct {
Name string
Property string
Required *bool
InterfaceType string
Description string
RequiredWhen string
Enum []string
}
// ApplyParamDecls emits parameter declarations as dws.schema.* annotations on the
// command's flags. Called at assembly time (runtimeToolSpecFromContractFinal)
// when all flags exist on the fully-built command tree. The decls come from
// the ContractFinalPayload, so no separate storage is needed.
func ApplyParamDecls(cmd *cobra.Command, decls []ParamDecl) {
if cmd == nil || len(decls) == 0 {
return
}
for _, p := range decls {
name := strings.TrimSpace(p.Name)
if name == "" {
continue
}
if prop := strings.TrimSpace(p.Property); prop != "" {
AnnotateRuntimeFlagProperty(cmd, name, prop)
}
if p.Required != nil {
AnnotateRuntimeFlagRequiredValue(cmd, name, *p.Required)
}
if it := strings.TrimSpace(p.InterfaceType); it != "" {
AnnotateRuntimeFlagInterfaceType(cmd, name, it)
}
if desc := strings.TrimSpace(p.Description); desc != "" {
AnnotateRuntimeFlagDescription(cmd, name, desc)
}
if rw := strings.TrimSpace(p.RequiredWhen); rw != "" {
AnnotateRuntimeFlagRequiredWhen(cmd, name, rw)
}
if len(p.Enum) > 0 {
AnnotateRuntimeFlagEnum(cmd, name, p.Enum...)
}
}
}
// RegisterRuntimeContractFinal stores the typed final Schema overlay for a leaf.
// Light runtime write: one map store, no JSON, no deep clone.
func RegisterRuntimeContractFinal(cmd *cobra.Command, payload ContractFinalPayload) {
+155 -3
View File
@@ -14,6 +14,8 @@
package cli
import (
"errors"
"strings"
"testing"
"github.com/spf13/cobra"
@@ -45,7 +47,7 @@ func TestContractFinalTypedRegistryNoJSON(t *testing.T) {
}
}
func TestRuntimeToolSpecFromContractFinalPassThrough(t *testing.T) {
func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalPassThrough(t *testing.T) {
cmd := &cobra.Command{Use: "create", Short: "s", Long: "l"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(cmd) })
cmd.Flags().String("mode", "", "usage")
@@ -99,7 +101,7 @@ func TestRuntimeToolSpecFromContractFinalPassThrough(t *testing.T) {
}
}
func TestRuntimeToolSpecFromContractFinalIdentityMismatchFails(t *testing.T) {
func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalIdentityMismatchFails(t *testing.T) {
entry := runtimeSchemaEntry{
ProductID: "dev",
ToolName: "create_thing",
@@ -133,7 +135,7 @@ func TestRuntimeToolSpecFromContractFinalIdentityMismatchFails(t *testing.T) {
}
}
func TestRuntimeToolSpecFromContractFinalRejectsReviewedSelection(t *testing.T) {
func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalRejectsReviewedSelection(t *testing.T) {
reviewed := true
entry := runtimeSchemaEntry{
ProductID: "dev",
@@ -156,3 +158,153 @@ func mustFinal(t *testing.T, cmd *cobra.Command) ContractFinalPayload {
}
return final
}
func TestCrossPlatformCoverageContractFinalNilCommandGuards(t *testing.T) {
// Nil command registration/lookup must be inert no-ops.
RegisterRuntimeContractFinal(nil, ContractFinalPayload{Title: "ignored"})
if _, ok := RuntimeContractFinal(nil); ok {
t.Fatal("RuntimeContractFinal(nil) must report no payload")
}
if HasRuntimeContractFinal(nil) {
t.Fatal("HasRuntimeContractFinal(nil) must be false")
}
ClearRuntimeContractFinalForTest(nil)
}
func TestCrossPlatformCoverageRuntimeContractFinalRejectsForeignStoredValue(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(cmd) })
// Defensive branch: a stored value that is not a *ContractFinalPayload
// (or a typed nil) must fail the read instead of panicking.
contractFinalByCommand.Store(cmd, "not-a-payload")
if _, ok := RuntimeContractFinal(cmd); ok {
t.Fatal("foreign stored value must not decode as ContractFinalPayload")
}
contractFinalByCommand.Store(cmd, (*ContractFinalPayload)(nil))
if _, ok := RuntimeContractFinal(cmd); ok {
t.Fatal("typed nil payload must not decode as ContractFinalPayload")
}
}
func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalIdentityOverridesApplied(t *testing.T) {
cmd := &cobra.Command{Use: "create"}
entry := runtimeSchemaEntry{
ProductID: "dev",
SourceProductID: "src",
ToolName: "create_thing",
CLIName: "create",
Group: "dev thing",
CLIPath: "dev create",
PrimaryCLIPath: "dev create",
Aliases: []string{"dev alt"},
Command: cmd,
Source: "test",
}
final := ContractFinalPayload{Identity: &ToolIdentitySpec{
ProductID: "dev",
SourceProductID: "src",
Name: "create_thing",
CLIName: "create",
CanonicalPath: "dev.create_thing",
CLIPath: "dev create",
PrimaryCLIPath: "dev create",
Group: "dev thing",
Aliases: []string{"dev alt"},
Source: "test",
}}
spec, err := runtimeToolSpecFromContractFinal(entry, final, runtimeSchemaMetadataSources{})
if err != nil {
t.Fatalf("consistent declared identity with all override fields failed: %v", err)
}
if spec.Identity.SourceProductID != "src" {
t.Fatalf("source_product_id = %q, want src", spec.Identity.SourceProductID)
}
if spec.Identity.Group != "dev thing" {
t.Fatalf("group = %q, want dev thing", spec.Identity.Group)
}
if len(spec.Identity.Aliases) != 1 || spec.Identity.Aliases[0] != "dev alt" {
t.Fatalf("aliases = %v, want [dev alt]", spec.Identity.Aliases)
}
if spec.Identity.CanonicalPath != "dev.create_thing" || spec.Identity.Source != "test" {
t.Fatalf("identity = %#v", spec.Identity)
}
}
func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalSameLengthAliasMismatchFails(t *testing.T) {
entry := runtimeSchemaEntry{
ProductID: "dev",
ToolName: "create_thing",
CLIName: "create",
CLIPath: "dev create",
PrimaryCLIPath: "dev create",
Aliases: []string{"dev rm"},
Command: &cobra.Command{Use: "create"},
Source: "test",
}
// Same-length alias sets with different members must be detected by the
// element-wise comparison, not only by the length fast-path.
final := ContractFinalPayload{Identity: &ToolIdentitySpec{Aliases: []string{"dev other"}}}
_, err := runtimeToolSpecFromContractFinal(entry, final, runtimeSchemaMetadataSources{})
if err == nil || !strings.Contains(err.Error(), "aliases") {
t.Fatalf("same-length alias mismatch error = %v, want aliases mismatch", err)
}
}
func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalSafetyAnnotationFallbacks(t *testing.T) {
entry := runtimeSchemaEntry{
ProductID: "dev",
ToolName: "create_thing",
CLIName: "create",
CLIPath: "dev create",
PrimaryCLIPath: "dev create",
Source: "test",
}
// Safety nil + Contract Risk annotation: Risk overlay wins.
riskCmd := &cobra.Command{Use: "create"}
AnnotateRuntimeRisk(riskCmd, "write")
entry.Command = riskCmd
spec, err := runtimeToolSpecFromContractFinal(entry, ContractFinalPayload{}, runtimeSchemaMetadataSources{})
if err != nil {
t.Fatalf("risk-annotated declared leaf failed: %v", err)
}
if spec.Safety.Effect != "write" || spec.Safety.Risk != "medium" || spec.Safety.Confirmation != "user_required" {
t.Fatalf("risk fallback safety = %#v", spec.Safety)
}
if spec.Safety.EffectSource != "corecmd.contract" {
t.Fatalf("risk fallback effect source = %q", spec.Safety.EffectSource)
}
// Safety nil + runtime gate annotation: gate overlay wins.
gateCmd := &cobra.Command{Use: "create"}
AnnotateRuntimeGate(gateCmd, "devAppRequireWriteGuard")
entry.Command = gateCmd
spec, err = runtimeToolSpecFromContractFinal(entry, ContractFinalPayload{}, runtimeSchemaMetadataSources{})
if err != nil {
t.Fatalf("gate-annotated declared leaf failed: %v", err)
}
if spec.Safety.Confirmation != "user_required" || spec.Safety.Effect != "write" || spec.Safety.Risk != "medium" {
t.Fatalf("gate fallback safety = %#v", spec.Safety)
}
if spec.Safety.EffectSource != "corecmd.contract_gate" {
t.Fatalf("gate fallback effect source = %q", spec.Safety.EffectSource)
}
}
func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalParameterResolutionError(t *testing.T) {
oldParameters := resolveRuntimeParameters
t.Cleanup(func() { resolveRuntimeParameters = oldParameters })
resolveRuntimeParameters = func(*cobra.Command, string, map[string]ParameterSchemaHint, map[string]embeddedMCPParamMeta, RuntimeSchemaConstraints) ([]ParameterSpec, error) {
return nil, errors.New("parameters failed")
}
entry := runtimeSchemaEntry{
ProductID: "dev",
ToolName: "create_thing",
Command: &cobra.Command{Use: "create"},
}
_, err := runtimeToolSpecFromContractFinal(entry, ContractFinalPayload{}, runtimeSchemaMetadataSources{})
if err == nil || !strings.Contains(err.Error(), "resolve Contract Schema parameters") {
t.Fatalf("parameter resolution error = %v, want resolve Contract Schema parameters", err)
}
}
+49 -4
View File
@@ -19,7 +19,7 @@ import (
"github.com/spf13/cobra"
)
func TestAnnotateRuntimeRiskEmbedsContractMarker(t *testing.T) {
func TestCrossPlatformCoverageAnnotateRuntimeRiskEmbedsContractMarker(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
AnnotateRuntimeRisk(cmd, "write")
got, ok := RuntimeContractRisk(cmd)
@@ -35,7 +35,7 @@ func TestAnnotateRuntimeRiskEmbedsContractMarker(t *testing.T) {
}
}
func TestApplyContractRiskToSafety(t *testing.T) {
func TestCrossPlatformCoverageApplyContractRiskToSafety(t *testing.T) {
base := SafetySpec{Idempotency: "unknown", Risk: "low", Confirmation: "not_required", Effect: "read"}
got := applyContractRiskToSafety(base, "high-risk-write")
if got.Effect != "destructive" || got.Risk != "high" || got.Confirmation != "user_required" {
@@ -54,7 +54,7 @@ func TestApplyContractRiskToSafety(t *testing.T) {
}
}
func TestAnnotateRuntimeGateDeclareOrAnnotate(t *testing.T) {
func TestCrossPlatformCoverageAnnotateRuntimeGateDeclareOrAnnotate(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
if HasDeclaredOrAnnotatedConfirmation(cmd) {
t.Fatal("bare command must not claim confirmation coverage")
@@ -72,7 +72,7 @@ func TestAnnotateRuntimeGateDeclareOrAnnotate(t *testing.T) {
}
}
func TestApplyContractGateToSafety(t *testing.T) {
func TestCrossPlatformCoverageApplyContractGateToSafety(t *testing.T) {
base := SafetySpec{Confirmation: "not_required", Effect: "read", Risk: "low"}
got := applyContractGateToSafety(base, "devAppRequireWriteGuard")
if got.Confirmation != "user_required" {
@@ -89,4 +89,49 @@ func TestApplyContractGateToSafety(t *testing.T) {
if got.Effect != "destructive" || got.Risk != "high" || got.Confirmation != "user_required" {
t.Fatalf("gate must keep reviewed effect/risk: %#v", got)
}
if got = applyContractGateToSafety(base, " "); got != base {
t.Fatalf("blank gate must be a no-op, got %#v", got)
}
}
func TestCrossPlatformCoverageRuntimeContractAnnotationNilAndBlankGuards(t *testing.T) {
// Nil-command annotators must be safe no-ops.
AnnotateRuntimeFlagDescription(nil, "flag", "description")
AnnotateRuntimeContract(nil)
AnnotateRuntimeRisk(nil, "write")
AnnotateRuntimeGate(nil, "devAppRequireWriteGuard")
cmd := &cobra.Command{Use: "x"}
AnnotateRuntimeGate(cmd, " ")
if _, ok := RuntimeContractGate(cmd); ok {
t.Fatal("blank AnnotateRuntimeGate must not record a gate")
}
if cmd.Annotations != nil && cmd.Annotations[runtimeSchemaContractAnnotation] == "command" {
t.Fatal("blank AnnotateRuntimeGate must not mark the command as Contract")
}
// A present-but-blank gate annotation must read back as no gate.
blank := &cobra.Command{Use: "y", Annotations: map[string]string{
runtimeSchemaRuntimeGateAnnotation: " ",
}}
if _, ok := RuntimeContractGate(blank); ok {
t.Fatal("blank runtime_gate annotation must not report a gate")
}
}
func TestCrossPlatformCoverageHasDeclaredOrAnnotatedConfirmationDeclaredAndRiskBranches(t *testing.T) {
declared := &cobra.Command{Use: "declared"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(declared) })
RegisterRuntimeContractFinal(declared, ContractFinalPayload{
Safety: &SafetySpec{Confirmation: "not_required"},
})
if !HasDeclaredOrAnnotatedConfirmation(declared) {
t.Fatal("typed Contract SafetySpec confirmation must satisfy declare-OR-annotate")
}
risky := &cobra.Command{Use: "risky"}
AnnotateRuntimeRisk(risky, "read")
if !HasDeclaredOrAnnotatedConfirmation(risky) {
t.Fatal("Contract Risk annotation must satisfy declare-OR-annotate")
}
}
+34
View File
@@ -0,0 +1,34 @@
package cli_test
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
)
func TestContractFinalFoundForHrbrain(t *testing.T) {
root := app.NewSchemaSourceRootCommand()
hrbrain, _, err := root.Find([]string{"hrbrain", "search", "employees"})
if err != nil || hrbrain == nil {
t.Fatalf("hrbrain search employees not found: %v", err)
}
final, ok := cli.RuntimeContractFinal(hrbrain)
if !ok {
t.Fatal("ContractFinal NOT found for hrbrain search employees — this is the root cause of ParamDecl not working")
}
t.Logf("ContractFinal found: title=%s params=%d", final.Title, len(final.Parameters))
}
func TestContractFinalFoundForAisearch(t *testing.T) {
root := app.NewSchemaSourceRootCommand()
enterprise, _, err := root.Find([]string{"aisearch", "enterprise"})
if err != nil || enterprise == nil {
t.Fatalf("aisearch enterprise not found: %v", err)
}
final, ok := cli.RuntimeContractFinal(enterprise)
if !ok {
t.Fatal("ContractFinal NOT found for aisearch enterprise")
}
t.Logf("ContractFinal found: title=%s params=%d", final.Title, len(final.Parameters))
}
+27
View File
@@ -0,0 +1,27 @@
package cli_test
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
)
func TestParamDeclAnnotationCompare(t *testing.T) {
root := app.NewSchemaSourceRootCommand()
for _, path := range [][]string{
{"aisearch", "enterprise"},
{"hrbrain", "search", "employees"},
{"chat", "add-emoji"},
} {
cmd, _, err := root.Find(path)
if err != nil || cmd == nil {
t.Logf("%v: NOT FOUND", path)
continue
}
ann := cmd.Annotations["dws.schema.param_decls"]
if ann == "" {
t.Logf("%v: NO param_decls annotation", path)
} else {
t.Logf("%v: HAS param_decls (%d bytes)", path, len(ann))
}
}
}
+30
View File
@@ -269,6 +269,23 @@ func AnnotateRuntimeRequiredFlags(cmd *cobra.Command, flagNames ...string) {
}
}
// AnnotateRuntimeFlagRequiredValue sets an explicit required value ("true" or
// "false") on a flag's dws.schema.required annotation. Use this when a
// declaration needs to override a stale cobra-level or MCP-level required=true
// with an explicit false.
func AnnotateRuntimeFlagRequiredValue(cmd *cobra.Command, flagName string, required bool) {
if cmd == nil {
return
}
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
v := "false"
if required {
v = "true"
}
setFlagAnnotation(flag, runtimeSchemaFlagRequiredAnnotation, v)
}
}
// AnnotateRuntimeFlagDescription records the Schema parameter description.
func AnnotateRuntimeFlagDescription(cmd *cobra.Command, flagName, description string) {
if cmd == nil {
@@ -301,6 +318,19 @@ func AnnotateRuntimeFlagFormat(cmd *cobra.Command, flagName, format string) {
}
}
// AnnotateRuntimeFlagInterfaceType records the wire type for a flag's interface
// property (e.g. "string", "integer", "boolean", "array"). The resolver reads
// this as a native_annotation candidate for interface_type, outranking
// tool_schema_hint.
func AnnotateRuntimeFlagInterfaceType(cmd *cobra.Command, flagName, interfaceType string) {
if cmd == nil {
return
}
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
setFlagAnnotation(flag, runtimeSchemaFlagTypeAnnotation, strings.TrimSpace(interfaceType))
}
}
// AnnotateRuntimeFlagEnum records the accepted values for a flag.
func AnnotateRuntimeFlagEnum(cmd *cobra.Command, flagName string, values ...string) {
if cmd == nil {
@@ -1,6 +1,6 @@
{
"version": 1,
"source_hash": "sha256:dc81f9a6bf1c4137d03f3b0aa7d2b17fcd72c5defdfc10ad26ee4911c4257098",
"source_hash": "sha256:2f14b54dfbe69542f486e442787b47d28d14f7fd65816a803957852bd2ac58d9",
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
"coverage": {
"surface_products": 26,
@@ -1,6 +1,6 @@
{
"version": 1,
"source_hash": "sha256:dc81f9a6bf1c4137d03f3b0aa7d2b17fcd72c5defdfc10ad26ee4911c4257098",
"source_hash": "sha256:2f14b54dfbe69542f486e442787b47d28d14f7fd65816a803957852bd2ac58d9",
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
"source_files": 160,
"hint_files": 54,
+21 -1
View File
@@ -52,7 +52,7 @@ func TestBuildManualAgentSelectionEvalFixtureUsesReviewedScenariosAndRealCommand
}
}
func TestBuildManualAgentSelectionEvalFixtureRejectsFactualContractDrift(t *testing.T) {
func TestCrossPlatformCoverageBuildManualAgentSelectionEvalFixtureRejectsFactualContractDrift(t *testing.T) {
tests := []struct {
name string
mutate func(*BoundCommandRegistry, *ManualAgentHintSet)
@@ -143,3 +143,23 @@ func manualAgentSelectionBoundFixture() BoundCommandRegistry {
ByCLIPath: map[string]BoundCommandSpec{spec.PrimaryCLIPath: spec},
}
}
func TestCrossPlatformCoverageContractFinalSelectionHintWithoutDeclaredSelection(t *testing.T) {
// No ContractFinal at all: the synthesized hint stays empty but reviewed.
hint := contractFinalSelectionHint(&cobra.Command{Use: "bare"})
if !hint.Reviewed || hint.Revision != "contract" {
t.Fatalf("undeclared hint = %#v", hint)
}
if hint.AgentSummary != "" || hint.UseWhen != nil || hint.Examples != nil {
t.Fatalf("undeclared hint must carry no selection prose: %#v", hint)
}
// ContractFinal without Selection: same early return.
cmd := &cobra.Command{Use: "declared"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(cmd) })
RegisterRuntimeContractFinal(cmd, ContractFinalPayload{Title: "T"})
hint = contractFinalSelectionHint(cmd)
if !hint.Reviewed || hint.AgentSummary != "" || hint.UseWhen != nil {
t.Fatalf("selection-less declared hint = %#v", hint)
}
}
+9 -2
View File
@@ -269,9 +269,16 @@ func loadSchemaCatalogSnapshot(snapshot SchemaCatalogSnapshot) (loadedSchemaCata
if len(snapshot.Catalog) == 0 || len(snapshot.Tools) == 0 {
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot is empty")
}
if snapshot.SourceHash == "" || snapshot.SourceHash != schemaCatalogSnapshotHash(snapshot) {
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot source_hash does not match its content")
if snapshot.SourceHash == "" {
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot is missing source_hash")
}
// source_hash equality is a build-time property, enforced by
// check-generated-drift.sh, which regenerates and byte-compares the whole
// artifact set — strictly stronger than this hash, since it also catches a
// shard set that is self-consistent but stale. Recomputing it here re-marshals
// every decoded map (measured: 86.9ms, 65MB, 997k allocs) on each invocation
// that touches ResolveMeta, to re-derive a fact about an embedded artifact
// that cannot change at runtime.
registry, index, err := schemaRegistryFromSnapshot(snapshot)
if err != nil {
return loadedSchemaCatalog{}, fmt.Errorf("load typed Schema registry: %w", err)
+2 -2
View File
@@ -1,12 +1,12 @@
{
"version": 1,
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
"source_hash": "sha256:b4de50d5e8239d4ccdd637f8abd4e020e467869863f31a98e9df947e500bbcf1",
"source_hash": "sha256:52d33a7c6f4bd324f275517cd72d5ab58bf17f5707d1cc15e9996d87ddac8daa",
"catalog": {
"agent_metadata": {
"products_with_metadata": 26,
"source": "embedded-skill-metadata",
"source_hash": "sha256:dc81f9a6bf1c4137d03f3b0aa7d2b17fcd72c5defdfc10ad26ee4911c4257098",
"source_hash": "sha256:2f14b54dfbe69542f486e442787b47d28d14f7fd65816a803957852bd2ac58d9",
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
"surface_products": 26,
"surface_tools": 845,
@@ -1020,10 +1020,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -1039,10 +1038,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -1126,10 +1124,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -1145,10 +1142,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
File diff suppressed because it is too large. Load diff
+104 -156
View File
@@ -379,10 +379,9 @@
"interface_type": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "array"
},
{
@@ -392,19 +391,17 @@
"value": "string"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "array"
},
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "depts"
},
{
@@ -414,19 +411,17 @@
"value": "depts"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "depts"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -436,10 +431,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -502,10 +496,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "orgUserMobile"
},
{
@@ -515,19 +508,17 @@
"value": "orgUserMobile"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "orgUserMobile"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
{
@@ -543,10 +534,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -608,10 +598,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "orgUserName"
},
{
@@ -621,19 +610,17 @@
"value": "orgUserName"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "orgUserName"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
{
@@ -649,10 +636,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -3355,10 +3341,9 @@
"interface_type": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "array"
},
{
@@ -3368,19 +3353,17 @@
"value": "string"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "array"
},
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "deptIds"
},
{
@@ -3390,19 +3373,17 @@
"value": "deptIds"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "deptIds"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -3412,10 +3393,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -3478,10 +3458,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "email"
},
{
@@ -3491,19 +3470,17 @@
"value": "email"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "email"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -3513,10 +3490,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -3578,10 +3554,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "loginId"
},
{
@@ -3591,19 +3566,17 @@
"value": "loginId"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "loginId"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
{
@@ -3619,10 +3592,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -3684,10 +3656,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "orgUserMobile"
},
{
@@ -3697,19 +3668,17 @@
"value": "orgUserMobile"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "orgUserMobile"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -3719,10 +3688,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -3784,10 +3752,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "orgUserName"
},
{
@@ -3797,19 +3764,17 @@
"value": "orgUserName"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "orgUserName"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
{
@@ -3825,10 +3790,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -3890,10 +3854,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "sendPwdViaSms"
},
{
@@ -3903,19 +3866,17 @@
"value": "sendPwdViaSms"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "sendPwdViaSms"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -3925,10 +3886,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -5434,10 +5394,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed helper deliberately permits either or both optional filters and passes an empty request to query the caller's authorized roster scope.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -5453,10 +5412,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed helper deliberately permits either or both optional filters and passes an empty request to query the caller's authorized roster scope.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -5540,10 +5498,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed helper deliberately permits either or both optional filters and passes an empty request to query the caller's authorized roster scope.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -5559,10 +5516,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed helper deliberately permits either or both optional filters and passes an empty request to query the caller's authorized roster scope.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -9036,10 +8992,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires an organization name and creator display name and calls org_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "creatorUsername"
},
{
@@ -9049,19 +9004,17 @@
"value": "creatorUsername"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires an organization name and creator display name and calls org_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "creatorUsername"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires an organization name and creator display name and calls org_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
{
@@ -9077,10 +9030,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires an organization name and creator display name and calls org_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -9142,10 +9094,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires an organization name and creator display name and calls org_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "orgName"
},
{
@@ -9155,19 +9106,17 @@
"value": "orgName"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires an organization name and creator display name and calls org_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "orgName"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed Wukong-compatible handler requires an organization name and creator display name and calls org_create without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
{
@@ -9183,10 +9132,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed Wukong-compatible handler requires an organization name and creator display name and calls org_create without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
+55 -83
View File
@@ -2088,10 +2088,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler normalizes repeated or comma-separated group conversation IDs into a stable de-duplicated array while preserving existing user mentions.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -2101,10 +2100,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler normalizes repeated or comma-separated group conversation IDs into a stable de-duplicated array while preserving existing user mentions.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -9019,16 +9017,15 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"precedence": "versioned_binding",
"selected": true,
"source": "reviewed_manual_hint",
"source": "versioned_parameter_binding",
"value": "format"
},
{
"precedence": "versioned_binding",
"precedence": "native_annotation",
"selected": false,
"source": "versioned_parameter_binding",
"source": "native_annotation",
"value": "format"
},
{
@@ -9038,19 +9035,17 @@
"value": "contentFormat"
}
],
"precedence": "reviewed_manual",
"precedence": "versioned_binding",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"source": "reviewed_manual_hint",
"source": "versioned_parameter_binding",
"value": "format"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -9066,10 +9061,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -9154,10 +9148,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -9167,10 +9160,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -9253,10 +9245,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -9266,10 +9257,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -9594,10 +9584,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -9607,10 +9596,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -9693,10 +9681,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -9706,24 +9693,16 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"selected": true,
"source": "reviewed_manual_hint",
"value": "--scope=range or --scope=section"
},
{
"precedence": "native_annotation",
"selected": false,
"selected": true,
"source": "native_annotation",
"value": "--scope=range or --scope=section"
},
@@ -9734,10 +9713,9 @@
"value": ""
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "--scope=range or --scope=section"
},
"type": {
@@ -9807,10 +9785,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -9826,24 +9803,16 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"selected": true,
"source": "reviewed_manual_hint",
"value": "--scope=tags"
},
{
"precedence": "native_annotation",
"selected": false,
"selected": true,
"source": "native_annotation",
"value": "--scope=tags"
},
@@ -9854,10 +9823,9 @@
"value": ""
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "--scope=tags"
},
"type": {
@@ -19426,13 +19394,19 @@
"parameter_count": 2,
"parameters": {
"name": {
"description": "新名称 (必填;原样传给服务端,不做扩展名规范化;如需根据节点类型和当前后缀规范化,请使用 drive rename)",
"description": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "cobra_contract",
"precedence": "native_annotation",
"selected": true,
"source": "native_annotation",
"value": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"
},
{
"precedence": "cobra_contract",
"selected": false,
"source": "cobra_usage",
"value": "新名称 (必填;原样传给服务端,不做扩展名规范化;如需根据节点类型和当前后缀规范化,请使用 drive rename)"
},
@@ -19449,10 +19423,10 @@
"value": ""
}
],
"precedence": "cobra_contract",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"source": "cobra_usage",
"value": "新名称 (必填;原样传给服务端,不做扩展名规范化;如需根据节点类型和当前后缀规范化,请使用 drive rename)"
"source": "native_annotation",
"value": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"
},
"property": {
"candidates": [
@@ -20476,10 +20450,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler normalizes group conversation IDs into a stable de-duplicated array and rejects group mentions for emoji replies, which the remote operation does not support.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -20489,10 +20462,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler normalizes group conversation IDs into a stable de-duplicated array and rejects group mentions for emoji replies, which the remote operation does not support.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
+5 -13
View File
@@ -19620,20 +19620,13 @@
"parameter_count": 2,
"parameters": {
"name": {
"description": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名",
"description": "新名称 (必填;实际执行时仅去掉与节点当前扩展名完全匹配的一层后缀)",
"field_provenance": {
"description": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.",
"selected": true,
"source": "reviewed_manual_hint",
"value": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"
},
{
"precedence": "cobra_contract",
"selected": false,
"selected": true,
"source": "cobra_usage",
"value": "新名称 (必填;实际执行时仅去掉与节点当前扩展名完全匹配的一层后缀)"
},
@@ -19650,11 +19643,10 @@
"value": ""
}
],
"precedence": "reviewed_manual",
"precedence": "cobra_contract",
"resolution": "highest_precedence",
"review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.",
"source": "reviewed_manual_hint",
"value": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"
"source": "cobra_usage",
"value": "新名称 (必填;实际执行时仅去掉与节点当前扩展名完全匹配的一层后缀)"
},
"property": {
"candidates": [
File diff suppressed because it is too large. Load diff
+30 -40
View File
@@ -17875,16 +17875,15 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed CLI accepts either a fuzzy keyword or an exact employee number, represented by a typed require-one-of constraint rather than two unconditional requirements.",
"precedence": "command_constraint",
"selected": true,
"source": "reviewed_manual_hint",
"source": "require_one_of_constraint",
"value": false
},
{
"precedence": "command_constraint",
"precedence": "native_annotation",
"selected": false,
"source": "require_one_of_constraint",
"source": "native_annotation",
"value": false
},
{
@@ -17900,10 +17899,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "command_constraint",
"resolution": "highest_precedence",
"review_reason": "The reviewed CLI accepts either a fuzzy keyword or an exact employee number, represented by a typed require-one-of constraint rather than two unconditional requirements.",
"source": "reviewed_manual_hint",
"source": "require_one_of_constraint",
"value": false
},
"required_when": {
@@ -17986,16 +17984,15 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed CLI accepts either a fuzzy keyword or an exact employee number, represented by a typed require-one-of constraint rather than two unconditional requirements.",
"precedence": "command_constraint",
"selected": true,
"source": "reviewed_manual_hint",
"source": "require_one_of_constraint",
"value": false
},
{
"precedence": "command_constraint",
"precedence": "native_annotation",
"selected": false,
"source": "require_one_of_constraint",
"source": "native_annotation",
"value": false
},
{
@@ -18011,10 +18008,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "command_constraint",
"resolution": "highest_precedence",
"review_reason": "The reviewed CLI accepts either a fuzzy keyword or an exact employee number, represented by a typed require-one-of constraint rather than two unconditional requirements.",
"source": "reviewed_manual_hint",
"source": "require_one_of_constraint",
"value": false
},
"required_when": {
@@ -29679,16 +29675,15 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed patch command accepts any non-empty subset of mutable template fields; a typed require-one-of constraint owns that requirement.",
"precedence": "command_constraint",
"selected": true,
"source": "reviewed_manual_hint",
"source": "require_one_of_constraint",
"value": false
},
{
"precedence": "command_constraint",
"precedence": "native_annotation",
"selected": false,
"source": "require_one_of_constraint",
"source": "native_annotation",
"value": false
},
{
@@ -29704,10 +29699,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "command_constraint",
"resolution": "highest_precedence",
"review_reason": "The reviewed patch command accepts any non-empty subset of mutable template fields; a typed require-one-of constraint owns that requirement.",
"source": "reviewed_manual_hint",
"source": "require_one_of_constraint",
"value": false
},
"required_when": {
@@ -30093,16 +30087,15 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed patch command accepts any non-empty subset of mutable template fields; a typed require-one-of constraint owns that requirement.",
"precedence": "command_constraint",
"selected": true,
"source": "reviewed_manual_hint",
"source": "require_one_of_constraint",
"value": false
},
{
"precedence": "command_constraint",
"precedence": "native_annotation",
"selected": false,
"source": "require_one_of_constraint",
"source": "native_annotation",
"value": false
},
{
@@ -30118,10 +30111,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "command_constraint",
"resolution": "highest_precedence",
"review_reason": "The reviewed patch command accepts any non-empty subset of mutable template fields; a typed require-one-of constraint owns that requirement.",
"source": "reviewed_manual_hint",
"source": "require_one_of_constraint",
"value": false
},
"required_when": {
@@ -30204,16 +30196,15 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed patch command accepts any non-empty subset of mutable template fields; a typed require-one-of constraint owns that requirement.",
"precedence": "command_constraint",
"selected": true,
"source": "reviewed_manual_hint",
"source": "require_one_of_constraint",
"value": false
},
{
"precedence": "command_constraint",
"precedence": "native_annotation",
"selected": false,
"source": "require_one_of_constraint",
"source": "native_annotation",
"value": false
},
{
@@ -30229,10 +30220,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "command_constraint",
"resolution": "highest_precedence",
"review_reason": "The reviewed patch command accepts any non-empty subset of mutable template fields; a typed require-one-of constraint owns that requirement.",
"source": "reviewed_manual_hint",
"source": "require_one_of_constraint",
"value": false
},
"required_when": {
@@ -883,10 +883,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed optional --to-chat switch defaults to false and only controls post-submit delivery; it is not required to create a report.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -902,10 +901,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed optional --to-chat switch defaults to false and only controls post-submit delivery; it is not required to create a report.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
+76 -114
View File
@@ -5264,10 +5264,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires a non-blank tag name, builds a one-element userTags request, and invokes create_todo_tag without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "name"
},
{
@@ -5277,19 +5276,17 @@
"value": "name"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires a non-blank tag name, builds a one-element userTags request, and invokes create_todo_tag without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "name"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires a non-blank tag name, builds a one-element userTags request, and invokes create_todo_tag without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
{
@@ -5305,10 +5302,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires a non-blank tag name, builds a one-element userTags request, and invokes create_todo_tag without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -6759,10 +6755,9 @@
"interface_type": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires at least one non-empty tag code, permits caller-authoritative dry-run, and requires explicit --yes before invoking delete_todo_tag.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "array"
},
{
@@ -6772,19 +6767,17 @@
"value": "string"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires at least one non-empty tag code, permits caller-authoritative dry-run, and requires explicit --yes before invoking delete_todo_tag.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "array"
},
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires at least one non-empty tag code, permits caller-authoritative dry-run, and requires explicit --yes before invoking delete_todo_tag.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "tagCodes"
},
{
@@ -6794,19 +6787,17 @@
"value": "tagCodes"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires at least one non-empty tag code, permits caller-authoritative dry-run, and requires explicit --yes before invoking delete_todo_tag.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "tagCodes"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires at least one non-empty tag code, permits caller-authoritative dry-run, and requires explicit --yes before invoking delete_todo_tag.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
{
@@ -6822,10 +6813,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires at least one non-empty tag code, permits caller-authoritative dry-run, and requires explicit --yes before invoking delete_todo_tag.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -8187,10 +8177,9 @@
"description": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler defaults omitted role-types to executor, preserves current-organization behavior by default, and switches both single-page and auto-page calls to get_user_todos only when --query-all is explicitly true.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "为 true 时跨组织查询全部待办;默认仅查询当前组织待办"
},
{
@@ -8206,10 +8195,9 @@
"value": ""
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler defaults omitted role-types to executor, preserves current-organization behavior by default, and switches both single-page and auto-page calls to get_user_todos only when --query-all is explicitly true.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "为 true 时跨组织查询全部待办;默认仅查询当前组织待办"
},
"property": {
@@ -8237,10 +8225,9 @@
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler defaults omitted role-types to executor, preserves current-organization behavior by default, and switches both single-page and auto-page calls to get_user_todos only when --query-all is explicitly true.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -8250,10 +8237,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler defaults omitted role-types to executor, preserves current-organization behavior by default, and switches both single-page and auto-page calls to get_user_todos only when --query-all is explicitly true.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -8295,10 +8281,9 @@
"description": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler defaults omitted role-types to executor, preserves current-organization behavior by default, and switches both single-page and auto-page calls to get_user_todos only when --query-all is explicitly true.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "角色类型列表;省略时运行时默认使用 executor"
},
{
@@ -8320,10 +8305,9 @@
"value": ""
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler defaults omitted role-types to executor, preserves current-organization behavior by default, and switches both single-page and auto-page calls to get_user_todos only when --query-all is explicitly true.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "角色类型列表;省略时运行时默认使用 executor"
},
"example": {
@@ -8369,10 +8353,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler defaults omitted role-types to executor, preserves current-organization behavior by default, and switches both single-page and auto-page calls to get_user_todos only when --query-all is explicitly true.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "roleTypes"
},
{
@@ -8382,19 +8365,17 @@
"value": "roleTypes"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler defaults omitted role-types to executor, preserves current-organization behavior by default, and switches both single-page and auto-page calls to get_user_todos only when --query-all is explicitly true.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "roleTypes"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler defaults omitted role-types to executor, preserves current-organization behavior by default, and switches both single-page and auto-page calls to get_user_todos only when --query-all is explicitly true.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
{
@@ -8410,10 +8391,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler defaults omitted role-types to executor, preserves current-organization behavior by default, and switches both single-page and auto-page calls to get_user_todos only when --query-all is explicitly true.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": false
},
"required_when": {
@@ -11642,10 +11622,9 @@
"description": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "提醒规则 JSON 数组;不传表示清除,显式传值必须为对象数组,且每条按 baseTime 提供整数 dueDateOffset 或 ISO8601 reminderTimeStamp"
},
{
@@ -11661,10 +11640,9 @@
"value": ""
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "提醒规则 JSON 数组;不传表示清除,显式传值必须为对象数组,且每条按 baseTime 提供整数 dueDateOffset 或 ISO8601 reminderTimeStamp"
},
"property": {
@@ -18163,10 +18141,9 @@
"interface_type": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires a task ID and at least one non-empty tag code before invoking tag_todo.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "array"
},
{
@@ -18176,19 +18153,17 @@
"value": "string"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires a task ID and at least one non-empty tag code before invoking tag_todo.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "array"
},
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires a task ID and at least one non-empty tag code before invoking tag_todo.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "tagCodes"
},
{
@@ -18198,19 +18173,17 @@
"value": "tagCodes"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires a task ID and at least one non-empty tag code before invoking tag_todo.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "tagCodes"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires a task ID and at least one non-empty tag code before invoking tag_todo.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
{
@@ -18226,10 +18199,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires a task ID and at least one non-empty tag code before invoking tag_todo.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -18292,10 +18264,9 @@
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires a task ID and at least one non-empty tag code before invoking tag_todo.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "taskId"
},
{
@@ -18305,19 +18276,17 @@
"value": "taskId"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires a task ID and at least one non-empty tag code before invoking tag_todo.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "taskId"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires a task ID and at least one non-empty tag code before invoking tag_todo.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
{
@@ -18333,10 +18302,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires a task ID and at least one non-empty tag code before invoking tag_todo.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -19335,10 +19303,9 @@
"interface_type": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires a non-null JSON array of tag update records and invokes update_todo_tag without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "array"
},
{
@@ -19348,19 +19315,17 @@
"value": "string"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires a non-null JSON array of tag update records and invokes update_todo_tag without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "array"
},
"property": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires a non-null JSON array of tag update records and invokes update_todo_tag without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "userTags"
},
{
@@ -19370,19 +19335,17 @@
"value": "userTags"
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires a non-null JSON array of tag update records and invokes update_todo_tag without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": "userTags"
},
"required": {
"candidates": [
{
"precedence": "reviewed_manual",
"review_reason": "The reviewed handler requires a non-null JSON array of tag update records and invokes update_todo_tag without a runtime confirmation gate.",
"precedence": "native_annotation",
"selected": true,
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
{
@@ -19398,10 +19361,9 @@
"value": false
}
],
"precedence": "reviewed_manual",
"precedence": "native_annotation",
"resolution": "highest_precedence",
"review_reason": "The reviewed handler requires a non-null JSON array of tag update records and invokes update_todo_tag without a runtime confirmation gate.",
"source": "reviewed_manual_hint",
"source": "native_annotation",
"value": true
},
"required_when": {
@@ -86,10 +86,13 @@ func TestCrossPlatformCoverageLoadSchemaCatalogSnapshotGateFailures(t *testing.T
if _, err := loadSchemaCatalogSnapshot(SchemaCatalogSnapshot{Version: SchemaCatalogSnapshotVersion}); err == nil {
t.Fatal("empty snapshot succeeded")
}
badHash := cloneSnapshotForCoverage(t, base)
badHash.SourceHash = "wrong"
if _, err := loadSchemaCatalogSnapshot(badHash); err == nil {
t.Fatal("bad snapshot hash succeeded")
// source_hash equality is a build-time property (check-generated-drift.sh);
// the runtime loader only rejects a missing hash. A wrong but present hash
// succeeds because the embedded artifact cannot change at runtime.
missingHash := cloneSnapshotForCoverage(t, base)
missingHash.SourceHash = ""
if _, err := loadSchemaCatalogSnapshot(missingHash); err == nil {
t.Fatal("missing snapshot hash succeeded")
}
badTyped := cloneSnapshotForCoverage(t, base)
badTyped.Catalog["unknown"] = true
+5 -1
View File
@@ -134,7 +134,11 @@ func TestEmbeddedCatalogPreservesRegistryIdentityAndManualParameterContract(t *t
t.Helper()
provenance := schemaMap(parameters[flagName]["field_provenance"])
winner := provenance[field]
if winner["source"] != "reviewed_manual_hint" || winner["precedence"] != "reviewed_manual" {
src, _ := winner["source"].(string)
prec, _ := winner["precedence"].(string)
// Accept both the legacy hints source and the migrated declaration source.
if (src != "reviewed_manual_hint" || prec != "reviewed_manual") &&
(src != "native_annotation" || prec != "native_annotation") {
t.Fatalf("%s.%s provenance = %#v", flagName, field, winner)
}
}
+48 -3
View File
@@ -300,6 +300,45 @@ func validateCompatibilityLeafContract(spec CommandSpec, primary, alias *cobra.C
)
}
// filterSchemaAnnotations returns a copy of annotations without dws.schema.*
// keys. Those are Schema-level metadata (ParamDecl / AttachSchema) that do not
// affect command execution and are resolved through the shared assembly path.
func filterSchemaAnnotations(annotations map[string][]string) map[string][]string {
if len(annotations) == 0 {
return nil
}
out := make(map[string][]string, len(annotations))
for k, v := range annotations {
if !strings.HasPrefix(k, "dws.schema.") {
out[k] = v
}
}
if len(out) == 0 {
return nil
}
return out
}
// requiredContractEquivalent compares two Required contracts, tolerating
// NativeRequired / NativeRequiredWhen being present on one side but empty on
// the other. This happens when a ParamDecl annotation exists on the primary
// command but not on its compatibility alias. Genuine drift (both non-empty
// but different) is still caught.
func requiredContractEquivalent(a, b compatibilityFlagRequiredContract) bool {
// Compare all fields except NativeRequired / NativeRequiredWhen strictly.
if a.CobraRequired != b.CobraRequired || a.UsageRequired != b.UsageRequired ||
a.UsageDefault != b.UsageDefault || a.MetadataRequired != b.MetadataRequired ||
a.MetadataRequiredWhen != b.MetadataRequiredWhen {
return false
}
// NativeRequired / NativeRequiredWhen: tolerate one-sided presence.
nativeEquiv := func(x, y string) bool {
return x == y || x == "" || y == ""
}
return nativeEquiv(a.NativeRequired, b.NativeRequired) &&
nativeEquiv(a.NativeRequiredWhen, b.NativeRequiredWhen)
}
func compatibilityHandlerContractProblems(primary, alias *cobra.Command) []string {
if primary == nil || alias == nil {
return []string{"execution handlers cannot be compared for a nil command"}
@@ -450,13 +489,19 @@ func compatibilityFlagContractProblems(canonicalPath string, primary, alias *cob
if primaryFlag.Origin != aliasFlag.Origin {
problems = append(problems, fmt.Sprintf("flag --%s local/persistent/inherited behavior differs: primary=%s compatibility=%s", name, primaryFlag.Origin, aliasFlag.Origin))
}
if !reflect.DeepEqual(primaryFlag.Required, aliasFlag.Required) {
if !requiredContractEquivalent(primaryFlag.Required, aliasFlag.Required) {
problems = append(problems, fmt.Sprintf("flag --%s required/required_when facts differ: primary=%s compatibility=%s",
name, compatibilityJSON(primaryFlag.Required), compatibilityJSON(aliasFlag.Required)))
}
if !reflect.DeepEqual(primaryFlag.Annotations, aliasFlag.Annotations) {
// Compare annotations excluding dws.schema.* keys: those are Schema-level
// metadata declarations (ParamDecl / AttachSchema) that do not affect
// command execution. The compatibility check verifies executable equivalence;
// Schema facts are resolved through the shared assembly path.
primaryExecAnnotations := filterSchemaAnnotations(primaryFlag.Annotations)
aliasExecAnnotations := filterSchemaAnnotations(aliasFlag.Annotations)
if !reflect.DeepEqual(primaryExecAnnotations, aliasExecAnnotations) {
problems = append(problems, fmt.Sprintf("flag --%s annotations differ: primary=%s compatibility=%s",
name, compatibilityJSON(primaryFlag.Annotations), compatibilityJSON(aliasFlag.Annotations)))
name, compatibilityJSON(primaryExecAnnotations), compatibilityJSON(aliasExecAnnotations)))
}
}
return problems, nil
+43
View File
@@ -987,3 +987,46 @@ func annotateTestCompatibilityPair(primary, alias *cobra.Command) {
Reviewed: true,
})
}
// TestCrossPlatformCoverageBindEffectiveCommandRegistryAttachesResidualHintDecl pins the bind-time
// migration path: an undeclared leaf whose canonical still has a compiled
// schema_hints declaration receives ContractFinal without replacing RunE.
func TestCrossPlatformCoverageBindEffectiveCommandRegistryAttachesResidualHintDecl(t *testing.T) {
root := commandRegistryTestRoot("item get")
leaf := exactSchemaCommand(root, "item get")
if leaf == nil {
t.Fatal("missing item get leaf")
}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(leaf) })
schemaHintDeclsByCanonical["item.get_item"] = testSchemaHintDeclFixture()
t.Cleanup(func() { delete(schemaHintDeclsByCanonical, "item.get_item") })
reviewed := mustCommandRegistry(t, []CommandSpec{{
CanonicalPath: "item.get_item",
PrimaryCLIPath: "item get",
}})
manual := ManualSchemaHintSnapshot{
Version: manualSchemaHintVersion,
Commands: []ManualSchemaCommandHint{{
CLIPath: "item get",
CanonicalPath: "item.get_item",
Reason: "Confirms the reviewed primary identity",
Reviewed: true,
}},
}
effective, err := buildEffectiveCommandRegistry(root, reviewed, manual)
if err != nil {
t.Fatalf("buildEffectiveCommandRegistry() error = %v", err)
}
if _, err := BindEffectiveCommandRegistry(root, effective); err != nil {
t.Fatalf("BindEffectiveCommandRegistry() error = %v", err)
}
if !HasRuntimeContractFinal(leaf) {
t.Fatal("residual hint decl must attach ContractFinal at bind time")
}
final, ok := RuntimeContractFinal(leaf)
if !ok || final.Description != testSchemaHintDeclFixture().Description {
t.Fatalf("attached ContractFinal = %#v ok=%v", final, ok)
}
}
@@ -0,0 +1,188 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"crypto/sha256"
"io/fs"
"strings"
"testing"
)
// These benchmarks attribute the Schema consumption cost that ResolveMeta pays
// on its first hit. Both production entry points are sync.Once memoized, so the
// benchmarks call the underlying work directly — measuring the exported wrappers
// would time one real run followed by N no-ops.
//
// They establish the RFC §M0 deliverable-11 baseline for embedded parse and
// lookup construction, which §M4's per-PR budget needs in order to mean anything.
// BenchmarkAssembleEmbeddedSchemaCatalog measures decoding the embedded release
// snapshot: the global envelope plus every per-product tools shard.
func BenchmarkAssembleEmbeddedSchemaCatalog(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
loaded, err := assembleEmbeddedSchemaCatalog()
if err != nil {
b.Fatal(err)
}
if len(loaded.Snapshot.Tools) == 0 {
b.Fatal("decoded an empty catalog")
}
}
}
// BenchmarkBuildMetaByCLIPath measures turning the decoded snapshot into the
// ResolveMeta lookup map, isolated from the decode above.
func BenchmarkBuildMetaByCLIPath(b *testing.B) {
loaded := embeddedSchemaCatalog()
if len(loaded.Snapshot.Tools) == 0 {
b.Skip("embedded catalog unavailable")
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if len(buildMetaByCLIPath(loaded)) == 0 {
b.Fatal("built an empty lookup")
}
}
}
// BenchmarkResolveMetaSteadyState measures a warm lookup, i.e. what every
// --help Safety annotation costs once the map exists.
func BenchmarkResolveMetaSteadyState(b *testing.B) {
if _, ok := ResolveMeta("dev app delete"); !ok {
b.Skip("embedded catalog unavailable")
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, ok := ResolveMeta("dev app delete"); !ok {
b.Fatal("warm lookup missed")
}
}
}
// The benchmarks below attribute the cost *inside* assembleEmbeddedSchemaCatalog,
// which the whole-path benchmark shows costs ~1.5s and ~817 MB. Optimizing the
// wrong stage is the main risk here, so each stage is measured separately.
// BenchmarkCatalogStageDecodeShards measures only the JSON decode of the
// envelope plus the 27 per-product shards into the untyped snapshot maps.
func BenchmarkCatalogStageDecodeShards(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
snapshot, err := assembleSchemaCatalogSnapshot(
embeddedSchemaCatalogEnvelopeJSON, embeddedSchemaCatalogTools, "schema_catalog/tools")
if err != nil {
b.Fatal(err)
}
if len(snapshot.Tools) == 0 {
b.Fatal("decoded an empty snapshot")
}
}
}
// BenchmarkCatalogStageSourceHash measures re-hashing the decoded snapshot,
// which the loader does on every process start to verify source_hash.
func BenchmarkCatalogStageSourceHash(b *testing.B) {
snapshot, err := assembleSchemaCatalogSnapshot(
embeddedSchemaCatalogEnvelopeJSON, embeddedSchemaCatalogTools, "schema_catalog/tools")
if err != nil {
b.Skip("embedded catalog unavailable")
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if schemaCatalogSnapshotHash(snapshot) == "" {
b.Fatal("empty hash")
}
}
}
// BenchmarkCatalogStageTypedRegistry measures converting the untyped snapshot
// maps into the typed registry and index.
func BenchmarkCatalogStageTypedRegistry(b *testing.B) {
snapshot, err := assembleSchemaCatalogSnapshot(
embeddedSchemaCatalogEnvelopeJSON, embeddedSchemaCatalogTools, "schema_catalog/tools")
if err != nil {
b.Skip("embedded catalog unavailable")
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
registry, _, err := schemaRegistryFromSnapshot(snapshot)
if err != nil {
b.Fatal(err)
}
if len(registry.Products) == 0 {
b.Fatal("empty registry")
}
}
}
// BenchmarkCatalogStageValidations measures the three whole-registry validation
// passes the loader runs after conversion.
func BenchmarkCatalogStageValidations(b *testing.B) {
loaded := embeddedSchemaCatalog()
if len(loaded.Registry.Products) == 0 {
b.Skip("embedded catalog unavailable")
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if err := loadCatalogValidateInterfaces(loaded.Registry); err != nil {
b.Fatal(err)
}
if err := loadCatalogValidateProvenance(loaded.Registry); err != nil {
b.Fatal(err)
}
if err := loadCatalogValidateAgentMetadata(loaded.Registry); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkCatalogStageRawByteHash measures hashing the embedded bytes directly,
// as an alternative to schemaCatalogSnapshotHash, which re-marshals the decoded
// maps. It is the cost a raw-bytes integrity check would pay instead.
func BenchmarkCatalogStageRawByteHash(b *testing.B) {
entries, err := fs.ReadDir(embeddedSchemaCatalogTools, "schema_catalog/tools")
if err != nil {
b.Skip("embedded catalog unavailable")
}
shards := make([][]byte, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
data, readErr := fs.ReadFile(embeddedSchemaCatalogTools, "schema_catalog/tools/"+entry.Name())
if readErr != nil {
b.Skip("embedded shard unavailable")
}
shards = append(shards, data)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
sum := sha256.New()
sum.Write(embeddedSchemaCatalogEnvelopeJSON)
for _, shard := range shards {
sum.Write(shard)
}
if len(sum.Sum(nil)) != sha256.Size {
b.Fatal("short digest")
}
}
}
+88
View File
@@ -5,6 +5,7 @@
package cli
import (
"strings"
"testing"
"github.com/spf13/cobra"
@@ -69,3 +70,90 @@ func TestSchemaHintDeclLookupCoverage(t *testing.T) {
t.Fatal("doc.create_document must not remain in compiled hint decls")
}
}
func TestCrossPlatformCoverageLookupSchemaHintDeclBlankCanonical(t *testing.T) {
if _, ok := lookupSchemaHintDecl(""); ok {
t.Fatal("empty canonical must not resolve a hint decl")
}
if _, ok := lookupSchemaHintDecl(" "); ok {
t.Fatal("blank canonical must not resolve a hint decl")
}
}
func TestCrossPlatformCoverageAttachSchemaHintDeclNilCommandIsNoOp(t *testing.T) {
// Must not panic and must not register anything.
attachSchemaHintDecl(nil, testSchemaHintDeclFixture())
}
func TestCrossPlatformCoverageAttachSchemaHintDeclCompletenessPanics(t *testing.T) {
mustPanic := func(t *testing.T, wantFragment string, decl schemaHintDecl) {
t.Helper()
cmd := &cobra.Command{Use: "probe", Short: "probe"}
defer func() {
recovered := recover()
if recovered == nil {
t.Fatalf("attachSchemaHintDecl must panic for %q", wantFragment)
}
message, ok := recovered.(string)
if !ok || !strings.Contains(message, wantFragment) {
t.Fatalf("panic = %v, want fragment %q", recovered, wantFragment)
}
if HasRuntimeContractFinal(cmd) {
t.Fatal("panicking attach must not register ContractFinal")
}
}()
attachSchemaHintDecl(cmd, decl)
}
t.Run("missing description", func(t *testing.T) {
decl := testSchemaHintDeclFixture()
decl.Description = " "
mustPanic(t, "missing Description", decl)
})
t.Run("missing selection prose", func(t *testing.T) {
decl := testSchemaHintDeclFixture()
decl.Selection.UseWhen = nil
mustPanic(t, "missing Selection prose", decl)
})
t.Run("missing interface", func(t *testing.T) {
decl := testSchemaHintDeclFixture()
decl.Interface = nil
mustPanic(t, "missing Interface", decl)
})
t.Run("composite without reason", func(t *testing.T) {
decl := testSchemaHintDeclFixture()
decl.Interface = &InterfaceSpec{Mode: InterfaceModeComposite, Availability: InterfaceAvailable}
mustPanic(t, "missing Interface.Reason", decl)
})
}
func TestCrossPlatformCoverageAttachSchemaHintDeclDefaultsEmptySafetyToRead(t *testing.T) {
decl := testSchemaHintDeclFixture()
decl.Safety = SafetySpec{}
cmd := &cobra.Command{Use: "probe", Short: "probe"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(cmd) })
attachSchemaHintDecl(cmd, decl)
final, ok := RuntimeContractFinal(cmd)
if !ok || final.Safety == nil {
t.Fatalf("ContractFinal after attach = %#v ok=%v", final, ok)
}
if final.Safety.Effect != "read" || final.Safety.Risk != "low" ||
final.Safety.Confirmation != "not_required" || final.Safety.Idempotency != "idempotent" {
t.Fatalf("defaulted safety = %#v", final.Safety)
}
if final.Safety.EffectSource != "corecmd.contract" {
t.Fatalf("effect source = %q, want corecmd.contract", final.Safety.EffectSource)
}
}
func TestFirstNonEmptyTrim(t *testing.T) {
if got := firstNonEmptyTrim(" ", "", "\t"); got != "" {
t.Fatalf("all-blank firstNonEmptyTrim = %q, want empty", got)
}
if got := firstNonEmptyTrim(); got != "" {
t.Fatalf("no-arg firstNonEmptyTrim = %q, want empty", got)
}
if got := firstNonEmptyTrim("", " value "); got != "value" {
t.Fatalf("firstNonEmptyTrim = %q, want value", got)
}
}
@@ -9,17 +9,9 @@
},
"tools": {
"aisearch.search_enterprise": {
"parameters": {
"queries": {
"required": false
},
"time-range": {
"required": false
}
},
"reviewed": true,
"cli_path": "aisearch enterprise",
"review_reason": "The reviewed CLI supports an all-content search with default --types all, while --queries and --time-range are optional filters; imported MCP required markers must not turn them into mandatory CLI input."
}
}
}
}
+1 -253
View File
@@ -9,47 +9,11 @@
},
"tools": {
"chat.add_emoji_reaction": {
"parameters": {
"chat": {
"property": "openConversationId",
"required": false
},
"conversation-id": {
"property": "openConversationId",
"required": false
},
"group": {
"property": "openConversationId",
"required": false
},
"id": {
"property": "openConversationId",
"required": false
}
},
"reviewed": true,
"cli_path": "chat message add-emoji",
"review_reason": "The four reviewed conversation locator flags are aliases for one openConversationId property; the typed require-one-of constraint owns their conditional requirement."
},
"chat.add_text_emotion": {
"parameters": {
"chat": {
"property": "openConversationId",
"required": false
},
"conversation-id": {
"property": "openConversationId",
"required": false
},
"group": {
"property": "openConversationId",
"required": false
},
"id": {
"property": "openConversationId",
"required": false
}
},
"reviewed": true,
"cli_path": "chat message add-text-emotion",
"review_reason": "The four reviewed conversation locator flags are aliases for one openConversationId property; the typed require-one-of constraint owns their conditional requirement."
@@ -89,233 +53,66 @@
"review_reason": "Preserve the reviewed CLI-to-interface conversion: Cobra validates --size as an integer from 1 to 100, then the helper serializes it to the string-valued list_message_favorites size property."
},
"chat.list_owned_or_admin_groups": {
"parameters": {
"limit": {
"required": false
},
"role": {
"required": false
}
},
"reviewed": true,
"cli_path": "chat group list-my-groups",
"review_reason": "The reviewed CLI lists all owned or administered groups when --role and --limit are omitted; both flags are optional filters despite stricter imported interface metadata."
},
"chat.remove_emoji_reaction": {
"parameters": {
"chat": {
"property": "openConversationId",
"required": false
},
"conversation-id": {
"property": "openConversationId",
"required": false
},
"group": {
"property": "openConversationId",
"required": false
},
"id": {
"property": "openConversationId",
"required": false
}
},
"reviewed": true,
"cli_path": "chat message remove-emoji",
"review_reason": "The four reviewed conversation locator flags are aliases for one openConversationId property; the typed require-one-of constraint owns their conditional requirement."
},
"chat.remove_text_emotion": {
"parameters": {
"chat": {
"property": "openConversationId",
"required": false
},
"conversation-id": {
"property": "openConversationId",
"required": false
},
"group": {
"property": "openConversationId",
"required": false
},
"id": {
"property": "openConversationId",
"required": false
}
},
"reviewed": true,
"cli_path": "chat message remove-text-emotion",
"review_reason": "The four reviewed conversation locator flags are aliases for one openConversationId property; the typed require-one-of constraint owns their conditional requirement."
},
"chat.set_group_member_mute_list": {
"parameters": {
"off": {
"required": false
}
},
"reviewed": true,
"cli_path": "chat group-mute-member",
"review_reason": "The reviewed Cobra contract makes --off an optional mode switch: omission adds members to the mute list, while --off removes them. The shared MCP schema incorrectly marks the boolean as unconditionally required."
},
"chat.set_group_mute": {
"parameters": {
"off": {
"required": false
}
},
"reviewed": true,
"cli_path": "chat group-mute",
"review_reason": "The reviewed optional --off switch selects unmute; omission deliberately selects mute, so false is a valid default rather than missing required input."
},
"chat.set_top_conversation": {
"parameters": {
"off": {
"required": false
}
},
"reviewed": true,
"cli_path": "chat set-top",
"review_reason": "The reviewed optional --off switch selects unpin; omission deliberately selects pin, so false is a valid default rather than missing required input."
},
"chat.update_conv_member_roles": {
"parameters": {
"off": {
"required": false
}
},
"reviewed": true,
"cli_path": "chat group set-admin",
"review_reason": "The reviewed optional --off switch selects administrator removal; omission deliberately selects administrator assignment."
},
"chat.update_notification_off": {
"parameters": {
"chat": {
"property": "openConversationId",
"required": false
},
"conversation-id": {
"property": "openConversationId",
"required": false
},
"id": {
"property": "openConversationId",
"required": false
},
"off": {
"required": false
}
},
"reviewed": true,
"cli_path": "chat mute",
"review_reason": "The reviewed locator aliases feed one openConversationId property, while optional --off selects unmute; typed constraints require one locator without making the mode switch mandatory."
},
"chat.edit_message": {
"parameters": {
"conversation-id": {
"property": "openConversationId",
"required": true
},
"group": {
"property": "openConversationId",
"required": false
},
"id": {
"property": "openConversationId",
"required": false
},
"chat": {
"property": "openConversationId",
"required": false
},
"msg-id": {
"property": "openMessageId",
"required": true
},
"text": {
"property": "text",
"required": false
},
"title": {
"property": "title",
"required": false
},
"content": {
"property": "content",
"required": false
},
"at-all": {
"property": "atAll",
"required": false,
"interface_type": "boolean"
},
"at-open-dingtalk-ids": {
"property": "atOpenDingTalkIds",
"required": false,
"interface_type": "array"
}
},
"reviewed": true,
"cli_path": "chat message edit",
"review_reason": "The reviewed handler requires one conversation locator, a message ID, and exactly one of text or raw content; it optionally derives a title and maps mention controls before invoking edit_message."
},
"chat.get_conv_categories_info": {
"parameters": {
"category-ids": {
"property": "categoryIds",
"required": true,
"interface_type": "array"
}
},
"reviewed": true,
"cli_path": "chat category batch-info",
"review_reason": "The reviewed handler requires a comma-separated category-ID list, parses it to integers, and invokes get_conv_categories_info without a mutation or confirmation gate."
},
"chat.list_conv_categories_by_conv": {
"parameters": {
"group": {
"property": "openConversationId",
"required": true
},
"conversation-id": {
"property": "openConversationId",
"required": false
},
"id": {
"property": "openConversationId",
"required": false
}
},
"reviewed": true,
"cli_path": "chat category list-by-conv",
"review_reason": "The reviewed handler accepts one of three conversation locator aliases and invokes list_conv_categories_by_conv without a mutation or confirmation gate."
},
"chat.update_group_nick": {
"parameters": {
"group": {
"property": "openConversationId",
"required": true
},
"nick": {
"property": "nick",
"required": false
}
},
"reviewed": true,
"cli_path": "chat group update-nick",
"review_reason": "The reviewed handler requires the target conversation while intentionally allowing omitted --nick to send an empty nickname and clear the current user's group nickname."
},
"chat.upgrade_group_to_external": {
"parameters": {
"group": {
"property": "openConversationId",
"required": true
},
"extension": {
"property": "extension",
"required": false,
"interface_type": "object"
}
},
"reviewed": true,
"cli_path": "chat group upgrade-to-external",
"review_reason": "The reviewed handler validates the target group and optional string-valued extension object, permits caller-authoritative dry-run, and requires explicit --yes before the irreversible upgrade."
@@ -332,58 +129,9 @@
"review_reason": "Reviewed against authenticated contact-search resolution, create-only, two-action dry-run, create/update success, missing bizId, and partial-failure behavior that preserves the created bizId; the explicit openDingTalkId flag is bound to the lower interface's exact receiverOpenDingTalkId property."
},
"chat.update_text_emotion": {
"parameters": {
"background-id": {
"property": "backgroundId",
"description": "新文字表情的背景 ID",
"required": true
},
"chat": {
"property": "openConversationId",
"required": false
},
"conversation-id": {
"property": "openConversationId",
"description": "会话 openConversationId;与 --group、--id、--chat 四选一",
"required": false
},
"emotion-id": {
"property": "emotionId",
"description": "新的文字表情 ID,可通过 create-text-emotion 获取",
"required": true
},
"emotion-name": {
"property": "emotionName",
"description": "新的文字表情名称",
"required": true
},
"group": {
"property": "openConversationId",
"required": false
},
"id": {
"property": "openConversationId",
"required": false
},
"msg-id": {
"property": "openMsgId",
"description": "需要原地更新文字表情的消息 openMsgId",
"required": true
},
"old-emotion-id": {
"property": "oldEmotionId",
"description": "消息上当前文字表情的 emotionId",
"required": true
},
"text": {
"property": "text",
"description": "新的文字表情内容",
"required": true
}
},
"reviewed": true,
"cli_path": "chat message update-text-emotion",
"review_reason": "Ported from dws-wukong commits d6831318 and bcf56ccf, merged at 30f13f02. The four conversation locators alias one property; the other six business parameters are required by Cobra before the single im/update_text_emotion call."
}
}
}
}
@@ -9,82 +9,21 @@
},
"tools": {
"contact.add_employee": {
"parameters": {
"org-user-name": {
"property": "orgUserName",
"required": true
},
"org-user-mobile": {
"property": "orgUserMobile",
"required": true
},
"depts": {
"property": "depts",
"required": false,
"interface_type": "array"
}
},
"reviewed": true,
"cli_path": "contact user invite",
"review_reason": "The reviewed Wukong-compatible handler requires employee name and mobile, decodes optional --depts JSON to an array, and calls add_employee without a runtime confirmation gate."
},
"contact.exclusive_account_create": {
"parameters": {
"org-user-name": {
"property": "orgUserName",
"required": true
},
"login-id": {
"property": "loginId",
"required": true
},
"org-user-mobile": {
"property": "orgUserMobile",
"required": false
},
"email": {
"property": "email",
"required": false
},
"dept-ids": {
"property": "deptIds",
"required": false,
"interface_type": "array"
},
"send-pwd-via-sms": {
"property": "sendPwdViaSms",
"required": false,
"interface_type": "boolean"
}
},
"reviewed": true,
"cli_path": "contact account create",
"review_reason": "The reviewed Wukong-compatible handler requires enterprise display name and login ID, maps optional contact fields and department IDs, and calls exclusive_account_create without a runtime confirmation gate."
},
"contact.get_authorized_emp_rosterInfo": {
"parameters": {
"fields": {
"required": false
},
"staff-id": {
"required": false
}
},
"reviewed": true,
"cli_path": "contact user profile get",
"review_reason": "The reviewed helper deliberately permits either or both optional filters and passes an empty request to query the caller's authorized roster scope."
},
"contact.org_create": {
"parameters": {
"org-name": {
"property": "orgName",
"required": true
},
"creator-username": {
"property": "creatorUsername",
"required": true
}
},
"reviewed": true,
"cli_path": "contact org create",
"review_reason": "The reviewed Wukong-compatible handler requires an organization name and creator display name and calls org_create without a runtime confirmation gate."
@@ -267,4 +206,4 @@
"review_reason": "The reviewed handler requires at least one of nickname or avatar file ID, confirms, then invokes self_user_profile_update for the current user."
}
}
}
}
+1 -37
View File
@@ -9,52 +9,16 @@
},
"tools": {
"doc.create_comment": {
"parameters": {
"mentioned-open-conversation-id": {
"required": false,
"interface_type": "array"
}
},
"reviewed": true,
"cli_path": "doc comment create",
"review_reason": "The reviewed handler normalizes repeated or comma-separated group conversation IDs into a stable de-duplicated array while preserving existing user mentions."
},
"doc.get_document_content": {
"parameters": {
"content-format": {
"property": "format",
"required": false
},
"scope": {
"required": false
},
"tags": {
"required": false,
"required_when": "--scope=tags"
},
"max-depth": {
"required": false,
"interface_type": "integer"
},
"start-block-id": {
"required": false,
"required_when": "--scope=range or --scope=section"
},
"end-block-id": {
"required": false
}
},
"reviewed": true,
"cli_path": "doc read",
"review_reason": "The reviewed handler preserves full Markdown/JSONML reads and adds validated outline, range, section, and tag-scoped JSONML fragment reads with optional file output."
},
"doc.reply_comment": {
"parameters": {
"mentioned-open-conversation-id": {
"required": false,
"interface_type": "array"
}
},
"reviewed": true,
"cli_path": "doc comment reply",
"review_reason": "The reviewed handler normalizes group conversation IDs into a stable de-duplicated array and rejects group mentions for emoji replies, which the remote operation does not support."
@@ -75,4 +39,4 @@
"review_reason": "Preserve the reviewed user-mention conversion and normalize repeated or comma-separated group conversation IDs into mentionedOpenConversationIds."
}
}
}
}
@@ -9,11 +9,6 @@
},
"tools": {
"drive.rename_document": {
"parameters": {
"name": {
"description": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"
}
},
"reviewed": true,
"cli_path": "drive rename",
"review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist."
@@ -30,4 +25,4 @@
"review_reason": "The reviewed helper preserves the stable command-level Schema contract for ordinary uploads; --node switches to overwrite mode and the runtime still requires explicit confirmation unless --yes is supplied."
}
}
}
}
+1 -137
View File
@@ -9,190 +9,54 @@
},
"tools": {
"hrbrain.list_talent_pools": {
"parameters": {
"keyword": {
"property": "keyword",
"required": false
},
"pool-type": {
"property": "poolType",
"required": false
},
"creator": {
"property": "creator",
"required": false
},
"labels": {
"property": "labels",
"required": false
},
"page": {
"property": "currentPage",
"required": false
},
"page-size": {
"property": "pageSize",
"required": false
}
},
"reviewed": true,
"cli_path": "hrbrain talent-pool list",
"review_reason": "Reviewed unpinned remote adapter: flags map 1:1 to the list_talent_pools MCP call arguments observed in internal/helpers/hrbrain.go."
},
"hrbrain.get_talent_pool_detail": {
"parameters": {
"pool-code": {
"property": "poolCode",
"required": true
}
},
"reviewed": true,
"cli_path": "hrbrain talent-pool detail",
"review_reason": "Reviewed unpinned remote adapter: --pool-code maps 1:1 to the poolCode MCP call argument observed in internal/helpers/hrbrain.go."
},
"hrbrain.list_pool_employees": {
"parameters": {
"pool-code": {
"property": "poolCode",
"required": true
},
"page": {
"property": "currentPage",
"required": false
},
"page-size": {
"property": "pageSize",
"required": false
}
},
"reviewed": true,
"cli_path": "hrbrain talent-pool employees",
"review_reason": "Reviewed unpinned remote adapter: flags map 1:1 to the list_pool_employees MCP call arguments observed in internal/helpers/hrbrain.go."
},
"hrbrain.get_profile_metadata": {
"parameters": {
"work-no": {
"property": "workNo",
"required": true
}
},
"reviewed": true,
"cli_path": "hrbrain profile metadata",
"review_reason": "Reviewed unpinned remote adapter: --work-no maps 1:1 to the workNo MCP call argument observed in internal/helpers/hrbrain.go."
},
"hrbrain.query_profile_data": {
"parameters": {
"work-no": {
"property": "workNo",
"required": true
},
"data-queries": {
"property": "dataQueries",
"required": true
}
},
"reviewed": true,
"cli_path": "hrbrain profile query",
"review_reason": "Reviewed unpinned remote adapter: flags map 1:1 to the query_profile_data MCP call arguments observed in internal/helpers/hrbrain.go."
},
"hrbrain.get_profile_label": {
"parameters": {
"staff-ids": {
"property": "staffIds",
"required": true
},
"all-label": {
"property": "allLabel",
"required": false
}
},
"reviewed": true,
"cli_path": "hrbrain profile labels",
"review_reason": "Reviewed unpinned remote adapter: flags map 1:1 to the get_profile_label MCP call arguments observed in internal/helpers/hrbrain.go."
},
"hrbrain.get_employee_career": {
"parameters": {
"work-no": {
"property": "workNo",
"required": true
}
},
"reviewed": true,
"cli_path": "hrbrain profile career",
"review_reason": "Reviewed unpinned remote adapter: --work-no maps 1:1 to the workNo MCP call argument observed in internal/helpers/hrbrain.go."
},
"hrbrain.get_employee_performance": {
"parameters": {
"work-no": {
"property": "workNo",
"required": true
}
},
"reviewed": true,
"cli_path": "hrbrain profile performance",
"review_reason": "Reviewed unpinned remote adapter: --work-no maps 1:1 to the workNo MCP call argument observed in internal/helpers/hrbrain.go."
},
"hrbrain.search_employees": {
"parameters": {
"keyword": {
"property": "keyword",
"required": false
},
"dept-name": {
"property": "deptName",
"required": false
},
"position-name": {
"property": "positionName",
"required": false
},
"job-level": {
"property": "jobLevel",
"required": false
},
"pool-code": {
"property": "poolCode",
"required": false
},
"page": {
"property": "currentPage",
"required": false
},
"page-size": {
"property": "pageSize",
"required": false
}
},
"reviewed": true,
"cli_path": "hrbrain search employees",
"review_reason": "Reviewed unpinned remote adapter: flags map 1:1 to the search_employees MCP call arguments observed in internal/helpers/hrbrain.go."
},
"hrbrain.search_employees_structured": {
"parameters": {
"origin-json": {
"property": "originJson",
"required": true
},
"fields": {
"property": "fields",
"required": true
},
"order-by": {
"property": "orderByClauses",
"required": false
},
"page": {
"property": "currentPage",
"required": false
},
"page-size": {
"property": "pageSize",
"required": false
}
},
"reviewed": true,
"cli_path": "hrbrain search employees-structured",
"review_reason": "Reviewed unpinned remote adapter: flags map 1:1 to the search_employees_structured MCP call arguments observed in internal/helpers/hrbrain.go."
}
}
}
}
+1 -20
View File
@@ -19,33 +19,14 @@
"review_reason": "Preserve the reviewed composite wrapper contract: the CLI maps --limit to the search_emails size property after constructing a folder KQL query."
},
"mail.search_mail_users": {
"parameters": {
"employee-no": {
"required": false
},
"keyword": {
"required": false
}
},
"reviewed": true,
"cli_path": "mail user search",
"review_reason": "The reviewed CLI accepts either a fuzzy keyword or an exact employee number, represented by a typed require-one-of constraint rather than two unconditional requirements."
},
"mail.update_user_message_template": {
"parameters": {
"content": {
"required": false
},
"name": {
"required": false
},
"subject": {
"required": false
}
},
"reviewed": true,
"cli_path": "mail template update",
"review_reason": "The reviewed patch command accepts any non-empty subset of mutable template fields; a typed require-one-of constraint owns that requirement."
}
}
}
}
@@ -9,14 +9,9 @@
},
"tools": {
"report.create_report": {
"parameters": {
"to-chat": {
"required": false
}
},
"reviewed": true,
"cli_path": "report entry submit",
"review_reason": "The reviewed optional --to-chat switch defaults to false and only controls post-submit delivery; it is not required to create a report."
}
}
}
}
+1 -49
View File
@@ -9,82 +9,34 @@
},
"tools": {
"todo.get_user_todos_in_current_org": {
"parameters": {
"role-types": {
"property": "roleTypes",
"required": false,
"description": "角色类型列表;省略时运行时默认使用 executor"
},
"query-all": {
"required": false,
"interface_type": "boolean",
"description": "为 true 时跨组织查询全部待办;默认仅查询当前组织待办"
}
},
"reviewed": true,
"cli_path": "todo task list",
"review_reason": "The reviewed handler defaults omitted role-types to executor, preserves current-organization behavior by default, and switches both single-page and auto-page calls to get_user_todos only when --query-all is explicitly true."
},
"todo.reset_todo_reminder": {
"parameters": {
"reminder-rules": {
"description": "提醒规则 JSON 数组;不传表示清除,显式传值必须为对象数组,且每条按 baseTime 提供整数 dueDateOffset 或 ISO8601 reminderTimeStamp"
}
},
"reviewed": true,
"cli_path": "todo task reset-reminder",
"review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms."
},
"todo.create_todo_tag": {
"parameters": {
"name": {
"property": "name",
"required": true
}
},
"reviewed": true,
"cli_path": "todo tag create",
"review_reason": "The reviewed handler requires a non-blank tag name, builds a one-element userTags request, and invokes create_todo_tag without a runtime confirmation gate."
},
"todo.delete_todo_tag": {
"parameters": {
"tag-codes": {
"property": "tagCodes",
"required": true,
"interface_type": "array"
}
},
"reviewed": true,
"cli_path": "todo tag delete",
"review_reason": "The reviewed handler requires at least one non-empty tag code, permits caller-authoritative dry-run, and requires explicit --yes before invoking delete_todo_tag."
},
"todo.tag_todo": {
"parameters": {
"task-id": {
"property": "taskId",
"required": true
},
"tag-codes": {
"property": "tagCodes",
"required": true,
"interface_type": "array"
}
},
"reviewed": true,
"cli_path": "todo tag add",
"review_reason": "The reviewed handler requires a task ID and at least one non-empty tag code before invoking tag_todo."
},
"todo.update_todo_tag": {
"parameters": {
"user-tags": {
"property": "userTags",
"required": true,
"interface_type": "array"
}
},
"reviewed": true,
"cli_path": "todo tag update",
"review_reason": "The reviewed handler requires a non-null JSON array of tag update records and invokes update_todo_tag without a runtime confirmation gate."
}
}
}
}
@@ -483,3 +483,29 @@ func TestCrossPlatformCoverageEmbeddedManualSchemaHintWrapperErrors(t *testing.T
t.Fatalf("ValidateEmbeddedManualAgentExampleDelivery() error = %v", err)
}
}
// TestCrossPlatformCoverageBuildManualAgentExampleExecutionPlanDefaultsMissingHintToEmpty pins the
// defensive default: a canonical collected from the declared set whose bound
// entry resolves without a ContractFinal payload and without a hint-file row
// gets an empty hint instead of failing or reusing stale prose.
func TestCrossPlatformCoverageBuildManualAgentExampleExecutionPlanDefaultsMissingHintToEmpty(t *testing.T) {
declaredCmd := &cobra.Command{Use: "get", Run: func(*cobra.Command, []string) {}}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(declaredCmd) })
RegisterRuntimeContractFinal(declaredCmd, ContractFinalPayload{Title: "T"})
bareCmd := &cobra.Command{Use: "get", Run: func(*cobra.Command, []string) {}}
commandSpec := CommandSpec{CanonicalPath: "sample.get", PrimaryCLIPath: "sample get"}
bound := BoundCommandRegistry{
Commands: []BoundCommandSpec{{CommandSpec: commandSpec, PrimaryCommand: declaredCmd}},
ByCanonical: map[string]BoundCommandSpec{
"sample.get": {CommandSpec: commandSpec, PrimaryCommand: bareCmd},
},
}
plan, err := buildManualAgentExampleExecutionPlan(bound, nil, ManualAgentHintSet{})
if err != nil {
t.Fatalf("buildManualAgentExampleExecutionPlan() error = %v", err)
}
if plan.Total != 0 || len(plan.Examples) != 0 {
t.Fatalf("empty defensive hint must plan no examples, got %#v", plan)
}
}
+6
View File
@@ -200,6 +200,12 @@ func runtimeToolSpecFromContractFinal(entry runtimeSchemaEntry, final ContractFi
constraints := runtimeCommandConstraints(entry.Command)
hint := runtimeSchemaHintForEntry(entry)
embeddedMeta, _ := embeddedMCPMetadataForEntryFrom(entry, metadata.Agent, metadata.MCP)
// Apply parameter declarations from the ContractFinalPayload before the
// resolver reads them. The decls were put there by AttachSchema at
// DeclareLeafMetadata time; now that all flags exist on the fully-built
// command tree, they can be emitted as dws.schema.* annotations (rank
// native_annotation 620, outranking tool_schema_hint 500).
ApplyParamDecls(entry.Command, final.Parameters)
parameters, err := resolveRuntimeParameters(entry.Command, canonicalPath, hint.Parameters, embeddedMeta.Parameters, constraints)
if err != nil {
return ToolSpec{}, fmt.Errorf("resolve Contract Schema parameters for %s: %w", canonicalPath, err)
@@ -329,3 +329,72 @@ func TestCrossPlatformCoverageFinalSchemaProvenanceRejectsInvalidTool(t *testing
t.Fatalf("provenance error = %v", err)
}
}
// TestCrossPlatformCoverageLegacyMetadataContractOverlaysAndDryRunProvenance
// pins the legacy (non-declared) assembly overlays: embedded Contract Risk and
// runtime-gate annotations rewrite safety with reviewed provenance, and a
// reviewed dry-run capability records its provenance winner.
func TestCrossPlatformCoverageLegacyMetadataContractOverlaysAndDryRunProvenance(t *testing.T) {
entry := runtimeSchemaEntry{
ProductID: "sample",
ToolName: "run",
CLIName: "run",
CLIPath: "sample run",
PrimaryCLIPath: "sample run",
Source: "test",
IdentityField: commandRegistryIdentityProvenance(BoundCommandSpec{CommandSpec: CommandSpec{
CanonicalPath: "sample.run",
PrimaryCLIPath: "sample run",
Source: "test",
}}),
}
riskCmd := &cobra.Command{Use: "run", Short: "short", Long: "long"}
AnnotateRuntimeRisk(riskCmd, "write")
entry.Command = riskCmd
spec, err := runtimeToolSpecFromLegacyMetadata(entry, runtimeSchemaMetadataSources{})
if err != nil {
t.Fatalf("legacy risk overlay error = %v", err)
}
if spec.Safety.Effect != "write" || spec.Safety.Risk != "medium" || spec.Safety.Confirmation != "user_required" {
t.Fatalf("legacy risk safety = %#v", spec.Safety)
}
for _, field := range []string{"risk", "confirmation", "effect"} {
if got := spec.FieldProvenance[field]; got.Source != "corecmd.contract" || got.SourceRef != "dws.schema.risk" {
t.Fatalf("risk provenance[%s] = %#v", field, got)
}
}
gateCmd := &cobra.Command{Use: "run", Short: "short", Long: "long"}
AnnotateRuntimeGate(gateCmd, "devAppRequireWriteGuard")
entry.Command = gateCmd
spec, err = runtimeToolSpecFromLegacyMetadata(entry, runtimeSchemaMetadataSources{})
if err != nil {
t.Fatalf("legacy gate overlay error = %v", err)
}
if spec.Safety.Confirmation != "user_required" || spec.Safety.Effect != "write" {
t.Fatalf("legacy gate safety = %#v", spec.Safety)
}
for _, field := range []string{"runtime_gate", "confirmation"} {
if got := spec.FieldProvenance[field]; got.Source != "corecmd.contract" || got.SourceRef != "dws.schema.runtime_gate" {
t.Fatalf("gate provenance[%s] = %#v", field, got)
}
}
oldDryRun := resolveReviewedDryRun
t.Cleanup(func() { resolveReviewedDryRun = oldDryRun })
resolveReviewedDryRun = func(string) (*DryRunSpec, error) {
return &DryRunSpec{PreviewKind: DryRunPreviewInvocation}, nil
}
entry.Command = &cobra.Command{Use: "run", Short: "short", Long: "long"}
spec, err = runtimeToolSpecFromLegacyMetadata(entry, runtimeSchemaMetadataSources{})
if err != nil {
t.Fatalf("legacy dry-run error = %v", err)
}
if spec.DryRun == nil || spec.DryRun.PreviewKind != DryRunPreviewInvocation {
t.Fatalf("legacy dry_run = %#v", spec.DryRun)
}
if got := spec.FieldProvenance["dry_run"]; got.Source != "reviewed_dry_run_registry" {
t.Fatalf("dry_run provenance = %#v", got)
}
}
+39 -2
View File
@@ -388,10 +388,10 @@ func resetReviewedDryRunCapabilitiesForTest() {
reviewedDryRunCapabilitiesLazy.err = nil
}
// TestDeclaredDryRunCapabilityAutoReviewed pins the single-source rule: a
// TestCrossPlatformCoverageDeclaredDryRunCapabilityAutoReviewed pins the single-source rule: a
// Contract-declared dry_run needs no manual allowlist entry to pass the
// delivery gate, and a conflicting manual entry is a hard error.
func TestDeclaredDryRunCapabilityAutoReviewed(t *testing.T) {
func TestCrossPlatformCoverageDeclaredDryRunCapabilityAutoReviewed(t *testing.T) {
clearDeclaredDryRunCapabilitiesForTest()
t.Cleanup(clearDeclaredDryRunCapabilitiesForTest)
originalGroups := reviewedDryRunCapabilityGroups
@@ -546,3 +546,40 @@ func cloneCoverageSchemaMap(source map[string]any) map[string]any {
}
return out
}
// TestCrossPlatformCoverageDeclaredDryRunCapabilityIndexGuards pins the defensive edges of the
// declared dry-run index: blank canonicals are ignored, a successful lookup
// returns a copy, and corrupted index entries fail closed.
func TestCrossPlatformCoverageDeclaredDryRunCapabilityIndexGuards(t *testing.T) {
clearDeclaredDryRunCapabilitiesForTest()
t.Cleanup(clearDeclaredDryRunCapabilitiesForTest)
recordDeclaredDryRunCapability(" ", DryRunSpec{PreviewKind: DryRunPreviewInvocation})
capabilities, err := loadReviewedDryRunCapabilities()
if err != nil {
t.Fatal(err)
}
for canonical := range capabilities {
if strings.TrimSpace(canonical) == "" {
t.Fatal("blank canonical must not be recorded")
}
}
recordDeclaredDryRunCapability("sample.lookup", DryRunSpec{PreviewKind: DryRunPreviewInvocation})
spec, err := reviewedDryRunCapability(" sample.lookup ")
if err != nil || spec == nil || spec.PreviewKind != DryRunPreviewInvocation {
t.Fatalf("declared capability lookup = %#v, %v", spec, err)
}
clearDeclaredDryRunCapabilitiesForTest()
declaredDryRunCapabilities.Store(42, DryRunSpec{PreviewKind: DryRunPreviewInvocation})
if _, err := loadReviewedDryRunCapabilities(); err == nil || !strings.Contains(err.Error(), "non-string key") {
t.Fatalf("non-string key error = %v", err)
}
declaredDryRunCapabilities.Delete(42)
declaredDryRunCapabilities.Store("sample.bad", "not-a-spec")
if _, err := loadReviewedDryRunCapabilities(); err == nil || !strings.Contains(err.Error(), "non-DryRunSpec value") {
t.Fatalf("non-DryRunSpec value error = %v", err)
}
}
+49 -4
View File
@@ -733,14 +733,39 @@ func sliceValue(cmd *cobra.Command, flag FlagSpec) []string {
return nil
}
// bindKey is the toolArgs key for a flag: the declared Bind, or the flag name
// converted kebab-to-camel.
//
// The alternative default — sending the kebab name verbatim — is accepted by no
// backend, so a forgotten Bind used to produce a silently wrong payload key;
// deriving it makes that failure mode correct instead.
//
// It is deliberately *not* a licence to drop the explicit declarations: Bind is
// also the declared source of the Schema property mapping, and relying on this
// derivation downgrades that field's provenance to flag_name_inference, which
// the "declared or annotated, never inference-only" rule forbids (measured:
// dropping the 93 mechanical declarations produced 552 lines of catalog drift).
func bindKey(flag FlagSpec) string {
if flag.Bind != "" {
return flag.Bind
}
parts := strings.Split(flag.Name, "-")
if len(parts) <= 1 {
return flag.Name
}
for i := 1; i < len(parts); i++ {
if parts[i] != "" {
parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
}
}
return strings.Join(parts, "")
}
// BuildArgs assembles toolArgs from the flag set by binding relationship.
func BuildArgs(cmd *cobra.Command, flags []FlagSpec) (map[string]any, error) {
toolArgs := map[string]any{}
for _, flag := range flags {
bind := flag.Bind
if bind == "" {
bind = flag.Name
}
bind := bindKey(flag)
if flag.Kind == KindInt {
v, err := integerValue(cmd, flag)
if err != nil {
@@ -1117,6 +1142,12 @@ func embedContractIntoSchema(cmd *cobra.Command, spec Spec) {
if len(flag.Enum) > 0 {
cli.AnnotateRuntimeFlagEnum(cmd, name, flag.Enum...)
}
// Same class as Enum: a declared rule the reviewed registry cannot
// derive. Without it a credential that is mandatory under one
// identity is published as plainly optional.
if flag.RequiredWhen != "" {
cli.AnnotateRuntimeFlagRequiredWhen(cmd, name, flag.RequiredWhen)
}
}
embedSchemaDecl(cmd, spec)
return
@@ -1237,6 +1268,20 @@ func AttachSchema(cmd *cobra.Command, safety cli.SafetySpec, schema SchemaDecl,
Aliases: id.Aliases, Source: strings.TrimSpace(id.Source),
}
}
if len(schema.Parameters) > 0 {
payload.Parameters = make([]cli.ParamDecl, 0, len(schema.Parameters))
for _, p := range schema.Parameters {
payload.Parameters = append(payload.Parameters, cli.ParamDecl{
Name: p.Name,
Property: p.Property,
Required: p.Required,
InterfaceType: p.InterfaceType,
Description: p.Description,
RequiredWhen: p.RequiredWhen,
Enum: p.Enum,
})
}
}
cli.RegisterRuntimeContractFinal(cmd, payload)
}
+361 -1
View File
@@ -16,6 +16,7 @@ package corecmd
import (
"errors"
"reflect"
"runtime"
"strings"
"testing"
@@ -1201,7 +1202,7 @@ func TestRegisterFlagTypedDefaults(t *testing.T) {
}
}
func TestBuildArgsIntArgDefaultFloor(t *testing.T) {
func TestCrossPlatformCoverageBuildArgsIntArgDefaultFloor(t *testing.T) {
flags := []FlagSpec{{
Name: "page-size", Usage: "P", Kind: KindInt,
Default: "20", ArgDefault: "20", Bind: "pageSize",
@@ -1249,3 +1250,362 @@ func TestNewCommandMergesConstParams(t *testing.T) {
t.Fatalf("precheckOnly = %#v, want false", got["precheckOnly"])
}
}
func TestCrossPlatformCoverageNewCommandConfirmFirstAnnotationAndOrder(t *testing.T) {
ran := false
build := func(postMount func(*cobra.Command)) *cobra.Command {
cmd := New(Spec{
Use: "guard-first",
Safety: testWriteSafety(),
ConfirmFirst: true,
Flags: []FlagSpec{{Name: "x", Usage: "X", Required: true}},
PostMount: postMount,
Invoke: func(*Ctx, map[string]any) error {
ran = true
return nil
},
})
cmd.PersistentFlags().Bool("yes", false, "")
cmd.PersistentFlags().Bool("dry-run", false, "")
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetErr(&strings.Builder{})
return cmd
}
// The declared marker is stamped even when a PostMount hook dropped the
// annotations map (defensive re-init path).
cleared := build(func(c *cobra.Command) { c.Annotations = nil })
if cleared.Annotations[ConfirmFirstAnnotation] != "true" {
t.Fatalf("annotations = %#v, want declared ConfirmFirst marker", cleared.Annotations)
}
if !HasDeclaredConfirmFirst(cleared) {
t.Fatal("HasDeclaredConfirmFirst must report a declared guard-first command")
}
if HasDeclaredConfirmFirst(nil) || HasDeclaredConfirmFirst(newTestCommand()) {
t.Fatal("HasDeclaredConfirmFirst must be false for nil/undeclared commands")
}
// Guard-first confirms BEFORE required validation: declining cancels even
// though the required --x is missing.
ran = false
declined := build(nil)
declined.SetIn(strings.NewReader("no\n"))
declined.SetArgs(nil)
if err := declined.Execute(); err == nil || !strings.Contains(err.Error(), "用户取消了操作") {
t.Fatalf("declined guard-first err = %v", err)
}
if ran {
t.Fatal("declined guard-first must not dispatch")
}
// After confirming, the declared Required check still gates dispatch.
ran = false
confirmed := build(nil)
confirmed.SetIn(strings.NewReader("yes\n"))
confirmed.SetArgs(nil)
if err := confirmed.Execute(); err == nil || !strings.Contains(err.Error(), "x") {
t.Fatalf("confirmed guard-first required err = %v", err)
}
if ran {
t.Fatal("guard-first must not dispatch with the required flag missing")
}
// A satisfied declaration dispatches exactly once, without a second prompt.
ran = false
satisfied := build(nil)
satisfied.SetIn(strings.NewReader("yes\n"))
satisfied.SetArgs([]string{"--x", "v"})
if err := satisfied.Execute(); err != nil {
t.Fatalf("satisfied guard-first err = %v", err)
}
if !ran {
t.Fatal("confirmed guard-first did not dispatch")
}
}
func TestCrossPlatformCoverageNewCommandRunEHonorsConfirmation(t *testing.T) {
// The escape hatch keeps the declared confirmation gate: an unanswerable
// prompt fails closed before the RunE body runs.
ran := false
build := func() *cobra.Command {
cmd := New(Spec{
Use: "guarded-rune",
Safety: testWriteSafety(),
Flags: []FlagSpec{{Name: "x", Usage: "X"}},
RunE: func(*cobra.Command, []string) error {
ran = true
return nil
},
})
cmd.PersistentFlags().Bool("yes", false, "")
cmd.PersistentFlags().Bool("dry-run", false, "")
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetErr(&strings.Builder{})
return cmd
}
blocked := build()
blocked.SetIn(strings.NewReader(""))
blocked.SetArgs(nil)
err := blocked.Execute()
var appErr *apperrors.Error
if !errors.As(err, &appErr) || appErr.Reason != "confirmation_required" {
t.Fatalf("RunE confirmation err = %#v, want confirmation_required", err)
}
if ran {
t.Fatal("RunE body must not run when confirmation fails")
}
confirmed := build()
confirmed.SetArgs([]string{"--yes"})
if err := confirmed.Execute(); err != nil {
t.Fatalf("confirmed RunE err = %v", err)
}
if !ran {
t.Fatal("confirmed RunE did not run")
}
}
func TestCrossPlatformCoverageNewCommandPreflightEnumGate(t *testing.T) {
dispatched := false
cmd := New(Spec{
Use: "enum-gate",
Flags: []FlagSpec{{Name: "mode", Usage: "M", Enum: []string{"a", "b"}}},
Invoke: func(*Ctx, map[string]any) error {
dispatched = true
return nil
},
})
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetArgs([]string{"--mode", "zzz"})
if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), `参数 --mode 取值 "zzz" 不合法`) {
t.Fatalf("preflight enum err = %v", err)
}
if dispatched {
t.Fatal("enum failure must stop before dispatch")
}
}
func TestCrossPlatformCoverageValidateRequiredShortcutMessages(t *testing.T) {
// Without an authored RequiredError the unified default wording applies.
plain := []FlagSpec{{Name: "target", Usage: "T", Required: true, ValidationMode: ValidationShortcut}}
cmd := newTestCommand()
RegisterFlags(cmd, plain)
if err := ValidateRequired(cmd, plain); err == nil || err.Error() != "缺少必填参数 --target" {
t.Fatalf("shortcut default message = %v", err)
}
// A provided-but-blank list still fails with the emptiness wording.
slice := []FlagSpec{{
Name: "ids", Usage: "I", Kind: KindStringSlice, Required: true,
ValidationMode: ValidationShortcut, RequiredError: "缺少必填参数 --ids:列表",
}}
cmd = newTestCommand()
RegisterFlags(cmd, slice)
_ = cmd.Flags().Set("ids", " , ")
if err := ValidateRequired(cmd, slice); err == nil || !strings.Contains(err.Error(), "必填参数 --ids 不能为空") {
t.Fatalf("shortcut blank slice message = %v", err)
}
// A provided-but-blank string fails with the same emptiness wording.
blank := []FlagSpec{{Name: "name", Usage: "N", Required: true, ValidationMode: ValidationShortcut}}
cmd = newTestCommand()
RegisterFlags(cmd, blank)
_ = cmd.Flags().Set("name", " ")
if err := ValidateRequired(cmd, blank); err == nil || !strings.Contains(err.Error(), "必填参数 --name 不能为空") {
t.Fatalf("shortcut blank string message = %v", err)
}
// Shortcut-mode enum violations surface through ValidateRequired too.
enum := []FlagSpec{{Name: "mode", Usage: "M", ValidationMode: ValidationShortcut, Enum: []string{"a", "b"}}}
cmd = newTestCommand()
RegisterFlags(cmd, enum)
_ = cmd.Flags().Set("mode", "z")
if err := ValidateRequired(cmd, enum); err == nil || !strings.Contains(err.Error(), `参数 --mode 取值 "z" 不合法`) {
t.Fatalf("shortcut enum message = %v", err)
}
}
func TestCrossPlatformCoverageBuildArgsInvalidArgDefault(t *testing.T) {
flags := []FlagSpec{{Name: "page-size", Usage: "P", Kind: KindInt, ArgDefault: "not-a-number"}}
cmd := newTestCommand()
RegisterFlags(cmd, flags)
if _, err := BuildArgs(cmd, flags); err == nil || !strings.Contains(err.Error(), `invalid ArgDefault "not-a-number"`) {
t.Fatalf("invalid ArgDefault err = %v", err)
}
}
func TestCrossPlatformCoverageBindKeyKebabToCamel(t *testing.T) {
// Declared Bind always wins.
if got := bindKey(FlagSpec{Name: "disable-ssl-verify", Bind: "disableSSLVerify"}); got != "disableSSLVerify" {
t.Fatalf("explicit bind = %q", got)
}
// Single-word names pass through unchanged.
if got := bindKey(FlagSpec{Name: "plain"}); got != "plain" {
t.Fatalf("single-word bind = %q", got)
}
// Kebab names camelize mechanically; empty segments are skipped.
if got := bindKey(FlagSpec{Name: "page-size"}); got != "pageSize" {
t.Fatalf("kebab bind = %q", got)
}
if got := bindKey(FlagSpec{Name: "a--b-c"}); got != "aBC" {
t.Fatalf("empty-segment bind = %q", got)
}
// The derived key is what BuildArgs actually transmits.
flags := []FlagSpec{{Name: "page-size", Usage: "P", Kind: KindInt}}
cmd := newTestCommand()
RegisterFlags(cmd, flags)
_ = cmd.Flags().Set("page-size", "5")
args, err := BuildArgs(cmd, flags)
if err != nil {
t.Fatal(err)
}
if args["pageSize"] != 5 {
t.Fatalf("camelized args = %#v, want pageSize=5", args)
}
}
// failingReader returns a non-EOF error so the confirmation read path must
// fail closed instead of treating the answer as a decline.
type failingReader struct{}
func (failingReader) Read([]byte) (int, error) { return 0, errors.New("stdin read failed") }
func TestCrossPlatformCoverageConfirmSafetyReadErrorFailsClosed(t *testing.T) {
cmd := newTestCommand()
cmd.PersistentFlags().Bool("yes", false, "")
cmd.SetIn(failingReader{})
cmd.SetErr(&strings.Builder{})
err := ConfirmSafety(cmd, testWriteSafety())
var appErr *apperrors.Error
if !errors.As(err, &appErr) || appErr.Reason != "confirmation_required" {
t.Fatalf("non-EOF stdin error must be confirmation_required, got %#v", err)
}
}
func TestCrossPlatformCoverageConfirmSafetyTerminalPromptsBeforeReading(t *testing.T) {
master, slave, ok := openTestPTY(t)
if !ok {
t.Skipf("no native pty on %s: the interactive prompt branch is terminal-only (unix)", runtime.GOOS)
}
defer master.Close()
defer slave.Close()
// Queue the answer on the master side before ConfirmSafety reads so the
// canonical-mode read can never block the test.
if _, err := master.WriteString("yes\n"); err != nil {
t.Fatalf("write pty answer: %v", err)
}
var stderr strings.Builder
cmd := newTestCommand()
cmd.PersistentFlags().Bool("yes", false, "")
cmd.PersistentFlags().Bool("dry-run", false, "")
cmd.SetIn(slave)
cmd.SetErr(&stderr)
if err := ConfirmSafety(cmd, testWriteSafety()); err != nil {
t.Fatalf("terminal yes answer must confirm, got %v", err)
}
if !strings.Contains(stderr.String(), "确认继续") {
t.Fatalf("terminal stdin must print the interactive prompt, got %q", stderr.String())
}
}
func TestCrossPlatformCoverageBoolFlagGettersNilCommand(t *testing.T) {
if got := boolFlagGetters(nil); got != nil {
t.Fatalf("boolFlagGetters(nil) = %#v, want nil", got)
}
}
func TestCrossPlatformCoverageEmbedContractCobraProjection(t *testing.T) {
cmd := New(Spec{
Use: "shortcut-leaf",
Short: "short",
ParameterProjection: ProjectCobraParameters,
Flags: []FlagSpec{
{
Name: "mode", Usage: "M", Required: true,
Enum: []string{"a", "b"}, RequiredWhen: "when identity=user",
},
{Name: "internal", Usage: "I", Hidden: true, Required: true},
},
Safety: testWriteSafety(),
Schema: SchemaDecl{
Description: "desc",
Interface: &InterfaceDecl{Mode: "mcp", Availability: "available", ProductID: "dev", RPCName: "op"},
Selection: SelectionDecl{
AgentSummary: "s", UseWhen: []string{"u"},
AvoidWhen: []string{"a"}, Examples: []string{"dws shortcut-leaf"},
},
},
Invoke: func(*Ctx, map[string]any) error { return nil },
})
mode := cmd.Flags().Lookup("mode")
if got := mode.Annotations["dws.schema.required"]; len(got) == 0 || got[0] != "true" {
t.Fatalf("cobra projection must annotate Required, got %#v", mode.Annotations)
}
if got := mode.Annotations["x-cli-enum"]; len(got) != 2 {
t.Fatalf("cobra projection must annotate Enum, got %#v", got)
}
if got := mode.Annotations["dws.schema.required_when"]; len(got) == 0 || got[0] != "when identity=user" {
t.Fatalf("cobra projection must annotate RequiredWhen, got %#v", got)
}
// Hidden flags stay Cobra-only facts: no projection annotations.
if got := cmd.Flags().Lookup("internal").Annotations["dws.schema.required"]; len(got) != 0 {
t.Fatalf("hidden flag must not be projected, got %#v", got)
}
// The authored SchemaDecl still lands as the typed ContractFinal.
final, ok := cli.RuntimeContractFinal(cmd)
if !ok || final.Description != "desc" {
t.Fatalf("cobra projection must still embed SchemaDecl, final=%#v ok=%v", final, ok)
}
}
func TestCrossPlatformCoverageAttachSchemaNilAndEmptyGuards(t *testing.T) {
// Both guards are no-ops: nil command, and a command with no authored decl.
AttachSchema(nil, testWriteSafety(), SchemaDecl{Description: "d"}, "s", "l")
cmd := newTestCommand()
AttachSchema(cmd, testWriteSafety(), SchemaDecl{}, "s", "l")
if cli.HasRuntimeContractFinal(cmd) {
t.Fatal("empty SchemaDecl must not register a ContractFinal")
}
}
func TestCrossPlatformCoverageEffectiveSafetySpecZeroValueDefaultsToRead(t *testing.T) {
got := effectiveSafetySpec(cli.SafetySpec{})
want := cli.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}
if got != want {
t.Fatalf("effectiveSafetySpec(zero) = %+v, want %+v", got, want)
}
partial := effectiveSafetySpec(cli.SafetySpec{Effect: " write ", Risk: "high", Confirmation: "user_required", Idempotency: "unknown"})
if partial.Effect != "write" || partial.Risk != "high" {
t.Fatalf("effectiveSafetySpec(trim) = %+v", partial)
}
}
func TestCrossPlatformCoverageEmbedContractSkipsBlankAndHiddenFlags(t *testing.T) {
cmd := &cobra.Command{Use: "skip-blank-hidden"}
cmd.Flags().String("visible", "", "visible flag")
cmd.Flags().String("ghost", "", "hidden flag")
embedContractIntoSchema(cmd, Spec{
Use: "skip-blank-hidden",
Flags: []FlagSpec{
{Name: " ", Usage: "blank name is skipped"},
{Name: "ghost", Usage: "hidden is skipped", Hidden: true},
{Name: "visible", Usage: "annotated", Required: true},
},
})
anns := cmd.Annotations
if anns == nil {
t.Fatal("expected runtime contract annotations")
}
for key := range anns {
if strings.Contains(key, "ghost") {
t.Fatalf("hidden flag must not be annotated: %s", key)
}
}
}
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build darwin
package corecmd
import (
"os"
"strings"
"testing"
"unsafe"
"golang.org/x/sys/unix"
)
// openTestPTY allocates a real pty pair so the terminal-only interactive
// prompt branch of ConfirmSafety can run natively on macOS. The slave side is
// a genuine TTY (isatty true); the master side feeds it the scripted answer.
// It reports ok=false instead of failing when the environment cannot allocate
// a pty (e.g. a sandbox without /dev/ptmx), so callers skip gracefully.
func openTestPTY(t *testing.T) (master, slave *os.File, ok bool) {
t.Helper()
m, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
if err != nil {
t.Logf("open /dev/ptmx: %v", err)
return nil, nil, false
}
fd := int(m.Fd())
if err := unix.IoctlSetInt(fd, unix.TIOCPTYGRANT, 0); err != nil {
t.Logf("grantpt: %v", err)
_ = m.Close()
return nil, nil, false
}
if err := unix.IoctlSetInt(fd, unix.TIOCPTYUNLK, 0); err != nil {
t.Logf("unlockpt: %v", err)
_ = m.Close()
return nil, nil, false
}
// ptsname(3): TIOCPTYGNAME fills a 128-byte buffer with the slave path.
buf := make([]byte, 128)
if _, _, errno := unix.Syscall(
unix.SYS_IOCTL, uintptr(fd), uintptr(unix.TIOCPTYGNAME), uintptr(unsafe.Pointer(&buf[0])),
); errno != 0 {
t.Logf("ptsname ioctl: %v", errno)
_ = m.Close()
return nil, nil, false
}
name := strings.TrimRight(string(buf), "\x00")
s, err := os.OpenFile(name, os.O_RDWR, 0)
if err != nil {
t.Logf("open pty slave %s: %v", name, err)
_ = m.Close()
return nil, nil, false
}
return m, s, true
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !darwin
package corecmd
import (
"os"
"testing"
)
// openTestPTY reports no native pty support off macOS. ConfirmSafety's
// interactive prompt branch requires a real terminal, which cannot be
// synthesized portably on Windows; the caller skips with an explicit reason
// so the platform coverage gate records the unix-only branch honestly.
func openTestPTY(t *testing.T) (master, slave *os.File, ok bool) {
t.Helper()
return nil, nil, false
}
+35 -2
View File
@@ -25,18 +25,48 @@ import (
// Cobra leaf and Schema assembly pass-throughs the values — no reviewed /
// hints parallel authority for those fields.
//
// Shape mirrors cli.ToolSpec groups (minus Parameters/Constraints, which come
// from Flags/Constraints, and minus FieldProvenance, which is derived).
// Shape mirrors cli.ToolSpec groups (minus Constraints, which come from
// Constraints, and minus FieldProvenance, which is derived).
type SchemaDecl struct {
Title string
Description string
Positionals []PositionalDecl
Parameters []ParamDecl
DryRun *DryRunDecl
Interface *InterfaceDecl
Selection SelectionDecl
Identity IdentityDecl
}
// ParamDecl declares Schema facts about a flag that the command registers
// itself. It exists because metadata-mode commands (DeclareLeafMetadata) own
// their flag registration and cannot use FlagSpec; without this channel their
// parameter facts could only live in schema_hints/metadata overlays.
//
// Each field maps to a dws.schema.* annotation at rank native_annotation (620),
// which outranks tool_schema_hint (500). A declared value therefore wins over
// any remaining hint overlay for the same flag, making the overlay redundant
// once the declaration is in place.
type ParamDecl struct {
// Name is the cobra flag name (kebab-case), e.g. "record-ids".
Name string
// Property is the interface property name (camelCase), e.g. "recordIds".
// Empty means the flag name is used as-is.
Property string
// Required overrides the cobra-level required marker for Schema purposes.
// Use a pointer so "not declared" (nil) is distinct from "explicitly false".
Required *bool
// InterfaceType is the wire type for the interface property, e.g. "string",
// "integer", "boolean", "array". Empty means derived from the flag kind.
InterfaceType string
// Description overrides the flag's help text for Schema purposes.
Description string
// RequiredWhen is a conditional-required expression, e.g. "identity=bot".
RequiredWhen string
// Enum restricts accepted values (string flags only).
Enum []string
}
// PositionalDecl is one ordered CLI argument in the Schema positionals list.
type PositionalDecl struct {
Name string
@@ -146,6 +176,9 @@ func (s SchemaDecl) empty() bool {
if len(s.Positionals) > 0 {
return false
}
if len(s.Parameters) > 0 {
return false
}
if s.DryRun != nil && strings.TrimSpace(s.DryRun.PreviewKind) != "" {
return false
}
+36 -3
View File
@@ -20,7 +20,7 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
)
func TestNewCommandEmbedsFullSchemaDeclAsFinalSource(t *testing.T) {
func TestCrossPlatformCoverageNewCommandEmbedsFullSchemaDeclAsFinalSource(t *testing.T) {
cmd := New(Spec{
Use: "create",
Short: "short",
@@ -142,7 +142,7 @@ func TestNewCommandFallsBackToDeclaredDescriptionWithoutLong(t *testing.T) {
}
}
func TestNewCommandPanicsOnPartialSchemaDecl(t *testing.T) {
func TestCrossPlatformCoverageNewCommandPanicsOnPartialSchemaDecl(t *testing.T) {
for _, tc := range []struct {
name string
schema SchemaDecl
@@ -279,7 +279,7 @@ func TestSchemaDeclEmptySkipsFinal(t *testing.T) {
}
}
func TestNewCommandRejectsPartialSafetySpec(t *testing.T) {
func TestCrossPlatformCoverageNewCommandRejectsPartialSafetySpec(t *testing.T) {
defer func() {
recovered := recover()
if recovered == nil {
@@ -295,3 +295,36 @@ func TestNewCommandRejectsPartialSafetySpec(t *testing.T) {
Invoke: func(*Ctx, map[string]any) error { return nil },
})
}
func TestCrossPlatformCoverageSchemaDeclEmptyReportsEveryAuthoredSection(t *testing.T) {
if !(SchemaDecl{}).Empty() {
t.Fatal("zero SchemaDecl must be empty")
}
authored := map[string]SchemaDecl{
"title": {Title: "T"},
"description": {Description: "D"},
"positionals": {Positionals: []PositionalDecl{{Name: "id"}}},
"dry_run": {DryRun: &DryRunDecl{PreviewKind: "request"}},
"interface": {Interface: &InterfaceDecl{Mode: "mcp"}},
"selection": {Selection: SelectionDecl{Tips: []string{"tip"}}},
"identity": {Identity: IdentityDecl{Name: "tool"}},
}
for name, decl := range authored {
if decl.Empty() {
t.Fatalf("%s: authored section must make SchemaDecl non-empty", name)
}
}
// Pointers without any authored payload still count as empty: a bare
// &DryRunDecl{} / &InterfaceDecl{} carries no final Schema fact.
unauthored := map[string]SchemaDecl{
"dry_run pointer": {DryRun: &DryRunDecl{RemoteReads: true}},
"interface pointer": {Interface: &InterfaceDecl{}},
}
for name, decl := range unauthored {
if !decl.Empty() {
t.Fatalf("%s: payload-free pointer must stay empty", name)
}
}
}
+14 -3
View File
@@ -1,12 +1,17 @@
package helpers
import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"strings"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
)
// boolPtr returns a pointer to v, for ParamDecl.Required declarations.
func boolPtr(v bool) *bool { return &v }
// aisearchKeywordAliases 是 --keyword flag 的同义瞎猜兜底列表。
// 模型可能写 --name / --q / --query / --text,这些都被识别为 keyword。
var aisearchKeywordAliases = []string{"name", "q", "query", "text"}
@@ -272,6 +277,10 @@ func newAisearchCommand() *cobra.Command {
dws aisearch enterprise --queries "OKR" --types document,im,mail`,
RunE: runAisearchEnterprise,
}
// Register flags that carry ParamDecl before DeclareLeafMetadata so
// AttachSchema can emit their dws.schema.* annotations.
enterpriseCmd.Flags().String("queries", "", "内容关键词列表,多个用逗号分隔;汇总类场景可留空")
enterpriseCmd.Flags().String("time-range", "", "时间范围,仅当用户显式给出时间词时填写,如 今天/本周/9月/过去一周")
DeclareLeafMetadata(enterpriseCmd, LeafSpec{
Safety: cli.SafetySpec{
Effect: "read", Risk: "low",
@@ -299,15 +308,17 @@ func newAisearchCommand() *cobra.Command {
"dws aisearch enterprise --queries \"OKR\" --types mail --time-range \"最近\" --format json",
},
},
Parameters: []corecmd.ParamDecl{
{Name: "queries", Required: boolPtr(false)},
{Name: "time-range", Required: boolPtr(false)},
},
},
})
enterpriseCmd.Flags().String("queries", "", "内容关键词列表,多个用逗号分隔;汇总类场景可留空")
enterpriseCmd.Flags().String("types", "all", "搜索类型: all/document/im/calendar/todo/minute/report/image/link/notable/baike/mail,多个用逗号分隔")
enterpriseCmd.Flags().String("search-types", "", "--types 的别名")
_ = enterpriseCmd.Flags().MarkHidden("search-types")
enterpriseCmd.Flags().String("searchTypes", "", "--types 的别名")
_ = enterpriseCmd.Flags().MarkHidden("searchTypes")
enterpriseCmd.Flags().String("time-range", "", "时间范围,仅当用户显式给出时间词时填写,如 今天/本周/9月/过去一周")
enterpriseCmd.Flags().String("timeRange", "", "--time-range 的别名")
_ = enterpriseCmd.Flags().MarkHidden("timeRange")
addAisearchKeywordCompatibilityFlag(enterpriseCmd)
+88
View File
@@ -20,6 +20,8 @@ import (
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
)
func resolveMessageForward(cmd *cobra.Command, defaultForward bool) (bool, error) {
@@ -2860,6 +2862,18 @@ func newChatCommand() *cobra.Command {
AvoidWhen: []string{"发送新消息应使用 chat message send;撤回消息应使用 chat message recall"},
Examples: []string{"dws chat message edit --conversation-id <openConversationId> --msg-id <openMessageId> --text \"更新后的内容\""},
},
Parameters: []corecmd.ParamDecl{
{Name: "at-all", Property: "atAll", Required: boolPtr(false), InterfaceType: "boolean"},
{Name: "at-open-dingtalk-ids", Property: "atOpenDingTalkIds", Required: boolPtr(false), InterfaceType: "array"},
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "content", Property: "content", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(true)},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "msg-id", Property: "openMessageId", Required: boolPtr(true)},
{Name: "text", Property: "text", Required: boolPtr(false)},
{Name: "title", Property: "title", Required: boolPtr(false)},
},
},
})
@@ -3694,6 +3708,11 @@ func newChatCommand() *cobra.Command {
AvoidWhen: []string{"列出全部自定义分组应使用 chat category list;按 categoryId 查详情应使用 chat category batch-info"},
Examples: []string{"dws chat category list-by-conv --group <openConversationId>"},
},
Parameters: []corecmd.ParamDecl{
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
@@ -3733,6 +3752,9 @@ func newChatCommand() *cobra.Command {
AvoidWhen: []string{"不知道 categoryId 时先用 chat category list;按会话反查所属分组应使用 chat category list-by-conv"},
Examples: []string{"dws chat category batch-info --category-ids 123,456"},
},
Parameters: []corecmd.ParamDecl{
{Name: "category-ids", Property: "categoryIds", Required: boolPtr(true), InterfaceType: "array"},
},
},
})
@@ -3848,6 +3870,12 @@ func newChatCommand() *cobra.Command {
AvoidWhen: []string{"发送文本消息或文字表情时不要使用"},
Examples: []string{"dws chat message add-emoji --conversation-id <openConversationId> --msg-id <openMessageId> --emoji \"赞\""},
},
Parameters: []corecmd.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
chatMessageAddEmojiCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
@@ -3893,6 +3921,12 @@ func newChatCommand() *cobra.Command {
AvoidWhen: []string{"移除文字表情时使用 chat message remove-text-emotion"},
Examples: []string{"dws chat message remove-emoji --conversation-id <openConversationId> --msg-id <openMessageId> --emoji \"赞\""},
},
Parameters: []corecmd.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
chatMessageRemoveEmojiCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
@@ -3940,6 +3974,12 @@ func newChatCommand() *cobra.Command {
AvoidWhen: []string{"需要先创建文字表情资源时使用 chat message create-text-emotion"},
Examples: []string{"dws chat message add-text-emotion --conversation-id <openConversationId> --msg-id <openMessageId> --emotion-id <emotionId> --emotion-name \"赞\" --text \"nice\" --background-id im_bg_5"},
},
Parameters: []corecmd.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
chatMessageAddTextEmotionCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
@@ -3990,6 +4030,12 @@ func newChatCommand() *cobra.Command {
AvoidWhen: []string{"移除普通 emoji reaction 时使用 chat message remove-emoji"},
Examples: []string{"dws chat message remove-text-emotion --conversation-id <openConversationId> --msg-id <openMessageId> --emotion-id <emotionId> --emotion-name \"赞\" --text \"nice\" --background-id im_bg_5"},
},
Parameters: []corecmd.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
},
},
})
chatMessageRemoveTextEmotionCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
@@ -4041,6 +4087,18 @@ func newChatCommand() *cobra.Command {
},
Examples: []string{"dws chat message update-text-emotion --conversation-id <openConversationId> --msg-id <openMessageId> --old-emotion-id <oldEmotionId> --emotion-id <emotionId> --emotion-name \"处理中\" --text \"处理中 2 分钟\" --background-id im_bg_5"},
},
Parameters: []corecmd.ParamDecl{
{Name: "background-id", Property: "backgroundId", Required: boolPtr(true), Description: "新文字表情的背景 ID"},
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false), Description: "会话 openConversationId;与 --group、--id、--chat 四选一"},
{Name: "emotion-id", Property: "emotionId", Required: boolPtr(true), Description: "新的文字表情 ID,可通过 create-text-emotion 获取"},
{Name: "emotion-name", Property: "emotionName", Required: boolPtr(true), Description: "新的文字表情名称"},
{Name: "group", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "msg-id", Property: "openMsgId", Required: boolPtr(true), Description: "需要原地更新文字表情的消息 openMsgId"},
{Name: "old-emotion-id", Property: "oldEmotionId", Required: boolPtr(true), Description: "消息上当前文字表情的 emotionId"},
{Name: "text", Property: "text", Required: boolPtr(true), Description: "新的文字表情内容"},
},
},
})
chatMessageUpdateTextEmotionCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
@@ -4514,6 +4572,12 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
AvoidWhen: []string{"群成员禁言属于发言权限,应使用 group-mute 命令"},
Examples: []string{"dws chat mute --conversation-id <openConversationId>"},
},
Parameters: []corecmd.ParamDecl{
{Name: "chat", Property: "openConversationId", Required: boolPtr(false)},
{Name: "conversation-id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "id", Property: "openConversationId", Required: boolPtr(false)},
{Name: "off", Required: boolPtr(false)},
},
},
})
chatMuteCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
@@ -4849,6 +4913,9 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
AvoidWhen: []string{"只查看置顶清单时使用 chat list-top-conversations"},
Examples: []string{"dws chat set-top --conversation-id <openConversationId>"},
},
Parameters: []corecmd.ParamDecl{
{Name: "off", Required: boolPtr(false)},
},
},
})
chatSetTopCmd.Flags().String("conversation-id", "", "会话 openConversationId (必填,支持单聊/群聊)")
@@ -4931,6 +4998,9 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
AvoidWhen: []string{"只禁言指定成员时使用 chat group-mute-member"},
Examples: []string{"dws chat group-mute --group <openConversationId>"},
},
Parameters: []corecmd.ParamDecl{
{Name: "off", Required: boolPtr(false)},
},
},
})
chatGroupMuteCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
@@ -5014,6 +5084,9 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
AvoidWhen: []string{"需要全员禁言时使用 chat group-mute"},
Examples: []string{"dws chat group-mute-member --group <openConversationId> --users userId1,userId2 --mute-time 3600000"},
},
Parameters: []corecmd.ParamDecl{
{Name: "off", Required: boolPtr(false)},
},
},
})
chatGroupMuteMemberCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
@@ -5081,6 +5154,9 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
AvoidWhen: []string{"自定义业务角色应使用 chat group-role 系列命令"},
Examples: []string{"dws chat group set-admin --group <openConversationId> --users userId1,userId2"},
},
Parameters: []corecmd.ParamDecl{
{Name: "off", Required: boolPtr(false)},
},
},
})
chatGroupSetAdminCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
@@ -6131,6 +6207,10 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
AvoidWhen: []string{"按名称搜索任意可见群时使用 chat search"},
Examples: []string{"dws chat group list-my-groups --role OWNER --limit 100"},
},
Parameters: []corecmd.ParamDecl{
{Name: "limit", Required: boolPtr(false)},
{Name: "role", Required: boolPtr(false)},
},
},
})
chatGroupListMyGroupsCmd.Flags().String("role", "", "角色过滤: OWNER(仅群主) / ADMIN(仅管理员),不传返回全部")
@@ -6494,6 +6574,10 @@ status 可选值:
"dws chat group update-nick --group <openConversationId>",
},
},
Parameters: []corecmd.ParamDecl{
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
{Name: "nick", Property: "nick", Required: boolPtr(false)},
},
},
})
chatGroupUpdateNickCmd.Flags().String("group", "", "群聊 openConversationId (必填)")
@@ -6863,6 +6947,10 @@ status 可选值:
AvoidWhen: []string{"新建外部群应使用 chat group create --type EXTERNAL;未确认群主身份和不可逆影响时不要执行"},
Examples: []string{"dws chat group upgrade-to-external --group <openConversationId>"},
},
Parameters: []corecmd.ParamDecl{
{Name: "extension", Property: "extension", Required: boolPtr(false), InterfaceType: "object"},
{Name: "group", Property: "openConversationId", Required: boolPtr(true)},
},
},
})
chatGroupUpgradeToExternalCmd.Flags().String("group", "", "待升级普通群的 openConversationId (必填)")
@@ -0,0 +1,524 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
import (
"context"
"fmt"
"io"
"reflect"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
// contractCoverageSchema returns a minimal LeafSchema accepted by
// corecmd.AttachSchema's declaration completeness rules (test fixture).
func contractCoverageSchema(desc string) LeafSchema {
return LeafSchema{
Description: desc,
Interface: &LeafInterfaceDecl{
Mode: "composite", Availability: "available",
Reason: "unit test fixture",
},
Selection: LeafSelectionDecl{
AgentSummary: desc,
UseWhen: []string{"unit test"},
AvoidWhen: []string{"unit test"},
Examples: []string{"dws test"},
},
}
}
// TestCrossPlatformCoverageContractRuntimeStoreLoadNilGuards covers the nil/miss guards of the
// contractRuntime registry (store nil args, load nil cmd, load miss) and the
// ContractValidate not-registered fallback.
func TestCrossPlatformCoverageContractRuntimeStoreLoadNilGuards(t *testing.T) {
// Nil guards: none of these may panic or register anything.
storeContractRuntime(nil, &contractRuntime{})
storeContractRuntime(&cobra.Command{Use: "nil-rt"}, nil)
storeContractRuntime(nil, nil)
if rt, ok := loadContractRuntime(nil); ok || rt != nil {
t.Fatalf("loadContractRuntime(nil) = (%v, %v), want (nil, false)", rt, ok)
}
fresh := &cobra.Command{Use: "fresh"}
if rt, ok := loadContractRuntime(fresh); ok || rt != nil {
t.Fatalf("loadContractRuntime(fresh) = (%v, %v), want (nil, false)", rt, ok)
}
if ContractValidate(fresh) != nil {
t.Fatal("ContractValidate on unregistered command must return nil")
}
if ContractValidate(nil) != nil {
t.Fatal("ContractValidate(nil) must return nil")
}
// Positive path: stored runtime is returned as-is.
stored := &contractRuntime{validate: func(*cobra.Command, []string) error { return nil }}
cmd := &cobra.Command{Use: "stored"}
storeContractRuntime(cmd, stored)
rt, ok := loadContractRuntime(cmd)
if !ok || rt != stored {
t.Fatalf("loadContractRuntime(stored) = (%v, %v), want stored runtime", rt, ok)
}
if ContractValidate(cmd) == nil {
t.Fatal("ContractValidate must return the stored Validate hook")
}
}
// TestCrossPlatformCoverageDeclareLeafMetadataPanicsOnExecutionFields covers every metadata-only
// misuse guard in DeclareLeafMetadata (nil cmd + all forbidden LeafSpec
// fields + missing Schema).
func TestCrossPlatformCoverageDeclareLeafMetadataPanicsOnExecutionFields(t *testing.T) {
cases := []struct {
name string
cmd *cobra.Command
spec LeafSpec
want string
}{
{"nil cmd", nil, LeafSpec{}, "cmd is nil"},
{"flags", &cobra.Command{Use: "leaf"}, LeafSpec{Flags: []LeafFlag{{Name: "x", Usage: "x"}}}, "Flags must be empty"},
{"constraints", &cobra.Command{Use: "leaf"}, LeafSpec{Constraints: []LeafConstraint{{Kind: LeafAtLeastOne, Flags: []string{"a", "b"}}}}, "Constraints must be empty"},
{"const params", &cobra.Command{Use: "leaf"}, LeafSpec{ConstParams: map[string]any{"k": true}}, "ConstParams must be empty"},
{"call", &cobra.Command{Use: "leaf"}, LeafSpec{Call: func(*cobra.Command, string, map[string]any) error { return nil }}, "Call must be nil"},
{"runE", &cobra.Command{Use: "leaf"}, LeafSpec{RunE: func(*cobra.Command, []string) error { return nil }}, "RunE must be nil"},
{"postMount", &cobra.Command{Use: "leaf"}, LeafSpec{PostMount: func(*cobra.Command) {}}, "PostMount must be nil"},
{"confirmFirst", &cobra.Command{Use: "leaf"}, LeafSpec{ConfirmFirst: true}, "ConfirmFirst must be false"},
{"server", &cobra.Command{Use: "leaf"}, LeafSpec{Server: "srv"}, "Server/Tool must be empty"},
{"tool", &cobra.Command{Use: "leaf"}, LeafSpec{Tool: "tool"}, "Server/Tool must be empty"},
{"empty schema", &cobra.Command{Use: "leaf"}, LeafSpec{}, "Schema is required"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer func() {
r := recover()
if r == nil {
t.Fatalf("DeclareLeafMetadata did not panic, want %q", c.want)
}
if msg := fmt.Sprint(r); !strings.Contains(msg, c.want) {
t.Fatalf("panic = %q, want contain %q", msg, c.want)
}
}()
DeclareLeafMetadata(c.cmd, c.spec)
})
}
}
// TestCrossPlatformCoverageInstallContractRunEPipelinePanics covers the fail-closed guards of the
// pipeline installer: nil cmd/runtime and a leaf without RunE.
func TestCrossPlatformCoverageInstallContractRunEPipelinePanics(t *testing.T) {
assertPanic := func(name, want string, fn func()) {
t.Helper()
defer func() {
r := recover()
if r == nil {
t.Errorf("%s did not panic", name)
return
}
if msg := fmt.Sprint(r); !strings.Contains(msg, want) {
t.Errorf("%s panic = %q, want contain %q", name, msg, want)
}
}()
fn()
}
assertPanic("nil cmd/runtime", "nil cmd/runtime", func() {
installContractRunEPipeline(nil, nil)
})
assertPanic("nil RunE", "RunE is nil", func() {
installContractRunEPipeline(&cobra.Command{Use: "leaf"}, &contractRuntime{})
})
}
// TestCrossPlatformCoverageDeclareLeafMetadataValidateWithoutConfirmRunsInner covers the
// non-confirm pipeline: Validate runs first, then the inner RunE executes
// directly (no ConfirmSafety layer for not_required leaves).
func TestCrossPlatformCoverageDeclareLeafMetadataValidateWithoutConfirmRunsInner(t *testing.T) {
ran := false
cmd := &cobra.Command{
Use: "lookup",
RunE: func(*cobra.Command, []string) error {
ran = true
return nil
},
}
cmd.Flags().String("id", "", "")
DeclareLeafMetadata(cmd, LeafSpec{
Safety: cli.SafetySpec{
Effect: "read", Risk: "low",
Confirmation: "not_required", Idempotency: "idempotent",
},
Validate: func(c *cobra.Command, args []string) error {
id, _ := c.Flags().GetString("id")
if strings.TrimSpace(id) == "" {
return fmt.Errorf("flag --id is required")
}
return nil
},
Schema: contractCoverageSchema("test lookup"),
})
if !HasContractValidate(cmd) {
t.Fatal("expected contract Validate annotation")
}
if HasContractConfirmSafety(cmd) || HasContractConfirmDeferred(cmd) {
t.Fatal("not_required leaf must not carry confirm annotations")
}
cmd.SetIn(strings.NewReader(""))
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{})
// Validate failure short-circuits before the inner RunE.
if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "flag --id is required") {
t.Fatalf("Execute() error = %v, want Validate failure", err)
}
if ran {
t.Fatal("inner RunE must not run when Validate fails")
}
// Validate pass: inner RunE runs without any confirmation gate.
if err := cmd.Flags().Set("id", "x"); err != nil {
t.Fatal(err)
}
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v, want nil for not_required leaf", err)
}
if !ran {
t.Fatal("inner RunE must run after Validate passes")
}
}
// TestCrossPlatformCoverageDeclareLeafMetadataConfirmFallbackWithoutCaller covers the
// no-Validate/no-Caller fallback: without deps the deferred CallTool gate has
// nothing to hook, so ConfirmSafety runs before the inner RunE (fail closed).
func TestCrossPlatformCoverageDeclareLeafMetadataConfirmFallbackWithoutCaller(t *testing.T) {
prev := deps
deps = nil
t.Cleanup(func() { deps = prev })
ran := false
cmd := &cobra.Command{
Use: "purge",
RunE: func(*cobra.Command, []string) error {
ran = true
return nil
},
}
cmd.Flags().Bool("yes", false, "")
cmd.Flags().Bool("dry-run", false, "")
DeclareLeafMetadata(cmd, LeafSpec{
Safety: cli.SafetySpec{
Effect: "destructive", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
},
Schema: contractCoverageSchema("test purge"),
})
if !HasContractConfirmSafety(cmd) || !HasContractConfirmDeferred(cmd) {
t.Fatal("expected confirm + deferred-confirm annotations")
}
cmd.SetIn(strings.NewReader(""))
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{})
err := cmd.Execute()
if err == nil || (!strings.Contains(err.Error(), "confirmation_required") && !strings.Contains(err.Error(), "需要用户确认")) {
t.Fatalf("Execute() without deps/--yes error = %v, want confirmation_required", err)
}
if ran {
t.Fatal("inner RunE must not run before the fallback confirmation")
}
if err := cmd.Flags().Set("yes", "true"); err != nil {
t.Fatal(err)
}
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() with --yes error = %v", err)
}
if !ran {
t.Fatal("inner RunE must run once --yes bypasses the fallback confirmation")
}
}
// TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithoutCallTool
// covers the deferred-confirm post-RunE fallback: with a deps.Caller present
// and an inner RunE that never dispatches through CallTool, the gate stays
// unconfirmed and ConfirmSafety must still run after the inner RunE.
func TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithoutCallTool(t *testing.T) {
prev := deps
caller := &deferConfirmTestCaller{}
deps = &Deps{Caller: caller, Out: NewFormatter()}
t.Cleanup(func() { deps = prev })
ran := 0
cmd := &cobra.Command{
Use: "archive",
RunE: func(*cobra.Command, []string) error {
ran++
return nil // never calls deps.Caller.CallTool
},
}
cmd.Flags().Bool("yes", false, "")
cmd.Flags().Bool("dry-run", false, "")
DeclareLeafMetadata(cmd, LeafSpec{
Safety: cli.SafetySpec{
Effect: "destructive", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
},
Schema: contractCoverageSchema("test archive"),
})
cmd.SetIn(strings.NewReader(""))
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{})
// Without --yes the post-RunE fallback confirmation must fail closed.
err := cmd.Execute()
if err == nil || (!strings.Contains(err.Error(), "confirmation_required") && !strings.Contains(err.Error(), "需要用户确认")) {
t.Fatalf("Execute() without --yes error = %v, want confirmation_required", err)
}
if ran != 1 {
t.Fatalf("inner RunE ran %d times, want 1 (deferred gate confirms after RunE)", ran)
}
if caller.calls != 0 {
t.Fatalf("CallTool calls = %d, want 0", caller.calls)
}
// With --yes the post-RunE fallback confirmation passes.
if err := cmd.Flags().Set("yes", "true"); err != nil {
t.Fatal(err)
}
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() with --yes error = %v", err)
}
if ran != 2 {
t.Fatalf("inner RunE ran %d times, want 2", ran)
}
}
// readCapableLeafTestCaller is a ToolCaller that also implements the optional
// ReadToolCaller extension (pre-confirm reads).
type readCapableLeafTestCaller struct {
deferConfirmTestCaller
readCalls int
}
func (c *readCapableLeafTestCaller) CallReadTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
c.readCalls++
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil
}
// TestCrossPlatformCoverageContractConfirmReadCallerDelegatesReads covers the ReadToolCaller
// preservation branch of wrapContractConfirmCaller and the ungated
// CallReadTool delegation of contractConfirmReadCaller.
func TestCrossPlatformCoverageContractConfirmReadCallerDelegatesReads(t *testing.T) {
rc := &readCapableLeafTestCaller{}
cmd := &cobra.Command{Use: "read"}
gate := &contractConfirmCaller{
inner: rc, cmd: cmd,
safety: cli.SafetySpec{
Effect: "destructive", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
},
}
wrapped := wrapContractConfirmCaller(gate, rc)
readWrapped, ok := wrapped.(*contractConfirmReadCaller)
if !ok {
t.Fatalf("wrapContractConfirmCaller = %T, want *contractConfirmReadCaller", wrapped)
}
res, err := readWrapped.CallReadTool(context.Background(), "product", "read_tool", nil)
if err != nil || res == nil {
t.Fatalf("CallReadTool() = (%v, %v), want ungated success", res, err)
}
if rc.readCalls != 1 {
t.Fatalf("readCalls = %d, want 1", rc.readCalls)
}
if gate.confirmed {
t.Fatal("reads must not flip the confirmation gate")
}
// A caller without ReadToolCaller keeps the plain gate.
plain := &deferConfirmTestCaller{}
gate2 := &contractConfirmCaller{inner: plain, cmd: cmd}
if wrapped2 := wrapContractConfirmCaller(gate2, plain); wrapped2 != edition.ToolCaller(gate2) {
t.Fatalf("wrapContractConfirmCaller(plain) = %T, want the gate itself", wrapped2)
}
}
// TestCrossPlatformCoverageContractAnnotationHelpersNilAndMarkedCommands covers the nil-safety of
// the exported contract annotation predicates (incl. HasContractConfirmDeferred).
func TestCrossPlatformCoverageContractAnnotationHelpersNilAndMarkedCommands(t *testing.T) {
if HasContractConfirmSafety(nil) || HasContractValidate(nil) || HasContractConfirmDeferred(nil) {
t.Fatal("nil command must not report contract annotations")
}
bare := &cobra.Command{Use: "bare"}
if HasContractConfirmSafety(bare) || HasContractValidate(bare) || HasContractConfirmDeferred(bare) {
t.Fatal("command without annotations must report false")
}
marked := &cobra.Command{Use: "marked", Annotations: map[string]string{
contractConfirmSafetyAnnotation: "true",
contractValidateAnnotation: "true",
contractConfirmDeferredAnnotation: "true",
}}
if !HasContractConfirmSafety(marked) || !HasContractValidate(marked) || !HasContractConfirmDeferred(marked) {
t.Fatal("marked command must report all contract annotations")
}
}
// TestCrossPlatformCoverageDevAppMemberMutationValidateRejectsSeparatorOnlyUserIDs covers the
// member add/remove Validate error branch: a separator-only --user-ids value
// passes the required check but parses to an empty list and must be rejected.
func TestCrossPlatformCoverageDevAppMemberMutationValidateRejectsSeparatorOnlyUserIDs(t *testing.T) {
cases := []struct {
name string
build func(*fakeDevAppRunner) *cobra.Command
}{
{"add", func(r *fakeDevAppRunner) *cobra.Command { return newDevAppMemberAddCommand(r) }},
{"remove", func(r *fakeDevAppRunner) *cobra.Command { return newDevAppMemberRemoveCommand(r) }},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
r := &fakeDevAppRunner{}
cmd := c.build(r)
cmd.Flags().Bool("yes", false, "")
if err := cmd.Flags().Set("yes", "true"); err != nil {
t.Fatal(err)
}
_ = cmd.Flags().Set("unified-app-id", "APP-1")
_ = cmd.Flags().Set("user-ids", " , ; ")
_ = cmd.Flags().Set("member-type", "DEVELOPER")
err := cmd.RunE(cmd, nil)
if err == nil || !strings.Contains(err.Error(), "--user-ids 至少包含一个 userId") {
t.Fatalf("err = %v, want --user-ids 至少包含一个 userId", err)
}
if r.got.Tool != "" {
t.Fatalf("dispatched tool = %q, want no dispatch on Validate failure", r.got.Tool)
}
})
}
}
// TestCrossPlatformCoverageDevAppListTransformsOmitEmptyLists covers the empty-result branches of
// transformDevAppListParam and transformDevAppScopeValues (nil → key omitted).
func TestCrossPlatformCoverageDevAppListTransformsOmitEmptyLists(t *testing.T) {
for _, raw := range []string{"", " ", ",", " ; , "} {
v, err := transformDevAppListParam(raw)
if err != nil || v != nil {
t.Fatalf("transformDevAppListParam(%q) = (%#v, %v), want (nil, nil)", raw, v, err)
}
v, err = transformDevAppScopeValues(raw)
if err != nil || v != nil {
t.Fatalf("transformDevAppScopeValues(%q) = (%#v, %v), want (nil, nil)", raw, v, err)
}
}
v, err := transformDevAppListParam("a, b")
if err != nil || !reflect.DeepEqual(v, []string{"a", "b"}) {
t.Fatalf("transformDevAppListParam(a, b) = (%#v, %v), want [a b]", v, err)
}
v, err = transformDevAppScopeValues("a,b; c")
if err != nil || !reflect.DeepEqual(v, []string{"a", "b", "c"}) {
t.Fatalf("transformDevAppScopeValues(a,b; c) = (%#v, %v), want [a b c]", v, err)
}
}
// TestCrossPlatformCoverageDevDocSearchRequiresQueryFlag covers the required-query error branch of
// `dws dev doc search` (no positional keyword and no --query/--keyword).
func TestCrossPlatformCoverageDevDocSearchRequiresQueryFlag(t *testing.T) {
cmd := newDevDocSearchCommand()
err := cmd.RunE(cmd, nil)
if err == nil || !strings.Contains(err.Error(), "query") {
t.Fatalf("RunE() without query error = %v, want required --query error", err)
}
}
// TestCrossPlatformCoverageSheetMutationGuardRunsContractValidateBeforeTargets covers the outer
// Sheet guard's ContractValidate branch: a DeclareLeafMetadata Validate hook
// must run (and fail) before the --node target check and the --yes-only gate.
func TestCrossPlatformCoverageSheetMutationGuardRunsContractValidateBeforeTargets(t *testing.T) {
ran := false
cmd := &cobra.Command{
Use: "clear-range",
RunE: func(*cobra.Command, []string) error {
ran = true
return nil
},
}
cmd.Flags().Bool("yes", false, "")
cmd.Flags().Bool("dry-run", false, "")
cmd.Flags().String("node", "", "")
cmd.Flags().String("range", "", "")
DeclareLeafMetadata(cmd, LeafSpec{
Safety: cli.SafetySpec{
Effect: "destructive", Risk: "high",
Confirmation: "user_required", Idempotency: "unknown",
},
Validate: func(c *cobra.Command, args []string) error {
rng, _ := c.Flags().GetString("range")
if strings.TrimSpace(rng) == "" {
return fmt.Errorf("flag --range is required")
}
return nil
},
Schema: contractCoverageSchema("test clear range"),
})
protectSheetMutationCommand(cmd, "清除范围", "文档和目标范围")
if !HasContractValidate(cmd) || !HasSheetMutationConfirmationGuard(cmd) {
t.Fatal("expected contract Validate + sheet mutation guard markers")
}
cmd.SetIn(strings.NewReader(""))
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{})
// Validate failure must win over missing --node and the --yes-only prompt.
err := cmd.Execute()
if err == nil || !strings.Contains(err.Error(), "flag --range is required") {
t.Fatalf("Execute() error = %v, want ContractValidate failure first", err)
}
if ran {
t.Fatal("inner RunE must not run when ContractValidate fails")
}
// Full pass: Validate + --node + --yes reach the inner RunE.
if err := cmd.Flags().Set("range", "A1:B2"); err != nil {
t.Fatal(err)
}
if err := cmd.Flags().Set("node", "node-1"); err != nil {
t.Fatal(err)
}
if err := cmd.Flags().Set("yes", "true"); err != nil {
t.Fatal(err)
}
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() with valid flags error = %v", err)
}
if !ran {
t.Fatal("inner RunE must run after Validate/--node/--yes all pass")
}
}
// TestCrossPlatformCoverageRequireSheetMutationTargetsSkipsCommandsWithoutNodeFlag covers the skip
// path for nil commands and leaves that do not declare a --node flag.
func TestCrossPlatformCoverageRequireSheetMutationTargetsSkipsCommandsWithoutNodeFlag(t *testing.T) {
if err := requireSheetMutationTargets(nil); err != nil {
t.Fatalf("requireSheetMutationTargets(nil) = %v, want nil", err)
}
cmd := &cobra.Command{Use: "no-node"}
if err := requireSheetMutationTargets(cmd); err != nil {
t.Fatalf("requireSheetMutationTargets(no --node) = %v, want nil", err)
}
}
+23
View File
@@ -9,6 +9,8 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
)
// ──────────────────────────────────────────────────────────
@@ -954,6 +956,10 @@ contact user profile fields 获取可用字段列表。
},
Examples: []string{"dws contact user profile get --staff-id STAFF_ID --fields fieldCode1,fieldCode2"},
},
Parameters: []corecmd.ParamDecl{
{Name: "fields", Required: boolPtr(false)},
{Name: "staff-id", Required: boolPtr(false)},
},
},
})
contactUserProfileGetCmd.Flags().String("staff-id", "", "查询员工 ID(可选)")
@@ -1128,6 +1134,11 @@ contact user profile fields 获取可用字段列表。
AvoidWhen: []string{"需要创建企业专属登录账号时使用 contact account create;需要创建企业组织本身时使用 contact org create"},
Examples: []string{"dws contact user invite --org-user-name \"张三\" --org-user-mobile \"13800138000\" --depts '[{\"deptId\":1}]'"},
},
Parameters: []corecmd.ParamDecl{
{Name: "depts", Property: "depts", Required: boolPtr(false), InterfaceType: "array"},
{Name: "org-user-mobile", Property: "orgUserMobile", Required: boolPtr(true)},
{Name: "org-user-name", Property: "orgUserName", Required: boolPtr(true)},
},
},
})
contactUserInviteCmd.Flags().String("org-user-name", "", "员工在企业内的名称 (必填)")
@@ -1327,6 +1338,10 @@ contact user profile fields 获取可用字段列表。
AvoidWhen: []string{"请求中包含企业账号、专属账号或登录账号时改用 contact account create;邀请员工时改用 contact user invite"},
Examples: []string{"dws contact org create --org-name \"我的企业\" --creator-username \"张三\""},
},
Parameters: []corecmd.ParamDecl{
{Name: "creator-username", Property: "creatorUsername", Required: boolPtr(true)},
{Name: "org-name", Property: "orgName", Required: boolPtr(true)},
},
},
})
contactOrgCreateCmd.Flags().String("org-name", "", "企业名称 (必填)")
@@ -1406,6 +1421,14 @@ contact user profile fields 获取可用字段列表。
AvoidWhen: []string{"创建企业组织本身应使用 contact org create;仅邀请已有手机号员工入企应使用 contact user invite"},
Examples: []string{"dws contact account create --org-user-name \"张三\" --login-id \"zhangsan001\" --org-user-mobile \"13800138000\""},
},
Parameters: []corecmd.ParamDecl{
{Name: "dept-ids", Property: "deptIds", Required: boolPtr(false), InterfaceType: "array"},
{Name: "email", Property: "email", Required: boolPtr(false)},
{Name: "login-id", Property: "loginId", Required: boolPtr(true)},
{Name: "org-user-mobile", Property: "orgUserMobile", Required: boolPtr(false)},
{Name: "org-user-name", Property: "orgUserName", Required: boolPtr(true)},
{Name: "send-pwd-via-sms", Property: "sendPwdViaSms", Required: boolPtr(false), InterfaceType: "boolean"},
},
},
})
contactAccountCreateCmd.Flags().String("org-user-name", "", "员工在企业内的名称 (必填)")
+19
View File
@@ -16,6 +16,8 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
)
// ──────────────────────────────────────────────────────────
@@ -1094,6 +1096,14 @@ func newDocCommand() *cobra.Command {
"dws doc read --node <DOC_ID> --content-format jsonml --scope outline --max-depth 3",
},
},
Parameters: []corecmd.ParamDecl{
{Name: "content-format", Property: "format", Required: boolPtr(false)},
{Name: "end-block-id", Required: boolPtr(false)},
{Name: "max-depth", Required: boolPtr(false), InterfaceType: "integer"},
{Name: "scope", Required: boolPtr(false)},
{Name: "start-block-id", Required: boolPtr(false), RequiredWhen: "--scope=range or --scope=section"},
{Name: "tags", Required: boolPtr(false), RequiredWhen: "--scope=tags"},
},
},
})
@@ -1968,6 +1978,9 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。
},
Examples: []string{"dws doc rename --node <DOC_ID> --name \"新名称\" --format json"},
},
Parameters: []corecmd.ParamDecl{
{Name: "name", Description: "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名"},
},
},
})
@@ -2476,6 +2489,9 @@ resourceId 需通过 dws doc block list 获取:查询目标文档的块列表
"dws doc comment create --node <DOC_ID> --content \"请review\" --mention uid1,uid2 --mentioned-open-conversation-id <openConversationId> --format json",
},
},
Parameters: []corecmd.ParamDecl{
{Name: "mentioned-open-conversation-id", Required: boolPtr(false), InterfaceType: "array"},
},
},
})
@@ -2555,6 +2571,9 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中
"dws doc comment reply --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"比心\" --emoji --format json",
},
},
Parameters: []corecmd.ParamDecl{
{Name: "mentioned-open-conversation-id", Required: boolPtr(false), InterfaceType: "array"},
},
},
})
+51
View File
@@ -6,6 +6,8 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
)
// ──────────────────────────────────────────────────────────
@@ -88,6 +90,14 @@ func newHrbrainCommand() *cobra.Command {
"dws hrbrain talent-pool list --keyword \"储备干部\"",
},
},
Parameters: []corecmd.ParamDecl{
{Name: "creator", Property: "creator", Required: boolPtr(false)},
{Name: "keyword", Property: "keyword", Required: boolPtr(false)},
{Name: "labels", Property: "labels", Required: boolPtr(false)},
{Name: "page", Property: "currentPage", Required: boolPtr(false)},
{Name: "page-size", Property: "pageSize", Required: boolPtr(false)},
{Name: "pool-type", Property: "poolType", Required: boolPtr(false)},
},
},
})
talentPoolListCmd.Flags().String("keyword", "", "人才池名称关键词 (可选)")
@@ -128,6 +138,9 @@ func newHrbrainCommand() *cobra.Command {
AvoidWhen: []string{"尚未取得 poolCode 时先用 dws hrbrain talent-pool list 查找"},
Examples: []string{"dws hrbrain talent-pool detail --pool-code POOL_CODE"},
},
Parameters: []corecmd.ParamDecl{
{Name: "pool-code", Property: "poolCode", Required: boolPtr(true)},
},
},
})
talentPoolDetailCmd.Flags().String("pool-code", "", "人才池编码 (必填)")
@@ -170,6 +183,11 @@ func newHrbrainCommand() *cobra.Command {
},
Examples: []string{"dws hrbrain talent-pool employees --pool-code POOL_CODE --page 1 --page-size 20"},
},
Parameters: []corecmd.ParamDecl{
{Name: "page", Property: "currentPage", Required: boolPtr(false)},
{Name: "page-size", Property: "pageSize", Required: boolPtr(false)},
{Name: "pool-code", Property: "poolCode", Required: boolPtr(true)},
},
},
})
talentPoolEmployeesCmd.Flags().String("pool-code", "", "人才池编码 (必填)")
@@ -213,6 +231,9 @@ func newHrbrainCommand() *cobra.Command {
AvoidWhen: []string{"已知模块与字段编码,直接查询档案数据时改用 dws hrbrain profile query"},
Examples: []string{"dws hrbrain profile metadata --work-no WORK_NO"},
},
Parameters: []corecmd.ParamDecl{
{Name: "work-no", Property: "workNo", Required: boolPtr(true)},
},
},
})
profileMetadataCmd.Flags().String("work-no", "", "员工工号 (必填)")
@@ -262,6 +283,10 @@ func newHrbrainCommand() *cobra.Command {
},
Examples: []string{"dws hrbrain profile query --work-no WORK_NO --data-queries '[{\"modelCode\":\"basic\",\"fields\":[\"name\",\"dept\"]}]'"},
},
Parameters: []corecmd.ParamDecl{
{Name: "data-queries", Property: "dataQueries", Required: boolPtr(true)},
{Name: "work-no", Property: "workNo", Required: boolPtr(true)},
},
},
})
profileQueryCmd.Flags().String("work-no", "", "目标员工工号 (必填)")
@@ -303,6 +328,10 @@ func newHrbrainCommand() *cobra.Command {
AvoidWhen: []string{"要查看职业历程或绩效记录时改用 dws hrbrain profile career / profile performance"},
Examples: []string{"dws hrbrain profile labels --staff-ids WORK_NO1,WORK_NO2"},
},
Parameters: []corecmd.ParamDecl{
{Name: "all-label", Property: "allLabel", Required: boolPtr(false)},
{Name: "staff-ids", Property: "staffIds", Required: boolPtr(true)},
},
},
})
profileLabelsCmd.Flags().String("staff-ids", "", "员工工号列表,逗号分隔 (必填)")
@@ -338,6 +367,9 @@ func newHrbrainCommand() *cobra.Command {
AvoidWhen: []string{"要查看绩效记录时改用 dws hrbrain profile performance"},
Examples: []string{"dws hrbrain profile career --work-no WORK_NO"},
},
Parameters: []corecmd.ParamDecl{
{Name: "work-no", Property: "workNo", Required: boolPtr(true)},
},
},
})
profileCareerCmd.Flags().String("work-no", "", "员工工号 (必填)")
@@ -372,6 +404,9 @@ func newHrbrainCommand() *cobra.Command {
AvoidWhen: []string{"要查看职业历程时改用 dws hrbrain profile career"},
Examples: []string{"dws hrbrain profile performance --work-no WORK_NO"},
},
Parameters: []corecmd.ParamDecl{
{Name: "work-no", Property: "workNo", Required: boolPtr(true)},
},
},
})
profilePerformanceCmd.Flags().String("work-no", "", "员工工号 (必填)")
@@ -436,6 +471,15 @@ func newHrbrainCommand() *cobra.Command {
"dws hrbrain search employees --dept-name \"技术部\" --job-level P7",
},
},
Parameters: []corecmd.ParamDecl{
{Name: "dept-name", Property: "deptName", Required: boolPtr(false)},
{Name: "job-level", Property: "jobLevel", Required: boolPtr(false)},
{Name: "keyword", Property: "keyword", Required: boolPtr(false)},
{Name: "page", Property: "currentPage", Required: boolPtr(false)},
{Name: "page-size", Property: "pageSize", Required: boolPtr(false)},
{Name: "pool-code", Property: "poolCode", Required: boolPtr(false)},
{Name: "position-name", Property: "positionName", Required: boolPtr(false)},
},
},
})
employeeSearchCmd.Flags().String("keyword", "", "全文搜索关键词(姓名/工号等)(可选)")
@@ -507,6 +551,13 @@ func newHrbrainCommand() *cobra.Command {
},
Examples: []string{"dws hrbrain search employees-structured --origin-json '{\"rules\":[{\"field\":\"name\",\"operator\":\"contains\",\"value\":\"张\"}],\"combinator\":\"and\"}' --fields '[{\"label\":\"姓名\",\"value\":\"name\"}]'"},
},
Parameters: []corecmd.ParamDecl{
{Name: "fields", Property: "fields", Required: boolPtr(true)},
{Name: "order-by", Property: "orderByClauses", Required: boolPtr(false)},
{Name: "origin-json", Property: "originJson", Required: boolPtr(true)},
{Name: "page", Property: "currentPage", Required: boolPtr(false)},
{Name: "page-size", Property: "pageSize", Required: boolPtr(false)},
},
},
})
employeeSearchStructuredCmd.Flags().String("origin-json", "", "搜索条件 JSON 表达式 (必填)")
+2 -3
View File
@@ -250,9 +250,8 @@ func DeclareLeafMetadata(cmd *cobra.Command, spec LeafSpec) *cobra.Command {
if spec.Validate == nil && !confirm {
return cmd
}
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
// cmd.Annotations is always non-nil here: AttachSchema above registers the
// runtime-contract annotation on every declared leaf.
rt := &contractRuntime{validate: spec.Validate, confirm: confirm}
if confirm {
rt.safety = spec.Safety
+11
View File
@@ -13,6 +13,8 @@ import (
"time"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
)
// ──────────────────────────────────────────────────────────
@@ -2126,6 +2128,10 @@ user 对象字段:
AvoidWhen: []string{"列出当前用户自己的邮箱使用 mail mailbox list,搜索邮件内容使用 message search"},
Examples: []string{"dws mail user search --email user@company.com --keyword \"张三\" --limit 20"},
},
Parameters: []corecmd.ParamDecl{
{Name: "employee-no", Required: boolPtr(false)},
{Name: "keyword", Required: boolPtr(false)},
},
},
})
@@ -2376,6 +2382,11 @@ user 对象字段:
AvoidWhen: []string{"创建新模板时使用 mail template create"},
Examples: []string{"dws mail template update --email user@company.com --id <templateId> --subject \"新标题\" --content \"新正文\""},
},
Parameters: []corecmd.ParamDecl{
{Name: "content", Required: boolPtr(false)},
{Name: "name", Required: boolPtr(false)},
{Name: "subject", Required: boolPtr(false)},
},
},
})
+5
View File
@@ -14,6 +14,8 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
)
const (
@@ -253,6 +255,9 @@ func newReportCommand() *cobra.Command {
"dws report entry submit --template-id <templateId> --contents '[{\"key\":\"今日完成\",\"sort\":\"0\",\"content\":\"完成了需求评审\",\"contentType\":\"markdown\",\"type\":\"1\"}]' --format json",
},
},
Parameters: []corecmd.ParamDecl{
{Name: "to-chat", Required: boolPtr(false)},
},
},
})
addReportCreateFlags(entrySubmitCmd)
+22
View File
@@ -15,6 +15,8 @@ import (
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
)
// ──────────────────────────────────────────────────────────
@@ -263,6 +265,10 @@ func newTodoCommand() *cobra.Command {
"dws todo task list --page 1 --size 20 --status false --format json",
},
},
Parameters: []corecmd.ParamDecl{
{Name: "query-all", Required: boolPtr(false), InterfaceType: "boolean", Description: "为 true 时跨组织查询全部待办;默认仅查询当前组织待办"},
{Name: "role-types", Property: "roleTypes", Required: boolPtr(false), Description: "角色类型列表;省略时运行时默认使用 executor"},
},
},
})
@@ -831,6 +837,9 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作
"dws todo task reset-reminder --task-id <taskId> --reminder-rules '[{\"dueDateOffset\":-30,\"baseTime\":\"dueTime\"},{\"reminderTimeStamp\":\"2026-03-10T18:00:00+08:00\",\"baseTime\":\"customTime\"}]'",
},
},
Parameters: []corecmd.ParamDecl{
{Name: "reminder-rules", Description: "提醒规则 JSON 数组;不传表示清除,显式传值必须为对象数组,且每条按 baseTime 提供整数 dueDateOffset 或 ISO8601 reminderTimeStamp"},
},
},
})
@@ -1263,6 +1272,10 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作
AvoidWhen: []string{"尚无标签 code 时先用 todo tag list 或 todo tag create;修改标签定义应使用 todo tag update"},
Examples: []string{"dws todo tag add --task-id <taskId> --tag-codes code1,code2"},
},
Parameters: []corecmd.ParamDecl{
{Name: "tag-codes", Property: "tagCodes", Required: boolPtr(true), InterfaceType: "array"},
{Name: "task-id", Property: "taskId", Required: boolPtr(true)},
},
},
})
@@ -1304,6 +1317,9 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作
AvoidWhen: []string{"只需从某个待办移除标签或重命名标签时不要删除标签定义;未确认 code 时先用 todo tag list"},
Examples: []string{"dws todo tag delete --tag-codes code1,code2"},
},
Parameters: []corecmd.ParamDecl{
{Name: "tag-codes", Property: "tagCodes", Required: boolPtr(true), InterfaceType: "array"},
},
},
})
@@ -1352,6 +1368,9 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作
AvoidWhen: []string{"给待办打标签应使用 todo tag add;创建没有 code 的新标签应使用 todo tag create"},
Examples: []string{"dws todo tag update --user-tags '[{\"code\":\"code1\",\"name\":\"新名称\"}]'"},
},
Parameters: []corecmd.ParamDecl{
{Name: "user-tags", Property: "userTags", Required: boolPtr(true), InterfaceType: "array"},
},
},
})
@@ -1419,6 +1438,9 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作
AvoidWhen: []string{"给已有待办打上现有标签应使用 todo tag add;重命名已有标签应使用 todo tag update"},
Examples: []string{"dws todo tag create --name \"项目标签\""},
},
Parameters: []corecmd.ParamDecl{
{Name: "name", Property: "name", Required: boolPtr(true)},
},
},
})
+1
View File
@@ -138,6 +138,7 @@ func fromShortcutFlags(flags []Flag) []corecmd.FlagSpec {
Default: f.Default,
Hidden: f.Hidden,
Required: f.Required,
RequiredWhen: f.RequiredWhen,
ValidationMode: corecmd.ValidationShortcut,
RequiredError: fmt.Sprintf("缺少必填参数 --%s:%s", f.Name, f.Desc),
Enum: append([]string(nil), f.Enum...),
@@ -0,0 +1,24 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package builtin_test
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/builtin"
)
// BenchmarkShortcutCommands measures the Go-literal storage model: the 376
// shortcut definitions are compiled-in struct literals registered from init(),
// so there is no parse step at all — this times only turning them into the Cobra
// tree. It is the comparison point for the embedded JSON catalog, whose first
// ResolveMeta hit spends ~1.4s and ~740MB decoding.
func BenchmarkShortcutCommands(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if len(builtin.Commands()) == 0 {
b.Fatal("no shortcut commands")
}
}
}
+2 -2
View File
@@ -61,8 +61,8 @@ var MessagesSend = shortcut.Shortcut{
{Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊接收者 openDingTalkId(user);受发送身份能力矩阵约束"},
{Name: "users", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 userId(bot);受发送身份能力矩阵约束"},
{Name: "open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 openDingTalkId(bot);受发送身份能力矩阵约束"},
{Name: "robot-code", Type: shortcut.FlagString, Desc: "机器人 Code(identity=bot 时使用);受发送身份能力矩阵约束"},
{Name: "webhook-token", Type: shortcut.FlagString, Desc: "自定义机器人 Webhook token(identity=webhook 时使用);受发送身份能力矩阵约束"},
{Name: "robot-code", Type: shortcut.FlagString, RequiredWhen: "identity=bot", Desc: "机器人 Code(identity=bot 时使用);受发送身份能力矩阵约束"},
{Name: "webhook-token", Type: shortcut.FlagString, RequiredWhen: "identity=webhook", Desc: "自定义机器人 Webhook token(identity=webhook 时使用);受发送身份能力矩阵约束"},
{Name: "title", Type: shortcut.FlagString, Desc: "消息标题(不传则从正文生成)"},
{Name: "text", Type: shortcut.FlagString, Desc: "纯文本正文"},
{Name: "markdown", Type: shortcut.FlagString, Desc: "Markdown 正文"},
+5
View File
@@ -89,6 +89,11 @@ type Flag struct {
Desc string `json:"description"`
// Required, when true, makes the framework error if the flag is not set.
Required bool `json:"required"`
// RequiredWhen publishes a conditional-required rule that Required cannot
// express, e.g. "identity=bot" for a credential only mandatory under one
// identity. It is published Schema metadata so an Agent can predict the
// failure; enforcement stays in Validate, which remains authoritative.
RequiredWhen string `json:"required_when,omitempty"`
// Enum, when non-empty, restricts the accepted values (string flags only).
Enum []string `json:"enum"`
// Hidden hides the flag from --help while keeping it usable.
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""One-shot feasibility probe: render one product's catalog shard as Go literals.
Answers whether the shortcut storage model (compiled-in struct literals, zero
runtime parse) is viable for the 845-tool Schema catalog, by measuring compile
time, binary size and access cost on a single product first.
Deliberately drops field_provenance: it is 50% of the bytes and no runtime
consumer reads it, so a codegen design would keep it in JSON for audit only.
Keeping it here would make the size numbers pessimistic and the design wrong.
"""
import json
import sys
def q(s: str) -> str:
return json.dumps(s if s is not None else "", ensure_ascii=False)
def strs(v) -> str:
if not v:
return "nil"
return "[]string{" + ", ".join(q(x) for x in v) + "}"
def render_param(name, p) -> str:
fields = [
f"Name: {q(p.get('name') or name)}",
f"Type: {q(p.get('type'))}",
f"Description: {q(p.get('description'))}",
f"Property: {q(p.get('property'))}",
f"Required: {str(bool(p.get('required'))).lower()}",
f"CLIRequired: {str(bool(p.get('cli_required'))).lower()}",
f"RequiredWhen: {q(p.get('required_when'))}",
f"Format: {q(p.get('format'))}",
f"Enum: {strs(p.get('enum'))}",
]
return "{" + ", ".join(fields) + "}"
def render_tool(canonical, t) -> str:
params = t.get("parameters") or {}
lines = [
f"\t{q(canonical)}: {{",
f"\t\tName: {q(t.get('name'))},",
f"\t\tCanonicalPath: {q(t.get('canonical_path'))},",
f"\t\tCLIPath: {q(t.get('cli_path'))},",
f"\t\tPrimaryCLIPath: {q(t.get('primary_cli_path'))},",
f"\t\tProductID: {q(t.get('product_id'))},",
f"\t\tGroup: {q(t.get('group'))},",
f"\t\tTitle: {q(t.get('title'))},",
f"\t\tDescription: {q(t.get('description'))},",
f"\t\tAliases: {strs(t.get('aliases'))},",
f"\t\tIsAlias: {str(bool(t.get('is_alias'))).lower()},",
f"\t\tEffect: {q(t.get('effect'))},",
f"\t\tRisk: {q(t.get('risk'))},",
f"\t\tConfirmation: {q(t.get('confirmation'))},",
f"\t\tIdempotency: {q(t.get('idempotency'))},",
f"\t\tInterfaceMode: {q(t.get('interface_mode'))},",
f"\t\tAvailability: {q(t.get('availability'))},",
f"\t\tAgentSummary: {q(t.get('agent_summary'))},",
f"\t\tUseWhen: {strs(t.get('use_when'))},",
f"\t\tAvoidWhen: {strs(t.get('avoid_when'))},",
f"\t\tExamples: {strs(t.get('examples'))},",
f"\t\tHasParameters: {str(bool(t.get('has_parameters'))).lower()},",
f"\t\tParameterCount: {int(t.get('parameter_count') or 0)},",
]
if params:
lines.append("\t\tParameters: map[string]Param{")
for pname in sorted(params):
lines.append(f"\t\t\t{q(pname)}: {render_param(pname, params[pname])},")
lines.append("\t\t},")
lines.append("\t},")
return "\n".join(lines)
def main() -> int:
src, product, out = sys.argv[1], sys.argv[2], sys.argv[3]
tools = json.load(open(src, encoding="utf-8"))["tools"]
body = "\n".join(render_tool(c, tools[c]) for c in sorted(tools))
with open(out, "w", encoding="utf-8") as f:
f.write(f"""// Copyright 2026 Alibaba Group
// Code generated by scripts/dev/catalog_codegen_probe.py; DO NOT EDIT.
//
// Feasibility probe for storing the Schema catalog as compiled-in Go literals
// instead of embedded JSON, mirroring how internal/shortcut stores its 376
// definitions. field_provenance is omitted: no runtime consumer reads it.
package catalogcodegen
// Tools holds the {product} product's leaf ToolSpecs as static data, so there is
// no decode step on the first lookup.
var Tools = map[string]Tool{{
{body}
}}
""")
print(f"generated {out}: {len(tools)} tools")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Generate ParamDecl Go snippets from schema_hints/metadata overlays.
For each product with parameter overlays, emits the Go code that should be
inserted into the corresponding DeclareLeafMetadata Schema block. Also reports
whether the overlaid flags are registered before or after the DeclareLeafMetadata
call, so the operator knows which need reordering.
Usage: python3 scripts/dev/gen_param_decls.py
"""
import json
import glob
import os
import re
import sys
HELPER_DIR = "internal/helpers"
# Map product hint filename to helper Go file(s)
PRODUCT_FILES = {
"aisearch": ["aisearch.go"],
"aitable": ["aitable.go"],
"attendance": ["attendance.go"],
"chat": ["chat.go"],
"contact": ["contact.go"],
"devdoc": ["devdoc.go"],
"doc": ["doc.go"],
"drive": ["drive.go"],
"hrbrain": ["hrbrain.go"],
"mail": ["mail.go"],
"markdown": ["markdown.go"],
"minutes": ["minutes.go"],
"report": ["report.go"],
"sheet": ["sheet.go"],
"todo": ["todo.go"],
}
def bool_ptr(v: bool) -> str:
return "boolPtr(true)" if v else "boolPtr(false)"
def render_param_decl(name: str, overlay: dict) -> str:
parts = [f'Name: "{name}"']
if "property" in overlay:
parts.append(f'Property: "{overlay["property"]}"')
if "required" in overlay:
parts.append(f"Required: {bool_ptr(overlay['required'])}")
if "interface_type" in overlay:
parts.append(f'InterfaceType: "{overlay["interface_type"]}"')
if "description" in overlay:
desc = overlay["description"].replace('"', '\\"')
parts.append(f'Description: "{desc}"')
if "required_when" in overlay:
parts.append(f'RequiredWhen: "{overlay["required_when"]}"')
if "enum" in overlay and overlay["enum"]:
vals = ", ".join(f'"{v}"' for v in overlay["enum"])
parts.append(f"Enum: []string{{{vals}}}")
return "{" + ", ".join(parts) + "}"
def find_declare_line(src: str, tool_name: str) -> int:
"""Find the line number of DeclareLeafMetadata for a tool whose RPCName matches."""
# Try to find by RPCName in the Schema block
pattern = re.compile(
r'DeclareLeafMetadata\(\w+,\s*LeafSpec\{.*?RPCName:\s*"' + re.escape(tool_name) + '"',
re.S,
)
m = pattern.search(src)
if m:
return src[: m.start()].count("\n") + 1
return -1
def find_flag_reg_line(src: str, flag_name: str) -> int:
"""Find the first line where a flag is registered."""
pattern = re.compile(r'\.Flags\(\)\.\w+\("' + re.escape(flag_name) + '"')
m = pattern.search(src)
if m:
return src[: m.start()].count("\n") + 1
return -1
def main():
for hint_file in sorted(glob.glob("internal/cli/schema_hints/metadata/*.json")):
product = os.path.basename(hint_file).replace(".json", "")
d = json.load(open(hint_file, encoding="utf-8"))
tools = d.get("tools") or {}
overlays = {t: (v or {}).get("parameters") or {} for t, v in tools.items()}
overlays = {t: p for t, p in overlays.items() if p}
if not overlays:
continue
helper_files = PRODUCT_FILES.get(product, [])
src = ""
for hf in helper_files:
path = os.path.join(HELPER_DIR, hf)
if os.path.exists(path):
src += open(path, encoding="utf-8").read() + "\n"
print(f"\n// === {product} ({len(overlays)} tools) ===")
for tool_name, params in sorted(overlays.items()):
decl_line = find_declare_line(src, tool_name)
print(f"\n// tool: {tool_name} (DeclareLeafMetadata at line {decl_line})")
print(f"Parameters: []corecmd.ParamDecl{{")
for pname in sorted(params):
overlay = params[pname]
reg_line = find_flag_reg_line(src, pname)
order = ""
if decl_line > 0 and reg_line > 0:
if reg_line > decl_line:
order = f" // ⚠ flag registered AFTER DeclareLeafMetadata (line {reg_line} > {decl_line}) — MOVE BEFORE"
else:
order = f" // ✓ flag registered before (line {reg_line})"
print(f"\t{render_param_decl(pname, overlay)},{order}")
print("},")
if __name__ == "__main__":
main()
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Migrate one product's parameter overlays from hints into ParamDecl declarations.
Usage: python3 scripts/dev/migrate_product_params.py <product>
For each tool with parameter overlays in schema_hints/metadata/<product>.json:
1. Finds the DeclareLeafMetadata block containing the tool's RPCName.
2. Inserts Parameters: []corecmd.ParamDecl{...} into the Schema block.
3. Moves any overlaid flag registrations before the DeclareLeafMetadata call.
4. Clears the tool's parameters from the hints file.
Run for one product at a time; verify compilation and schema output between runs.
"""
import json
import os
import re
import sys
HELPER_DIR = "internal/helpers"
HINTS_DIR = "internal/cli/schema_hints/metadata"
# Primary file per product (used for hints clearing only; RPCName search covers all files)
PRODUCT_FILES = {
"aisearch": "aisearch.go", "aitable": "aitable.go", "attendance": "attendance.go",
"chat": "chat.go", "contact": "contact.go", "devdoc": "devdoc.go",
"doc": "doc.go", "drive": "drive.go", "hrbrain": "hrbrain.go",
"mail": "mail.go", "markdown": "markdown.go", "minutes": "minutes.go",
"report": "report.go", "sheet": "sheet.go", "todo": "todo.go",
}
def all_helper_files():
"""Return all non-test .go files in internal/helpers/."""
files = []
for f in sorted(os.listdir(HELPER_DIR)):
if f.endswith(".go") and not f.endswith("_test.go"):
files.append(os.path.join(HELPER_DIR, f))
return files
def bool_ptr(v):
return "boolPtr(true)" if v else "boolPtr(false)"
def render_param(name, ov):
parts = [f'Name: "{name}"']
if "property" in ov:
parts.append(f'Property: "{ov["property"]}"')
if "required" in ov:
parts.append(f"Required: {bool_ptr(ov['required'])}")
if "interface_type" in ov:
parts.append(f'InterfaceType: "{ov["interface_type"]}"')
if "description" in ov:
parts.append(f'Description: "{ov["description"]}"')
if "required_when" in ov:
parts.append(f'RequiredWhen: "{ov["required_when"]}"')
if "enum" in ov and ov["enum"]:
vals = ", ".join(f'"{v}"' for v in ov["enum"])
parts.append(f"Enum: []string{{{vals}}}")
return "\t\t\t\t{" + ", ".join(parts) + "},"
def find_block_end(lines, start_idx):
"""Find the line index of the closing '})' or '},' for a block starting at start_idx."""
depth = 0
for i in range(start_idx, len(lines)):
depth += lines[i].count("{") - lines[i].count("}")
if depth <= 0 and i > start_idx:
return i
return -1
def migrate_product(product):
hint_path = os.path.join(HINTS_DIR, f"{product}.json")
hints = json.load(open(hint_path, encoding="utf-8"))
tools = hints.get("tools") or {}
overlays = {t: (v or {}).get("parameters") or {} for t, v in tools.items()}
overlays = {t: p for t, p in overlays.items() if p}
if not overlays:
print(f"{product}: no overlays, skipping")
return True
# Load all helper files into memory
file_contents = {}
for fp in all_helper_files():
file_contents[fp] = open(fp, encoding="utf-8").read()
modified_files = set()
flags_to_move = []
migrated_tools = set() # only clear hints for tools we actually migrated
for tool_name, params in sorted(overlays.items()):
rpc_name = tool_name.split(".")[-1]
# Strategy 1: RPCName field in Schema.Interface
rpc_pattern = re.compile(r'RPCName:\s*"' + re.escape(rpc_name) + '"')
# Strategy 2: callMCPTool / CallMCP call sites
call_patterns = [
re.compile(r'callMCPTool\("' + re.escape(rpc_name) + '"'),
re.compile(r'callMCPToolOnServer\([^,]+,\s*"' + re.escape(rpc_name) + '"'),
re.compile(r'rt\.CallMCP\w*\("' + re.escape(rpc_name) + '"'),
]
found_file = None
found_line = -1
for fp, src in file_contents.items():
lines_src = src.split("\n")
# Try strategy 1 first
for i, line in enumerate(lines_src):
if rpc_pattern.search(line):
found_file = fp
found_line = i
break
if found_file:
break
# Try strategy 2
for i, line in enumerate(lines_src):
for cp in call_patterns:
if cp.search(line):
found_file = fp
found_line = i
break
if found_file:
break
if found_file:
break
if not found_file:
print(f" WARNING: {tool_name} — RPCName '{rpc_name}' not found in any helper file")
continue
lines = file_contents[found_file].split("\n")
# Find DeclareLeafMetadata: search backwards first, then forwards
decl_start = -1
search_range = list(range(found_line, max(found_line - 60, -1), -1)) + \
list(range(found_line + 1, min(found_line + 60, len(lines))))
for i in search_range:
if "DeclareLeafMetadata(" in lines[i]:
# Verify this DeclareLeafMetadata's Schema contains the target
# RPCName (or the tool name) to avoid matching the wrong command.
block_end = find_block_end(lines, i)
block_text = "\n".join(lines[i:block_end + 1])
if rpc_name in block_text or tool_name in block_text:
decl_start = i
break
# Also check for callMCPTool("rpc_name") inside the block
if f'callMCPTool("{rpc_name}"' in block_text or \
f'callMCPToolOnServer(' in block_text and rpc_name in block_text:
decl_start = i
break
# Skip this DeclareLeafMetadata — it doesn't belong to our tool
if decl_start < 0:
print(f" WARNING: {tool_name} — no matching DeclareLeafMetadata near line {found_line + 1} in {os.path.basename(found_file)}")
continue
# Find Schema block
schema_start = -1
for i in range(decl_start, min(decl_start + 80, len(lines))):
if "Schema: LeafSchema{" in lines[i] or "Schema: corecmd.SchemaDecl{" in lines[i]:
schema_start = i
break
if schema_start < 0:
print(f" WARNING: {tool_name} — no Schema block in {os.path.basename(found_file)}")
continue
schema_end = find_block_end(lines, schema_start)
schema_text = "\n".join(lines[schema_start:schema_end + 1])
if "Parameters:" in schema_text:
print(f" SKIP: {tool_name} — Parameters already declared")
migrated_tools.add(tool_name)
continue
# Build and insert Parameters
param_lines = ["\t\t\tParameters: []corecmd.ParamDecl{"]
for pname in sorted(params):
param_lines.append(render_param(pname, params[pname]))
param_lines.append("\t\t\t},")
insert_at = schema_end
for i in range(schema_end - 1, schema_start, -1):
stripped = lines[i].strip()
if stripped and not stripped.startswith("//"):
insert_at = i + 1
break
for j, pl in enumerate(param_lines):
lines.insert(insert_at + j, pl)
modified_files.add(found_file)
file_contents[found_file] = "\n".join(lines)
migrated_tools.add(tool_name)
print(f" OK: {tool_name} — inserted {len(params)} param decls in {os.path.basename(found_file)} at line {insert_at + 1}")
# Check flag registration order
for pname in params:
flag_pattern = re.compile(r'\.Flags\(\)\.\w+\("' + re.escape(pname) + '"')
for i, line in enumerate(lines):
if flag_pattern.search(line):
if i > decl_start:
flags_to_move.append((pname, i, decl_start, os.path.basename(found_file)))
break
# Clear hints for migrated/skipped tools (even if no source files changed)
if migrated_tools:
for tool_name in migrated_tools:
if tool_name in hints.get("tools", {}):
hints["tools"][tool_name].pop("parameters", None)
if not hints["tools"][tool_name]:
del hints["tools"][tool_name]
if not hints.get("tools"):
hints["tools"] = {}
json.dump(hints, open(hint_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
print(f"{product}: cleared hints for {len(migrated_tools)} tools ({len(overlays) - len(migrated_tools)} remaining)")
if not modified_files:
if not migrated_tools:
print(f"{product}: no changes needed")
return True
# Write modified files and ensure corecmd import
for fp in modified_files:
src = file_contents[fp]
if "internal/corecmd" not in src:
m = re.search(r'(import \(\n(?:.*\n)*?\))', src)
if m:
block = m.group(1)
new_block = block.rstrip(")\n") + '\n\n\t"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"\n)'
src = src[:m.start()] + new_block + src[m.end():]
print(f" added corecmd import to {os.path.basename(fp)}")
open(fp, "w", encoding="utf-8").write(src)
if flags_to_move:
print(f" ⚠ {len(flags_to_move)} flags registered AFTER DeclareLeafMetadata — need manual reorder:")
for fname, reg_line, decl_line, fname_file in flags_to_move:
print(f" --{fname} in {fname_file}: reg line {reg_line + 1} > decl line {decl_line + 1}")
return True
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: migrate_product_params.py <product>")
sys.exit(1)
ok = migrate_product(sys.argv[1])
sys.exit(0 if ok else 1)