Files
dingtalk-workspace-cli/internal/helpers/aitable_schema_test.go
玉澜 b1cefba808 refactor(schema): retire reviewed registry; collector is the single identity source
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
2026-08-03 16:18:20 +08:00

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
}