Files
dingtalk-workspace-cli/internal/cli/schema_catalog_test.go

1483 lines
59 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", "version", "password"} {
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"])
}
if got := parameters["version"]["property"]; got != "historyVersion" {
t.Fatalf("doc read --version property = %#v, want historyVersion", got)
}
if parameters["version"]["type"] != "integer" {
t.Fatalf("doc read --version type = %#v, want integer", parameters["version"]["type"])
}
if got := parameters["password"]["property"]; got != "password" {
t.Fatalf("doc read --password property = %#v, want password", got)
}
}
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)
}
leaf["future_audit_field"] = "must not leak into Agent view"
for _, raw := range schemaMap(leaf["parameters"]) {
raw["future_mapping_field"] = "must not leak into Agent view"
}
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", "future_audit_field"} {
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", "future_mapping_field"} {
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" {
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", "message-id", "openMessageId", true, ""},
{"chat message edit", "at-open-dingtalk-ids", "atOpenDingTalkIds", false, "array"},
{"chat message update-text-emotion", "message-id", "openMsgId", true, ""},
{"chat message send", "idempotency-key", "uuid", false, ""},
{"chat message send-card", "at-all", "atAll", false, ""},
{"chat message send-card", "at-open-dingtalk-ids", "atOpenDingTalkIds", false, "array"},
{"chat message update-text-emotion", "old-emotion-id", "oldEmotionId", true, ""},
{"chat category batch-info", "category-ids", "categoryIds", true, "array"},
{"chat category list-by-conv", "conversation-id", "openConversationId", true, ""},
{"chat group update-nick", "conversation-id", "", 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)
}
}
}
// Manifest-covered migrations hide legacy aliases; manifest-external
// commands keep their existing visible flags for compatibility.
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)
}
}
sendLeaf, err = queryDeliverySchemaPayload([]string{"chat message send"})
if err != nil {
t.Fatal(err)
}
sendParams = schemaMap(sendLeaf["parameters"])
if _, ok := sendParams["uuid"]; ok {
t.Fatal("chat message send unexpectedly publishes hidden alias --uuid")
}
listByConv, err := queryDeliverySchemaPayload([]string{"chat category list-by-conv"})
if err != nil {
t.Fatal(err)
}
listParams := schemaMap(listByConv["parameters"])
if _, ok := listParams["conversation-id"]; !ok {
t.Fatalf("chat category list-by-conv missing public canonical --conversation-id")
}
if _, ok := listParams["group"]; !ok {
t.Fatalf("chat category list-by-conv unexpectedly hides manifest-external --group")
}
if _, ok := listParams["id"]; ok {
t.Fatalf("chat category list-by-conv unexpectedly publishes hidden alias --id")
}
for _, path := range []string{
"chat message add-emoji",
"chat message remove-emoji",
"chat message add-text-emotion",
"chat message remove-text-emotion",
} {
leaf, err := queryDeliverySchemaPayload([]string{path})
if err != nil {
t.Fatal(err)
}
params := schemaMap(leaf["parameters"])
if _, ok := params["conversation-id"]; !ok {
t.Fatalf("%s missing public canonical --conversation-id", path)
}
for _, visible := range []string{"group", "id", "chat"} {
if _, ok := params[visible]; !ok {
t.Fatalf("%s unexpectedly hides manifest-external --%s", path, visible)
}
}
}
groupBots, err := queryDeliverySchemaPayload([]string{"chat group bots"})
if err != nil {
t.Fatal(err)
}
groupBotsParams := schemaMap(groupBots["parameters"])
group := groupBotsParams["group"]
if group == nil {
t.Fatal("chat group bots missing public legacy --group")
}
if group["property"] != "openConversationId" {
t.Fatalf("chat group bots --group property = %#v, want openConversationId", group["property"])
}
for _, migrated := range []string{"conversation-id", "group-name"} {
if _, ok := groupBotsParams[migrated]; ok {
t.Fatalf("chat group bots unexpectedly publishes migrated --%s", migrated)
}
}
}