embeddedMCPMetadata only feeds interface validation now; its summary was projected onto every schema payload as a constant-empty blob. Remove the SchemaRegistry field, all three payload projections, the snapshot wire field, the overview copy, the compact strip entry, and the jq policy gate. Also delete the redundant io.Discard dead-code suppressor in event_command.go (io has five genuine uses there).
1415 lines
56 KiB
Go
1415 lines
56 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 TestDeliverySchemaCatalogIntegrity(t *testing.T) {
|
|
loaded := deliverySchemaCatalog()
|
|
if !deliverySchemaCatalogAvailable() {
|
|
t.Fatal("delivery schema catalog is unavailable or failed integrity validation")
|
|
}
|
|
if got := loaded.Registry.Source; got != SchemaSourceRuntimeAssembled {
|
|
t.Fatalf("catalog source = %q, want %q", got, SchemaSourceRuntimeAssembled)
|
|
}
|
|
}
|
|
|
|
func TestDeliverySchemaCatalogProgressiveQueries(t *testing.T) {
|
|
overview, err := deliverySchemaOverviewPayload()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := schemaProductToolCount(map[string]any{"tools": overview["products"]}), len(deliverySchemaCatalog().Registry.Products); got != want {
|
|
t.Fatalf("compact product count = %d, want %d", got, want)
|
|
}
|
|
|
|
leaf, err := queryDeliverySchemaPayload([]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 := queryDeliverySchemaPayload([]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 := queryDeliverySchemaPayload([]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 TestDeliverySchemaAllPayloadContainsEveryFullLeaf(t *testing.T) {
|
|
loaded := mustDeliverySchemaCatalogMaps(t)
|
|
payload, err := deliverySchemaAllPayload()
|
|
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 TestDeliveryCatalogPreservesRegistryIdentityAndManualParameterContract(t *testing.T) {
|
|
leaf, err := queryDeliverySchemaPayload([]string{"chat category create-smart"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := schemaString(leaf["source"]); got != "contract_identity" {
|
|
t.Fatalf("source = %q, want contract_identity", got)
|
|
}
|
|
identity := schemaMap(leaf["field_provenance"])["canonical_path"]
|
|
// Migrated ContractFinal tools stamp identity provenance as contract_final
|
|
// pass-through while the collector remains the path index; accept either.
|
|
switch identity["precedence"] {
|
|
case "command_registry", "contract_final":
|
|
default:
|
|
t.Fatalf("canonical identity provenance = %#v", identity)
|
|
}
|
|
if identity["precedence"] == "command_registry" && identity["source"] != "contract_identity" {
|
|
t.Fatalf("canonical identity provenance = %#v", identity)
|
|
}
|
|
parameters := schemaMap(leaf["parameters"])
|
|
assertNativeAnnotation := func(flagName, field string) {
|
|
t.Helper()
|
|
provenance := schemaMap(parameters[flagName]["field_provenance"])
|
|
winner := provenance[field]
|
|
src, _ := winner["source"].(string)
|
|
prec, _ := winner["precedence"].(string)
|
|
if src != "native_annotation" || prec != "native_annotation" {
|
|
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)
|
|
}
|
|
assertNativeAnnotation("name", "property")
|
|
assertNativeAnnotation("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"} {
|
|
assertNativeAnnotation(flagName, field)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogMailMessageListLimitMapsToSize(t *testing.T) {
|
|
// mail message list is a composite wrapper over search_emails and shares
|
|
// that RPC with mail message search. list has no versioned binding entry
|
|
// (composite identity), so clearing hints without a leaf-local contract.ParamDecl
|
|
// previously fell back to flag_name_inference → "limit".
|
|
listLeaf, err := queryDeliverySchemaPayload([]string{"mail message list"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listParams := schemaMap(listLeaf["parameters"])
|
|
listLimit := listParams["limit"]
|
|
if listLimit["property"] != "size" {
|
|
t.Fatalf("mail message list --limit property = %#v, want size", listLimit["property"])
|
|
}
|
|
listProv := schemaMap(listLimit["field_provenance"])["property"]
|
|
if src, _ := listProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("mail message list --limit property source = %#v, want native_annotation", listProv)
|
|
}
|
|
if _, ok := listParams["folder-id"]; !ok {
|
|
t.Fatal("mail message list must keep --folder-id on its composite surface")
|
|
}
|
|
if _, ok := listParams["query"]; ok {
|
|
t.Fatal("mail message list must not publish search-only --query")
|
|
}
|
|
|
|
// Sibling search leaf maps the same --limit → size via binding + contract.ParamDecl
|
|
// and must keep the free-form KQL surface (not fold into list).
|
|
searchLeaf, err := queryDeliverySchemaPayload([]string{"mail message search"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
searchParams := schemaMap(searchLeaf["parameters"])
|
|
searchLimit := searchParams["limit"]
|
|
if searchLimit["property"] != "size" {
|
|
t.Fatalf("mail message search --limit property = %#v, want size", searchLimit["property"])
|
|
}
|
|
searchProv := schemaMap(searchLimit["field_provenance"])["property"]
|
|
src, _ := searchProv["source"].(string)
|
|
if src != "native_annotation" && src != "versioned_parameter_binding" {
|
|
t.Fatalf("mail message search --limit property source = %#v, want native_annotation or versioned_parameter_binding", searchProv)
|
|
}
|
|
if _, ok := searchParams["query"]; !ok {
|
|
t.Fatal("mail message search must keep --query")
|
|
}
|
|
if _, ok := searchParams["folder-id"]; ok {
|
|
t.Fatal("mail message search must not publish list-only --folder-id")
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogSharedRenameDocumentParamDeclsStayOnDriveLeaf(t *testing.T) {
|
|
// drive rename and doc rename share RPC rename_document. The reviewed
|
|
// extension-stripping description must stay on drive rename only.
|
|
driveLeaf, err := queryDeliverySchemaPayload([]string{"drive rename"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
driveName := schemaMap(driveLeaf["parameters"])["name"]
|
|
driveDesc, _ := driveName["description"].(string)
|
|
if driveDesc == "" || !strings.Contains(driveDesc, "扩展名") {
|
|
t.Fatalf("drive rename --name description = %q, want reviewed extension-stripping prose", driveDesc)
|
|
}
|
|
driveDescProv := schemaMap(driveName["field_provenance"])["description"]
|
|
if src, _ := driveDescProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("drive rename --name description source = %#v, want native_annotation", driveDescProv)
|
|
}
|
|
|
|
docLeaf, err := queryDeliverySchemaPayload([]string{"doc rename"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
docName := schemaMap(docLeaf["parameters"])["name"]
|
|
docDesc, _ := docName["description"].(string)
|
|
// Cobra usage may mention 扩展名 when pointing users at drive rename; the
|
|
// failure mode is inheriting the drive-only reviewed overlay prose.
|
|
if strings.Contains(docDesc, "避免双扩展名") || strings.Contains(docDesc, "仅对非文件夹") {
|
|
t.Fatalf("doc rename --name description incorrectly inherited drive rename prose: %q", docDesc)
|
|
}
|
|
docDescProv := schemaMap(docName["field_provenance"])["description"]
|
|
if src, _ := docDescProv["source"].(string); src == "native_annotation" && strings.Contains(docDesc, "读取节点类型") {
|
|
t.Fatalf("doc rename --name description source/value look like a misplaced drive contract.ParamDecl: %#v", docDescProv)
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogDriveUploadCompositeNodeParamDecl(t *testing.T) {
|
|
// drive.upload is a composite multi-step leaf (no single RPCName /
|
|
// interface_ref). The reviewed --node → nodeId overlay must stay on the
|
|
// upload leaf via contract.ParamDecl, not on the manual upload-info / commit steps.
|
|
leaf, err := queryDeliverySchemaPayload([]string{"drive upload"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := schemaString(leaf["interface_mode"]); got != "composite" {
|
|
t.Fatalf("drive upload interface_mode = %q, want composite", got)
|
|
}
|
|
if leaf["interface_ref"] != nil {
|
|
t.Fatalf("drive upload interface_ref = %#v, want nil for multi-step composite", leaf["interface_ref"])
|
|
}
|
|
parameters := schemaMap(leaf["parameters"])
|
|
node := parameters["node"]
|
|
if node["property"] != "nodeId" || node["required"] != false {
|
|
t.Fatalf("drive upload --node = %#v, want property=nodeId required=false", node)
|
|
}
|
|
for _, field := range []string{"property", "required"} {
|
|
provenance := schemaMap(node["field_provenance"])[field]
|
|
if src, _ := provenance["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("drive upload --node %s source = %#v, want native_annotation", field, provenance)
|
|
}
|
|
}
|
|
|
|
for _, path := range []string{"drive upload-info", "drive commit"} {
|
|
sibling, err := queryDeliverySchemaPayload([]string{path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
siblingParams := schemaMap(sibling["parameters"])
|
|
if _, ok := siblingParams["node"]; ok {
|
|
t.Fatalf("%s unexpectedly publishes --node (upload overwrite contract.ParamDecl leaked)", path)
|
|
}
|
|
if got := schemaString(sibling["interface_mode"]); got != "mcp" {
|
|
t.Fatalf("%s interface_mode = %q, want mcp", path, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogDocReadParamDeclsMatchMergeBaseContract(t *testing.T) {
|
|
// Former schema_hints overlays for get_document_content must stay on the
|
|
// doc read leaf after contract.ParamDecl migration (87910880 value parity).
|
|
leaf, err := queryDeliverySchemaPayload([]string{"doc read"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parameters := schemaMap(leaf["parameters"])
|
|
|
|
contentFormat := parameters["content-format"]
|
|
if contentFormat["property"] != "format" {
|
|
t.Fatalf("doc read --content-format property = %#v, want format", contentFormat["property"])
|
|
}
|
|
if contentFormat["required"] != false {
|
|
t.Fatalf("doc read --content-format required = %#v, want false", contentFormat["required"])
|
|
}
|
|
|
|
for _, flagName := range []string{"scope", "tags", "max-depth", "start-block-id", "end-block-id"} {
|
|
if parameters[flagName]["required"] != false {
|
|
t.Fatalf("doc read --%s required = %#v, want false", flagName, parameters[flagName]["required"])
|
|
}
|
|
}
|
|
if got := parameters["tags"]["required_when"]; got != "--scope=tags" {
|
|
t.Fatalf("doc read --tags required_when = %#v", got)
|
|
}
|
|
if got := parameters["start-block-id"]["required_when"]; got != "--scope=range or --scope=section" {
|
|
t.Fatalf("doc read --start-block-id required_when = %#v", got)
|
|
}
|
|
if parameters["max-depth"]["type"] != "integer" {
|
|
t.Fatalf("doc read --max-depth type = %#v, want integer", parameters["max-depth"]["type"])
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogDocCommentParamDeclsMatchMergeBaseContract(t *testing.T) {
|
|
// create/reply/update mentioned-open-conversation-id overlays + update
|
|
// mention interface_type must survive hint clearance.
|
|
for _, path := range []string{"doc comment create", "doc comment reply"} {
|
|
leaf, err := queryDeliverySchemaPayload([]string{path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
param := schemaMap(leaf["parameters"])["mentioned-open-conversation-id"]
|
|
if param["required"] != false {
|
|
t.Fatalf("%s --mentioned-open-conversation-id required = %#v, want false", path, param["required"])
|
|
}
|
|
if param["type"] != "array" {
|
|
t.Fatalf("%s --mentioned-open-conversation-id type = %#v, want array", path, param["type"])
|
|
}
|
|
prov := schemaMap(param["field_provenance"])["required"]
|
|
if src, _ := prov["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("%s --mentioned-open-conversation-id required source = %#v, want native_annotation", path, prov)
|
|
}
|
|
}
|
|
|
|
updateLeaf, err := queryDeliverySchemaPayload([]string{"doc comment update"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
updateParams := schemaMap(updateLeaf["parameters"])
|
|
mention := updateParams["mention"]
|
|
if mention["interface_type"] != "array" {
|
|
t.Fatalf("doc comment update --mention interface_type = %#v, want array", mention["interface_type"])
|
|
}
|
|
mentionTypeProv := schemaMap(mention["field_provenance"])["interface_type"]
|
|
if src, _ := mentionTypeProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("doc comment update --mention interface_type source = %#v, want native_annotation", mentionTypeProv)
|
|
}
|
|
groupMention := updateParams["mentioned-open-conversation-id"]
|
|
if groupMention["property"] != "mentionedOpenConversationIds" || groupMention["required"] != false {
|
|
t.Fatalf("doc comment update --mentioned-open-conversation-id = %#v", groupMention)
|
|
}
|
|
groupPropProv := schemaMap(groupMention["field_provenance"])["property"]
|
|
if src, _ := groupPropProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("doc comment update --mentioned-open-conversation-id property source = %#v, want native_annotation", groupPropProv)
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogDocRenameKeepsPassthroughNameDescription(t *testing.T) {
|
|
// Shared-RPC sibling of drive rename: doc must keep the merge-base
|
|
// passthrough usage, not the drive extension-stripping contract.ParamDecl.
|
|
leaf, err := queryDeliverySchemaPayload([]string{"doc rename"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
name := schemaMap(leaf["parameters"])["name"]
|
|
desc, _ := name["description"].(string)
|
|
if !strings.Contains(desc, "原样传给服务端") {
|
|
t.Fatalf("doc rename --name description = %q, want passthrough Cobra usage", desc)
|
|
}
|
|
if strings.Contains(desc, "避免双扩展名") || strings.Contains(desc, "仅对非文件夹") {
|
|
t.Fatalf("doc rename --name description incorrectly inherited drive contract.ParamDecl: %q", desc)
|
|
}
|
|
descProv := schemaMap(name["field_provenance"])["description"]
|
|
if src, _ := descProv["source"].(string); src != "cobra_usage" {
|
|
t.Fatalf("doc rename --name description source = %#v, want cobra_usage", descProv)
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogDevdocQueryRemainsOptionalInRequireOneOf(t *testing.T) {
|
|
// Former hint required=false keeps query optional; positional keyword is
|
|
// the other require_one_of member (constraint may outrank the annotation).
|
|
leaf, err := queryDeliverySchemaPayload([]string{"devdoc article search"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
query := schemaMap(leaf["parameters"])["query"]
|
|
if query["required"] != false {
|
|
t.Fatalf("devdoc article search --query required = %#v, want false", query["required"])
|
|
}
|
|
prov := schemaMap(query["field_provenance"])["required"]
|
|
src, _ := prov["source"].(string)
|
|
if src != "native_annotation" && src != "require_one_of_constraint" {
|
|
t.Fatalf("devdoc article search --query required source = %#v, want native_annotation or require_one_of_constraint", prov)
|
|
}
|
|
constraints, ok := leaf["constraints"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("constraints = %#v", leaf["constraints"])
|
|
}
|
|
groups, _ := constraints["require_one_of"].([]any)
|
|
found := false
|
|
for _, raw := range groups {
|
|
group, _ := raw.([]any)
|
|
names := make([]string, 0, len(group))
|
|
for _, item := range group {
|
|
if s, ok := item.(string); ok {
|
|
names = append(names, s)
|
|
}
|
|
}
|
|
if len(names) == 2 && ((names[0] == "query" && names[1] == "keyword") || (names[0] == "keyword" && names[1] == "query")) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("devdoc article search require_one_of missing [query,keyword]: %#v", groups)
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogModelsAitableExportBranches(t *testing.T) {
|
|
leaf, err := queryDeliverySchemaPayload([]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 TestDeliveryCatalogKeepsSharedFlagSemanticsCommandScoped(t *testing.T) {
|
|
queryLeaf, err := queryDeliverySchemaPayload([]string{"aitable record query"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
getLeaf, err := queryDeliverySchemaPayload([]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 := queryDeliverySchemaPayload([]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 := queryDeliverySchemaPayload([]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 := deliverySchemaOverviewPayload()
|
|
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 at top level.
|
|
for _, key := range []string{"agent_metadata", "source"} {
|
|
if _, ok := stripped[key]; ok {
|
|
t.Fatalf("compact overview still contains stripped key %q", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStripSchemaPayloadCompactProduct(t *testing.T) {
|
|
product, err := queryDeliverySchemaPayload([]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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogHrbrainUnpinnedParamDeclsKeepExplicitMappings(t *testing.T) {
|
|
// hrbrain leaves are Reviewed unpinned remote adapters. Clearing hint
|
|
// overlays must keep non-inferable properties on native_annotation
|
|
// (page→currentPage, order-by→orderByClauses), not flag_name_inference.
|
|
listLeaf, err := queryDeliverySchemaPayload([]string{"hrbrain talent-pool list"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listParams := schemaMap(listLeaf["parameters"])
|
|
page := listParams["page"]
|
|
if page["property"] != "currentPage" || page["required"] != false {
|
|
t.Fatalf("hrbrain talent-pool list --page = %#v, want property=currentPage required=false", page)
|
|
}
|
|
pageProv := schemaMap(page["field_provenance"])["property"]
|
|
if src, _ := pageProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("hrbrain talent-pool list --page property source = %#v, want native_annotation", pageProv)
|
|
}
|
|
|
|
structuredLeaf, err := queryDeliverySchemaPayload([]string{"hrbrain search employees-structured"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
structuredParams := schemaMap(structuredLeaf["parameters"])
|
|
orderBy := structuredParams["order-by"]
|
|
if orderBy["property"] != "orderByClauses" || orderBy["required"] != false {
|
|
t.Fatalf("hrbrain search employees-structured --order-by = %#v, want property=orderByClauses required=false", orderBy)
|
|
}
|
|
orderByProv := schemaMap(orderBy["field_provenance"])["property"]
|
|
if src, _ := orderByProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("hrbrain search employees-structured --order-by property source = %#v, want native_annotation", orderByProv)
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogAttendanceSearchRequiredStaysNativeWithoutProperty(t *testing.T) {
|
|
// attendance * search overlays only reviewed required=false (no property).
|
|
// After clearing hints, Required must stay native_annotation and property
|
|
// must remain the reviewed mapping exclusion (empty), not inference.
|
|
for _, path := range []string{
|
|
"attendance adjustment search",
|
|
"attendance group search",
|
|
"attendance overtime search",
|
|
} {
|
|
leaf, err := queryDeliverySchemaPayload([]string{path})
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", path, err)
|
|
}
|
|
parameters := schemaMap(leaf["parameters"])
|
|
for _, flagName := range []string{"page", "limit"} {
|
|
param := parameters[flagName]
|
|
if param["required"] != false {
|
|
t.Fatalf("%s --%s required = %#v, want false", path, flagName, param["required"])
|
|
}
|
|
if param["property"] != nil && param["property"] != "" {
|
|
t.Fatalf("%s --%s property = %#v, want omitted/empty mapping exclusion", path, flagName, param["property"])
|
|
}
|
|
reqProv := schemaMap(param["field_provenance"])["required"]
|
|
if src, _ := reqProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("%s --%s required source = %#v, want native_annotation", path, flagName, reqProv)
|
|
}
|
|
propProv := schemaMap(param["field_provenance"])["property"]
|
|
if src, _ := propProv["source"].(string); src != "reviewed_mapping_exclusion" {
|
|
t.Fatalf("%s --%s property source = %#v, want reviewed_mapping_exclusion", path, flagName, propProv)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogMarkdownFetchOmitsHiddenIDAlias(t *testing.T) {
|
|
// markdown fetch keeps a hidden --id Cobra compat alias, but Schema must
|
|
// publish only --node (merge-base / 87910880 catalog parity).
|
|
leaf, err := queryDeliverySchemaPayload([]string{"markdown fetch"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parameters := schemaMap(leaf["parameters"])
|
|
if _, ok := parameters["id"]; ok {
|
|
t.Fatal("markdown fetch must not publish hidden --id in Schema parameters")
|
|
}
|
|
node := parameters["node"]
|
|
if node["property"] != "nodeId" || node["required"] != true {
|
|
t.Fatalf("markdown fetch --node = %#v, want property=nodeId required=true", node)
|
|
}
|
|
prov := schemaMap(node["field_provenance"])["property"]
|
|
if src, _ := prov["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("markdown fetch --node property source = %#v, want native_annotation", prov)
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogMarkdownPatchContentMapsToReplacement(t *testing.T) {
|
|
// --content is the CLI flag; MCP/workflow property is replacement.
|
|
leaf, err := queryDeliverySchemaPayload([]string{"markdown patch"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content := schemaMap(leaf["parameters"])["content"]
|
|
if content["property"] != "replacement" || content["required"] != true {
|
|
t.Fatalf("markdown patch --content = %#v, want property=replacement required=true", content)
|
|
}
|
|
prov := schemaMap(content["field_provenance"])["property"]
|
|
if src, _ := prov["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("markdown patch --content property source = %#v, want native_annotation", prov)
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogMinutesReplaceBatchPairDescriptionIsNative(t *testing.T) {
|
|
// Former metadata overlay on minutes +replace-batch --pair description.
|
|
leaf, err := queryDeliverySchemaPayload([]string{"minutes +replace-batch"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pair := schemaMap(leaf["parameters"])["pair"]
|
|
desc, _ := pair["description"].(string)
|
|
if desc == "" || !strings.Contains(desc, "原文=>替换") {
|
|
t.Fatalf("minutes +replace-batch --pair description = %q, want reviewed pair-format prose", desc)
|
|
}
|
|
prov := schemaMap(pair["field_provenance"])["description"]
|
|
if src, _ := prov["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("minutes +replace-batch --pair description source = %#v, want native_annotation", prov)
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogTodoParamDeclsFrom87910880Reviewed(t *testing.T) {
|
|
// Spot-check former metadata/todo.json overlays now declared as contract.ParamDecl (87910880).
|
|
// --query-all is a Cobra Bool: merge-base catalog publishes type=boolean from
|
|
// cobra_flag_type (no separate interface_type field), while required/description
|
|
// stay native_annotation after contract.ParamDecl migration.
|
|
listLeaf, err := queryDeliverySchemaPayload([]string{"todo task list"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listParams := schemaMap(listLeaf["parameters"])
|
|
queryAll := listParams["query-all"]
|
|
if queryAll["required"] != false || queryAll["type"] != "boolean" {
|
|
t.Fatalf("todo task list --query-all = %#v, want required=false type=boolean", queryAll)
|
|
}
|
|
if desc, _ := queryAll["description"].(string); !strings.Contains(desc, "跨组织") {
|
|
t.Fatalf("todo task list --query-all description = %q, want cross-org prose", desc)
|
|
}
|
|
for _, field := range []string{"required", "description"} {
|
|
prov := schemaMap(queryAll["field_provenance"])[field]
|
|
if src, _ := prov["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("todo task list --query-all %s source = %#v, want native_annotation", field, prov)
|
|
}
|
|
}
|
|
typeProv := schemaMap(queryAll["field_provenance"])["type"]
|
|
if src, _ := typeProv["source"].(string); src != "cobra_flag_type" {
|
|
t.Fatalf("todo task list --query-all type source = %#v, want cobra_flag_type", typeProv)
|
|
}
|
|
roleTypes := listParams["role-types"]
|
|
if roleTypes["property"] != "roleTypes" || roleTypes["required"] != false {
|
|
t.Fatalf("todo task list --role-types = %#v, want property=roleTypes required=false", roleTypes)
|
|
}
|
|
rolePropProv := schemaMap(roleTypes["field_provenance"])["property"]
|
|
if src, _ := rolePropProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("todo task list --role-types property source = %#v, want native_annotation", rolePropProv)
|
|
}
|
|
if desc, _ := roleTypes["description"].(string); !strings.Contains(desc, "executor") {
|
|
t.Fatalf("todo task list --role-types description = %q, want executor default prose", desc)
|
|
}
|
|
roleDescProv := schemaMap(roleTypes["field_provenance"])["description"]
|
|
if src, _ := roleDescProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("todo task list --role-types description source = %#v, want native_annotation", roleDescProv)
|
|
}
|
|
|
|
resetLeaf, err := queryDeliverySchemaPayload([]string{"todo task reset-reminder"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rules := schemaMap(resetLeaf["parameters"])["reminder-rules"]
|
|
desc, _ := rules["description"].(string)
|
|
if !strings.Contains(desc, "不传表示清除") {
|
|
t.Fatalf("todo task reset-reminder --reminder-rules description = %q", desc)
|
|
}
|
|
rulesDescProv := schemaMap(rules["field_provenance"])["description"]
|
|
if src, _ := rulesDescProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("todo task reset-reminder --reminder-rules description source = %#v, want native_annotation", rulesDescProv)
|
|
}
|
|
|
|
createTag, err := queryDeliverySchemaPayload([]string{"todo tag create"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
name := schemaMap(createTag["parameters"])["name"]
|
|
if name["property"] != "name" || name["required"] != true {
|
|
t.Fatalf("todo tag create --name = %#v, want property=name required=true", name)
|
|
}
|
|
|
|
deleteTag, err := queryDeliverySchemaPayload([]string{"todo tag delete"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
deleteCodes := schemaMap(deleteTag["parameters"])["tag-codes"]
|
|
if deleteCodes["property"] != "tagCodes" || deleteCodes["interface_type"] != "array" || deleteCodes["required"] != true {
|
|
t.Fatalf("todo tag delete --tag-codes = %#v", deleteCodes)
|
|
}
|
|
|
|
addTag, err := queryDeliverySchemaPayload([]string{"todo tag add"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
addParams := schemaMap(addTag["parameters"])
|
|
if addParams["task-id"]["property"] != "taskId" || addParams["task-id"]["required"] != true {
|
|
t.Fatalf("todo tag add --task-id = %#v", addParams["task-id"])
|
|
}
|
|
if addParams["tag-codes"]["property"] != "tagCodes" || addParams["tag-codes"]["interface_type"] != "array" || addParams["tag-codes"]["required"] != true {
|
|
t.Fatalf("todo tag add --tag-codes = %#v", addParams["tag-codes"])
|
|
}
|
|
|
|
updateTag, err := queryDeliverySchemaPayload([]string{"todo tag update"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
userTags := schemaMap(updateTag["parameters"])["user-tags"]
|
|
if userTags["property"] != "userTags" || userTags["interface_type"] != "array" || userTags["required"] != true {
|
|
t.Fatalf("todo tag update --user-tags = %#v", userTags)
|
|
}
|
|
for _, tc := range []struct {
|
|
path string
|
|
flag string
|
|
field string
|
|
}{
|
|
{"todo tag create", "name", "property"},
|
|
{"todo tag delete", "tag-codes", "interface_type"},
|
|
{"todo tag add", "task-id", "property"},
|
|
{"todo tag update", "user-tags", "property"},
|
|
} {
|
|
leaf, err := queryDeliverySchemaPayload([]string{tc.path})
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", tc.path, err)
|
|
}
|
|
prov := schemaMap(schemaMap(leaf["parameters"])[tc.flag]["field_provenance"])[tc.field]
|
|
if src, _ := prov["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("%s --%s %s source = %#v, want native_annotation", tc.path, tc.flag, tc.field, prov)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogSheetParamDeclsFrom87910880Reviewed(t *testing.T) {
|
|
// Former metadata/sheet.json overlays now declared as contract.ParamDecl (87910880).
|
|
for _, tc := range []struct {
|
|
path string
|
|
flag string
|
|
want string
|
|
}{
|
|
{"sheet pivot-table create", "properties", "object"},
|
|
{"sheet pivot-table update", "properties", "object"},
|
|
{"sheet table-put", "sheets", "array"},
|
|
} {
|
|
leaf, err := queryDeliverySchemaPayload([]string{tc.path})
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", tc.path, err)
|
|
}
|
|
param := schemaMap(leaf["parameters"])[tc.flag]
|
|
if param["interface_type"] != tc.want {
|
|
t.Fatalf("%s --%s interface_type = %#v, want %q", tc.path, tc.flag, param["interface_type"], tc.want)
|
|
}
|
|
prov := schemaMap(param["field_provenance"])["interface_type"]
|
|
if src, _ := prov["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("%s --%s interface_type source = %#v, want native_annotation", tc.path, tc.flag, prov)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogPatSheetWikiParamDecls(t *testing.T) {
|
|
// pat/sheet/wiki required and required_when pins publish via ParamDecl.
|
|
for _, tc := range []struct {
|
|
path string
|
|
flag string
|
|
wantRequired bool
|
|
requiredWhen string
|
|
}{
|
|
{"pat chmod", "session-id", false, "grant-type is session"},
|
|
{"sheet create", "name", true, ""},
|
|
{"sheet list", "node", true, ""},
|
|
{"sheet new", "node", true, ""},
|
|
{"sheet new", "name", true, ""},
|
|
{"wiki space create", "name", true, ""},
|
|
{"wiki space get", "workspace", true, ""},
|
|
} {
|
|
leaf, err := queryDeliverySchemaPayload([]string{tc.path})
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", tc.path, err)
|
|
}
|
|
param := schemaMap(leaf["parameters"])[tc.flag]
|
|
if param["required"] != tc.wantRequired {
|
|
t.Fatalf("%s --%s required = %#v, want %v", tc.path, tc.flag, param["required"], tc.wantRequired)
|
|
}
|
|
reqProv := schemaMap(param["field_provenance"])["required"]
|
|
if got, _ := reqProv["source"].(string); got != "native_annotation" {
|
|
t.Fatalf("%s --%s required source = %#v, want native_annotation", tc.path, tc.flag, reqProv)
|
|
}
|
|
if tc.requiredWhen == "" {
|
|
continue
|
|
}
|
|
if param["required_when"] != tc.requiredWhen {
|
|
t.Fatalf("%s --%s required_when = %#v, want %q", tc.path, tc.flag, param["required_when"], tc.requiredWhen)
|
|
}
|
|
whenProv := schemaMap(param["field_provenance"])["required_when"]
|
|
if got, _ := whenProv["source"].(string); got != "native_annotation" {
|
|
t.Fatalf("%s --%s required_when source = %#v, want native_annotation", tc.path, tc.flag, whenProv)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogReportCreateToChatRequiredAndAliasView(t *testing.T) {
|
|
// Former report.create_report overlay: --to-chat required=false (MCP marks
|
|
// it required). Alias "report create" must keep the same contract.
|
|
primary, err := queryDeliverySchemaPayload([]string{"report entry submit"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if schemaString(primary["canonical_path"]) != "report.create_report" {
|
|
t.Fatalf("report entry submit canonical = %q", schemaString(primary["canonical_path"]))
|
|
}
|
|
toChat := schemaMap(primary["parameters"])["to-chat"]
|
|
if toChat["required"] != false {
|
|
t.Fatalf("report entry submit --to-chat required = %#v, want false", toChat["required"])
|
|
}
|
|
reqProv := schemaMap(toChat["field_provenance"])["required"]
|
|
if src, _ := reqProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("report entry submit --to-chat required source = %#v, want native_annotation", reqProv)
|
|
}
|
|
|
|
alias, err := queryDeliverySchemaPayload([]string{"report create"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if alias["is_alias"] != true || schemaString(alias["cli_path"]) != "report create" {
|
|
t.Fatalf("report create alias view = %#v", alias)
|
|
}
|
|
if schemaString(alias["canonical_path"]) != "report.create_report" {
|
|
t.Fatalf("report create canonical = %q", schemaString(alias["canonical_path"]))
|
|
}
|
|
aliasToChat := schemaMap(alias["parameters"])["to-chat"]
|
|
if aliasToChat["required"] != false {
|
|
t.Fatalf("report create --to-chat required = %#v, want false (same as primary)", aliasToChat["required"])
|
|
}
|
|
aliasReqProv := schemaMap(aliasToChat["field_provenance"])["required"]
|
|
if src, _ := aliasReqProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("report create --to-chat required source = %#v, want native_annotation", aliasReqProv)
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogAitableParamDeclsMatchMergeBaseContract(t *testing.T) {
|
|
// Former schema_hints overlays (87910880) must stay on helpers leaves via
|
|
// contract.ParamDecl after hint clearance — not on aitable +shortcuts.
|
|
queryLeaf, err := queryDeliverySchemaPayload([]string{"aitable record query"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
queryRecordIDs := schemaMap(queryLeaf["parameters"])["record-ids"]
|
|
if queryRecordIDs["required"] != false {
|
|
t.Fatalf("aitable record query --record-ids required = %#v, want false", queryRecordIDs["required"])
|
|
}
|
|
queryReqProv := schemaMap(queryRecordIDs["field_provenance"])["required"]
|
|
if src, _ := queryReqProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("aitable record query --record-ids required source = %#v, want native_annotation", queryReqProv)
|
|
}
|
|
|
|
// Sibling atomic get keeps Cobra-required --record-ids; shortcut has its
|
|
// own optional StringSlice surface and must not inherit helpers contract.ParamDecl.
|
|
getLeaf, err := queryDeliverySchemaPayload([]string{"aitable record get"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if schemaMap(getLeaf["parameters"])["record-ids"]["required"] != true {
|
|
t.Fatalf("aitable record get --record-ids must stay required")
|
|
}
|
|
shortcutLeaf, err := queryDeliverySchemaPayload([]string{"aitable +record-query"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
shortcutRecordIDs := schemaMap(shortcutLeaf["parameters"])["record-ids"]
|
|
if shortcutRecordIDs["required"] != false {
|
|
t.Fatalf("aitable +record-query --record-ids required = %#v, want false", shortcutRecordIDs["required"])
|
|
}
|
|
if shortcutRecordIDs["type"] != "array" {
|
|
t.Fatalf("aitable +record-query --record-ids type = %#v, want array (shortcut StringSlice)", shortcutRecordIDs["type"])
|
|
}
|
|
|
|
for _, path := range []string{
|
|
"aitable view update aggregate",
|
|
"aitable view update card",
|
|
"aitable view update field-widths",
|
|
"aitable view update timebar",
|
|
} {
|
|
leaf, err := queryDeliverySchemaPayload([]string{path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
jsonParam := schemaMap(leaf["parameters"])["json"]
|
|
if jsonParam["required"] != false {
|
|
t.Fatalf("%s --json required = %#v, want false", path, jsonParam["required"])
|
|
}
|
|
reqProv := schemaMap(jsonParam["field_provenance"])["required"]
|
|
src, _ := reqProv["source"].(string)
|
|
if src != "native_annotation" && src != "require_one_of_constraint" {
|
|
t.Fatalf("%s --json required source = %#v, want native_annotation or require_one_of_constraint", path, reqProv)
|
|
}
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
path string
|
|
flag string
|
|
prop string
|
|
}{
|
|
{"aitable workflow create", "base-id", "baseId"},
|
|
{"aitable workflow create", "dsl", "dsl"},
|
|
{"aitable workflow create", "locale", "locale"},
|
|
{"aitable workflow update", "base-id", "baseId"},
|
|
{"aitable workflow update", "workflow-id", "workflowId"},
|
|
{"aitable workflow update", "dsl", "dsl"},
|
|
{"aitable workflow update", "locale", "locale"},
|
|
} {
|
|
leaf, err := queryDeliverySchemaPayload([]string{tc.path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
param := schemaMap(leaf["parameters"])[tc.flag]
|
|
if param["property"] != tc.prop {
|
|
t.Fatalf("%s --%s property = %#v, want %s", tc.path, tc.flag, param["property"], tc.prop)
|
|
}
|
|
propProv := schemaMap(param["field_provenance"])["property"]
|
|
if src, _ := propProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("%s --%s property source = %#v, want native_annotation", tc.path, tc.flag, propProv)
|
|
}
|
|
}
|
|
|
|
createLeaf, err := queryDeliverySchemaPayload([]string{"aitable workflow create"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createParams := schemaMap(createLeaf["parameters"])
|
|
for _, flagName := range []string{"base-id", "dsl"} {
|
|
if createParams[flagName]["required"] != true {
|
|
t.Fatalf("aitable workflow create --%s required = %#v, want true", flagName, createParams[flagName]["required"])
|
|
}
|
|
}
|
|
if createParams["locale"]["required"] != false {
|
|
t.Fatalf("aitable workflow create --locale required = %#v, want false", createParams["locale"]["required"])
|
|
}
|
|
dslTypeProv := schemaMap(createParams["dsl"]["field_provenance"])["interface_type"]
|
|
if src, _ := dslTypeProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("aitable workflow create --dsl interface_type source = %#v, want native_annotation", dslTypeProv)
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogAitableQueryKeywordAndParamDecls(t *testing.T) {
|
|
// helpers record query: --query → keyword must not fall back to
|
|
// flag_name_inference; JSON list flags keep wire interface_type via ParamDecl.
|
|
queryLeaf, err := queryDeliverySchemaPayload([]string{"aitable record query"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
queryParams := schemaMap(queryLeaf["parameters"])
|
|
queryFlag := queryParams["query"]
|
|
if queryFlag["property"] != "keyword" {
|
|
t.Fatalf("aitable record query --query property = %#v, want keyword", queryFlag["property"])
|
|
}
|
|
queryPropProv := schemaMap(queryFlag["field_provenance"])["property"]
|
|
src, _ := queryPropProv["source"].(string)
|
|
if src != "native_annotation" && src != "versioned_parameter_binding" {
|
|
t.Fatalf("aitable record query --query property source = %#v, want native_annotation or versioned_parameter_binding", queryPropProv)
|
|
}
|
|
for flagName, wantType := range map[string]string{
|
|
"filters": "object",
|
|
"sort": "array",
|
|
"record-ids": "array",
|
|
"field-ids": "array",
|
|
} {
|
|
param := queryParams[flagName]
|
|
if param["interface_type"] != wantType {
|
|
t.Fatalf("aitable record query --%s interface_type = %#v, want %s", flagName, param["interface_type"], wantType)
|
|
}
|
|
typeProv := schemaMap(param["field_provenance"])["interface_type"]
|
|
if got, _ := typeProv["source"].(string); got != "native_annotation" {
|
|
t.Fatalf("aitable record query --%s interface_type source = %#v, want native_annotation", flagName, typeProv)
|
|
}
|
|
}
|
|
|
|
// shortcut +record-query Execute maps --query → MCP keyword at runtime, but
|
|
// merge-base Schema published property=query (flag_name_inference). Keep that
|
|
// delivery for schema-compatibility; do not reintroduce contract.ParamDecl keyword.
|
|
shortcutLeaf, err := queryDeliverySchemaPayload([]string{"aitable +record-query"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
shortcutQuery := schemaMap(shortcutLeaf["parameters"])["query"]
|
|
if shortcutQuery["property"] != "query" {
|
|
t.Fatalf("aitable +record-query --query property = %#v, want query (merge-base)", shortcutQuery["property"])
|
|
}
|
|
if _, has := shortcutQuery["interface_type"]; has {
|
|
t.Fatalf("aitable +record-query --query unexpectedly publishes interface_type: %#v", shortcutQuery["interface_type"])
|
|
}
|
|
for _, flagName := range []string{"filters", "sort"} {
|
|
param := schemaMap(shortcutLeaf["parameters"])[flagName]
|
|
if _, has := param["interface_type"]; has {
|
|
t.Fatalf("aitable +record-query --%s unexpectedly publishes interface_type: %#v", flagName, param["interface_type"])
|
|
}
|
|
}
|
|
|
|
// Required pins that match runtime validation.
|
|
for _, tc := range []struct {
|
|
path string
|
|
flag string
|
|
}{
|
|
{"aitable attachment upload", "size"},
|
|
{"aitable chart update", "config"},
|
|
{"aitable record upsert", "records"},
|
|
{"aitable field create", "fields"},
|
|
{"aitable dashboard create", "name"},
|
|
{"aitable dashboard update", "name"},
|
|
{"aitable view update visible-fields", "field-ids"},
|
|
} {
|
|
leaf, err := queryDeliverySchemaPayload([]string{tc.path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
param := schemaMap(leaf["parameters"])[tc.flag]
|
|
if param["required"] != true {
|
|
t.Fatalf("%s --%s required = %#v, want true", tc.path, tc.flag, param["required"])
|
|
}
|
|
reqProv := schemaMap(param["field_provenance"])["required"]
|
|
if got, _ := reqProv["source"].(string); got != "native_annotation" {
|
|
t.Fatalf("%s --%s required source = %#v, want native_annotation", tc.path, tc.flag, reqProv)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogShortcutQueryKeepsMergeBaseProperty(t *testing.T) {
|
|
// Composite shortcuts whose Execute maps --query → MCP keyword still publish
|
|
// property=query at merge-base (flag_name_inference). Publishing keyword via
|
|
// contract.ParamDecl breaks schema-compatibility; keep Schema delivery aligned.
|
|
for _, path := range []string{
|
|
"aitable +find-record",
|
|
"doc +find-doc",
|
|
"drive +find-file",
|
|
"mail +find-mail-user",
|
|
"chat +search-msg",
|
|
"doc +search",
|
|
"drive +search",
|
|
"drive +search-docs",
|
|
"minutes +list-mine",
|
|
"minutes +list-shared",
|
|
"minutes +list-all",
|
|
"wiki +space-search",
|
|
"contact +search-user",
|
|
"chat +chat-search",
|
|
"chat +bot-find",
|
|
} {
|
|
leaf, err := queryDeliverySchemaPayload([]string{path})
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", path, err)
|
|
}
|
|
query := schemaMap(leaf["parameters"])["query"]
|
|
if query["property"] != "query" {
|
|
t.Fatalf("%s --query property = %#v, want query (merge-base)", path, query["property"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogContactParamDeclsMatchMergeBaseContract(t *testing.T) {
|
|
// Former metadata/contact.json overlays at 87910880 must stay on the
|
|
// published primary flags after contract.ParamDecl migration. Hidden Cobra
|
|
// aliases (--id/--userid/--dept-name/--super-dept*) must not appear.
|
|
type wantParam struct {
|
|
property string
|
|
required bool
|
|
interfaceType string // empty → do not assert
|
|
}
|
|
cases := []struct {
|
|
path string
|
|
params map[string]wantParam
|
|
absent []string
|
|
}{
|
|
{
|
|
path: "contact user invite",
|
|
params: map[string]wantParam{
|
|
"org-user-name": {property: "orgUserName", required: true},
|
|
"org-user-mobile": {property: "orgUserMobile", required: true},
|
|
"depts": {property: "depts", required: false, interfaceType: "array"},
|
|
},
|
|
},
|
|
{
|
|
path: "contact user update",
|
|
params: map[string]wantParam{
|
|
"user-id": {property: "userId", required: true},
|
|
"org-user-name": {property: "orgUserName", required: false},
|
|
"depts": {property: "depts", required: false, interfaceType: "array"},
|
|
"master-user-id": {property: "masterUserId", required: false},
|
|
},
|
|
absent: []string{"id", "userid"},
|
|
},
|
|
{
|
|
path: "contact user update-self",
|
|
params: map[string]wantParam{
|
|
"nick": {property: "nick", required: false},
|
|
"avatar-file-id": {property: "avatarFileId", required: false},
|
|
},
|
|
},
|
|
{
|
|
path: "contact user profile get",
|
|
params: map[string]wantParam{
|
|
"fields": {property: "fieldCodeList", required: false},
|
|
"staff-id": {property: "staffId", required: false},
|
|
},
|
|
},
|
|
{
|
|
path: "contact dept create",
|
|
params: map[string]wantParam{
|
|
"name": {property: "deptName", required: true},
|
|
"parent": {property: "superDeptId", required: false, interfaceType: "integer"},
|
|
"create-dept-group": {property: "createDeptGroup", required: true},
|
|
},
|
|
absent: []string{"dept-name", "super-dept", "super-dept-id"},
|
|
},
|
|
{
|
|
path: "contact dept update",
|
|
params: map[string]wantParam{
|
|
"dept": {property: "deptId", required: true, interfaceType: "integer"},
|
|
"name": {property: "deptName", required: true},
|
|
"parent": {property: "superDeptId", required: false, interfaceType: "integer"},
|
|
},
|
|
absent: []string{"id", "ids", "dept-id", "dept-ids", "dept-name", "super-dept", "super-dept-id"},
|
|
},
|
|
{
|
|
path: "contact org create",
|
|
params: map[string]wantParam{
|
|
"org-name": {property: "orgName", required: true},
|
|
"creator-username": {property: "creatorUsername", required: true},
|
|
},
|
|
},
|
|
{
|
|
path: "contact account create",
|
|
params: map[string]wantParam{
|
|
"org-user-name": {property: "orgUserName", required: true},
|
|
"login-id": {property: "loginId", required: true},
|
|
"org-user-mobile": {property: "orgUserMobile", required: false},
|
|
"email": {property: "email", required: false},
|
|
"dept-ids": {property: "deptIds", required: false, interfaceType: "array"},
|
|
"send-pwd-via-sms": {property: "sendPwdViaSms", required: false},
|
|
},
|
|
},
|
|
{
|
|
path: "contact account update",
|
|
params: map[string]wantParam{
|
|
"user-id": {property: "userId", required: true},
|
|
"org-user-name": {property: "orgUserName", required: false},
|
|
"depts": {property: "depts", required: false, interfaceType: "array"},
|
|
"master-user-id": {property: "masterUserId", required: false},
|
|
"nick": {property: "nick", required: false},
|
|
"avatar-file-id": {property: "avatarFileId", required: false},
|
|
},
|
|
absent: []string{"id", "userid"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
leaf, err := queryDeliverySchemaPayload([]string{tc.path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parameters := schemaMap(leaf["parameters"])
|
|
for flagName, want := range tc.params {
|
|
param := parameters[flagName]
|
|
if param["property"] != want.property || param["required"] != want.required {
|
|
t.Fatalf("%s --%s = %#v, want property=%s required=%v",
|
|
tc.path, flagName, param, want.property, want.required)
|
|
}
|
|
if want.interfaceType != "" && param["interface_type"] != want.interfaceType {
|
|
t.Fatalf("%s --%s interface_type = %#v, want %s",
|
|
tc.path, flagName, param["interface_type"], want.interfaceType)
|
|
}
|
|
propProv := schemaMap(param["field_provenance"])["property"]
|
|
if src, _ := propProv["source"].(string); src != "native_annotation" &&
|
|
src != "versioned_parameter_binding" && src != "flag_name_inference" {
|
|
t.Fatalf("%s --%s property source = %#v, want native_annotation/binding/inference",
|
|
tc.path, flagName, propProv)
|
|
}
|
|
reqProv := schemaMap(param["field_provenance"])["required"]
|
|
if src, _ := reqProv["source"].(string); src != "native_annotation" && src != "require_one_of_constraint" {
|
|
t.Fatalf("%s --%s required source = %#v, want native_annotation or require_one_of_constraint",
|
|
tc.path, flagName, reqProv)
|
|
}
|
|
if want.interfaceType != "" {
|
|
prov := schemaMap(param["field_provenance"])["interface_type"]
|
|
if src, _ := prov["source"].(string); src != "native_annotation" && src != "mcp_metadata" {
|
|
t.Fatalf("%s --%s interface_type source = %#v", tc.path, flagName, prov)
|
|
}
|
|
}
|
|
}
|
|
for _, flagName := range tc.absent {
|
|
if _, ok := parameters[flagName]; ok {
|
|
t.Fatalf("%s unexpectedly publishes hidden alias --%s", tc.path, flagName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeliveryCatalogChatParamDeclsFrom87910880Reviewed(t *testing.T) {
|
|
// CI P1: +messages-send must not publish RequiredWhen for bot/webhook
|
|
// credentials. Runtime validateMessagesSend already enforces them;
|
|
// re-adding Flag.RequiredWhen breaks merge-base schema-compatibility.
|
|
sendLeaf, err := queryDeliverySchemaPayload([]string{"chat +messages-send"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sendParams := schemaMap(sendLeaf["parameters"])
|
|
for _, flagName := range []string{"robot-code", "webhook-token"} {
|
|
param := sendParams[flagName]
|
|
if param == nil {
|
|
t.Fatalf("chat +messages-send missing --%s", flagName)
|
|
}
|
|
if rw := param["required_when"]; rw != nil && rw != "" {
|
|
t.Fatalf("chat +messages-send --%s required_when = %#v, want absent/empty (no Schema gate)", flagName, rw)
|
|
}
|
|
rwProv := schemaMap(param["field_provenance"])["required_when"]
|
|
if src, _ := rwProv["source"].(string); src != "default" && src != "" {
|
|
t.Fatalf("chat +messages-send --%s required_when source = %#v, want default", flagName, rwProv)
|
|
}
|
|
}
|
|
|
|
// Former metadata/chat.json overlays (87910880) must stay on
|
|
// helpers/shortcut leaves via contract.ParamDecl after hint clearance.
|
|
for _, tc := range []struct {
|
|
path string
|
|
flag string
|
|
property string
|
|
required any
|
|
interfaceType string
|
|
}{
|
|
{"chat message edit", "conversation-id", "openConversationId", true, ""},
|
|
{"chat message edit", "msg-id", "openMessageId", true, ""},
|
|
{"chat message edit", "at-open-dingtalk-ids", "atOpenDingTalkIds", false, "array"},
|
|
{"chat message update-text-emotion", "msg-id", "openMsgId", true, ""},
|
|
{"chat message update-text-emotion", "old-emotion-id", "oldEmotionId", true, ""},
|
|
{"chat category batch-info", "category-ids", "categoryIds", true, "array"},
|
|
{"chat category list-by-conv", "group", "openConversationId", true, ""},
|
|
{"chat group update-nick", "group", "openConversationId", true, ""},
|
|
{"chat group upgrade-to-external", "extension", "extension", false, "object"},
|
|
{"chat +messages-send-card", "receiver-open-dingtalk-id", "receiverOpenDingTalkId", false, ""},
|
|
{"chat message list-favorites", "size", "", false, "string"},
|
|
} {
|
|
leaf, err := queryDeliverySchemaPayload([]string{tc.path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
param := schemaMap(leaf["parameters"])[tc.flag]
|
|
if tc.property != "" && param["property"] != tc.property {
|
|
t.Fatalf("%s --%s property = %#v, want %s", tc.path, tc.flag, param["property"], tc.property)
|
|
}
|
|
if tc.required != nil && param["required"] != tc.required {
|
|
t.Fatalf("%s --%s required = %#v, want %#v", tc.path, tc.flag, param["required"], tc.required)
|
|
}
|
|
if tc.interfaceType != "" && param["interface_type"] != tc.interfaceType {
|
|
t.Fatalf("%s --%s interface_type = %#v, want %s", tc.path, tc.flag, param["interface_type"], tc.interfaceType)
|
|
}
|
|
if tc.property != "" {
|
|
propProv := schemaMap(param["field_provenance"])["property"]
|
|
src, _ := propProv["source"].(string)
|
|
if src != "native_annotation" && src != "versioned_parameter_binding" && src != "reviewed_mapping_exclusion" {
|
|
t.Fatalf("%s --%s property source = %#v, want native_annotation/binding/exclusion", tc.path, tc.flag, propProv)
|
|
}
|
|
}
|
|
if tc.interfaceType != "" {
|
|
typeProv := schemaMap(param["field_provenance"])["interface_type"]
|
|
if src, _ := typeProv["source"].(string); src != "native_annotation" {
|
|
t.Fatalf("%s --%s interface_type source = %#v, want native_annotation", tc.path, tc.flag, typeProv)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hidden conversation aliases must stay unpublished (merge-base parity).
|
|
editLeaf, err := queryDeliverySchemaPayload([]string{"chat message edit"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
editParams := schemaMap(editLeaf["parameters"])
|
|
for _, hidden := range []string{"group", "id", "chat"} {
|
|
if _, ok := editParams[hidden]; ok {
|
|
t.Fatalf("chat message edit unexpectedly publishes hidden alias --%s", hidden)
|
|
}
|
|
}
|
|
listByConv, err := queryDeliverySchemaPayload([]string{"chat category list-by-conv"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listParams := schemaMap(listByConv["parameters"])
|
|
for _, hidden := range []string{"conversation-id", "id"} {
|
|
if _, ok := listParams[hidden]; ok {
|
|
t.Fatalf("chat category list-by-conv unexpectedly publishes hidden alias --%s", hidden)
|
|
}
|
|
}
|
|
}
|