349 lines
12 KiB
Go
349 lines
12 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
|
|
|
|
package cli
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestBuildSchemaCatalogSnapshotRejectsUnresolvedSource(t *testing.T) {
|
|
_, err := BuildSchemaCatalogSnapshot(ResolvedSchemaBuild{}, SchemaCatalogBuildOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "ResolveSchemaBuild") {
|
|
t.Fatalf("BuildSchemaCatalogSnapshot() error = %v, want resolved-source requirement", err)
|
|
}
|
|
}
|
|
|
|
func TestEmbeddedSchemaCatalogIntegrity(t *testing.T) {
|
|
loaded := embeddedSchemaCatalog()
|
|
if !embeddedSchemaCatalogAvailable() {
|
|
t.Fatal("embedded schema catalog is unavailable or failed integrity validation")
|
|
}
|
|
if got := schemaString(loaded.Snapshot.Catalog["source"]); got != "embedded-command-catalog" {
|
|
t.Fatalf("catalog source = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestEmbeddedSchemaCatalogProgressiveQueries(t *testing.T) {
|
|
overview, err := embeddedSchemaOverviewPayload()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := schemaProductToolCount(map[string]any{"tools": overview["products"]}), len(embeddedSchemaCatalog().Registry.Products); got != want {
|
|
t.Fatalf("compact product count = %d, want %d", got, want)
|
|
}
|
|
|
|
leaf, err := embeddedSchemaPayload([]string{"calendar event create"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := schemaString(leaf["canonical_path"]); got != "calendar.create_calendar_event" {
|
|
t.Fatalf("canonical path = %q", got)
|
|
}
|
|
if len(schemaMapSlice(leaf["parameters"])) != 0 {
|
|
t.Fatal("parameters unexpectedly decoded as a list")
|
|
}
|
|
if parameters, ok := leaf["parameters"].(map[string]any); !ok || len(parameters) == 0 {
|
|
t.Fatal("calendar.create_event parameters are empty")
|
|
}
|
|
|
|
group, err := embeddedSchemaPayload([]string{"calendar.event"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if schemaProductToolCount(map[string]any{"tools": group["tools"]}) == 0 {
|
|
t.Fatal("calendar.event group is empty")
|
|
}
|
|
|
|
alias, err := embeddedSchemaPayload([]string{"aitable record list"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if alias["is_alias"] != true || schemaString(alias["cli_path"]) != "aitable record list" {
|
|
t.Fatalf("alias query did not preserve compatibility path: %#v", alias)
|
|
}
|
|
if schemaString(alias["canonical_path"]) != "aitable.query_records" {
|
|
t.Fatalf("alias canonical path = %q", schemaString(alias["canonical_path"]))
|
|
}
|
|
}
|
|
|
|
func TestEmbeddedSchemaAllPayloadContainsEveryFullLeaf(t *testing.T) {
|
|
loaded := embeddedSchemaCatalog()
|
|
payload, err := embeddedSchemaAllPayload()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expanded := 0
|
|
parameterized := 0
|
|
for _, product := range schemaMapSlice(payload["products"]) {
|
|
for _, tool := range schemaMapSlice(product["tools"]) {
|
|
canonical := schemaString(tool["canonical_path"])
|
|
expected, ok := loaded.Snapshot.Tools[canonical]
|
|
if !ok {
|
|
t.Fatalf("full export contains unknown tool %q", canonical)
|
|
}
|
|
parameters, ok := tool["parameters"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("full export tool %s has no parameters object", canonical)
|
|
}
|
|
if len(parameters) > 0 {
|
|
parameterized++
|
|
}
|
|
if !schemaJSONEqual(tool, expected) {
|
|
t.Fatalf("full export tool %s differs from stored leaf Schema", canonical)
|
|
}
|
|
expanded++
|
|
}
|
|
}
|
|
if got, want := expanded, len(loaded.Snapshot.Tools); got != want {
|
|
t.Fatalf("full export tools = %d, want %d", got, want)
|
|
}
|
|
if parameterized == 0 {
|
|
t.Fatal("full export contains no parameterized tools")
|
|
}
|
|
}
|
|
|
|
func TestEmbeddedCatalogPreservesRegistryIdentityAndManualParameterContract(t *testing.T) {
|
|
leaf, err := embeddedSchemaPayload([]string{"chat category create-smart"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := schemaString(leaf["source"]); got != "reviewed_command_registry" {
|
|
t.Fatalf("source = %q, want reviewed_command_registry", got)
|
|
}
|
|
identity := schemaMap(leaf["field_provenance"])["canonical_path"]
|
|
if identity["source"] != "reviewed_command_registry" || identity["precedence"] != "command_registry" {
|
|
t.Fatalf("canonical identity provenance = %#v", identity)
|
|
}
|
|
parameters := schemaMap(leaf["parameters"])
|
|
assertReviewedManual := func(flagName, field string) {
|
|
t.Helper()
|
|
provenance := schemaMap(parameters[flagName]["field_provenance"])
|
|
winner := provenance[field]
|
|
if winner["source"] != "reviewed_manual_hint" || winner["precedence"] != "reviewed_manual" {
|
|
t.Fatalf("%s.%s provenance = %#v", flagName, field, winner)
|
|
}
|
|
}
|
|
name := parameters["name"]
|
|
if name["property"] != "categoryName" || name["required"] != true {
|
|
t.Fatalf("name parameter = %#v", name)
|
|
}
|
|
assertReviewedManual("name", "property")
|
|
assertReviewedManual("name", "required")
|
|
for flagName, property := range map[string]string{
|
|
"keywords": "groupNameKeywords",
|
|
"members": "memberOpenDingTalkIds",
|
|
} {
|
|
parameter := parameters[flagName]
|
|
if parameter["property"] != property || parameter["interface_type"] != "array" || parameter["required"] != false {
|
|
t.Fatalf("%s parameter = %#v", flagName, parameter)
|
|
}
|
|
for _, field := range []string{"property", "interface_type", "required"} {
|
|
assertReviewedManual(flagName, field)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEmbeddedCatalogModelsAitableExportBranches(t *testing.T) {
|
|
leaf, err := embeddedSchemaPayload([]string{"aitable export data"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parameters := schemaMap(leaf["parameters"])
|
|
if _, exists := parameters["format"]; exists {
|
|
t.Fatal("business export format still shadows the global --format output flag")
|
|
}
|
|
exportFormat := parameters["export-format"]
|
|
if exportFormat["property"] != "format" || exportFormat["required"] != false {
|
|
t.Fatalf("export-format parameter = %#v", exportFormat)
|
|
}
|
|
if got, want := schemaStringSlice(exportFormat["enum"]), []string{"excel", "attachment", "excel_and_attachment", "excel_with_inline_images"}; !equalStringSlices(got, want) {
|
|
t.Fatalf("export-format enum = %v, want %v", got, want)
|
|
}
|
|
if parameters["scope"]["required"] != false {
|
|
t.Fatalf("scope must be conditional, got %#v", parameters["scope"])
|
|
}
|
|
if got := parameters["table-id"]["required_when"]; got != "scope is table or view" {
|
|
t.Fatalf("table-id required_when = %#v", got)
|
|
}
|
|
if got := parameters["view-id"]["required_when"]; got != "scope is view" {
|
|
t.Fatalf("view-id required_when = %#v", got)
|
|
}
|
|
|
|
constraints, ok := leaf["constraints"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("constraints = %#v", leaf["constraints"])
|
|
}
|
|
hasGroup := func(field string, want ...string) bool {
|
|
groups, _ := constraints[field].([]any)
|
|
for _, raw := range groups {
|
|
if equalStringSlices(schemaStringSlice(raw), want) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
if !hasGroup("require_one_of", "scope", "task-id") ||
|
|
!hasGroup("require_one_of", "export-format", "task-id") ||
|
|
!hasGroup("require_together", "scope", "export-format") {
|
|
t.Fatalf("branch constraints = %#v", constraints)
|
|
}
|
|
}
|
|
|
|
func TestEmbeddedCatalogKeepsSharedFlagSemanticsCommandScoped(t *testing.T) {
|
|
queryLeaf, err := embeddedSchemaPayload([]string{"aitable record query"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
getLeaf, err := embeddedSchemaPayload([]string{"aitable record get"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
queryRecordIDs := schemaMap(queryLeaf["parameters"])["record-ids"]
|
|
getRecordIDs := schemaMap(getLeaf["parameters"])["record-ids"]
|
|
if queryRecordIDs["required"] != false {
|
|
t.Fatalf("record query --record-ids = %#v, want optional", queryRecordIDs)
|
|
}
|
|
if getRecordIDs["required"] != true {
|
|
t.Fatalf("record get --record-ids = %#v, want required", getRecordIDs)
|
|
}
|
|
getProvenance := schemaMap(getRecordIDs["field_provenance"])["required"]
|
|
if getProvenance["source"] != "typed_parameter_metadata" {
|
|
t.Fatalf("record get required provenance = %#v", getProvenance)
|
|
}
|
|
}
|
|
|
|
func equalStringSlices(left, right []string) bool {
|
|
if len(left) != len(right) {
|
|
return false
|
|
}
|
|
for index := range left {
|
|
if left[index] != right[index] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestStripSchemaPayloadCompactLeaf(t *testing.T) {
|
|
leaf, err := embeddedSchemaPayload([]string{"calendar event create"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stripped := stripSchemaPayloadCompact(leaf)
|
|
|
|
// Must keep agent-essential fields.
|
|
for _, key := range []string{"cli_path", "canonical_path", "description", "effect", "risk", "confirmation", "parameters", "constraints"} {
|
|
if _, ok := stripped[key]; !ok {
|
|
t.Fatalf("compact leaf missing essential key %q", key)
|
|
}
|
|
}
|
|
|
|
// Must strip provenance / redundant fields.
|
|
for _, key := range []string{"agent_metadata_source", "agent_source_refs", "agent_summary_source", "effect_source", "metadata_source", "primary_cli_path", "parameter_count", "has_parameters", "interface_ref", "source", "title", "display"} {
|
|
if _, ok := stripped[key]; ok {
|
|
t.Fatalf("compact leaf still contains stripped key %q", key)
|
|
}
|
|
}
|
|
|
|
// Parameters must not contain interface_description / property.
|
|
if params, ok := stripped["parameters"].(map[string]any); ok {
|
|
for name, p := range params {
|
|
if pm, ok := p.(map[string]any); ok {
|
|
for _, stripped := range []string{"interface_description", "interface_type", "property"} {
|
|
if _, present := pm[stripped]; present {
|
|
t.Fatalf("compact param %q still contains %q", name, stripped)
|
|
}
|
|
}
|
|
// Must keep type and required.
|
|
if _, present := pm["type"]; !present {
|
|
t.Fatalf("compact param %q missing type", name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStripSchemaPayloadCompactPreservesParameterIdentity(t *testing.T) {
|
|
leaf, err := embeddedSchemaPayload([]string{"chat category create-smart"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
full := schemaMap(leaf["parameters"])
|
|
compact := schemaMap(stripSchemaPayloadCompact(leaf)["parameters"])
|
|
if len(compact) != len(full) {
|
|
t.Fatalf("compact parameter count = %d, want %d: full=%v compact=%v", len(compact), len(full), sortedSchemaKeys(full), sortedSchemaKeys(compact))
|
|
}
|
|
name := compact["name"]
|
|
if name["required"] != true || name["type"] != "string" {
|
|
t.Fatalf("compact --name parameter = %#v", name)
|
|
}
|
|
|
|
synthetic := map[string]any{"parameters": map[string]any{}}
|
|
parameters := synthetic["parameters"].(map[string]any)
|
|
for _, parameterName := range []string{"name", "path", "source", "title", "group", "aliases"} {
|
|
parameters[parameterName] = map[string]any{"type": "string", "required": false, "field_provenance": map[string]any{"source": "test"}}
|
|
}
|
|
stripped := schemaMap(stripSchemaPayloadCompact(synthetic)["parameters"])
|
|
for parameterName := range parameters {
|
|
if _, ok := stripped[parameterName]; !ok {
|
|
t.Errorf("compact projection dropped parameter identity %q", parameterName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func sortedSchemaKeys(values map[string]map[string]any) []string {
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func TestStripSchemaPayloadCompactOverview(t *testing.T) {
|
|
overview, err := embeddedSchemaOverviewPayload()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stripped := stripSchemaPayloadCompact(overview)
|
|
|
|
// Overview must keep kind/level/count/products.
|
|
for _, key := range []string{"kind", "level", "count", "products"} {
|
|
if _, ok := stripped[key]; !ok {
|
|
t.Fatalf("compact overview missing key %q", key)
|
|
}
|
|
}
|
|
// Must strip agent_metadata / interface_metadata at top level.
|
|
for _, key := range []string{"agent_metadata", "interface_metadata", "source"} {
|
|
if _, ok := stripped[key]; ok {
|
|
t.Fatalf("compact overview still contains stripped key %q", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStripSchemaPayloadCompactProduct(t *testing.T) {
|
|
product, err := embeddedSchemaPayload([]string{"calendar"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stripped := stripSchemaPayloadCompact(product)
|
|
|
|
if _, ok := stripped["product"]; !ok {
|
|
t.Fatal("compact product missing 'product' key")
|
|
}
|
|
prod := stripped["product"].(map[string]any)
|
|
for _, key := range []string{"agent_metadata_source", "agent_source_refs", "source"} {
|
|
if _, ok := prod[key]; ok {
|
|
t.Fatalf("compact product still contains stripped key %q", key)
|
|
}
|
|
}
|
|
}
|