Centralize output contract validation

This commit is contained in:
2026-08-11 21:21:33 +00:00
parent 8cfc71c351
commit 1cb07c7d91
11 changed files with 432 additions and 49 deletions

View File

@@ -402,17 +402,14 @@ func normalizePromptDefinitionWithContent(raw *promptDefinitionFile, readContent
})
}
if !isValidOutputFormat(raw.Output.Format) {
return nil, fmt.Errorf("invalid output format: %q", raw.Output.Format)
outputContract := domain.OutputContract{
Format: raw.Output.Format,
ValidationMode: raw.Output.ValidationMode,
SchemaPath: strings.TrimSpace(raw.Output.SchemaPath),
RepairAttempts: raw.Output.RepairAttempts,
}
if !isValidValidationMode(raw.Output.ValidationMode) {
return nil, fmt.Errorf("invalid validation mode: %q", raw.Output.ValidationMode)
}
if raw.Output.ValidationMode == domain.ValidationJSONSchema && strings.TrimSpace(raw.Output.SchemaPath) == "" {
return nil, errors.New("output.schema_path is required when output.validation_mode is json_schema")
}
if raw.Output.RepairAttempts < 0 {
return nil, errors.New("output.repair_attempts must be greater than or equal to 0")
if err := domain.ValidateOutputContract(outputContract); err != nil {
return nil, fmt.Errorf("output: %w", err)
}
defaultProfile := ""
@@ -432,12 +429,7 @@ func normalizePromptDefinitionWithContent(raw *promptDefinitionFile, readContent
Inputs: inputs,
Templates: templates,
OutputFormat: raw.Output.Format,
Validation: domain.OutputContract{
Format: raw.Output.Format,
ValidationMode: raw.Output.ValidationMode,
SchemaPath: strings.TrimSpace(raw.Output.SchemaPath),
RepairAttempts: raw.Output.RepairAttempts,
},
Validation: outputContract,
}, nil
}
@@ -464,21 +456,3 @@ func normalizeCacheControl(raw *cacheControlFile) (*domain.CacheControl, error)
TTL: ttl,
}, nil
}
func isValidOutputFormat(f domain.OutputFormat) bool {
switch f {
case domain.FormatText, domain.FormatMarkdown, domain.FormatJSON:
return true
default:
return false
}
}
func isValidValidationMode(m domain.ValidationMode) bool {
switch m {
case domain.ValidationNone, domain.ValidationBasic, domain.ValidationJSON, domain.ValidationJSONSchema:
return true
default:
return false
}
}

View File

@@ -479,6 +479,77 @@ output:
}
}
func TestPromptRepositoriesApplyOutputContractRules(t *testing.T) {
tests := []struct {
name string
output string
useFilesystem bool
wantErr bool
wantDiagnostic string
wantSchemaPath string
}{
{
name: "operating-system source rejects unsupported format",
output: " format: binary\n validation_mode: none\n",
useFilesystem: true,
wantErr: true,
wantDiagnostic: "format",
},
{
name: "fs source rejects negative repair attempts",
output: " format: text\n validation_mode: none\n repair_attempts: -1\n",
wantErr: true,
wantDiagnostic: "repair_attempts",
},
{
name: "source normalization trims a valid schema path",
output: " format: json\n validation_mode: json_schema\n schema_path: ' schema.json '\n",
useFilesystem: true,
wantSchemaPath: "schema.json",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := `id: output-contract
version: "1"
messages:
- role: user
content: test
output:
` + tt.output
var repo Repository
if tt.useFilesystem {
dir := t.TempDir()
writePromptTestFile(t, filepath.Join(dir, "output-contract.yaml"), data)
repo = NewFilesystemRepository(dir)
} else {
repo = NewFSRepository(fstest.MapFS{
"output-contract.yaml": &fstest.MapFile{Data: []byte(data)},
}, ".")
}
got, err := repo.GetPromptDefinition(context.Background(), "output-contract", "")
if tt.wantErr {
if !errors.Is(err, ErrInvalidPromptDefinition) {
t.Fatalf("expected ErrInvalidPromptDefinition, got %v", err)
}
if !strings.Contains(err.Error(), tt.wantDiagnostic) {
t.Fatalf("expected error containing %q, got %v", tt.wantDiagnostic, err)
}
return
}
if err != nil {
t.Fatalf("load prompt definition: %v", err)
}
if got.Validation.SchemaPath != tt.wantSchemaPath {
t.Fatalf("schema path = %q, want %q", got.Validation.SchemaPath, tt.wantSchemaPath)
}
})
}
}
func assertCacheControl(t *testing.T, got *domain.CacheControl, wantType domain.CacheControlType, wantTTL string) {
t.Helper()
if got == nil {