Centralize output contract validation
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user