Files
dingtalk-workspace-cli/internal/compat/phantom_guard_test.go
修雨 49637d982e feat(compat): hide phantom override commands from --help (tool-existence guard)
Override leaves whose backing MCP tool isn't actually deployed rendered in
`dws <svc> --help` but failed at invocation with "tool not found" (43 phantom
commands across 391 overrides; attendance declared 38, only 4 deployed).

BuildDynamicCommands now takes an existingTools oracle (CLI slug -> live tool
set from the tools/ cache). A leaf whose tool is missing from its resolved
server's set is marked Hidden; groups left childless collapse. Safety rails:
acts only when a server's tool set is KNOWN and non-empty (cold cache / overlay
/ plugin paths pass nil and no-op, never blanking the tree); serverOverride
leaves resolve against the target server; pipeline leaves are never hidden.

Adds scripts/dev/check-phantom-overrides.py as a publish-time gate, and
phantom_guard_test.go covering hide / cold-cache-keep / serverOverride / pipeline
/ empty-group-collapse.
2026-06-29 10:53:03 +08:00

258 lines
8.4 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 compat
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/market"
"github.com/spf13/cobra"
)
// findLeaf returns the first leaf command with the given Use anywhere under
// root (depth-first), or nil.
func findLeaf(root *cobra.Command, name string) *cobra.Command {
for _, c := range root.Commands() {
if c.Name() == name {
return c
}
if got := findLeaf(c, name); got != nil {
return got
}
}
return nil
}
func toolSet(names ...string) map[string]struct{} {
s := make(map[string]struct{}, len(names))
for _, n := range names {
s[n] = struct{}{}
}
return s
}
// attendanceLike builds one server with a real tool and a phantom tool, the
// exact shape of the production drift (e.g. attendance: only a handful of the
// declared overrides map to deployed tools).
func attendanceLike() []market.ServerDescriptor {
return []market.ServerDescriptor{
{
Endpoint: "https://endpoint-attendance",
CLI: market.CLIOverlay{
ID: "attendance",
Command: "attendance",
ToolOverrides: map[string]market.CLIToolOverride{
"get_attendance_summary": {CLIName: "summary"}, // real
"get_overtime_rule": {CLIName: "overtime"}, // phantom
},
},
},
}
}
// TestPhantomGuard_HidesWhenToolSetKnown is the core behaviour: when the live
// tool set is known and non-empty, a leaf whose backing tool is absent is
// hidden from --help while the real leaf stays visible.
func TestPhantomGuard_HidesWhenToolSetKnown(t *testing.T) {
t.Parallel()
existing := map[string]map[string]struct{}{
"attendance": toolSet("get_attendance_summary"), // overtime is NOT deployed
}
cmds := BuildDynamicCommands(attendanceLike(), executor.EchoRunner{}, nil, existing)
summary := findLeaf(cmds[0], "summary")
overtime := findLeaf(cmds[0], "overtime")
if summary == nil || overtime == nil {
t.Fatalf("both leaves should still be registered (invocable); summary=%v overtime=%v", summary, overtime)
}
if summary.Hidden {
t.Error("real command 'summary' must stay visible in --help")
}
if !overtime.Hidden {
t.Error("phantom command 'overtime' must be hidden from --help")
}
}
// TestPhantomGuard_ColdCacheKeepsEverything is the safety rail that the prior
// (source-blind) plan got wrong: with no tool set available (nil map), the
// guard must do nothing — never blank the command tree on a cold cache.
func TestPhantomGuard_ColdCacheKeepsEverything(t *testing.T) {
t.Parallel()
cmds := BuildDynamicCommands(attendanceLike(), executor.EchoRunner{}, nil, nil)
for _, name := range []string{"summary", "overtime"} {
leaf := findLeaf(cmds[0], name)
if leaf == nil {
t.Fatalf("%q should be registered", name)
}
if leaf.Hidden {
t.Errorf("cold cache (nil existingTools) must not hide %q", name)
}
}
}
// TestPhantomGuard_EmptyOrAbsentSetKeepsEverything: an empty set for a server,
// or a server missing from the map entirely, both mean "unknown" — keep all.
func TestPhantomGuard_EmptyOrAbsentSetKeepsEverything(t *testing.T) {
t.Parallel()
cases := []struct {
name string
existing map[string]map[string]struct{}
}{
{"empty set for server", map[string]map[string]struct{}{"attendance": {}}},
{"server absent from map", map[string]map[string]struct{}{"someother": toolSet("x")}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cmds := BuildDynamicCommands(attendanceLike(), executor.EchoRunner{}, nil, tc.existing)
for _, name := range []string{"summary", "overtime"} {
leaf := findLeaf(cmds[0], name)
if leaf == nil {
t.Fatalf("%q should be registered", name)
}
if leaf.Hidden {
t.Errorf("%s: must not hide %q when tool set is unknown", tc.name, name)
}
}
})
}
}
// TestPhantomGuard_ServerOverrideRoutesToTargetSet: a leaf with serverOverride
// must be checked against the TARGET server's tool set, not the host's. This is
// what prevents false-flagging legit cross-server routes (contact→hrmregister,
// doc→doc-comment).
func TestPhantomGuard_ServerOverrideRoutesToTargetSet(t *testing.T) {
t.Parallel()
servers := []market.ServerDescriptor{
{
Endpoint: "https://endpoint-contact",
CLI: market.CLIOverlay{
ID: "contact",
Command: "contact",
ToolOverrides: map[string]market.CLIToolOverride{
// routed to hrmregister; the tool lives there, not in contact
"get_roster": {CLIName: "roster", ServerOverride: "hrmregister"},
},
},
},
}
// contact's own set is empty of get_roster, but hrmregister has it.
existing := map[string]map[string]struct{}{
"contact": toolSet("search_user"),
"hrmregister": toolSet("get_roster"),
}
cmds := BuildDynamicCommands(servers, executor.EchoRunner{}, nil, existing)
roster := findLeaf(cmds[0], "roster")
if roster == nil {
t.Fatal("roster leaf should be registered")
}
if roster.Hidden {
t.Error("serverOverride leaf must resolve against the target server's set and stay visible")
}
}
// TestPhantomGuard_EmptyGroupCollapses: a group all of whose overrides are
// hidden:true (so none of its leaves are built) must itself be hidden from
// help, while a group keeping at least one visible leaf stays. This runs
// regardless of the tools-cache oracle (envelope hidden:true is cache-
// independent), so existingTools is nil here.
func TestPhantomGuard_EmptyGroupCollapses(t *testing.T) {
t.Parallel()
servers := []market.ServerDescriptor{
{
Endpoint: "https://endpoint-attendance",
CLI: market.CLIOverlay{
ID: "attendance",
Command: "attendance",
Groups: map[string]market.CLIGroupDef{
"vacation": {Description: "假期管理"}, // all leaves hidden -> collapse
"record": {Description: "考勤记录"}, // keeps a visible leaf
},
ToolOverrides: map[string]market.CLIToolOverride{
"get_leave_types": {CLIName: "types", Group: "vacation", Hidden: true},
"get_leave_balance_quota": {CLIName: "balance", Group: "vacation", Hidden: true},
"get_user_attendance_record": {CLIName: "get", Group: "record"},
},
},
},
}
cmds := BuildDynamicCommands(servers, executor.EchoRunner{}, nil, nil)
vacation := findGroup(cmds[0], "vacation")
record := findGroup(cmds[0], "record")
if vacation == nil || record == nil {
t.Fatalf("both groups should exist as commands; vacation=%v record=%v", vacation, record)
}
if !vacation.Hidden {
t.Error("group 'vacation' with only hidden leaves must collapse (be hidden)")
}
if record.Hidden {
t.Error("group 'record' with a visible leaf must stay visible")
}
}
// findGroup returns a direct child of root with the given name (groups attach
// directly under the product root).
func findGroup(root *cobra.Command, name string) *cobra.Command {
for _, c := range root.Commands() {
if c.Name() == name {
return c
}
}
return nil
}
// TestPhantomGuard_PipelineLeafNeverHidden: pipeline leaves orchestrate multiple
// tools and have no single backing toolName, so the guard must skip them even
// when the override key is not a deployed tool.
func TestPhantomGuard_PipelineLeafNeverHidden(t *testing.T) {
t.Parallel()
servers := []market.ServerDescriptor{
{
Endpoint: "https://endpoint-im",
CLI: market.CLIOverlay{
ID: "im",
Command: "im",
ToolOverrides: map[string]market.CLIToolOverride{
"download_media": {
CLIName: "download-media",
Pipeline: []market.PipelineStep{
{Tool: "get_resource_download_url"},
},
},
},
},
},
}
// download_media itself is not a deployed tool name, but the pipeline is.
existing := map[string]map[string]struct{}{
"im": toolSet("get_resource_download_url"),
}
cmds := BuildDynamicCommands(servers, executor.EchoRunner{}, nil, existing)
dl := findLeaf(cmds[0], "download-media")
if dl == nil {
t.Fatal("download-media leaf should be registered")
}
if dl.Hidden {
t.Error("pipeline leaf must never be hidden by the tool-existence guard")
}
}