313 lines
13 KiB
Go
313 lines
13 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParamConceptsJSONSchemaDocumentsClosedShape(t *testing.T) {
|
|
var schema map[string]any
|
|
if err := json.Unmarshal(embeddedParamConceptsSchemaJSON, &schema); err != nil {
|
|
t.Fatalf("decode param_concepts.schema.json: %v", err)
|
|
}
|
|
if schema["$schema"] != "https://json-schema.org/draft/2020-12/schema" || schema["additionalProperties"] != false {
|
|
t.Fatalf("param concepts root schema is not closed: %#v", schema)
|
|
}
|
|
definitions := schema["$defs"].(map[string]any)
|
|
concept := definitions["concept"].(map[string]any)
|
|
if concept["additionalProperties"] != false {
|
|
t.Fatalf("concept schema allows unknown fields: %#v", concept)
|
|
}
|
|
properties := concept["properties"].(map[string]any)
|
|
for _, field := range []string{"denotes", "canonical_hint", "members", "excludes", "commands", "risk"} {
|
|
if _, ok := properties[field]; !ok {
|
|
t.Fatalf("concept schema is missing %s", field)
|
|
}
|
|
}
|
|
override := definitions["commandOverride"].(map[string]any)
|
|
if override["additionalProperties"] != false {
|
|
t.Fatalf("commandOverride schema allows unknown fields: %#v", override)
|
|
}
|
|
|
|
var source map[string]any
|
|
if err := json.Unmarshal(embeddedParamConceptsJSON, &source); err != nil {
|
|
t.Fatalf("decode param_concepts.json: %v", err)
|
|
}
|
|
if source["$schema"] != paramConceptsSchemaRef {
|
|
t.Fatalf("param concepts source $schema = %#v, want %q", source["$schema"], paramConceptsSchemaRef)
|
|
}
|
|
}
|
|
|
|
func TestEmbeddedParamConceptsLoadsAndSatisfiesInvariants(t *testing.T) {
|
|
concepts, err := LoadParamConcepts()
|
|
if err != nil {
|
|
t.Fatalf("LoadParamConcepts() error = %v", err)
|
|
}
|
|
if len(concepts.Concepts) == 0 {
|
|
t.Fatal("embedded param concepts declares no concepts")
|
|
}
|
|
|
|
// Members must be globally unique and disjoint from their own excludes.
|
|
memberOwner := make(map[string]string)
|
|
for _, concept := range concepts.Concepts {
|
|
if len(concept.Commands) == 0 {
|
|
t.Fatalf("concept %s has no reviewed command scope", concept.ID)
|
|
}
|
|
excludeSet := make(map[string]bool, len(concept.Excludes))
|
|
for _, exclude := range concept.Excludes {
|
|
excludeSet[exclude] = true
|
|
}
|
|
for _, member := range concept.Members {
|
|
if owner, exists := memberOwner[member]; exists {
|
|
t.Fatalf("member %q belongs to both concept %s and %s", member, owner, concept.ID)
|
|
}
|
|
memberOwner[member] = concept.ID
|
|
if excludeSet[member] {
|
|
t.Fatalf("concept %s lists %q as both member and exclude", concept.ID, member)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every bind target must reference a declared concept.
|
|
for _, override := range concepts.Overrides {
|
|
if override.Confirm || override.Investigate {
|
|
t.Fatalf("current override %q remains unresolved (confirm=%v investigate=%v)", override.CommandPath, override.Confirm, override.Investigate)
|
|
}
|
|
for flag, conceptID := range override.Bind {
|
|
if _, ok := concepts.ByConcept[conceptID]; !ok {
|
|
t.Fatalf("command_override %q binds %q to undeclared concept %q", override.CommandPath, flag, conceptID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fixture sentinels are limited to the two known did-you-mean forms.
|
|
for _, c := range concepts.Fixture {
|
|
if strings.HasPrefix(c.Expect, "did-you-mean:") &&
|
|
c.Expect != paramDidYouMeanAmbiguous && c.Expect != paramDidYouMeanBlocked {
|
|
t.Fatalf("fixture %q/%q has unknown sentinel %q", c.Command, c.Emitted, c.Expect)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParamConceptRiskAuditBoundaries(t *testing.T) {
|
|
concepts, err := LoadParamConcepts()
|
|
if err != nil {
|
|
t.Fatalf("LoadParamConcepts() error = %v", err)
|
|
}
|
|
assertMembers := func(id string, forbidden ...string) {
|
|
t.Helper()
|
|
concept, ok := concepts.ByConcept[id]
|
|
if !ok {
|
|
t.Fatalf("missing audited concept %q", id)
|
|
}
|
|
members := make(map[string]bool, len(concept.Members))
|
|
for _, member := range concept.Members {
|
|
members[member] = true
|
|
}
|
|
for _, name := range forbidden {
|
|
if members[name] {
|
|
t.Fatalf("audited concept %s still contains forbidden cross-semantics member %q", id, name)
|
|
}
|
|
}
|
|
}
|
|
assertMembers("user_id", "users", "user-ids")
|
|
assertMembers("user_ids", "user", "user-id")
|
|
assertMembers("dept_id", "depts", "dept-ids")
|
|
assertMembers("dept_ids", "dept", "dept-id")
|
|
assertMembers("group_id", "conversation-ids", "group-ids")
|
|
assertMembers("page_number", "page-index")
|
|
assertMembers("robot_code", "robot-id")
|
|
}
|
|
|
|
func TestDecodeParamConceptsRejectsUnknownFieldsAtEveryLevel(t *testing.T) {
|
|
valid := `{"$schema":"./param_concepts.schema.json","version":1,` +
|
|
`"concepts":{"search_query":{"denotes":"d","canonical_hint":"query","members":["query"],"commands":["demo cmd"],"risk":"green"}},` +
|
|
`"command_overrides":{"chat group rename":{"bind":{"id":"search_query"}}}}`
|
|
for name, input := range map[string]string{
|
|
"root": strings.Replace(valid, `"version":1`, `"version":1,"unknown":true`, 1),
|
|
"concept": strings.Replace(valid, `"risk":"green"`, `"risk":"green","unknown":true`, 1),
|
|
"override": strings.Replace(valid, `"bind":{"id":"search_query"}`, `"bind":{"id":"search_query"},"unknown":true`, 1),
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := decodeParamConcepts([]byte(input)); err == nil || !strings.Contains(err.Error(), "unknown field") {
|
|
t.Fatalf("decodeParamConcepts() error = %v, want unknown field", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeParamConceptsEnforcesReviewedConstraints(t *testing.T) {
|
|
wrap := func(body string) string {
|
|
return `{"$schema":"./param_concepts.schema.json","version":1,` + body + `}`
|
|
}
|
|
concept := func(members, excludes, risk string) string {
|
|
return `"concepts":{"c_one":{"denotes":"d","canonical_hint":"query","members":` + members + `,"excludes":` + excludes + `,"commands":["demo cmd"],"risk":"` + risk + `"}}`
|
|
}
|
|
tests := map[string]string{
|
|
"missing schema ref": `{"version":1,"concepts":{"c_one":{"denotes":"d","canonical_hint":"query","members":["query"],"risk":"green"}}}`,
|
|
"wrong version": `{"$schema":"./param_concepts.schema.json","version":2,"concepts":{"c_one":{"denotes":"d","canonical_hint":"query","members":["query"],"risk":"green"}}}`,
|
|
"no concepts": wrap(`"concepts":{}`),
|
|
"invalid concept id": wrap(`"concepts":{"BadID":{"denotes":"d","canonical_hint":"query","members":["query"],"risk":"green"}}`),
|
|
"empty denotes": wrap(`"concepts":{"c_one":{"denotes":"","canonical_hint":"query","members":["query"],"risk":"green"}}`),
|
|
"invalid risk": wrap(concept(`["query"]`, `[]`, "red")),
|
|
"no members": wrap(`"concepts":{"c_one":{"denotes":"d","canonical_hint":"query","members":[],"risk":"green"}}`),
|
|
"no command scope": wrap(`"concepts":{"c_one":{"denotes":"d","canonical_hint":"query","members":["query"],"commands":[],"risk":"green"}}`),
|
|
"member equals exclude": wrap(concept(`["query"]`, `["query"]`, "green")),
|
|
"member overlaps concepts": wrap(`"concepts":{"c_one":{"denotes":"d","canonical_hint":"query","members":["query"],"risk":"green"},"c_two":{"denotes":"d","canonical_hint":"query","members":["query"],"risk":"green"}}`),
|
|
"bind undeclared concept": wrap(concept(`["query"]`, `[]`, "green") + `,"command_overrides":{"chat group rename":{"bind":{"id":"missing"}}}`),
|
|
"empty override": wrap(concept(`["query"]`, `[]`, "green") + `,"command_overrides":{"chat group rename":{}}`),
|
|
"bad fixture sentinel": wrap(concept(`["query"]`, `[]`, "green") + `,"validation_fixture":{"cases":[{"command":"chat group rename","emitted":"group","expect":"did-you-mean:oops"}]}`),
|
|
"empty fixture cases": wrap(concept(`["query"]`, `[]`, "green") + `,"validation_fixture":{"cases":[]}`),
|
|
}
|
|
for name, input := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := decodeParamConcepts([]byte(input)); err == nil {
|
|
t.Fatal("decodeParamConcepts() unexpectedly accepted invalid reviewed source")
|
|
}
|
|
})
|
|
}
|
|
|
|
got, err := decodeParamConcepts([]byte(wrap(concept(`["query","keyword"]`, `["name"]`, "green"))))
|
|
if err != nil {
|
|
t.Fatalf("decodeParamConcepts() valid source error = %v", err)
|
|
}
|
|
if len(got.Concepts) != 1 || got.Concepts[0].ID != "c_one" {
|
|
t.Fatalf("decodeParamConcepts() concepts = %#v", got.Concepts)
|
|
}
|
|
}
|
|
|
|
func validParamConceptSpecFixture() paramConceptSpec {
|
|
return paramConceptSpec{
|
|
Denotes: "a reviewed value",
|
|
CanonicalHint: "query",
|
|
Members: []string{"query"},
|
|
Excludes: []string{"name"},
|
|
Commands: []string{"demo run"},
|
|
Risk: "green",
|
|
}
|
|
}
|
|
|
|
func TestDecodeParamConceptsRemainingSyntaxEdges(t *testing.T) {
|
|
valid := `{"$schema":"./param_concepts.schema.json","version":1,` +
|
|
`"concepts":{"c_one":{"denotes":"d","canonical_hint":"query","members":["query"],"commands":["demo run"],"risk":"green"}}}`
|
|
if _, err := decodeParamConcepts([]byte(valid + ` {}`)); err == nil || !strings.Contains(err.Error(), "multiple JSON values") {
|
|
t.Fatalf("multiple JSON values error = %v", err)
|
|
}
|
|
|
|
withEmptyMorphDescription := strings.Replace(
|
|
valid,
|
|
`"concepts":`,
|
|
`"morphological_rules":{"camel":{"desc":""}},"concepts":`,
|
|
1,
|
|
)
|
|
if _, err := decodeParamConcepts([]byte(withEmptyMorphDescription)); err == nil || !strings.Contains(err.Error(), "empty desc") {
|
|
t.Fatalf("empty morph description error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeParamConceptSpecsRemainingValidationEdges(t *testing.T) {
|
|
base := validParamConceptSpecFixture()
|
|
invalidCanonical := base
|
|
invalidCanonical.CanonicalHint = "bad token"
|
|
invalidMember := base
|
|
invalidMember.Members = []string{"bad token"}
|
|
repeatedMember := base
|
|
repeatedMember.Members = []string{"query", "query"}
|
|
invalidExclude := base
|
|
invalidExclude.Excludes = []string{"bad token"}
|
|
repeatedExclude := base
|
|
repeatedExclude.Excludes = []string{"name", "name"}
|
|
invalidCommand := base
|
|
invalidCommand.Commands = []string{"demo /bad"}
|
|
repeatedCommand := base
|
|
repeatedCommand.Commands = []string{"demo run", "demo run"}
|
|
|
|
tests := map[string]map[string]paramConceptSpec{
|
|
"invalid canonical hint": {"c_one": invalidCanonical},
|
|
"invalid member": {"c_one": invalidMember},
|
|
"repeated member": {"c_one": repeatedMember},
|
|
"invalid exclude": {"c_one": invalidExclude},
|
|
"repeated exclude": {"c_one": repeatedExclude},
|
|
"invalid command": {"c_one": invalidCommand},
|
|
"repeated command": {"c_one": repeatedCommand},
|
|
"cross-concept member": {
|
|
"c_one": base,
|
|
"c_two": base,
|
|
},
|
|
}
|
|
for name, specs := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, _, err := decodeParamConceptSpecs(specs); err == nil {
|
|
t.Fatal("decodeParamConceptSpecs() unexpectedly accepted invalid input")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeParamCommandOverridesRemainingValidationEdges(t *testing.T) {
|
|
byConcept := map[string]Concept{"c_one": {ID: "c_one"}}
|
|
tests := map[string]map[string]paramCommandOverride{
|
|
"invalid path": {
|
|
" demo run": {Block: []string{"name"}},
|
|
},
|
|
"invalid bind flag": {
|
|
"demo run": {Bind: map[string]string{"bad token": "c_one"}},
|
|
},
|
|
"invalid scoped emitted": {
|
|
"demo run": {ScopedAliases: map[string]string{"bad token": "query"}},
|
|
},
|
|
"invalid scoped target": {
|
|
"demo run": {ScopedAliases: map[string]string{"keyword": "bad token"}},
|
|
},
|
|
"invalid block token": {
|
|
"demo run": {Block: []string{"bad token"}},
|
|
},
|
|
"repeated ambiguous token": {
|
|
"demo run": {Ambiguous: []string{"uid", "uid"}},
|
|
},
|
|
}
|
|
for name, specs := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := decodeParamCommandOverrides(specs, byConcept); err == nil {
|
|
t.Fatal("decodeParamCommandOverrides() unexpectedly accepted invalid input")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeParamFixtureCasesRemainingValidationEdges(t *testing.T) {
|
|
tests := map[string]paramFixtureCaseSpec{
|
|
"invalid command": {Command: " demo run", Emitted: "query", Expect: "query"},
|
|
"invalid emitted": {Command: "demo run", Emitted: "bad token", Expect: "query"},
|
|
"empty expect": {Command: "demo run", Emitted: "query", Expect: " "},
|
|
"invalid expect": {Command: "demo run", Emitted: "query", Expect: "bad token"},
|
|
"negative occ": {Command: "demo run", Emitted: "query", Expect: "query", Occ: -1},
|
|
}
|
|
for name, fixture := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := decodeParamFixtureCases(¶mValidationFixtureSpec{Cases: []paramFixtureCaseSpec{fixture}}); err == nil {
|
|
t.Fatal("decodeParamFixtureCases() unexpectedly accepted invalid input")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParamConceptPathAndTokenValidationEdges(t *testing.T) {
|
|
if validParamCommandPath(" demo run") {
|
|
t.Fatal("command path with surrounding whitespace was accepted")
|
|
}
|
|
if validParamCommandPath("demo /bad") {
|
|
t.Fatal("command path with an invalid token was accepted")
|
|
}
|
|
if err := validParamTokenList("demo run", "block", []string{"bad token"}); err == nil {
|
|
t.Fatal("invalid parameter token was accepted")
|
|
}
|
|
if err := validParamTokenList("demo run", "block", []string{"uid", "uid"}); err == nil {
|
|
t.Fatal("repeated parameter token was accepted")
|
|
}
|
|
}
|