153 lines
5.7 KiB
Go
153 lines
5.7 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
|
)
|
|
|
|
func TestResultContractModelWireRoundTripAndCompactPolicy(t *testing.T) {
|
|
result := &contract.ResultSpec{
|
|
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeFailure, contract.ResultOutcomeSuccess},
|
|
DataSchema: json.RawMessage(`{ "type":"object", "properties":{"items":{"type":"array","description":"Business result records","items":{"type":"object"}}} }`),
|
|
SensitivePaths: []string{"items.secret", "credential"},
|
|
}
|
|
spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{
|
|
Identity: contract.ToolIdentitySpec{ProductID: "dev", Name: "list", CLIName: "list", CLIPath: "dev list"},
|
|
Parameters: []ParameterSpec{{Name: "cursor", Type: "string"}},
|
|
Result: result,
|
|
Pagination: &contract.PaginationSpec{Kind: contract.PaginationKindCursor, CursorParameter: "cursor"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ToolSpecFromRuntime() error = %v", err)
|
|
}
|
|
if got, want := spec.Result.Outcomes, []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("outcomes = %#v, want %#v", got, want)
|
|
}
|
|
result.Outcomes[0] = contract.ResultOutcomePending
|
|
result.DataSchema[0] = '['
|
|
if spec.Result.Outcomes[0] != contract.ResultOutcomeSuccess || spec.Result.DataSchema[0] != '{' {
|
|
t.Fatal("ToolSpec result aliases runtime input")
|
|
}
|
|
|
|
payload, err := spec.ToPayload()
|
|
if err != nil {
|
|
t.Fatalf("ToPayload() error = %v", err)
|
|
}
|
|
resultPayload, ok := payload["result"].(map[string]any)
|
|
if !ok || schemaString(resultPayload["data_schema"].(map[string]any)["type"]) != "object" {
|
|
t.Fatalf("result payload = %#v", payload["result"])
|
|
}
|
|
if _, exists := specResultSummary(t, spec)["result"]; exists {
|
|
t.Fatal("result must remain full-leaf-only")
|
|
}
|
|
compactResult, exists := stripSchemaPayloadCompact(payload)["result"].(map[string]any)
|
|
if !exists {
|
|
t.Fatal("compact leaf must include the reviewed result contract")
|
|
}
|
|
if outcomes, ok := compactResult["outcomes"].([]any); !ok || len(outcomes) != 2 {
|
|
t.Fatalf("compact result outcomes = %#v", compactResult["outcomes"])
|
|
}
|
|
if dataSchema, ok := compactResult["data_schema"].(map[string]any); !ok || schemaString(dataSchema["type"]) != "object" {
|
|
t.Fatalf("compact result data_schema = %#v", compactResult["data_schema"])
|
|
}
|
|
if !reflect.DeepEqual(compactResult, resultPayload) {
|
|
t.Fatalf("compact result must equal full-leaf result\ncompact: %#v\nfull: %#v", compactResult, resultPayload)
|
|
}
|
|
compactPagination, exists := stripSchemaPayloadCompact(payload)["pagination"].(map[string]any)
|
|
if !exists || schemaString(compactPagination["meta_path"]) != contract.PaginationMetaPath || schemaString(compactPagination["cursor_parameter"]) != "cursor" {
|
|
t.Fatalf("compact pagination = %#v", compactPagination)
|
|
}
|
|
|
|
wire, err := schemaToolWireFromPayload(payload)
|
|
if err != nil {
|
|
t.Fatalf("schemaToolWireFromPayload() error = %v", err)
|
|
}
|
|
roundTrip, err := schemaToolSpecFromWire(wire)
|
|
if err != nil {
|
|
t.Fatalf("schemaToolSpecFromWire() error = %v", err)
|
|
}
|
|
roundTripPayload, err := roundTrip.ToPayload()
|
|
if err != nil {
|
|
t.Fatalf("round-trip ToPayload() error = %v", err)
|
|
}
|
|
if !schemaJSONEqual(payload, roundTripPayload) {
|
|
t.Fatalf("result changed across wire round-trip\nfirst: %#v\nround: %#v", payload["result"], roundTripPayload["result"])
|
|
}
|
|
}
|
|
|
|
func specResultSummary(t *testing.T, spec ToolSpec) map[string]any {
|
|
t.Helper()
|
|
payload, err := spec.ToSummaryPayload()
|
|
if err != nil {
|
|
t.Fatalf("ToSummaryPayload() error = %v", err)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func TestToolWithoutResultKeepsResultAbsent(t *testing.T) {
|
|
spec, err := ToolSpecFromRuntime(RuntimeToolSpecInput{
|
|
Identity: contract.ToolIdentitySpec{ProductID: "dev", Name: "legacy", CLIName: "legacy", CLIPath: "dev legacy"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
payload, err := spec.ToPayload()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := payload["result"]; exists {
|
|
t.Fatal("tool without Result gained a result key")
|
|
}
|
|
}
|
|
|
|
func TestToolSpecRejectsInvalidResultInsteadOfDroppingIt(t *testing.T) {
|
|
_, err := ToolSpecFromRuntime(RuntimeToolSpecInput{
|
|
Identity: contract.ToolIdentitySpec{ProductID: "dev", Name: "invalid", CLIName: "invalid", CLIPath: "dev invalid"},
|
|
Result: &contract.ResultSpec{
|
|
Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess},
|
|
DataSchema: json.RawMessage(`[]`),
|
|
},
|
|
})
|
|
if err == nil {
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|