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.
166 lines
6.1 KiB
Go
166 lines
6.1 KiB
Go
// 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 (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func TestBuildManualAgentSelectionEvalFixtureUsesReviewedScenariosAndRealCommands(t *testing.T) {
|
|
bound := manualAgentSelectionBoundFixture()
|
|
hints := manualAgentHintSetFixture()
|
|
|
|
fixture, report, err := BuildManualAgentSelectionEvalFixture(bound, hints)
|
|
if err != nil {
|
|
t.Fatalf("BuildManualAgentSelectionEvalFixture() error = %v", err)
|
|
}
|
|
if report.Tools != 1 || report.PositiveAssertions != 1 || report.NegativeAssertions != 1 {
|
|
t.Fatalf("selection report = %+v", report)
|
|
}
|
|
if report.FixtureSHA256 == "" || len(fixture.Cases) != 2 {
|
|
t.Fatalf("selection fixture = %+v, report = %+v", fixture, report)
|
|
}
|
|
positive, negative := fixture.Cases[0], fixture.Cases[1]
|
|
if positive.ExpectedCanonical != "sample.search_items" || positive.ForbiddenCanonical != "" {
|
|
t.Fatalf("positive case = %+v", positive)
|
|
}
|
|
if negative.ForbiddenCanonical != "sample.search_items" || negative.ExpectedCanonical != "" {
|
|
t.Fatalf("negative case = %+v", negative)
|
|
}
|
|
if !reflect.DeepEqual(positive.CandidateCanonicals, []string{"sample.search_items"}) {
|
|
t.Fatalf("positive candidates = %#v", positive.CandidateCanonicals)
|
|
}
|
|
|
|
_, repeated, err := BuildManualAgentSelectionEvalFixture(bound, hints)
|
|
if err != nil || repeated.FixtureSHA256 != report.FixtureSHA256 {
|
|
t.Fatalf("repeated fixture digest = %q, err=%v; want %q", repeated.FixtureSHA256, err, report.FixtureSHA256)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageBuildManualAgentSelectionEvalFixtureRejectsFactualContractDrift(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*BoundCommandRegistry, *ManualAgentHintSet)
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "missing reviewed tool",
|
|
mutate: func(_ *BoundCommandRegistry, hints *ManualAgentHintSet) {
|
|
delete(hints.Tools, "sample.search_items")
|
|
},
|
|
wantErr: "selection tools do not exactly match",
|
|
},
|
|
{
|
|
name: "unbound primary command",
|
|
mutate: func(bound *BoundCommandRegistry, _ *ManualAgentHintSet) {
|
|
item := bound.ByCanonical["sample.search_items"]
|
|
item.PrimaryCommand = nil
|
|
bound.ByCanonical["sample.search_items"] = item
|
|
},
|
|
wantErr: "no bound primary Cobra command",
|
|
},
|
|
{
|
|
name: "literal positive negative contradiction",
|
|
mutate: func(_ *BoundCommandRegistry, hints *ManualAgentHintSet) {
|
|
item := hints.Tools["sample.search_items"]
|
|
item.AvoidWhen = append([]string(nil), item.UseWhen...)
|
|
hints.Tools["sample.search_items"] = item
|
|
},
|
|
wantErr: "same literal positive and negative",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
bound := manualAgentSelectionBoundFixture()
|
|
hints := manualAgentHintSetFixture()
|
|
test.mutate(&bound, &hints)
|
|
_, _, err := BuildManualAgentSelectionEvalFixture(bound, hints)
|
|
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
|
|
t.Fatalf("error = %v, want containing %q", err, test.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildManualAgentSelectionEvalFixtureRejectsOneScenarioWithTwoExpectedTools(t *testing.T) {
|
|
bound := manualAgentSelectionBoundFixture()
|
|
root := bound.ByCanonical["sample.search_items"].PrimaryCommand.Root()
|
|
create := &cobra.Command{Use: "create", RunE: func(*cobra.Command, []string) error { return nil }}
|
|
exactSchemaCommand(root, "sample item").AddCommand(create)
|
|
createSpec := BoundCommandSpec{
|
|
CommandSpec: CommandSpec{
|
|
CanonicalPath: "sample.create_item",
|
|
PrimaryCLIPath: "sample item create",
|
|
},
|
|
PrimaryCommand: create,
|
|
}
|
|
bound.Commands = append(bound.Commands, createSpec)
|
|
bound.ByCanonical[createSpec.CanonicalPath] = createSpec
|
|
bound.ByCLIPath[createSpec.PrimaryCLIPath] = createSpec
|
|
|
|
hints := manualAgentHintSetFixture()
|
|
search := hints.Tools["sample.search_items"]
|
|
createHint := search
|
|
createHint.AgentSummary = "Create a sample item"
|
|
createHint.Examples = []string{"dws sample item create"}
|
|
createHint.UseWhen = append([]string(nil), search.UseWhen...)
|
|
createHint.AvoidWhen = []string{"An existing sample item must be found"}
|
|
hints.Tools[createSpec.CanonicalPath] = createHint
|
|
|
|
_, _, err := BuildManualAgentSelectionEvalFixture(bound, hints)
|
|
if err == nil || !strings.Contains(err.Error(), "conflicting literal expectations") {
|
|
t.Fatalf("error = %v, want literal expectation conflict", err)
|
|
}
|
|
}
|
|
|
|
func manualAgentSelectionBoundFixture() BoundCommandRegistry {
|
|
_, leaf := manualSchemaHintTestTree()
|
|
spec := BoundCommandSpec{
|
|
CommandSpec: CommandSpec{
|
|
CanonicalPath: "sample.search_items",
|
|
PrimaryCLIPath: "sample item search",
|
|
},
|
|
PrimaryCommand: leaf,
|
|
}
|
|
return BoundCommandRegistry{
|
|
Commands: []BoundCommandSpec{spec},
|
|
ByCanonical: map[string]BoundCommandSpec{spec.CanonicalPath: spec},
|
|
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)
|
|
}
|
|
}
|