285 lines
8.2 KiB
Go
285 lines
8.2 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 ir
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/discovery"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/market"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
|
||
)
|
||
|
||
func TestBuildCatalogUsesEndpointSlugAndCanonicalPaths(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
catalog := BuildCatalog([]discovery.RuntimeServer{
|
||
{
|
||
Server: market.ServerDescriptor{
|
||
Key: "doc-key",
|
||
DisplayName: "文档",
|
||
Endpoint: "https://example.com/server/doc",
|
||
},
|
||
NegotiatedProtocolVersion: "2025-03-26",
|
||
Tools: []transport.ToolDescriptor{
|
||
{Name: "create_document", Title: "创建文档", Description: "创建文档", InputSchema: map[string]any{"type": "object"}},
|
||
{Name: "list_documents", Description: "列出文档", InputSchema: map[string]any{"type": "object"}},
|
||
},
|
||
Source: "live_runtime",
|
||
Degraded: false,
|
||
},
|
||
})
|
||
|
||
if len(catalog.Products) != 1 {
|
||
t.Fatalf("BuildCatalog() products len = %d, want 1", len(catalog.Products))
|
||
}
|
||
|
||
product := catalog.Products[0]
|
||
if product.ID != "doc" {
|
||
t.Fatalf("BuildCatalog() product ID = %q, want doc", product.ID)
|
||
}
|
||
if product.Tools[0].CanonicalPath != "doc.create_document" {
|
||
t.Fatalf("BuildCatalog() canonical path = %q, want doc.create_document", product.Tools[0].CanonicalPath)
|
||
}
|
||
if product.Tools[1].Title != "list_documents" {
|
||
t.Fatalf("BuildCatalog() title fallback = %q, want list_documents", product.Tools[1].Title)
|
||
}
|
||
}
|
||
|
||
func TestBuildCatalogAppliesCanonicalProductAlias(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
catalog := BuildCatalog([]discovery.RuntimeServer{
|
||
{
|
||
Server: market.ServerDescriptor{
|
||
Key: "aitable-key",
|
||
DisplayName: "多维表格",
|
||
Endpoint: "https://example.com/server/table",
|
||
CLI: market.CLIOverlay{
|
||
Command: "table",
|
||
},
|
||
},
|
||
Tools: []transport.ToolDescriptor{
|
||
{Name: "list_tables", InputSchema: map[string]any{"type": "object"}},
|
||
},
|
||
},
|
||
})
|
||
|
||
if len(catalog.Products) != 1 {
|
||
t.Fatalf("BuildCatalog() products len = %d, want 1", len(catalog.Products))
|
||
}
|
||
if got := catalog.Products[0].ID; got != "aitable" {
|
||
t.Fatalf("BuildCatalog() product ID = %q, want aitable", got)
|
||
}
|
||
if got := catalog.Products[0].Tools[0].CanonicalPath; got != "aitable.list_tables" {
|
||
t.Fatalf("BuildCatalog() canonical path = %q, want aitable.list_tables", got)
|
||
}
|
||
}
|
||
|
||
func TestBuildCatalogFallsBackToHashOnSlugCollision(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
catalog := BuildCatalog([]discovery.RuntimeServer{
|
||
{
|
||
Server: market.ServerDescriptor{
|
||
Key: "first-key",
|
||
DisplayName: "文档一",
|
||
Endpoint: "https://example.com/server/doc",
|
||
},
|
||
},
|
||
{
|
||
Server: market.ServerDescriptor{
|
||
Key: "second-key",
|
||
DisplayName: "文档二",
|
||
Endpoint: "https://example.com/another/doc",
|
||
},
|
||
},
|
||
})
|
||
|
||
if len(catalog.Products) != 2 {
|
||
t.Fatalf("BuildCatalog() products len = %d, want 2", len(catalog.Products))
|
||
}
|
||
if catalog.Products[0].ID != "doc" {
|
||
t.Fatalf("BuildCatalog() first product ID = %q, want doc", catalog.Products[0].ID)
|
||
}
|
||
if catalog.Products[1].ID == "doc" {
|
||
t.Fatalf("BuildCatalog() second product ID unexpectedly reused base slug")
|
||
}
|
||
}
|
||
|
||
func TestFindToolRejectsMalformedPath(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
catalog := Catalog{
|
||
Products: []CanonicalProduct{
|
||
{
|
||
ID: "doc",
|
||
Tools: []ToolDescriptor{
|
||
{RPCName: "create_document"},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
if _, _, ok := catalog.FindTool("doc"); ok {
|
||
t.Fatalf("FindTool() accepted malformed path")
|
||
}
|
||
if _, _, ok := catalog.FindTool("doc.create_document"); !ok {
|
||
t.Fatalf("FindTool() rejected valid path")
|
||
}
|
||
}
|
||
|
||
func TestBuildCatalogCarriesSensitiveFlagFromCLIMetadata(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
catalog := BuildCatalog([]discovery.RuntimeServer{
|
||
{
|
||
Server: market.ServerDescriptor{
|
||
Key: "doc-key",
|
||
DisplayName: "文档",
|
||
Endpoint: "https://example.com/server/doc",
|
||
CLI: market.CLIOverlay{
|
||
Tools: []market.CLITool{
|
||
{Name: "create_document", IsSensitive: true},
|
||
{Name: "search_documents", IsSensitive: false},
|
||
},
|
||
},
|
||
},
|
||
Tools: []transport.ToolDescriptor{
|
||
{Name: "create_document", Title: "创建文档", InputSchema: map[string]any{"type": "object"}},
|
||
{Name: "search_documents", Title: "搜索文档", InputSchema: map[string]any{"type": "object"}},
|
||
},
|
||
},
|
||
})
|
||
|
||
product := catalog.Products[0]
|
||
createTool, ok := product.FindTool("create_document")
|
||
if !ok {
|
||
t.Fatalf("FindTool(create_document) = not found")
|
||
}
|
||
if !createTool.Sensitive {
|
||
t.Fatalf("create_document sensitive = false, want true")
|
||
}
|
||
|
||
searchTool, ok := product.FindTool("search_documents")
|
||
if !ok {
|
||
t.Fatalf("FindTool(search_documents) = not found")
|
||
}
|
||
if searchTool.Sensitive {
|
||
t.Fatalf("search_documents sensitive = true, want false")
|
||
}
|
||
}
|
||
|
||
func TestBuildCatalogFallsBackToRuntimeSensitiveMetadata(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
catalog := BuildCatalog([]discovery.RuntimeServer{
|
||
{
|
||
Server: market.ServerDescriptor{
|
||
Key: "doc-key",
|
||
DisplayName: "文档",
|
||
Endpoint: "https://example.com/server/doc",
|
||
},
|
||
Tools: []transport.ToolDescriptor{
|
||
{
|
||
Name: "create_document",
|
||
Title: "创建文档",
|
||
Sensitive: true,
|
||
InputSchema: map[string]any{"type": "object"},
|
||
OutputSchema: map[string]any{"type": "object"},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
product := catalog.Products[0]
|
||
tool, ok := product.FindTool("create_document")
|
||
if !ok {
|
||
t.Fatalf("FindTool(create_document) = not found")
|
||
}
|
||
if !tool.Sensitive {
|
||
t.Fatalf("create_document sensitive = false, want true")
|
||
}
|
||
if tool.OutputSchema == nil || tool.OutputSchema["type"] != "object" {
|
||
t.Fatalf("create_document output_schema = %#v, want object schema", tool.OutputSchema)
|
||
}
|
||
}
|
||
|
||
func TestBuildCatalogConsumesCLIRouteMetadata(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
catalog := BuildCatalog([]discovery.RuntimeServer{
|
||
{
|
||
Server: market.ServerDescriptor{
|
||
Key: "doc-key",
|
||
DisplayName: "文档",
|
||
Endpoint: "https://example.com/server/doc",
|
||
CLI: market.CLIOverlay{
|
||
Command: "documents",
|
||
Group: "office",
|
||
Hidden: true,
|
||
Skip: false,
|
||
Description: "文档命令",
|
||
Tools: []market.CLITool{
|
||
{
|
||
Name: "create_document",
|
||
CLIName: "create",
|
||
Title: "创建文档(CLI)",
|
||
Description: "CLI 覆盖描述",
|
||
Hidden: true,
|
||
Flags: map[string]market.CLIFlagHint{
|
||
"title": {Alias: "name", Shorthand: "t"},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
Tools: []transport.ToolDescriptor{
|
||
{Name: "create_document", Title: "创建文档", InputSchema: map[string]any{"type": "object"}},
|
||
},
|
||
},
|
||
})
|
||
|
||
product := catalog.Products[0]
|
||
if product.ID != "documents" {
|
||
t.Fatalf("product.ID = %q, want documents", product.ID)
|
||
}
|
||
if product.CLI == nil || !product.CLI.Hidden || product.CLI.Group != "office" {
|
||
t.Fatalf("product.CLI = %#v, want hidden office group", product.CLI)
|
||
}
|
||
if product.Description != "文档命令" {
|
||
t.Fatalf("product.Description = %q, want 文档命令", product.Description)
|
||
}
|
||
tool, ok := product.FindTool("create_document")
|
||
if !ok {
|
||
t.Fatalf("FindTool(create_document) = not found")
|
||
}
|
||
if tool.CLIName != "create" {
|
||
t.Fatalf("tool.CLIName = %q, want create", tool.CLIName)
|
||
}
|
||
if tool.Title != "创建文档(CLI)" {
|
||
t.Fatalf("tool.Title = %q, want 创建文档(CLI)", tool.Title)
|
||
}
|
||
if tool.Description != "CLI 覆盖描述" {
|
||
t.Fatalf("tool.Description = %q, want CLI 覆盖描述", tool.Description)
|
||
}
|
||
if !tool.Hidden {
|
||
t.Fatalf("tool.Hidden = false, want true")
|
||
}
|
||
hint := tool.FlagHints["title"]
|
||
if hint.Alias != "name" || hint.Shorthand != "t" {
|
||
t.Fatalf("tool.FlagHints[title] = %#v, want alias=name shorthand=t", hint)
|
||
}
|
||
}
|