Allow clearing interface_type and expanding constraint group members so MCP-pin retirement and declare≡execute alias groups stay backward-compatible. Close platform coverage gaps with TestCrossPlatformCoverage* and bump the ContractFinal consistency count to 847. Co-authored-by: Cursor <cursoragent@cursor.com>
613 lines
22 KiB
Go
613 lines
22 KiB
Go
// Copyright 2026 Alibaba Group
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
||
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"math"
|
||
"os"
|
||
"path/filepath"
|
||
"reflect"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/syncdata"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
|
||
"github.com/spf13/cobra"
|
||
)
|
||
|
||
func TestCrossPlatformCoverageLoadRegistryInterfaceRefsCollectsIdentity(t *testing.T) {
|
||
var stderr bytes.Buffer
|
||
refs := loadRegistryInterfaceRefs(&stderr)
|
||
if len(refs) == 0 {
|
||
t.Fatal("loadRegistryInterfaceRefs() returned no collected commands")
|
||
}
|
||
|
||
got, ok := refs["calendar.list_calendars"]
|
||
if !ok {
|
||
t.Fatal("calendar.list_calendars missing from collected command identity")
|
||
}
|
||
if got["product_id"] != "calendar" || got["rpc_name"] != "list_calendars" {
|
||
t.Fatalf("calendar.list_calendars ref = %#v", got)
|
||
}
|
||
|
||
direct, err := collectedIdentityInterfaceRefs()
|
||
if err != nil {
|
||
t.Fatalf("collectedIdentityInterfaceRefs() error = %v", err)
|
||
}
|
||
if len(direct) == 0 || direct["calendar.list_calendars"]["rpc_name"] != "list_calendars" {
|
||
t.Fatalf("collectedIdentityInterfaceRefs() = %#v", direct["calendar.list_calendars"])
|
||
}
|
||
|
||
prevRegistry := registrySource
|
||
prevCollect := collectIdentitySpecs
|
||
t.Cleanup(func() {
|
||
registrySource = prevRegistry
|
||
collectIdentitySpecs = prevCollect
|
||
})
|
||
registrySource = func() (map[string]map[string]string, error) {
|
||
return nil, errors.New("collect boom")
|
||
}
|
||
stderr.Reset()
|
||
if got := loadRegistryInterfaceRefs(&stderr); len(got) != 0 || !strings.Contains(stderr.String(), "cannot collect command identity") {
|
||
t.Fatalf("loadRegistryInterfaceRefs error path = %#v stderr=%q", got, stderr.String())
|
||
}
|
||
|
||
collectIdentitySpecs = func(*cobra.Command) ([]cli.CommandSpec, cli.IdentityCollectionReport, error) {
|
||
return nil, cli.IdentityCollectionReport{}, errors.New("walk boom")
|
||
}
|
||
if _, err := collectedIdentityInterfaceRefs(); err == nil || !strings.Contains(err.Error(), "collect command identity") {
|
||
t.Fatalf("collectedIdentityInterfaceRefs wrap error = %v", err)
|
||
}
|
||
collectIdentitySpecs = func(*cobra.Command) ([]cli.CommandSpec, cli.IdentityCollectionReport, error) {
|
||
return []cli.CommandSpec{
|
||
{CanonicalPath: ""},
|
||
{CanonicalPath: "nodot"},
|
||
{CanonicalPath: "doc.create"},
|
||
}, cli.IdentityCollectionReport{}, nil
|
||
}
|
||
gotRefs, err := collectedIdentityInterfaceRefs()
|
||
if err != nil || len(gotRefs) != 1 || gotRefs["doc.create"]["rpc_name"] != "create" {
|
||
t.Fatalf("collectedIdentityInterfaceRefs skip = %#v err=%v", gotRefs, err)
|
||
}
|
||
}
|
||
|
||
func TestBuildCrossServerRefs(t *testing.T) {
|
||
registryMap := map[string]map[string]string{
|
||
"aitable.advperm_enable": {"product_id": "aitable", "rpc_name": "advperm_enable"},
|
||
"aitable.advperm_disable": {"product_id": "aitable", "rpc_name": "advperm_disable"},
|
||
"doc.copy_document": {"product_id": "doc", "rpc_name": "copy_document"},
|
||
}
|
||
prevTools := map[string]map[string]any{
|
||
// Fan-out: two canonicals share one live tool; insertion order must
|
||
// not affect the sorted result.
|
||
"aitable.advperm_enable": {
|
||
"interface_ref": map[string]any{"product_id": "aitable-helper", "rpc_name": "set_advanced_permission"},
|
||
},
|
||
"aitable.advperm_disable": {
|
||
"interface_ref": map[string]any{"product_id": "aitable-helper", "rpc_name": "set_advanced_permission"},
|
||
},
|
||
// Identity ref (live key == canonical) needs no cross entry.
|
||
"doc.copy_document": {
|
||
"interface_ref": map[string]any{"product_id": "doc", "rpc_name": "copy_document"},
|
||
},
|
||
// Not in the registry: must be ignored.
|
||
"ghost.tool": {
|
||
"interface_ref": map[string]any{"product_id": "ghost-helper", "rpc_name": "haunt"},
|
||
},
|
||
}
|
||
got := buildCrossServerRefs(prevTools, registryMap)
|
||
want := map[string][]string{
|
||
"aitable-helper.set_advanced_permission": {"aitable.advperm_disable", "aitable.advperm_enable"},
|
||
}
|
||
if len(got) != len(want) {
|
||
t.Fatalf("index = %#v, want %#v", got, want)
|
||
}
|
||
for k, v := range want {
|
||
if gv := got[k]; len(gv) != len(v) || gv[0] != v[0] || gv[1] != v[1] {
|
||
t.Fatalf("index[%q] = %v, want %v", k, gv, v)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBuildCrossServerRefsSkipsMalformedRefs(t *testing.T) {
|
||
registryMap := map[string]map[string]string{
|
||
"a.x": {"product_id": "a", "rpc_name": "x"},
|
||
"a.y": {"product_id": "a", "rpc_name": "y"},
|
||
"a.z": {"product_id": "a", "rpc_name": "z"},
|
||
}
|
||
prevTools := map[string]map[string]any{
|
||
"a.x": {"interface_ref": "not-a-map"},
|
||
"a.y": {"interface_ref": map[string]any{"product_id": "", "rpc_name": "r"}},
|
||
"a.z": {"title": "no ref at all"},
|
||
}
|
||
if got := buildCrossServerRefs(prevTools, registryMap); len(got) != 0 {
|
||
t.Fatalf("index = %#v, want empty", got)
|
||
}
|
||
}
|
||
|
||
func TestRunRefreshesCrossServerTools(t *testing.T) {
|
||
registry := func() (map[string]map[string]string, error) {
|
||
return map[string]map[string]string{
|
||
"aitable.advperm_enable": {"product_id": "aitable", "rpc_name": "advperm_enable"},
|
||
"aitable.advperm_disable": {"product_id": "aitable", "rpc_name": "advperm_disable"},
|
||
}, nil
|
||
}
|
||
servers := []syncdata.ServerInfo{{ID: "aitable-helper", Endpoint: "https://helper.example"}}
|
||
lister := &fakeLister{
|
||
results: map[string]transport.ToolsListResult{
|
||
"https://helper.example": {Tools: []transport.ToolDescriptor{
|
||
{Name: "set_advanced_permission", Title: "live title", Description: "live desc"},
|
||
}},
|
||
},
|
||
}
|
||
stubDeps(t, "env-token", nil, servers, lister, registry)
|
||
|
||
output := filepath.Join(t.TempDir(), "snapshot.json")
|
||
prev := `{"tools":{
|
||
"aitable.advperm_enable":{"title":"stale","interface_ref":{"product_id":"aitable-helper","rpc_name":"set_advanced_permission"}},
|
||
"aitable.advperm_disable":{"title":"stale","interface_ref":{"product_id":"aitable-helper","rpc_name":"set_advanced_permission"}}
|
||
}}`
|
||
if err := os.WriteFile(output, []byte(prev), 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
var stderr bytes.Buffer
|
||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||
}
|
||
if !strings.Contains(stderr.String(), "cross-server ref index: 1 live keys") {
|
||
t.Fatalf("stderr = %q, want cross-server index log", stderr.String())
|
||
}
|
||
|
||
data, err := os.ReadFile(output)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var snapshot struct {
|
||
Tools map[string]map[string]any `json:"tools"`
|
||
}
|
||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for _, canonical := range []string{"aitable.advperm_enable", "aitable.advperm_disable"} {
|
||
entry := snapshot.Tools[canonical]
|
||
if entry["title"] != "live title" || entry["description"] != "live desc" {
|
||
t.Fatalf("%s = %#v, want live refresh", canonical, entry)
|
||
}
|
||
ref := entry["interface_ref"].(map[string]any)
|
||
if ref["product_id"] != "aitable-helper" || ref["rpc_name"] != "set_advanced_permission" {
|
||
t.Fatalf("%s reviewed ref lost: %#v", canonical, ref)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestRunCrossOwnedCanonicalIgnoresNameCoincidence:canonical 拥有评审过的
|
||
// 跨 server 身份时,另一 server 上恰好同名的工具不得直连覆盖其元数据——
|
||
// 数据源只能是评审身份指向的 live 工具。
|
||
func TestRunCrossOwnedCanonicalIgnoresNameCoincidence(t *testing.T) {
|
||
registry := func() (map[string]map[string]string, error) {
|
||
return map[string]map[string]string{
|
||
"aitable.advperm_enable": {"product_id": "aitable", "rpc_name": "advperm_enable"},
|
||
}, nil
|
||
}
|
||
servers := []syncdata.ServerInfo{
|
||
{ID: "aitable", Endpoint: "https://aitable.example"},
|
||
{ID: "aitable-helper", Endpoint: "https://helper.example"},
|
||
}
|
||
lister := &fakeLister{
|
||
results: map[string]transport.ToolsListResult{
|
||
// 同名巧合:aitable server 上恰好也有 advperm_enable。
|
||
"https://aitable.example": {Tools: []transport.ToolDescriptor{
|
||
{Name: "advperm_enable", Title: "coincidence title", Description: "coincidence desc"},
|
||
}},
|
||
"https://helper.example": {Tools: []transport.ToolDescriptor{
|
||
{Name: "set_advanced_permission", Title: "owner title", Description: "owner desc"},
|
||
}},
|
||
},
|
||
}
|
||
stubDeps(t, "env-token", nil, servers, lister, registry)
|
||
|
||
output := filepath.Join(t.TempDir(), "snapshot.json")
|
||
prev := `{"tools":{"aitable.advperm_enable":{"title":"stale","interface_ref":{"product_id":"aitable-helper","rpc_name":"set_advanced_permission"}}}}`
|
||
if err := os.WriteFile(output, []byte(prev), 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
var stderr bytes.Buffer
|
||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||
}
|
||
|
||
data, err := os.ReadFile(output)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var snapshot struct {
|
||
Tools map[string]map[string]any `json:"tools"`
|
||
}
|
||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
entry := snapshot.Tools["aitable.advperm_enable"]
|
||
if entry["title"] != "owner title" || entry["description"] != "owner desc" {
|
||
t.Fatalf("entry = %#v, want reviewed-identity source to win over name coincidence", entry)
|
||
}
|
||
}
|
||
|
||
func TestMergeLiveMCPToolRefreshesExistingMetadata(t *testing.T) {
|
||
const canonical = "calendar.list_calendars"
|
||
reviewedRef := map[string]any{
|
||
"product_id": "calendar-helper",
|
||
"rpc_name": "list_user_calendars",
|
||
}
|
||
allTools := map[string]map[string]any{
|
||
canonical: {
|
||
"title": "old title",
|
||
"description": "old description",
|
||
"interface_ref": reviewedRef,
|
||
"parameters": map[string]any{
|
||
"stale": map[string]any{"type": "string"},
|
||
},
|
||
},
|
||
}
|
||
live := transport.ToolDescriptor{
|
||
Name: "list_calendars",
|
||
Title: "new title",
|
||
Description: "new description",
|
||
InputSchema: map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"cursor": map[string]any{
|
||
"type": "string",
|
||
"description": "next page cursor",
|
||
},
|
||
},
|
||
"required": []any{"cursor"},
|
||
},
|
||
}
|
||
fallbackRef := map[string]string{
|
||
"product_id": "calendar",
|
||
"rpc_name": "list_calendars",
|
||
}
|
||
|
||
mergeLiveMCPTool(allTools, canonical, live, fallbackRef)
|
||
|
||
got := allTools[canonical]
|
||
if got["title"] != "new title" || got["description"] != "new description" {
|
||
t.Fatalf("live metadata was not refreshed: %#v", got)
|
||
}
|
||
if !reflect.DeepEqual(got["interface_ref"], reviewedRef) {
|
||
t.Fatalf("interface_ref = %#v, want reviewed mapping %#v", got["interface_ref"], reviewedRef)
|
||
}
|
||
params, ok := got["parameters"].(map[string]map[string]any)
|
||
if !ok {
|
||
t.Fatalf("parameters type = %T, want refreshed parameter map", got["parameters"])
|
||
}
|
||
if _, stale := params["stale"]; stale {
|
||
t.Fatalf("stale parameter survived refresh: %#v", params)
|
||
}
|
||
if cursor := params["cursor"]; cursor["type"] != "string" || cursor["description"] != "next page cursor" || cursor["required"] != true {
|
||
t.Fatalf("cursor parameter = %#v", cursor)
|
||
}
|
||
}
|
||
|
||
func TestBuildCoverageReportsFailedServices(t *testing.T) {
|
||
got := buildCoverage(26, []string{"doc", "sheet"}, 800, 813, 40)
|
||
if got["source_services"] != 26 {
|
||
t.Fatalf("source_services = %v, want 26", got["source_services"])
|
||
}
|
||
if got["snapshot_services"] != 24 {
|
||
t.Fatalf("snapshot_services = %v, want 24 (26 sources - 2 failures)", got["snapshot_services"])
|
||
}
|
||
if !reflect.DeepEqual(got["missing_services"], []string{"doc", "sheet"}) {
|
||
t.Fatalf("missing_services = %#v, want failed service IDs", got["missing_services"])
|
||
}
|
||
// matched 必须剔除 stub 占位,unmatched 据实等于 stub 数。
|
||
if got["matched_tools"] != 773 || got["unmatched_tools"] != 40 {
|
||
t.Fatalf("matched/unmatched = %v/%v, want 773/40 (813 surface - 40 stubs)", got["matched_tools"], got["unmatched_tools"])
|
||
}
|
||
if got["source_tools"] != 800 || got["surface_tools"] != 813 {
|
||
t.Fatalf("tool counts = %#v", got)
|
||
}
|
||
}
|
||
|
||
func TestBuildCoverageFullSnapshotHasNoMissingServices(t *testing.T) {
|
||
got := buildCoverage(26, nil, 813, 813, 0)
|
||
if got["snapshot_services"] != 26 {
|
||
t.Fatalf("snapshot_services = %v, want 26", got["snapshot_services"])
|
||
}
|
||
if !reflect.DeepEqual(got["missing_services"], []string{}) {
|
||
t.Fatalf("missing_services = %#v, want empty non-nil slice", got["missing_services"])
|
||
}
|
||
if got["matched_tools"] != 813 || got["unmatched_tools"] != 0 {
|
||
t.Fatalf("matched/unmatched = %v/%v, want 813/0 for stub-free snapshot", got["matched_tools"], got["unmatched_tools"])
|
||
}
|
||
}
|
||
|
||
// fakeLister returns canned tools/list results per endpoint.
|
||
type fakeLister struct {
|
||
results map[string]transport.ToolsListResult
|
||
errs map[string]error
|
||
}
|
||
|
||
func (f *fakeLister) ListTools(_ context.Context, endpoint string) (transport.ToolsListResult, error) {
|
||
if err := f.errs[endpoint]; err != nil {
|
||
return transport.ToolsListResult{}, err
|
||
}
|
||
return f.results[endpoint], nil
|
||
}
|
||
|
||
// stubDeps swaps every injection point for the duration of one test.
|
||
func stubDeps(t *testing.T, token string, keychain func() (*auth.TokenData, error), servers []syncdata.ServerInfo, lister toolLister, registry func() (map[string]map[string]string, error)) {
|
||
t.Helper()
|
||
origGetenv, origLoad, origServers, origNew, origRegistry := getenv, loadTokenData, staticServers, newToolLister, registrySource
|
||
t.Cleanup(func() {
|
||
getenv, loadTokenData, staticServers, newToolLister, registrySource = origGetenv, origLoad, origServers, origNew, origRegistry
|
||
})
|
||
getenv = func(key string) string {
|
||
if key == "DWS_ACCESS_TOKEN" {
|
||
return token
|
||
}
|
||
return ""
|
||
}
|
||
loadTokenData = keychain
|
||
staticServers = func() []syncdata.ServerInfo { return servers }
|
||
newToolLister = func(string) toolLister { return lister }
|
||
registrySource = registry
|
||
}
|
||
|
||
func testRegistryRefs() (map[string]map[string]string, error) {
|
||
return map[string]map[string]string{
|
||
"doc.copy_document": {"product_id": "doc", "rpc_name": "copy_document"},
|
||
"doc.get_document": {"product_id": "doc", "rpc_name": "get_document"},
|
||
}, nil
|
||
}
|
||
|
||
func TestCrossPlatformCoverageRunRefusesRetiredPinnedMCPMetadataPath(t *testing.T) {
|
||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryRefs)
|
||
var stderr bytes.Buffer
|
||
code := run([]string{"--output", "internal/cli/schema_mcp_metadata.json"}, &stderr)
|
||
if code != 2 {
|
||
t.Fatalf("run(retired pin) = %d, want 2", code)
|
||
}
|
||
if !strings.Contains(stderr.String(), "refusing to write retired Schema pin") {
|
||
t.Fatalf("stderr = %q, want retired-pin refusal", stderr.String())
|
||
}
|
||
if !retiredPinnedMCPMetadataPath("internal/cli/schema_mcp_metadata.json") ||
|
||
!retiredPinnedMCPMetadataPath("/tmp/repo/internal/cli/schema_mcp_metadata.json") ||
|
||
retiredPinnedMCPMetadataPath("artifacts/mcp_metadata_diagnostic.json") {
|
||
t.Fatal("retiredPinnedMCPMetadataPath classification is incorrect")
|
||
}
|
||
}
|
||
|
||
func TestRunNoTokenFails(t *testing.T) {
|
||
stubDeps(t, "", func() (*auth.TokenData, error) { return nil, errors.New("no keychain") }, nil, &fakeLister{}, testRegistryRefs)
|
||
var stderr bytes.Buffer
|
||
if code := run(nil, &stderr); code != 1 {
|
||
t.Fatalf("run() = %d, want 1", code)
|
||
}
|
||
if !strings.Contains(stderr.String(), "no auth token") {
|
||
t.Fatalf("stderr = %q, want no-auth-token hint", stderr.String())
|
||
}
|
||
}
|
||
|
||
func TestRunInvalidFlagFails(t *testing.T) {
|
||
stubDeps(t, "tok", nil, nil, &fakeLister{}, testRegistryRefs)
|
||
var stderr bytes.Buffer
|
||
if code := run([]string{"--nonexistent"}, &stderr); code != 2 {
|
||
t.Fatalf("run() = %d, want 2", code)
|
||
}
|
||
}
|
||
|
||
func TestResolveTokenKeychainFallback(t *testing.T) {
|
||
stubDeps(t, "", func() (*auth.TokenData, error) {
|
||
return &auth.TokenData{AccessToken: "kc-token"}, nil
|
||
}, nil, &fakeLister{}, testRegistryRefs)
|
||
var stderr bytes.Buffer
|
||
if got := resolveToken(&stderr); got != "kc-token" {
|
||
t.Fatalf("resolveToken() = %q, want kc-token", got)
|
||
}
|
||
if !strings.Contains(stderr.String(), "loaded token from keychain") {
|
||
t.Fatalf("stderr = %q, want keychain log", stderr.String())
|
||
}
|
||
}
|
||
|
||
func TestResolveTokenEmptyKeychainToken(t *testing.T) {
|
||
stubDeps(t, "", func() (*auth.TokenData, error) { return &auth.TokenData{}, nil }, nil, &fakeLister{}, testRegistryRefs)
|
||
var stderr bytes.Buffer
|
||
if got := resolveToken(&stderr); got != "" {
|
||
t.Fatalf("resolveToken() = %q, want empty", got)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageRunWritesSnapshotWithHonestCoverage(t *testing.T) {
|
||
servers := []syncdata.ServerInfo{
|
||
{ID: "doc", Endpoint: "https://doc.example"},
|
||
{ID: "sheet", Endpoint: "https://sheet.example"},
|
||
{ID: "blank", Endpoint: " "},
|
||
}
|
||
lister := &fakeLister{
|
||
results: map[string]transport.ToolsListResult{
|
||
"https://doc.example": {Tools: []transport.ToolDescriptor{
|
||
{Name: "copy_document", Title: "复制文档", Description: "copy", InputSchema: map[string]any{
|
||
"type": "object",
|
||
"properties": map[string]any{
|
||
"doc_id": map[string]any{"type": "string", "description": "文档 ID", "default": "d", "enum": []any{"a", "b", 3}},
|
||
"bogus": "not-a-map",
|
||
},
|
||
"required": []any{"doc_id", 42},
|
||
}},
|
||
{Name: " "},
|
||
{Name: "not_in_registry"},
|
||
}},
|
||
},
|
||
errs: map[string]error{"https://sheet.example": errors.New("boom")},
|
||
}
|
||
stubDeps(t, "env-token", nil, servers, lister, testRegistryRefs)
|
||
|
||
dir := t.TempDir()
|
||
output := filepath.Join(dir, "snapshot.json")
|
||
prev := `{"tools":{"doc.get_document":{"interface_ref":{"product_id":"doc-helper","rpc_name":"fetch_document"}}}}`
|
||
if err := os.WriteFile(output, []byte(prev), 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
var stderr bytes.Buffer
|
||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||
}
|
||
|
||
data, err := os.ReadFile(output)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var snapshot struct {
|
||
Version int `json:"version"`
|
||
Coverage map[string]any `json:"coverage"`
|
||
Tools map[string]map[string]any
|
||
}
|
||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if snapshot.Version != 1 {
|
||
t.Fatalf("version = %d", snapshot.Version)
|
||
}
|
||
if got := snapshot.Coverage["snapshot_services"].(float64); got != 2 {
|
||
t.Fatalf("snapshot_services = %v, want 2 (3 servers - 1 failed; blank endpoint not counted as failed)", got)
|
||
}
|
||
if got := snapshot.Coverage["missing_services"].([]any); len(got) != 1 || got[0] != "sheet" {
|
||
t.Fatalf("missing_services = %v, want [sheet]", got)
|
||
}
|
||
live := snapshot.Tools["doc.copy_document"]
|
||
if live == nil || live["title"] != "复制文档" {
|
||
t.Fatalf("doc.copy_document = %#v, want live metadata", live)
|
||
}
|
||
params := live["parameters"].(map[string]any)
|
||
docID := params["doc_id"].(map[string]any)
|
||
if docID["type"] != "string" || docID["required"] != true || docID["default"] != "d" {
|
||
t.Fatalf("doc_id = %#v", docID)
|
||
}
|
||
if enum := docID["enum"].([]any); len(enum) != 2 {
|
||
t.Fatalf("enum = %v, want the 2 string members only", enum)
|
||
}
|
||
if _, ok := params["bogus"]; ok {
|
||
t.Fatal("non-map property should be skipped")
|
||
}
|
||
prevRef := snapshot.Tools["doc.get_document"]["interface_ref"].(map[string]any)
|
||
if prevRef["product_id"] != "doc-helper" {
|
||
t.Fatalf("previous reviewed ref lost: %#v", prevRef)
|
||
}
|
||
if _, ok := snapshot.Tools["not_in_registry"]; ok {
|
||
t.Fatal("tools outside the registry must be dropped")
|
||
}
|
||
if !strings.Contains(stderr.String(), "services unreachable: sheet") {
|
||
t.Fatalf("stderr = %q, want unreachable log", stderr.String())
|
||
}
|
||
}
|
||
|
||
func TestRunIgnoresCorruptPreviousSnapshot(t *testing.T) {
|
||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryRefs)
|
||
output := filepath.Join(t.TempDir(), "snapshot.json")
|
||
if err := os.WriteFile(output, []byte("{corrupt"), 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var stderr bytes.Buffer
|
||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageRunRegistryLoadFailureStillWritesStublessSnapshot(t *testing.T) {
|
||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, func() (map[string]map[string]string, error) { return nil, errors.New("no identity") })
|
||
output := filepath.Join(t.TempDir(), "snapshot.json")
|
||
var stderr bytes.Buffer
|
||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||
}
|
||
if !strings.Contains(stderr.String(), "cannot collect command identity") {
|
||
t.Fatalf("stderr = %q, want identity collection warning", stderr.String())
|
||
}
|
||
}
|
||
|
||
func TestRunWriteFailure(t *testing.T) {
|
||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryRefs)
|
||
var stderr bytes.Buffer
|
||
badPath := filepath.Join(t.TempDir(), "missing-dir", "snapshot.json")
|
||
if code := run([]string{"--output", badPath}, &stderr); code != 1 {
|
||
t.Fatalf("run() = %d, want 1 on write failure", code)
|
||
}
|
||
}
|
||
|
||
func TestWriteMetadataMarshalFailure(t *testing.T) {
|
||
err := writeMetadata(filepath.Join(t.TempDir(), "out.json"), map[string]any{"bad": math.NaN()})
|
||
if err == nil || !strings.Contains(err.Error(), "marshal failed") {
|
||
t.Fatalf("err = %v, want marshal failure", err)
|
||
}
|
||
}
|
||
|
||
func TestMainDelegatesToRun(t *testing.T) {
|
||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryRefs)
|
||
origExit, origArgs := osExit, os.Args
|
||
t.Cleanup(func() { osExit, os.Args = origExit, origArgs })
|
||
exitCode := -1
|
||
osExit = func(code int) { exitCode = code }
|
||
os.Args = []string{"fetch_mcp_metadata", "--output", filepath.Join(t.TempDir(), "snapshot.json")}
|
||
main()
|
||
if exitCode != 0 {
|
||
t.Fatalf("main() exited with %d, want 0", exitCode)
|
||
}
|
||
}
|
||
|
||
func TestExtractParamsNilAndNonObjectSchemas(t *testing.T) {
|
||
if got := extractParams(nil); got != nil {
|
||
t.Fatalf("extractParams(nil) = %v, want nil", got)
|
||
}
|
||
if got := extractParams(map[string]any{"type": "object"}); got != nil {
|
||
t.Fatalf("extractParams(no properties) = %v, want nil", got)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageNewToolListerBuildsAuthedClient(t *testing.T) {
|
||
if lister := newToolLister("tok"); lister == nil {
|
||
t.Fatal("newToolLister returned nil")
|
||
}
|
||
}
|
||
|
||
func TestRunRecordsSourceRevision(t *testing.T) {
|
||
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryRefs)
|
||
dir := t.TempDir()
|
||
head := filepath.Join(dir, "HEAD")
|
||
if err := os.WriteFile(head, []byte("ref: refs/heads/feature\n"), 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
origHead := gitHeadPath
|
||
t.Cleanup(func() { gitHeadPath = origHead })
|
||
gitHeadPath = head
|
||
|
||
output := filepath.Join(dir, "snapshot.json")
|
||
var stderr bytes.Buffer
|
||
if code := run([]string{"--output", output}, &stderr); code != 0 {
|
||
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
|
||
}
|
||
data, err := os.ReadFile(output)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var snapshot struct {
|
||
SourceRevision string `json:"source_revision"`
|
||
}
|
||
if err := json.Unmarshal(data, &snapshot); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if snapshot.SourceRevision != "ref: refs/heads/feature" {
|
||
t.Fatalf("source_revision = %q", snapshot.SourceRevision)
|
||
}
|
||
}
|