Enforce bounded output repair contracts
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const maxOutputRepairAttempts = 3
|
||||
|
||||
// ValidateOutputContract validates source-neutral output-contract invariants.
|
||||
func ValidateOutputContract(contract OutputContract) error {
|
||||
switch contract.Format {
|
||||
@@ -26,5 +28,11 @@ func ValidateOutputContract(contract OutputContract) error {
|
||||
if contract.RepairAttempts < 0 {
|
||||
return errors.New("repair_attempts must be greater than or equal to 0")
|
||||
}
|
||||
if contract.RepairAttempts > maxOutputRepairAttempts {
|
||||
return fmt.Errorf("repair_attempts must be less than or equal to %d", maxOutputRepairAttempts)
|
||||
}
|
||||
if contract.ValidationMode == ValidationNone && contract.RepairAttempts > 0 {
|
||||
return errors.New("repair_attempts requires basic, json, or json_schema validation")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -30,9 +30,28 @@ func TestValidateOutputContract(t *testing.T) {
|
||||
}},
|
||||
{name: "empty validation mode", change: func(c *OutputContract) { c.ValidationMode = "" }, wantErr: "validation mode"},
|
||||
{name: "unsupported validation mode", change: func(c *OutputContract) { c.ValidationMode = ValidationMode("unknown") }, wantErr: "validation mode"},
|
||||
{name: "negative repair attempts", change: func(c *OutputContract) { c.RepairAttempts = -1 }, wantErr: "repair_attempts"},
|
||||
{name: "negative repair attempts", change: func(c *OutputContract) {
|
||||
c.ValidationMode = ValidationBasic
|
||||
c.RepairAttempts = -1
|
||||
}, wantErr: "repair_attempts"},
|
||||
{name: "zero repair attempts", change: func(c *OutputContract) { c.RepairAttempts = 0 }},
|
||||
{name: "positive repair attempts", change: func(c *OutputContract) { c.RepairAttempts = 1 }},
|
||||
{name: "one repair attempt", change: func(c *OutputContract) {
|
||||
c.ValidationMode = ValidationBasic
|
||||
c.RepairAttempts = 1
|
||||
}},
|
||||
{name: "maximum repair attempts", change: func(c *OutputContract) {
|
||||
c.ValidationMode = ValidationJSON
|
||||
c.RepairAttempts = 3
|
||||
}},
|
||||
{name: "too many repair attempts", change: func(c *OutputContract) {
|
||||
c.ValidationMode = ValidationJSONSchema
|
||||
c.SchemaPath = "schema.json"
|
||||
c.RepairAttempts = 4
|
||||
}, wantErr: "repair_attempts"},
|
||||
{name: "none validation with repair attempts", change: func(c *OutputContract) {
|
||||
c.ValidationMode = ValidationNone
|
||||
c.RepairAttempts = 1
|
||||
}, wantErr: "repair_attempts"},
|
||||
{name: "json schema empty path", change: func(c *OutputContract) {
|
||||
c.ValidationMode = ValidationJSONSchema
|
||||
c.SchemaPath = ""
|
||||
|
||||
@@ -892,6 +892,38 @@ output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: -1
|
||||
`,
|
||||
wantErr: true,
|
||||
wantDiagnostic: "repair_attempts",
|
||||
},
|
||||
{
|
||||
name: "repair attempts above maximum",
|
||||
definition: `
|
||||
id: normalization-rule
|
||||
version: "1"
|
||||
messages:
|
||||
- role: user
|
||||
content: test
|
||||
output:
|
||||
format: text
|
||||
validation_mode: basic
|
||||
repair_attempts: 4
|
||||
`,
|
||||
wantErr: true,
|
||||
wantDiagnostic: "repair_attempts",
|
||||
},
|
||||
{
|
||||
name: "none validation with repair attempts",
|
||||
definition: `
|
||||
id: normalization-rule
|
||||
version: "1"
|
||||
messages:
|
||||
- role: user
|
||||
content: test
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 1
|
||||
`,
|
||||
wantErr: true,
|
||||
wantDiagnostic: "repair_attempts",
|
||||
|
||||
@@ -106,6 +106,8 @@ func TestRunnerPreparationRejectsInvalidOutputContractsBeforeCompletion(t *testi
|
||||
{name: "unsupported format", override: domain.OutputContract{Format: "binary", ValidationMode: domain.ValidationNone}},
|
||||
{name: "empty validation mode", override: domain.OutputContract{Format: domain.FormatText}},
|
||||
{name: "negative repair attempts", override: domain.OutputContract{Format: domain.FormatText, ValidationMode: domain.ValidationNone, RepairAttempts: -1}},
|
||||
{name: "repair attempts above maximum", override: domain.OutputContract{Format: domain.FormatText, ValidationMode: domain.ValidationBasic, RepairAttempts: 4}},
|
||||
{name: "none validation with repair attempts", override: domain.OutputContract{Format: domain.FormatText, ValidationMode: domain.ValidationNone, RepairAttempts: 1}},
|
||||
{name: "json schema without path", override: domain.OutputContract{Format: domain.FormatJSON, ValidationMode: domain.ValidationJSONSchema}},
|
||||
}
|
||||
|
||||
|
||||
@@ -2202,7 +2202,7 @@ func TestRunnerRepairStateMachine(t *testing.T) {
|
||||
{
|
||||
name: "successful repair stops below larger budget",
|
||||
mode: domain.ValidationJSON,
|
||||
budget: 4,
|
||||
budget: 3,
|
||||
validationResults: []domain.ValidationResult{
|
||||
failed(domain.ValidationJSON, "candidate zero"),
|
||||
failed(domain.ValidationJSON, "candidate one"),
|
||||
|
||||
@@ -174,9 +174,8 @@ func validateArtifact(ctx context.Context, artifact *domain.Artifact, contract d
|
||||
}
|
||||
|
||||
res := domain.ValidationResult{
|
||||
Mode: contract.ValidationMode,
|
||||
SchemaPath: contract.SchemaPath,
|
||||
RepairAttempts: contract.RepairAttempts,
|
||||
Mode: contract.ValidationMode,
|
||||
SchemaPath: contract.SchemaPath,
|
||||
}
|
||||
|
||||
if artifact == nil {
|
||||
|
||||
@@ -64,6 +64,43 @@ func TestStandardValidatorBasicFailureEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStandardValidatorReportsZeroRepairAttempts(t *testing.T) {
|
||||
v := NewStandardValidator("")
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
body string
|
||||
contract domain.OutputContract
|
||||
}{
|
||||
{
|
||||
name: "passed basic validation",
|
||||
body: "answer",
|
||||
contract: domain.OutputContract{
|
||||
ValidationMode: domain.ValidationBasic,
|
||||
RepairAttempts: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed JSON validation",
|
||||
body: `{"answer":`,
|
||||
contract: domain.OutputContract{
|
||||
ValidationMode: domain.ValidationJSON,
|
||||
RepairAttempts: 3,
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(tc.body)}, tc.contract)
|
||||
if err != nil {
|
||||
t.Fatalf("validate: %v", err)
|
||||
}
|
||||
if res.RepairAttempts != 0 {
|
||||
t.Fatalf("repair attempts = %d, want 0", res.RepairAttempts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStandardValidatorJSONSuccess(t *testing.T) {
|
||||
v := NewStandardValidator("")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user