package streamgate import ( "context" "testing" "time" ) // TestFailureCauseChainCapsAtFour verifies that FailureCauseChain enforces the // MaxFailureCauses (4) cap on construction and Append, that accessors return // defensive copies, that distinct codes prove the latest-four retention order, // and that invalid stage/code/consumer/filter/rule tokens are rejected. func TestFailureCauseChainCapsAtFour(t *testing.T) { // --- Empty chain --- empty, err := NewFailureCauseChain(nil) if err != nil { t.Fatalf("NewFailureCauseChain(nil): %v", err) } if empty.Len() != 0 { t.Errorf("empty chain Len() = %d, want 0", empty.Len()) } if empty.All() != nil { t.Errorf("empty chain All() = %v, want nil", empty.All()) } // --- Chain with 4 causes: all retained, distinct codes --- codes4 := []string{"code0", "code1", "code2", "code3"} causes4 := make([]FailureCause, MaxFailureCauses) for i, code := range codes4 { c, err := NewFailureCause("stage", code, "", "", "") if err != nil { t.Fatalf("NewFailureCause: %v", err) } causes4[i] = c } chain4, err := NewFailureCauseChain(causes4) if err != nil { t.Fatalf("NewFailureCauseChain(4): %v", err) } if chain4.Len() != 4 { t.Errorf("4-cause chain Len() = %d, want 4", chain4.Len()) } // Verify order and distinct codes. for i := 0; i < 4; i++ { c, err := chain4.At(i) if err != nil { t.Fatalf("At(%d): %v", i, err) } if c.Code() != codes4[i] { t.Errorf("At(%d) Code() = %q, want %q", i, c.Code(), codes4[i]) } } // --- Chain with 5 causes: oldest dropped, latest 4 retained in order --- codes5 := []string{"code0", "code1", "code2", "code3", "code4"} causes5 := make([]FailureCause, MaxFailureCauses+1) for i, code := range codes5 { c, err := NewFailureCause("stage", code, "", "", "") if err != nil { t.Fatalf("NewFailureCause: %v", err) } causes5[i] = c } chain5, err := NewFailureCauseChain(causes5) if err != nil { t.Fatalf("NewFailureCauseChain(5): %v", err) } if chain5.Len() != 4 { t.Errorf("5-cause chain Len() = %d, want 4", chain5.Len()) } // Latest 4 are code1..code4 in order. for i := 0; i < 4; i++ { c, err := chain5.At(i) if err != nil { t.Fatalf("At(%d): %v", i, err) } want := codes5[i+1] if c.Code() != want { t.Errorf("At(%d) Code() = %q, want %q (retention order wrong)", i, c.Code(), want) } } // --- Constructor input mutation does not affect stored chain --- t.Run("constructor_input_mutation_isolation", func(t *testing.T) { input := make([]FailureCause, MaxFailureCauses+1) for i, code := range codes5 { c, err := NewFailureCause("stage", code, "", "", "") if err != nil { t.Fatalf("NewFailureCause: %v", err) } input[i] = c } chain, err := NewFailureCauseChain(input) if err != nil { t.Fatalf("NewFailureCauseChain: %v", err) } // Replace the element with a different cause. mutated, err := NewFailureCause("stage", "mutated", "", "", "") if err != nil { t.Fatalf("NewFailureCause: %v", err) } input[0] = mutated input[1] = mutated input = append(input, mutated) // Stored chain should still be code1..code4. if chain.Len() != 4 { t.Errorf("chain Len() = %d, want 4 (input mutation leaked)", chain.Len()) } for i := 0; i < 4; i++ { c, err := chain.At(i) if err != nil { t.Fatalf("At(%d): %v", i, err) } want := codes5[i+1] if c.Code() != want { t.Errorf("At(%d) Code() = %q, want %q (input mutation leaked)", i, c.Code(), want) } } }) // --- Append to 4-cause chain: oldest dropped, latest 4 retained in order --- appendCause, err := NewFailureCause("stage", "code4", "", "", "") if err != nil { t.Fatalf("NewFailureCause: %v", err) } appended, err := chain4.Append(appendCause) if err != nil { t.Fatalf("Append: %v", err) } if appended.Len() != 4 { t.Errorf("appended chain Len() = %d, want 4", appended.Len()) } // Latest 4 are code1..code4 in order. for i := 0; i < 4; i++ { c, err := appended.At(i) if err != nil { t.Fatalf("At(%d): %v", i, err) } want := codes5[i+1] if c.Code() != want { t.Errorf("At(%d) Code() = %q, want %q (Append retention order wrong)", i, c.Code(), want) } } // --- Append returned chain mutation does not affect original --- t.Run("append_returned_chain_mutation_isolation", func(t *testing.T) { appendCause2, err := NewFailureCause("stage", "code4", "", "", "") if err != nil { t.Fatalf("NewFailureCause: %v", err) } appended2, err := chain4.Append(appendCause2) if err != nil { t.Fatalf("Append: %v", err) } // Replace retained elements in the returned chain directly. mutated, err := NewFailureCause("stage", "mutated", "", "", "") if err != nil { t.Fatalf("NewFailureCause: %v", err) } appended2.causes[0] = mutated appended2.causes = append(appended2.causes, mutated) // Original chain4 should be unchanged. if chain4.Len() != 4 { t.Errorf("chain4 Len() = %d, want 4 (Append mutation leaked)", chain4.Len()) } for i := 0; i < 4; i++ { c, err := chain4.At(i) if err != nil { t.Fatalf("At(%d): %v", i, err) } if c.Code() != codes4[i] { t.Errorf("At(%d) Code() = %q, want %q (Append mutation leaked)", i, c.Code(), codes4[i]) } } }) // --- Append to 3-cause chain: 4 retained --- chain3, err := NewFailureCauseChain(causes4[:3]) if err != nil { t.Fatalf("NewFailureCauseChain(3): %v", err) } appended3, err := chain3.Append(appendCause) if err != nil { t.Fatalf("Append: %v", err) } if appended3.Len() != 4 { t.Errorf("appended 3-cause chain Len() = %d, want 4", appended3.Len()) } // --- At() returns defensive copy --- orig, err := chain4.At(0) if err != nil { t.Fatalf("At(0): %v", err) } if orig.Stage() != "stage" { t.Errorf("At(0) Stage() = %q, want %q", orig.Stage(), "stage") } // --- At() out of range returns error --- if _, err := chain4.At(-1); err == nil { t.Error("At(-1) should return error") } if _, err := chain4.At(4); err == nil { t.Error("At(4) should return error") } // --- All() returns defensive copy --- all := chain4.All() if len(all) != 4 { t.Errorf("All() length = %d, want 4", len(all)) } all = append(all, orig) if chain4.Len() != 4 { t.Errorf("chain4 Len() after All() mutation = %d, want 4", chain4.Len()) } // --- All() element replacement does not affect stored --- all2 := chain4.All() mutated, err := NewFailureCause("stage", "mutated", "", "", "") if err != nil { t.Fatalf("NewFailureCause: %v", err) } all2[0] = mutated c, err := chain4.At(0) if err != nil { t.Fatalf("At(0): %v", err) } if c.Code() != codes4[0] { t.Errorf("chain4 At(0) Code() = %q, want %q (All mutation leaked)", c.Code(), codes4[0]) } // --- Copy() returns defensive copy --- cp := chain4.Copy() if cp.Len() != 4 { t.Errorf("Copy() Len() = %d, want 4", cp.Len()) } cpCauses := cp.All() cpCauses = append(cpCauses, orig) if chain4.Len() != 4 { t.Errorf("chain4 Len() after Copy() mutation = %d, want 4", chain4.Len()) } // --- Copy() element replacement does not affect original --- cp2 := chain4.Copy() cp2.causes[0] = mutated c, err = chain4.At(0) if err != nil { t.Fatalf("At(0): %v", err) } if c.Code() != codes4[0] { t.Errorf("chain4 At(0) Code() = %q, want %q (Copy mutation leaked)", c.Code(), codes4[0]) } // --- Invalid cause in chain is rejected by constructor --- invalidCause := FailureCause{ stage: StableToken{value: "INVALID"}, code: mustToken("code"), } if _, err := NewFailureCauseChain([]FailureCause{invalidCause}); err == nil { t.Error("NewFailureCauseChain with invalid cause should return error") } // --- Append rejects invalid cause --- if _, err := chain4.Append(invalidCause); err == nil { t.Error("Append with invalid cause should return error") } // --- Invalid token table --- t.Run("invalid_stage_empty", func(t *testing.T) { _, err := NewFailureCause("", "code", "", "", "") if err == nil { t.Fatal("empty stage should be rejected") } }) t.Run("invalid_stage_uppercase", func(t *testing.T) { _, err := NewFailureCause("STAGE", "code", "", "", "") if err == nil { t.Fatal("uppercase stage should be rejected") } }) t.Run("invalid_code_empty", func(t *testing.T) { _, err := NewFailureCause("stage", "", "", "", "") if err == nil { t.Fatal("empty code should be rejected") } }) t.Run("invalid_code_uppercase", func(t *testing.T) { _, err := NewFailureCause("stage", "CODE", "", "", "") if err == nil { t.Fatal("uppercase code should be rejected") } }) t.Run("invalid_consumer_uppercase", func(t *testing.T) { _, err := NewFailureCause("stage", "code", "CONSUMER", "", "") if err == nil { t.Fatal("uppercase consumer should be rejected") } }) t.Run("invalid_filter_uppercase", func(t *testing.T) { _, err := NewFailureCause("stage", "code", "", "FILTER", "") if err == nil { t.Fatal("uppercase filter should be rejected") } }) t.Run("invalid_rule_id_uppercase", func(t *testing.T) { _, err := NewFailureCause("stage", "code", "", "", "RULE") if err == nil { t.Fatal("uppercase rule id should be rejected") } }) } // TestTerminalResultValidation verifies that TerminalResult enforces the // success/error mutual exclusion, required channel/timestamp, required // external descriptor for error results, cause cap, and that accessors return // defensive copies. func TestTerminalResultValidation(t *testing.T) { ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) desc, err := NewExternalDescriptor("error.type", "code", "message", "") if err != nil { t.Fatalf("NewExternalDescriptor: %v", err) } // --- Success terminal: valid --- success, err := NewSuccessTerminalResult("ch", ts) if err != nil { t.Fatalf("NewSuccessTerminalResult: %v", err) } if !success.Success() { t.Error("success terminal should be success") } if success.Error() { t.Error("success terminal should not be error") } if success.ExternalDesc() != nil { t.Error("success terminal should not have external descriptor") } if success.FailureCauses().Len() != 0 { t.Errorf("success terminal FailureCauses().Len() = %d, want 0", success.FailureCauses().Len()) } if err := success.Validate(); err != nil { t.Errorf("success terminal Validate() = %v, want nil", err) } // --- Error terminal with descriptor: valid --- errorResult, err := NewErrorTerminalResult("ch", desc, FailureCauseChain{}, ts) if err != nil { t.Fatalf("NewErrorTerminalResult: %v", err) } if errorResult.Success() { t.Error("error terminal should not be success") } if !errorResult.Error() { t.Error("error terminal should be error") } if errorResult.ExternalDesc() == nil { t.Error("error terminal should have external descriptor") } if err := errorResult.Validate(); err != nil { t.Errorf("error terminal Validate() = %v, want nil", err) } // --- Error terminal with descriptor + 4 causes: valid --- codes4 := []string{"code0", "code1", "code2", "code3"} causes4 := make([]FailureCause, MaxFailureCauses) for i, code := range codes4 { c, err := NewFailureCause("stage", code, "", "", "") if err != nil { t.Fatalf("NewFailureCause: %v", err) } causes4[i] = c } chain4, err := NewFailureCauseChain(causes4) if err != nil { t.Fatalf("NewFailureCauseChain: %v", err) } errorWithCauses, err := NewErrorTerminalResult("ch", desc, chain4, ts) if err != nil { t.Fatalf("NewErrorTerminalResult with causes: %v", err) } if err := errorWithCauses.Validate(); err != nil { t.Errorf("error terminal with 4 causes Validate() = %v, want nil", err) } // --- Error terminal without descriptor: rejected --- if _, err := NewErrorTerminalResult("ch", ExternalDescriptor{}, FailureCauseChain{}, ts); err == nil { t.Fatal("error terminal without descriptor should return error") } // --- Error terminal with nil descriptor: rejected --- badErr := TerminalResult{ channel: "ch", err: true, occurredAt: ts, } if err := badErr.Validate(); err == nil { t.Fatal("error terminal with nil descriptor should fail Validate") } // --- Success terminal with descriptor: rejected --- badSuccess := TerminalResult{ channel: "ch", success: true, externalDesc: &desc, occurredAt: ts, } if err := badSuccess.Validate(); err == nil { t.Fatal("success terminal with descriptor should fail Validate") } // --- Success terminal with causes: rejected --- badSuccessCauses := TerminalResult{ channel: "ch", success: true, failureCauses: chain4, occurredAt: ts, } if err := badSuccessCauses.Validate(); err == nil { t.Fatal("success terminal with causes should fail Validate") } // --- Neither success nor error: rejected --- neither := TerminalResult{ channel: "ch", occurredAt: ts, } if err := neither.Validate(); err == nil { t.Fatal("terminal with neither success nor error should fail Validate") } // --- Both success and error: rejected --- both := TerminalResult{ channel: "ch", success: true, err: true, externalDesc: &desc, occurredAt: ts, } if err := both.Validate(); err == nil { t.Fatal("terminal with both success and error should fail Validate") } // --- Empty channel: rejected --- if _, err := NewSuccessTerminalResult("", ts); err == nil { t.Fatal("success terminal with empty channel should return error") } if _, err := NewErrorTerminalResult("", desc, FailureCauseChain{}, ts); err == nil { t.Fatal("error terminal with empty channel should return error") } // --- Zero timestamp: rejected --- if _, err := NewSuccessTerminalResult("ch", time.Time{}); err == nil { t.Fatal("success terminal with zero timestamp should return error") } if _, err := NewErrorTerminalResult("ch", desc, FailureCauseChain{}, time.Time{}); err == nil { t.Fatal("error terminal with zero timestamp should return error") } // --- Error with 5 causes: rejected --- causes5 := make([]FailureCause, MaxFailureCauses+1) for i := range causes5 { c, err := NewFailureCause("stage", "code", "", "", "") if err != nil { t.Fatalf("NewFailureCause: %v", err) } causes5[i] = c } chain5Raw := FailureCauseChain{causes: causes5} badTr := TerminalResult{ channel: "ch", err: true, externalDesc: &desc, failureCauses: chain5Raw, occurredAt: ts, } if err := badTr.Validate(); err == nil { t.Fatal("error terminal with 5 causes should fail Validate") } // --- ExternalDesc accessor returns defensive copy --- ed := errorResult.ExternalDesc() if ed == nil { t.Fatal("ExternalDesc() returned nil") } // Mutate the returned descriptor. *ed = ExternalDescriptor{} // The stored descriptor should be unchanged. ed2 := errorResult.ExternalDesc() if ed2 == nil { t.Fatal("ExternalDesc() returned nil after mutation") } if ed2.Type() != "error.type" { t.Errorf("ExternalDesc() Type() = %q, want %q (mutation leaked)", ed2.Type(), "error.type") } // --- FailureCauses accessor returns defensive copy --- fc := errorWithCauses.FailureCauses() if fc.Len() != 4 { t.Errorf("FailureCauses().Len() = %d, want 4", fc.Len()) } fcCauses := fc.All() fcCauses = append(fcCauses, fcCauses[0]) if errorWithCauses.FailureCauses().Len() != 4 { t.Error("FailureCauses() mutated (accessor mutation leaked)") } // --- FailureCauses accessor element mutation does not affect stored --- fc2 := errorWithCauses.FailureCauses() mutated, err := NewFailureCause("stage", "mutated", "", "", "") if err != nil { t.Fatalf("NewFailureCause: %v", err) } fc2.causes[0] = mutated fc3 := errorWithCauses.FailureCauses() if fc3.Len() != 4 { t.Errorf("FailureCauses().Len() after mutation = %d, want 4", fc3.Len()) } c, err := fc3.At(0) if err != nil { t.Fatalf("At(0): %v", err) } if c.Code() != codes4[0] { t.Errorf("FailureCauses().At(0) Code() = %q, want %q (accessor mutation leaked)", c.Code(), codes4[0]) } // --- FailureCause invalid token table --- t.Run("invalid_stage_empty", func(t *testing.T) { _, err := NewFailureCause("", "code", "", "", "") if err == nil { t.Fatal("empty stage should be rejected") } }) t.Run("invalid_stage_uppercase", func(t *testing.T) { _, err := NewFailureCause("STAGE", "code", "", "", "") if err == nil { t.Fatal("uppercase stage should be rejected") } }) t.Run("invalid_code_empty", func(t *testing.T) { _, err := NewFailureCause("stage", "", "", "", "") if err == nil { t.Fatal("empty code should be rejected") } }) t.Run("invalid_code_uppercase", func(t *testing.T) { _, err := NewFailureCause("stage", "CODE", "", "", "") if err == nil { t.Fatal("uppercase code should be rejected") } }) t.Run("invalid_consumer_uppercase", func(t *testing.T) { _, err := NewFailureCause("stage", "code", "CONSUMER", "", "") if err == nil { t.Fatal("uppercase consumer should be rejected") } }) t.Run("invalid_filter_uppercase", func(t *testing.T) { _, err := NewFailureCause("stage", "code", "", "FILTER", "") if err == nil { t.Fatal("uppercase filter should be rejected") } }) t.Run("invalid_rule_id_uppercase", func(t *testing.T) { _, err := NewFailureCause("stage", "code", "", "", "RULE") if err == nil { t.Fatal("uppercase rule id should be rejected") } }) } // fakeReleaseSink is a test-only implementation of ReleaseSink that records // all committed payloads. It is used solely for the compile-time interface // assertion; single-terminal behavior is verified in the host fixture. type fakeReleaseSink struct { responseStarts []ResponseStart releases []ReleaseEvent terminals []TerminalResult } // Compile-time assertion that fakeReleaseSink implements ReleaseSink. var _ ReleaseSink = (*fakeReleaseSink)(nil) func (f *fakeReleaseSink) CommitResponseStart(ctx context.Context, rs ResponseStart) (CommitState, error) { f.responseStarts = append(f.responseStarts, rs) return CommitStateStreamOpen, nil } func (f *fakeReleaseSink) Release(ctx context.Context, ev ReleaseEvent) (CommitState, error) { f.releases = append(f.releases, ev) return CommitStateStreamOpen, nil } func (f *fakeReleaseSink) CommitTerminal(ctx context.Context, tr TerminalResult) (CommitState, error) { f.terminals = append(f.terminals, tr) return CommitStateTerminalCommitted, nil } // TestReleaseSinkContractCompiles verifies that the ReleaseSink interface // contract is satisfied by a test-only fake sink via a compile-time assertion, // and that all three methods can be invoked through the interface. func TestReleaseSinkContractCompiles(t *testing.T) { ctx := context.Background() sink := &fakeReleaseSink{} // Verify the interface is usable through the interface type. var iface ReleaseSink = sink // CommitResponseStart rs, err := NewResponseStart("ch", 200, map[string]string{"x-custom": "val"}, time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) if err != nil { t.Fatalf("NewResponseStart: %v", err) } state, err := iface.CommitResponseStart(ctx, rs) if err != nil { t.Fatalf("CommitResponseStart: %v", err) } if state != CommitStateStreamOpen { t.Errorf("CommitResponseStart state = %q, want %q", state, CommitStateStreamOpen) } if len(sink.responseStarts) != 1 { t.Errorf("responseStarts length = %d, want 1", len(sink.responseStarts)) } // Release releaseEv, err := NewReleaseTextDeltaEvent("ch", "hello", time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) if err != nil { t.Fatalf("NewReleaseTextDeltaEvent: %v", err) } state, err = iface.Release(ctx, releaseEv) if err != nil { t.Fatalf("Release: %v", err) } if state != CommitStateStreamOpen { t.Errorf("Release state = %q, want %q", state, CommitStateStreamOpen) } if len(sink.releases) != 1 { t.Errorf("releases length = %d, want 1", len(sink.releases)) } // CommitTerminal terminal, err := NewSuccessTerminalResult("ch", time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) if err != nil { t.Fatalf("NewSuccessTerminalResult: %v", err) } state, err = iface.CommitTerminal(ctx, terminal) if err != nil { t.Fatalf("CommitTerminal: %v", err) } if state != CommitStateTerminalCommitted { t.Errorf("CommitTerminal state = %q, want %q", state, CommitStateTerminalCommitted) } if len(sink.terminals) != 1 { t.Errorf("terminals length = %d, want 1", len(sink.terminals)) } }