test: cover unified schema validation edges
This commit is contained in:
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 {
|
||||
|
||||
@@ -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},
|
||||
|
||||
Reference in new issue
Block a user