690 lines
21 KiB
Go
690 lines
21 KiB
Go
package schema_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/santhosh-tekuri/jsonschema/v6"
|
|
)
|
|
|
|
// getSchemaPath returns the absolute path to config.schema.json.
|
|
func getSchemaPath(t *testing.T) string {
|
|
t.Helper()
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("failed to get caller info")
|
|
}
|
|
schemaPath := filepath.Join(filepath.Dir(filename), "..", "config.schema.json")
|
|
if _, err := os.Stat(schemaPath); err != nil {
|
|
t.Fatalf("config.schema.json not found at %s", schemaPath)
|
|
}
|
|
return schemaPath
|
|
}
|
|
|
|
// navigateJSON traverses a nested JSON structure using a sequence of keys.
|
|
// Supports string keys for objects and int keys for arrays.
|
|
func navigateJSON(data interface{}, keys ...interface{}) (interface{}, bool) {
|
|
current := data
|
|
for _, key := range keys {
|
|
switch k := key.(type) {
|
|
case string:
|
|
m, ok := current.(map[string]interface{})
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
current, ok = m[k]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
case int:
|
|
arr, ok := current.([]interface{})
|
|
if !ok || k >= len(arr) {
|
|
return nil, false
|
|
}
|
|
current = arr[k]
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
return current, true
|
|
}
|
|
|
|
// findPostgresPortType finds the port type in a store's postgres config branch.
|
|
// It handles both anyOf and oneOf schema patterns used by config_store and logs_store.
|
|
func findPostgresPortType(schema map[string]interface{}, storeName string) (string, bool) {
|
|
configBlock, ok := navigateJSON(schema, "properties", storeName, "properties", "config")
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
configMap, ok := configBlock.(map[string]interface{})
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
var branches []interface{}
|
|
if anyOf, exists := configMap["anyOf"]; exists {
|
|
branches, _ = anyOf.([]interface{})
|
|
} else if oneOf, exists := configMap["oneOf"]; exists {
|
|
branches, _ = oneOf.([]interface{})
|
|
}
|
|
|
|
for _, branch := range branches {
|
|
thenBlock, ok := navigateJSON(branch, "then")
|
|
if !ok {
|
|
continue
|
|
}
|
|
portType, ok := navigateJSON(thenBlock, "properties", "port", "type")
|
|
if !ok {
|
|
continue
|
|
}
|
|
if typeStr, ok := portType.(string); ok {
|
|
return typeStr, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func TestSchemaLogsStorePortType(t *testing.T) {
|
|
schemaPath := getSchemaPath(t)
|
|
data, err := os.ReadFile(schemaPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read schema: %v", err)
|
|
}
|
|
|
|
var schema map[string]interface{}
|
|
if err := json.Unmarshal(data, &schema); err != nil {
|
|
t.Fatalf("failed to parse schema: %v", err)
|
|
}
|
|
|
|
t.Run("logs_store port type is string", func(t *testing.T) {
|
|
portType, found := findPostgresPortType(schema, "logs_store")
|
|
if !found {
|
|
t.Fatal("could not find logs_store postgres port type in schema")
|
|
}
|
|
if portType != "string" {
|
|
t.Errorf("logs_store.config.port type = %q, want %q (Go code uses *schemas.EnvVar)", portType, "string")
|
|
}
|
|
})
|
|
|
|
t.Run("config_store port type is string", func(t *testing.T) {
|
|
portType, found := findPostgresPortType(schema, "config_store")
|
|
if !found {
|
|
t.Fatal("could not find config_store postgres port type in schema")
|
|
}
|
|
if portType != "string" {
|
|
t.Errorf("config_store.config.port type = %q, want %q (Go code uses *schemas.EnvVar)", portType, "string")
|
|
}
|
|
})
|
|
|
|
t.Run("both store port types are consistent", func(t *testing.T) {
|
|
logsPortType, logsFound := findPostgresPortType(schema, "logs_store")
|
|
configPortType, configFound := findPostgresPortType(schema, "config_store")
|
|
if !logsFound || !configFound {
|
|
t.Fatal("both store port types must be found in schema")
|
|
}
|
|
if logsPortType != configPortType {
|
|
t.Errorf("port type mismatch: logs_store=%q, config_store=%q", logsPortType, configPortType)
|
|
}
|
|
})
|
|
}
|
|
|
|
// compileSchema loads and compiles the config.schema.json for validation tests.
|
|
func compileSchema(t *testing.T) *jsonschema.Schema {
|
|
t.Helper()
|
|
schemaPath := getSchemaPath(t)
|
|
data, err := os.ReadFile(schemaPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read schema: %v", err)
|
|
}
|
|
schemaDoc, err := jsonschema.UnmarshalJSON(bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("failed to parse schema JSON: %v", err)
|
|
}
|
|
c := jsonschema.NewCompiler()
|
|
if err := c.AddResource("config.schema.json", schemaDoc); err != nil {
|
|
t.Fatalf("failed to add schema resource: %v", err)
|
|
}
|
|
compiled, err := c.Compile("config.schema.json")
|
|
if err != nil {
|
|
t.Fatalf("failed to compile schema: %v", err)
|
|
}
|
|
return compiled
|
|
}
|
|
|
|
// validateConfig unmarshals a JSON config string and validates it against the schema.
|
|
func validateConfig(t *testing.T, schema *jsonschema.Schema, configJSON string) error {
|
|
t.Helper()
|
|
var v interface{}
|
|
if err := json.Unmarshal([]byte(configJSON), &v); err != nil {
|
|
t.Fatalf("invalid test JSON: %v", err)
|
|
}
|
|
return schema.Validate(v)
|
|
}
|
|
|
|
func TestSchemaKeyAliases(t *testing.T) {
|
|
schema := loadSchema(t)
|
|
|
|
t.Run("base_key $def includes aliases field", func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "$defs", "base_key", "properties", "aliases")
|
|
if !found {
|
|
t.Error("$defs/base_key is missing 'aliases' property — aliases replaced per-provider deployments maps")
|
|
}
|
|
})
|
|
|
|
t.Run("vertex_key $def includes project_number field", func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "$defs", "vertex_key", "allOf", 1, "properties", "vertex_key_config", "properties", "project_number")
|
|
if !found {
|
|
t.Error("$defs/vertex_key is missing 'project_number' property — VertexKeyConfig Go struct defines this field")
|
|
}
|
|
})
|
|
|
|
t.Run("vertex_key_config does not include deployments field", func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "$defs", "vertex_key", "allOf", 1, "properties", "vertex_key_config", "properties", "deployments")
|
|
if found {
|
|
t.Error("$defs/vertex_key still has 'deployments' in vertex_key_config — deployments were moved to top-level key aliases")
|
|
}
|
|
})
|
|
|
|
t.Run("key with aliases validates successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"providers": {
|
|
"vertex": {
|
|
"keys": [{
|
|
"name": "test",
|
|
"value": "",
|
|
"weight": 1,
|
|
"models": ["gemini-2.0-flash"],
|
|
"aliases": {"gemini-2.0-flash": "gemini-2.0-flash-001"},
|
|
"vertex_key_config": {
|
|
"project_id": "my-project",
|
|
"region": "us-central1",
|
|
"auth_credentials": "",
|
|
"project_number": "123456"
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("key with aliases should be valid, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("azure key with aliases validates successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"providers": {
|
|
"azure": {
|
|
"keys": [{
|
|
"name": "test",
|
|
"value": "my-api-key",
|
|
"weight": 1,
|
|
"models": ["gpt-4o"],
|
|
"aliases": {"gpt-4o": "gpt-4o-deployment"},
|
|
"azure_key_config": {
|
|
"endpoint": "https://my-resource.openai.azure.com",
|
|
"api_version": "2024-02-01"
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("azure key with aliases should be valid, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSchemaGovernanceModelConfigs(t *testing.T) {
|
|
schemaPath := getSchemaPath(t)
|
|
data, err := os.ReadFile(schemaPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read schema: %v", err)
|
|
}
|
|
var schema map[string]interface{}
|
|
if err := json.Unmarshal(data, &schema); err != nil {
|
|
t.Fatalf("failed to parse schema: %v", err)
|
|
}
|
|
|
|
t.Run("governance includes model_configs property", func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "properties", "governance", "properties", "model_configs")
|
|
if !found {
|
|
t.Error("governance is missing 'model_configs' property — GovernanceData struct and per-model rate limiting depend on it")
|
|
}
|
|
})
|
|
|
|
t.Run("governance with model_configs validates successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"governance": {
|
|
"rate_limits": [{"id": "rl-1", "token_max_limit": 1000000, "token_reset_duration": "1m"}],
|
|
"model_configs": [{"id": "mc-1", "model_name": "gemini-2.0-flash", "provider": "vertex", "rate_limit_id": "rl-1"}]
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("governance with model_configs should be valid, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// loadSchema reads and parses config.schema.json into a generic map.
|
|
func loadSchema(t *testing.T) map[string]interface{} {
|
|
t.Helper()
|
|
data, err := os.ReadFile(getSchemaPath(t))
|
|
if err != nil {
|
|
t.Fatalf("failed to read schema: %v", err)
|
|
}
|
|
var schema map[string]interface{}
|
|
if err := json.Unmarshal(data, &schema); err != nil {
|
|
t.Fatalf("failed to parse schema: %v", err)
|
|
}
|
|
return schema
|
|
}
|
|
|
|
func TestSchemaClientMCPFields(t *testing.T) {
|
|
schema := loadSchema(t)
|
|
fields := []string{
|
|
"allowed_headers",
|
|
"mcp_agent_depth",
|
|
"mcp_tool_execution_timeout",
|
|
"mcp_code_mode_binding_level",
|
|
"mcp_tool_sync_interval",
|
|
"mcp_disable_auto_tool_inject",
|
|
}
|
|
for _, field := range fields {
|
|
t.Run("client has "+field, func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "properties", "client", "properties", field)
|
|
if !found {
|
|
t.Errorf("client is missing '%s' property — ClientConfig Go struct defines this field", field)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("client MCP fields validate successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"client": {
|
|
"allowed_headers": ["X-Custom-Header"],
|
|
"mcp_agent_depth": 5,
|
|
"mcp_tool_execution_timeout": 60,
|
|
"mcp_code_mode_binding_level": "server",
|
|
"mcp_tool_sync_interval": 10,
|
|
"mcp_disable_auto_tool_inject": false
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("client with MCP fields should be valid, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSchemaProviderRawRequest(t *testing.T) {
|
|
schema := loadSchema(t)
|
|
|
|
for _, def := range []string{"provider", "provider_with_bedrock_config", "provider_with_vllm_config", "provider_with_azure_config", "provider_with_vertex_config"} {
|
|
t.Run(def+" has send_back_raw_request", func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "$defs", def, "properties", "send_back_raw_request")
|
|
if !found {
|
|
t.Errorf("$defs/%s is missing 'send_back_raw_request' property — ProviderConfig Go struct defines this field", def)
|
|
}
|
|
})
|
|
t.Run(def+" has custom_provider_config", func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "$defs", def, "properties", "custom_provider_config")
|
|
if !found {
|
|
t.Errorf("$defs/%s is missing 'custom_provider_config' property — ProviderConfig Go struct defines this field", def)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("provider with send_back_raw_request validates successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"providers": {
|
|
"openai": {
|
|
"keys": [{"name": "test", "value": "sk-test", "weight": 1, "models": ["gpt-4"]}],
|
|
"send_back_raw_request": true,
|
|
"send_back_raw_response": true
|
|
}
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("provider with send_back_raw_request should be valid, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("provider with custom_provider_config validates successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"providers": {
|
|
"openai": {
|
|
"keys": [{"name": "test", "value": "sk-test", "weight": 1, "models": ["gpt-4"]}],
|
|
"custom_provider_config": {
|
|
"base_provider_type": "openai",
|
|
"is_key_less": false,
|
|
"allowed_requests": {
|
|
"chat_completion": true,
|
|
"chat_completion_stream": true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("provider with custom_provider_config should be valid, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSchemaGovernanceProviders(t *testing.T) {
|
|
schema := loadSchema(t)
|
|
|
|
t.Run("governance includes providers property", func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "properties", "governance", "properties", "providers")
|
|
if !found {
|
|
t.Error("governance is missing 'providers' property — GovernanceConfig Go struct defines this field")
|
|
}
|
|
})
|
|
|
|
t.Run("governance with providers validates successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"governance": {
|
|
"providers": [
|
|
{"name": "openai", "budget_id": "b-1", "send_back_raw_request": true}
|
|
]
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("governance with providers should be valid, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSchemaMCPToolSyncInterval(t *testing.T) {
|
|
schema := loadSchema(t)
|
|
|
|
t.Run("mcp includes tool_sync_interval property", func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "properties", "mcp", "properties", "tool_sync_interval")
|
|
if !found {
|
|
t.Error("mcp is missing 'tool_sync_interval' property — MCPConfig Go struct defines this field")
|
|
}
|
|
})
|
|
|
|
t.Run("mcp with tool_sync_interval validates successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"mcp": {
|
|
"client_configs": [],
|
|
"tool_sync_interval": "10m"
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("mcp with tool_sync_interval should be valid, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSchemaMCPToolManagerCodeMode(t *testing.T) {
|
|
schema := loadSchema(t)
|
|
|
|
t.Run("mcp_tool_manager_config includes code_mode_binding_level", func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "$defs", "mcp_tool_manager_config", "properties", "code_mode_binding_level")
|
|
if !found {
|
|
t.Error("$defs/mcp_tool_manager_config is missing 'code_mode_binding_level' — MCPToolManagerConfig Go struct defines this field")
|
|
}
|
|
})
|
|
|
|
t.Run("tool_manager_config with code_mode_binding_level validates successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"mcp": {
|
|
"client_configs": [],
|
|
"tool_manager_config": {
|
|
"tool_execution_timeout": 30,
|
|
"max_agent_depth": 10,
|
|
"code_mode_binding_level": "tool"
|
|
}
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("tool_manager_config with code_mode_binding_level should be valid, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSchemaMCPClientConfigFields(t *testing.T) {
|
|
schema := loadSchema(t)
|
|
|
|
fields := []string{
|
|
"client_id",
|
|
"is_code_mode_client",
|
|
"connection_string",
|
|
"auth_type",
|
|
"oauth_config_id",
|
|
"headers",
|
|
"tools_to_execute",
|
|
"tools_to_auto_execute",
|
|
"tool_sync_interval",
|
|
}
|
|
for _, field := range fields {
|
|
t.Run("mcp_client_config has "+field, func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "$defs", "mcp_client_config", "properties", field)
|
|
if !found {
|
|
t.Errorf("$defs/mcp_client_config is missing '%s' property — MCPClientConfig Go struct defines this field", field)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("mcp_client_config with new fields validates (stdio)", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"mcp": {
|
|
"client_configs": [{
|
|
"client_id": "mcp-1",
|
|
"name": "test-mcp",
|
|
"is_code_mode_client": false,
|
|
"connection_type": "stdio",
|
|
"auth_type": "none",
|
|
"tools_to_execute": ["*"],
|
|
"tools_to_auto_execute": [],
|
|
"stdio_config": {
|
|
"command": "npx",
|
|
"args": ["-y", "@modelcontextprotocol/server-filesystem"]
|
|
}
|
|
}]
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("mcp_client_config with new fields (stdio) should be valid, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("mcp_client_config with SSE connection validates", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"mcp": {
|
|
"client_configs": [{
|
|
"name": "sse-client",
|
|
"connection_type": "sse",
|
|
"connection_string": "http://localhost:8080/sse",
|
|
"auth_type": "headers",
|
|
"headers": {"Authorization": "Bearer token123"}
|
|
}]
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("mcp_client_config with SSE connection should be valid, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSchemaMCPConnectionTypeSSE(t *testing.T) {
|
|
schema := loadSchema(t)
|
|
|
|
t.Run("connection_type enum includes sse", func(t *testing.T) {
|
|
enumVal, found := navigateJSON(schema, "$defs", "mcp_client_config", "properties", "connection_type", "enum")
|
|
if !found {
|
|
t.Fatal("could not find connection_type enum in mcp_client_config")
|
|
}
|
|
enumArr, ok := enumVal.([]interface{})
|
|
if !ok {
|
|
t.Fatal("connection_type enum is not an array")
|
|
}
|
|
hasSSE := false
|
|
for _, v := range enumArr {
|
|
if s, ok := v.(string); ok && s == "sse" {
|
|
hasSSE = true
|
|
break
|
|
}
|
|
}
|
|
if !hasSSE {
|
|
t.Error("connection_type enum does not include 'sse' — MCPConnectionType supports SSE")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSchemaAllowedOriginsWildcard(t *testing.T) {
|
|
schemaPath := getSchemaPath(t)
|
|
data, err := os.ReadFile(schemaPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read schema: %v", err)
|
|
}
|
|
var schema map[string]interface{}
|
|
if err := json.Unmarshal(data, &schema); err != nil {
|
|
t.Fatalf("failed to parse schema: %v", err)
|
|
}
|
|
|
|
t.Run("allowed_origins uses anyOf not oneOf", func(t *testing.T) {
|
|
items, found := navigateJSON(schema, "properties", "client", "properties", "allowed_origins", "items")
|
|
if !found {
|
|
t.Fatal("could not find allowed_origins.items in schema")
|
|
}
|
|
itemsMap, ok := items.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("allowed_origins.items is not an object")
|
|
}
|
|
if _, hasOneOf := itemsMap["oneOf"]; hasOneOf {
|
|
t.Error("allowed_origins.items uses 'oneOf' — should use 'anyOf' because '*' matches both const and format:uri subschemas")
|
|
}
|
|
if _, hasAnyOf := itemsMap["anyOf"]; !hasAnyOf {
|
|
t.Error("allowed_origins.items should use 'anyOf'")
|
|
}
|
|
})
|
|
|
|
t.Run("allowed_origins wildcard validates successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"client": {
|
|
"allowed_origins": ["*"]
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("allowed_origins with '*' should be valid, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSchemaBedrockKeyConfigSTSFields(t *testing.T) {
|
|
schema := loadSchema(t)
|
|
|
|
stsFields := []string{"role_arn", "external_id", "session_name"}
|
|
for _, field := range stsFields {
|
|
t.Run("$defs/bedrock_key has "+field, func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "$defs", "bedrock_key", "allOf", 1, "properties", "bedrock_key_config", "properties", field)
|
|
if !found {
|
|
t.Errorf("$defs/bedrock_key bedrock_key_config is missing '%s' — BedrockKeyConfig Go struct defines this field for STS AssumeRole", field)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("$defs/bedrock_key has batch_s3_config", func(t *testing.T) {
|
|
_, found := navigateJSON(schema, "$defs", "bedrock_key", "allOf", 1, "properties", "bedrock_key_config", "properties", "batch_s3_config")
|
|
if !found {
|
|
t.Error("$defs/bedrock_key bedrock_key_config is missing 'batch_s3_config' — BedrockKeyConfig Go struct defines this field for batch operations")
|
|
}
|
|
})
|
|
|
|
t.Run("bedrock config with STS fields validates successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"providers": {
|
|
"bedrock": {
|
|
"keys": [
|
|
{
|
|
"name": "cross-account",
|
|
"weight": 1,
|
|
"models": ["us.anthropic.claude-sonnet-4-20250514-v1:0"],
|
|
"bedrock_key_config": {
|
|
"region": "us-west-2",
|
|
"role_arn": "arn:aws:iam::123456789012:role/BedrockAccessRole",
|
|
"session_name": "bifrost-cross-account",
|
|
"external_id": "my-external-id"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("bedrock config with STS AssumeRole fields should be valid, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("bedrock config with batch_s3_config validates successfully", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"providers": {
|
|
"bedrock": {
|
|
"keys": [
|
|
{
|
|
"name": "batch-key",
|
|
"weight": 1,
|
|
"models": ["us.anthropic.claude-sonnet-4-20250514-v1:0"],
|
|
"bedrock_key_config": {
|
|
"region": "us-east-1",
|
|
"batch_s3_config": {
|
|
"buckets": [
|
|
{"bucket_name": "my-batch-bucket", "prefix": "bifrost/", "is_default": true},
|
|
{"bucket_name": "my-secondary-bucket"}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err != nil {
|
|
t.Errorf("bedrock config with batch_s3_config should be valid, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("bedrock config with unknown fields is rejected", func(t *testing.T) {
|
|
compiled := compileSchema(t)
|
|
config := `{
|
|
"providers": {
|
|
"bedrock": {
|
|
"keys": [
|
|
{
|
|
"name": "bad-key",
|
|
"weight": 1,
|
|
"models": ["us.anthropic.claude-sonnet-4-20250514-v1:0"],
|
|
"bedrock_key_config": {
|
|
"region": "us-east-1",
|
|
"unknown_field": "should-fail"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`
|
|
if err := validateConfig(t, compiled, config); err == nil {
|
|
t.Error("bedrock config with unknown fields should fail schema validation (additionalProperties: false)")
|
|
}
|
|
})
|
|
} |