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", "TaskLaunchAcknowledged", "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 + `"` }