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:
1 parent
8791088027
commit
e705012011
69 files changed
+4966
-2078
No files matched your search
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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", "", "员工在企业内的名称 (必填)")
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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 表达式 (必填)")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 正文"},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user