Files
dingtalk-workspace-cli/internal/cli/schema_catalog_test.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)
}
}
}