test: cover unified schema validation edges

This commit is contained in:
玉澜 committed 2026-08-10 22:15:04 +08:00
1 parent 12088f2d44
commit a37f614be4
5 files changed
+137

No files matched your search

@@ -145,6 +145,47 @@ func TestValidateCatalogStructureAcceptsStandalonePagination(t *testing.T) {
}
}
func TestValidateCatalogStructureRejectsMalformedStandalonePagination(t *testing.T) {
validPaginationEntry := func() map[string]any {
entry := validCatalogToolEntry()
parameters := entry["parameters"].(map[string]any)
parameters["cursor"] = map[string]any{
"description": "续页游标",
"field_provenance": map[string]any{},
"required": false,
"type": "string",
}
entry["parameter_count"] = float64(len(parameters))
entry["pagination"] = map[string]any{
"kind": contract.PaginationKindCursor,
"cursor_parameter": "cursor",
"meta_path": contract.PaginationMetaPath,
"endpoint_exhausted_path": contract.PaginationExhaustedPath,
"next_token_path": contract.PaginationNextTokenPath,
}
return entry
}
for _, tc := range []struct {
name string
mutate func(map[string]any)
want string
}{
{"not an object", func(entry map[string]any) { entry["pagination"] = "cursor" }, `field "pagination" must be an object`},
{"empty cursor", func(entry map[string]any) { entry["pagination"].(map[string]any)["cursor_parameter"] = " " }, "cursor_parameter must be a non-empty string"},
{"unknown cursor", func(entry map[string]any) { entry["pagination"].(map[string]any)["cursor_parameter"] = "page-token" }, "references missing parameter"},
} {
t.Run(tc.name, func(t *testing.T) {
entry := validPaginationEntry()
tc.mutate(entry)
err := ValidateCatalogStructure(catalogPayload(t, entry))
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("ValidateCatalogStructure() error = %v, want %q", err, tc.want)
}
})
}
}
func TestCrossPlatformCoverageValidateCatalogStructureRejectsViolations(t *testing.T) {
cases := []struct {
name string
@@ -6,6 +6,7 @@ package cli
import (
"encoding/json"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
@@ -118,3 +119,34 @@ func TestToolSpecRejectsInvalidResultInsteadOfDroppingIt(t *testing.T) {
t.Fatal("invalid result schema was silently dropped")
}
}
func TestToolSpecRejectsInvalidOrUndeclaredPaginationCursor(t *testing.T) {
identity := contract.ToolIdentitySpec{ProductID: "dev", Name: "list", CLIName: "list", CLIPath: "dev list"}
for _, tc := range []struct {
name string
parameters []ParameterSpec
pagination *contract.PaginationSpec
want string
}{
{
name: "invalid pagination declaration",
parameters: []ParameterSpec{{Name: "cursor", Type: "string"}},
pagination: &contract.PaginationSpec{Kind: "offset", CursorParameter: "cursor"},
want: "unsupported kind",
},
{
name: "cursor is not a parameter",
pagination: &contract.PaginationSpec{Kind: contract.PaginationKindCursor, CursorParameter: "cursor"},
want: "is not a declared parameter",
},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := ToolSpecFromRuntime(RuntimeToolSpecInput{
Identity: identity, Parameters: tc.parameters, Pagination: tc.pagination,
})
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("ToolSpecFromRuntime() error = %v, want %q", err, tc.want)
}
})
}
}
@@ -41,6 +41,38 @@ func TestFrameworkResultSpecValidationEdges(t *testing.T) {
}
}
func TestFrameworkResultSchemaDescriptionValidationEdges(t *testing.T) {
for _, tc := range []struct {
name string
raw string
want string
}{
{"invalid json", `{`, "must be one JSON Schema object"},
{"properties is not object", `{"properties":[]}`, "properties must be an object"},
{"property is not schema", `{"properties":{"id":"string"}}`, "properties.id must be a JSON Schema object"},
{"property description missing", `{"properties":{"id":{"type":"string"}}}`, "properties.id requires description"},
{"nested property invalid", `{"properties":{"item":{"description":"item","properties":[]}}}`, "properties.item.properties must be an object"},
{"items is not schema", `{"items":[]}`, "items must be a JSON Schema object"},
{"nested items invalid", `{"items":{"properties":[]}}`, "items.properties must be an object"},
{"composition is not array", `{"oneOf":{}}`, "oneOf must be an array"},
{"composition branch is not schema", `{"anyOf":["string"]}`, "anyOf[0] must be a JSON Schema object"},
{"nested composition invalid", `{"allOf":[{"properties":[]}]}`, "allOf[0].properties must be an object"},
} {
t.Run(tc.name, func(t *testing.T) {
err := validateResultSchemaDescriptions(json.RawMessage(tc.raw), "data_schema")
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("validation error = %v, want %q", err, tc.want)
}
})
}
}
func TestNormalizePaginationSpecNilIsAbsent(t *testing.T) {
if got, err := NormalizePaginationSpec(nil, ""); err != nil || got != nil {
t.Fatalf("NormalizePaginationSpec(nil) = (%#v, %v)", got, err)
}
}
func TestCrossPlatformCoverageDryRunSpecValidate(t *testing.T) {
for _, kind := range []string{DryRunPreviewInvocation, DryRunPreviewRequest, DryRunPreviewPlan, DryRunPreviewDiff} {
if err := (DryRunSpec{PreviewKind: kind}).Validate("sample.run"); err != nil {
+29
View File
@@ -46,6 +46,7 @@ func TestCrossPlatformCoverageNewCommandEmbedsFullContractDeclAsFinalSource(t *t
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess},
DataSchema: []byte(`{"type":"object"}`),
},
Pagination: &contract.PaginationSpec{Kind: contract.PaginationKindCursor, CursorParameter: "cursor"},
Interface: &contract.InterfaceSpec{
Mode: "mcp",
Availability: "available",
@@ -86,6 +87,9 @@ func TestCrossPlatformCoverageNewCommandEmbedsFullContractDeclAsFinalSource(t *t
if final.Result == nil || len(final.Result.Outcomes) != 1 {
t.Fatalf("result = %#v", final.Result)
}
if final.Pagination == nil || final.Pagination.CursorParameter != "cursor" || final.Pagination.MetaPath != contract.PaginationMetaPath {
t.Fatalf("pagination = %#v", final.Pagination)
}
if final.Interface == nil || final.Interface.Mode != "mcp" || final.Interface.Ref == nil || final.Interface.Ref.RPCName != "create_thing" {
t.Fatalf("interface = %#v", final.Interface)
}
@@ -140,6 +144,31 @@ func TestFrameworkContractDeclResultMarksNonEmptyAndRejectsInvalidSchema(t *test
})
}
func TestFrameworkContractDeclPaginationMarksNonEmptyAndRejectsInvalidSpec(t *testing.T) {
if (ContractDecl{Pagination: &contract.PaginationSpec{}}).Empty() {
t.Fatal("Pagination declaration was treated as empty")
}
defer func() {
recovered := recover()
if recovered == nil || !strings.Contains(recovered.(string), "invalid Contract.Pagination") {
t.Fatalf("panic=%v", recovered)
}
}()
New(Spec{
Use: "bad-pagination",
Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"},
Contract: ContractDecl{
Title: "Bad pagination",
Description: "bad pagination",
Pagination: &contract.PaginationSpec{Kind: "offset", CursorParameter: "cursor"},
Interface: &contract.InterfaceSpec{Mode: "local", Availability: "available"},
Selection: contract.SelectionSpec{AgentSummary: "bad", UseWhen: []string{"bad"}, AvoidWhen: []string{"good"}, Examples: []string{"dws bad-pagination"}},
Identity: contract.ToolIdentitySpec{ProductID: "sample", Name: "bad_pagination", CanonicalPath: "sample.bad_pagination", CLIPath: "bad-pagination", PrimaryCLIPath: "bad-pagination"},
},
Invoke: func(*Ctx, map[string]any) error { return nil },
})
}
func TestNewCommandFallsBackToDeclaredDescriptionWithoutLong(t *testing.T) {
// Long wins when authored; without one the mandatory declaration supplies it.
cmd := New(Spec{
@@ -145,6 +145,9 @@ func TestDevAppSharedResultMapperClassifiesServiceOutcomes(t *testing.T) {
}
func TestFrameworkDevAppMapperBoundaryMatrix(t *testing.T) {
if got := devAppDataWithoutPagination("scalar"); got != "scalar" {
t.Fatalf("non-object data = %#v, want scalar preserved", got)
}
for name, payload := range map[string]any{
"scalar": "value",
"bad cursor": map[string]any{"nextCursor": 1},