Files
orchestra/internal/domain/fuzz_test.go
T
2026-07-30 14:37:34 +04:00

150 lines
4.4 KiB
Go

package domain
import (
"encoding/json"
"testing"
)
// eventTypesUnderTest is the full lifecycle vocabulary the spec (§9 item 5)
// requires validators for; a validator that panics or accepts garbage for any
// of these on adversarial input is a defect regardless of whether real
// producers happen to send well-formed payloads.
var eventTypesUnderTest = []string{
"TaskCreated", "TaskLeased", "TaskReleased", "TaskCompleted", "TaskFailed",
"TaskBlocked", "TaskNeedsAttention", "ApprovalRequested", "ApprovalGranted", "ApprovalDenied",
"TaskAmended", "QuotaReported", "StandupAdvisory",
}
// FuzzValidatePayload feeds arbitrary JSON object shapes at every known event
// type's validator and requires it to either return a typed ErrInvalid or
// accept — never panic. The seed corpus below exercises adjacent-to-valid and
// wildly-malformed shapes (wrong types, huge strings, nested structures,
// nulls, NaN-adjacent floats via JSON) for each type.
func FuzzValidatePayload(f *testing.F) {
seeds := []string{
`{}`,
`null`,
`{"source":"jsonl","external_id":"1","project":"p"}`,
`{"source":123,"external_id":null,"project":[]}`,
`{"harness_id":"h1","ttl":60,"expected_version":1}`,
`{"harness_id":"h1","until_ns":1e300,"expected_version":1.5}`,
`{"handoff_ref":"` + fakeHash() + `","anchor_sha":"` + fakeSHA() + `"}`,
`{"handoff_ref":123,"anchor_sha":true}`,
`{"report_ref":"` + fakeHash() + `","receipt":{"harness_id":"h1","consumed":1}}`,
`{"report_ref":"","receipt":{}}`,
`{"reason":"x"}`,
`{"reason":123}`,
`{"blocker":"x","handoff_ref":"` + fakeHash() + `"}`,
`{"blocker":""}`,
`{"amendment":"x"}`,
`{"subject_ref":"x","options":["a","b"]}`,
`{"subject_ref":123,"options":null}`,
`{"harness_id":"h1","consumed":90.5}`,
`{"harness_id":"h1","consumed":-1}`,
`{"harness_id":"h1","consumed":"a lot"}`,
`{"items":["standup line"]}`,
`{"items":null}`,
`{"a":{"b":{"c":{"d":[1,2,3,{"e":"f"}]}}}}`,
`{"x":` + hugeString() + `}`,
}
for _, s := range seeds {
f.Add(s)
}
f.Fuzz(func(t *testing.T, raw string) {
var p map[string]any
if err := json.Unmarshal([]byte(raw), &p); err != nil {
return // not a JSON object; ValidateEvent itself rejects non-objects before reaching ValidatePayload
}
for _, typ := range eventTypesUnderTest {
func() {
defer func() {
if r := recover(); r != nil {
t.Fatalf("ValidatePayload(%q, %s) panicked: %v", typ, raw, r)
}
}()
err := ValidatePayload(typ, p)
if err != nil && err != ErrInvalid {
// must still be a typed validation error, wrapping ErrInvalid
if !isInvalid(err) {
t.Fatalf("ValidatePayload(%q, %s) returned non-typed error: %v", typ, raw, err)
}
}
}()
}
})
}
// FuzzValidateEvent exercises the full envelope path (schema version, surface
// requirement, type allow-list, payload size/parseability) with arbitrary
// type names, surfaces, and payload bytes, proving no combination panics.
func FuzzValidateEvent(f *testing.F) {
f.Add("TaskCreated", "system", 2, []byte(`{"source":"jsonl","external_id":"1","project":"p"}`))
f.Add("", "", 0, []byte(``))
f.Add("Bogus", "system", 2, []byte(`{}`))
f.Add("TaskCreated", "", 2, []byte(`{"source":"jsonl","external_id":"1","project":"p"}`))
f.Add("TaskCreated", "system", 1, []byte(`not json`))
f.Add("QuotaReported", "system", 2, []byte(`null`))
f.Add("TaskCompleted", "system", 99, []byte(`{"report_ref":"x","receipt":{}}`))
f.Fuzz(func(t *testing.T, typ, surface string, schema int, payload []byte) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("ValidateEvent panicked: type=%q surface=%q schema=%d payload=%q: %v", typ, surface, schema, payload, r)
}
}()
e := Event{
SchemaVersion: schema,
Type: typ,
TaskID: "t1",
Payload: payload,
Surface: surface,
}
_ = ValidateEvent(e)
})
}
func isInvalid(err error) bool {
for e := err; e != nil; {
if e == ErrInvalid {
return true
}
u, ok := e.(interface{ Unwrap() error })
if !ok {
return false
}
e = u.Unwrap()
}
return false
}
func fakeHash() string {
b := make([]byte, 32)
for i := range b {
b[i] = byte(i)
}
s := ""
for _, c := range b {
s += string("0123456789abcdef"[c>>4]) + string("0123456789abcdef"[c&0xf])
}
return s
}
func fakeSHA() string {
s := ""
for i := 0; i < 40; i++ {
s += "a"
}
return s
}
func hugeString() string {
b, _ := json.Marshal(make([]byte, 0))
_ = b
s := `"`
for i := 0; i < 5000; i++ {
s += "x"
}
return s + `"`
}