Phase 3 of identity-deregistry. BuildEffectiveCommandRegistry now builds the EffectiveCommandRegistry from CollectIdentitySpecs(root) instead of the embedded reviewed registry, and the registry source is removed atomically: - delete internal/cli/schema_command_registry/ (registry.json + products/), schema_command_registry.schema.json, the three //go:embed directives, and the registry-only loaders/validators (loadReviewedCommandRegistry, decodeCommandRegistry, ValidateCommandRegistrySource, shard assemble/merge helpers, ReviewedCommandRegistryMergedJSON/SourceHash, ReviewedCommandSpecs) - keep CommandSpec/CommandRegistry/EffectiveCommandRegistry, newEffectiveCommandRegistry/indexCommandSpecs, and SourceHash; the collector-built effective hash is byte-identical to the reviewed one, so catalog surface_hash/source_hash are unchanged - convert the Phase 2 dual-run gate into TestCollectedIdentityIsValidSingleSource: collected specs non-empty, no missing primaries, effective build succeeds, SourceHash stable across repeated collection walks - generators: catalog -surface and agent-metadata -registry/-surface become fail-closed retired valves; outputguard no longer protects the registry paths; fetch_mcp_metadata derives interface refs from collected identity instead of the merged registry JSON - retire scripts/policy/check-schema-command-registry.sh and its Makefile invocation; generate-schema/check-generated-drift now fail closed if schema_command_registry/ reappears - update AGENTS.md, docs/reference.md, and in-code reviewed-input notes
185 lines
5.6 KiB
Go
185 lines
5.6 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 helpers
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal"
|
|
)
|
|
|
|
func TestDeclareLeafMetadataRejectsExecutionSurface(t *testing.T) {
|
|
cmd := &cobra.Command{Use: "x", Short: "x"}
|
|
schema := LeafContract{
|
|
Identity: contract.ToolIdentitySpec{
|
|
ProductID: "dev", Name: "create_thing", CanonicalPath: "dev.create_thing",
|
|
CLIPath: "dev create", PrimaryCLIPath: "dev create",
|
|
},
|
|
Description: "d",
|
|
Interface: &contract.InterfaceSpec{Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "p", RPCName: "t"}},
|
|
Selection: contract.SelectionSpec{
|
|
AgentSummary: "s",
|
|
UseWhen: []string{"u"},
|
|
AvoidWhen: []string{"a"},
|
|
Examples: []string{"e"},
|
|
},
|
|
}
|
|
mustPanic := func(name string, fn func()) {
|
|
t.Helper()
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatalf("%s: expected panic", name)
|
|
}
|
|
}()
|
|
fn()
|
|
}
|
|
mustPanic("flags", func() {
|
|
DeclareLeafMetadata(cmd, LeafSpec{Contract: schema, Flags: []LeafFlag{{Name: "f"}}})
|
|
})
|
|
mustPanic("runE", func() {
|
|
DeclareLeafMetadata(cmd, LeafSpec{Contract: schema, RunE: func(*cobra.Command, []string) error { return nil }})
|
|
})
|
|
mustPanic("empty schema", func() {
|
|
DeclareLeafMetadata(cmd, LeafSpec{})
|
|
})
|
|
// Validate is the one execution hook allowed in metadata mode (PreRunE,
|
|
// before ConfirmSafety). It must not panic.
|
|
DeclareLeafMetadata(&cobra.Command{Use: "y", Short: "y", RunE: func(*cobra.Command, []string) error { return nil }}, LeafSpec{
|
|
Contract: schema,
|
|
Validate: func(*cobra.Command, []string) error { return nil },
|
|
})
|
|
}
|
|
|
|
func TestDeclareLeafMetadataDoesNotRewriteRunE(t *testing.T) {
|
|
run := func(*cobra.Command, []string) error { return nil }
|
|
cmd := &cobra.Command{Use: "x", Short: "x", RunE: run}
|
|
before := cmd.RunE
|
|
DeclareLeafMetadata(cmd, LeafSpec{
|
|
Safety: aitableSafetyRead(),
|
|
Contract: LeafContract{
|
|
Identity: contract.ToolIdentitySpec{
|
|
ProductID: "dev",
|
|
Name: "create_thing",
|
|
CanonicalPath: "dev.create_thing",
|
|
CLIPath: "dev create",
|
|
PrimaryCLIPath: "dev create",
|
|
},
|
|
|
|
Description: "d",
|
|
Interface: aitableMCPInterface("tool"),
|
|
Selection: contract.SelectionSpec{
|
|
AgentSummary: "s",
|
|
UseWhen: []string{"u"},
|
|
AvoidWhen: []string{"a"},
|
|
Examples: []string{"e"},
|
|
},
|
|
},
|
|
})
|
|
if !contractfinal.HasRuntimeContractFinal(cmd) {
|
|
t.Fatal("expected ContractFinal")
|
|
}
|
|
// function values are not comparable; ensure pointer identity via uintptr trick is unnecessary —
|
|
// just check RunE is still non-nil and command still has same Use.
|
|
if cmd.RunE == nil {
|
|
t.Fatal("RunE must remain set")
|
|
}
|
|
_ = before
|
|
if cmd.Use != "x" {
|
|
t.Fatalf("Use mutated: %q", cmd.Use)
|
|
}
|
|
}
|
|
|
|
// TestAitableDeclareLeafMetadataCoversRegistryHelpers asserts the aitable
|
|
// subtree is a complete identity source after the reviewed registry
|
|
// retirement: the collector finds a non-empty aitable identity set, every
|
|
// collected primary CLI path resolves back to a ContractFinal-bearing leaf in
|
|
// the tree, and every canonical stays inside the aitable product. Leaves
|
|
// without identity (compatibility aliases, hint stubs) remain allowed — they
|
|
// resolve to a primary, matching the collector's NoIdentity treatment.
|
|
func TestAitableDeclareLeafMetadataCoversRegistryHelpers(t *testing.T) {
|
|
root := newAitableCommand()
|
|
specs, _, err := cli.CollectIdentitySpecs(root)
|
|
if err != nil {
|
|
t.Fatalf("collect aitable identity specs: %v", err)
|
|
}
|
|
if len(specs) == 0 {
|
|
t.Fatal("expected identity-bearing aitable leaves")
|
|
}
|
|
for _, spec := range specs {
|
|
if !strings.HasPrefix(spec.CanonicalPath, "aitable.") {
|
|
t.Fatalf("canonical %q escapes the aitable product", spec.CanonicalPath)
|
|
}
|
|
// Shortcut paths (+...) stay on bind-time decls until Shortcut.Schema migration.
|
|
if containsPlus(spec.PrimaryCLIPath) {
|
|
continue
|
|
}
|
|
leaf := findCLIPath(root, spec.PrimaryCLIPath)
|
|
if leaf == nil {
|
|
t.Fatalf("primary path %q does not resolve in the aitable tree", spec.PrimaryCLIPath)
|
|
}
|
|
if !contractfinal.HasRuntimeContractFinal(leaf) {
|
|
t.Fatalf("primary path %q is missing ContractFinal", spec.PrimaryCLIPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
func findCLIPath(root *cobra.Command, cliPath string) *cobra.Command {
|
|
parts := splitCLI(cliPath)
|
|
if len(parts) == 0 || parts[0] != "aitable" {
|
|
return nil
|
|
}
|
|
cur := root
|
|
for _, p := range parts[1:] {
|
|
next, _, err := cur.Find(append([]string{}, p))
|
|
if err != nil || next == nil {
|
|
return nil
|
|
}
|
|
cur = next
|
|
}
|
|
return cur
|
|
}
|
|
|
|
func containsPlus(cliPath string) bool {
|
|
for _, p := range splitCLI(cliPath) {
|
|
if len(p) > 0 && p[0] == '+' {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func splitCLI(cliPath string) []string {
|
|
out := make([]string, 0)
|
|
cur := ""
|
|
for _, r := range cliPath {
|
|
if r == ' ' {
|
|
if cur != "" {
|
|
out = append(out, cur)
|
|
cur = ""
|
|
}
|
|
continue
|
|
}
|
|
cur += string(r)
|
|
}
|
|
if cur != "" {
|
|
out = append(out, cur)
|
|
}
|
|
return out
|
|
}
|