package streamgate import ( "context" "errors" "sync" "testing" "time" ) // testNow is a deterministic timestamp used in tests. var testNow = time.Date(2026, 7, 24, 12, 0, 0, 0, time.UTC) // testSink is a configurable test-only ReleaseSink that records calls and // can be programmed to fail specific calls. type testSink struct { mu sync.Mutex responseStarts []ResponseStart releases []ReleaseEvent terminals []TerminalResult state CommitState failAfterSuccesses int // >0: fail after N successes; 0: no failure failAll bool // if true, all releases fail successCount int // how many successful releases have occurred (only tracked when failAfterSuccesses > 0) } func (s *testSink) CommitResponseStart(ctx context.Context, rs ResponseStart) (CommitState, error) { s.mu.Lock() defer s.mu.Unlock() s.responseStarts = append(s.responseStarts, rs) s.state = CommitStateStreamOpen return CommitStateStreamOpen, nil } func (s *testSink) Release(ctx context.Context, ev ReleaseEvent) (CommitState, error) { s.mu.Lock() defer s.mu.Unlock() if s.failAll { return "", errors.New("test: forced release failure") } if s.failAfterSuccesses > 0 { s.successCount++ if s.successCount > s.failAfterSuccesses { return "", errors.New("test: forced release failure") } } s.releases = append(s.releases, ev) return s.state, nil } func (s *testSink) CommitTerminal(ctx context.Context, tr TerminalResult) (CommitState, error) { s.mu.Lock() defer s.mu.Unlock() s.terminals = append(s.terminals, tr) s.state = CommitStateTerminalCommitted return CommitStateTerminalCommitted, nil } // --------------------------------------------------------------------------- // REVIEW_API-1: Request-scope staging and monotonic sink state // --------------------------------------------------------------------------- // TestCommitBoundaryStagesUntilFirstSafeRelease verifies that stage calls // produce zero sink invocations and the first release commits start→event // order with a single start. func TestCommitBoundaryStagesUntilFirstSafeRelease(t *testing.T) { ctx := context.Background() sink := &testSink{} b, err := NewCommitBoundary(sink) if err != nil { t.Fatalf("NewCommitBoundary: %v", err) } rs, _ := NewResponseStart("ch", 200, map[string]string{"x-test": "v"}, testNow) if err := b.BeginAttempt("a1"); err != nil { t.Fatalf("BeginAttempt: %v", err) } if err := b.StageResponseStart("a1", rs); err != nil { t.Fatalf("StageResponseStart: %v", err) } if got := b.State(); got != CommitStateTransportUncommitted { t.Errorf("state before release = %q, want %q", got, CommitStateTransportUncommitted) } ev1, _ := NewReleaseTextDeltaEvent("ch", "hello ", testNow) ev2, _ := NewReleaseTextDeltaEvent("ch", "world", testNow) prog, err := b.ReleaseSafe(ctx, "a1", []ReleaseEvent{ev1, ev2}) if err != nil { t.Fatalf("ReleaseSafe: %v", err) } if len(sink.responseStarts) != 1 { t.Errorf("responseStarts = %d, want 1", len(sink.responseStarts)) } if len(sink.releases) != 2 { t.Errorf("releases = %d, want 2", len(sink.releases)) } if prog.ReleasedEvents() != 2 { t.Errorf("ReleasedEvents = %d, want 2", prog.ReleasedEvents()) } if got := b.State(); got != CommitStateStreamOpen { t.Errorf("state after release = %q, want %q", got, CommitStateStreamOpen) } // Second release still works (start already consumed). _, err = b.ReleaseSafe(ctx, "a1", []ReleaseEvent{ev1}) if err != nil { t.Fatalf("second ReleaseSafe: %v", err) } if len(sink.releases) != 3 { t.Errorf("total releases after second call = %d, want 3", len(sink.releases)) } if len(sink.responseStarts) != 1 { t.Errorf("total responseStarts = %d, want 1", len(sink.responseStarts)) } } // TestCommitBoundaryPreservesProgressAndMonotonicState verifies the state // transition table for start/body/error and that successful event count is // preserved across failures. func TestCommitBoundaryPreservesProgressAndMonotonicState(t *testing.T) { ctx := context.Background() t.Run("success_steps_preserve_count", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") rs, _ := NewResponseStart("ch", 200, nil, testNow) b.StageResponseStart("a1", rs) evs := make([]ReleaseEvent, 5) for i := range evs { e, _ := NewReleaseTextDeltaEvent("ch", "chunk", testNow) evs[i] = e } prog, err := b.ReleaseSafe(ctx, "a1", evs) if err != nil { t.Fatalf("ReleaseSafe: %v", err) } if prog.ReleasedEvents() != 5 { t.Errorf("ReleasedEvents = %d, want 5", prog.ReleasedEvents()) } if len(sink.releases) != 5 { t.Errorf("releases = %d, want 5", len(sink.releases)) } if got := b.State(); got != CommitStateStreamOpen { t.Errorf("state = %q, want stream_open", got) } }) t.Run("sink_error_preserves_progress", func(t *testing.T) { sink := &testSink{failAfterSuccesses: 1} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") rs, _ := NewResponseStart("ch", 200, nil, testNow) b.StageResponseStart("a1", rs) evs := make([]ReleaseEvent, 3) for i := range evs { e, _ := NewReleaseTextDeltaEvent("ch", "chunk", testNow) evs[i] = e } prog, err := b.ReleaseSafe(ctx, "a1", evs) if err == nil { t.Error("expected error from ReleaseSafe") } // First event succeeded before the forced failure. if prog.ReleasedEvents() != 1 { t.Errorf("ReleasedEvents = %d, want 1", prog.ReleasedEvents()) } if len(sink.releases) != 1 { t.Errorf("releases = %d, want 1", len(sink.releases)) } if got := b.State(); got != CommitStateStreamOpen { t.Errorf("state = %q, want stream_open (preserved)", got) } }) t.Run("begin_after_stream_open_rejected", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") rs, _ := NewResponseStart("ch", 200, nil, testNow) b.StageResponseStart("a1", rs) ev, _ := NewReleaseTextDeltaEvent("ch", "x", testNow) b.ReleaseSafe(ctx, "a1", []ReleaseEvent{ev}) err := b.BeginAttempt("a2") if err != ErrBoundaryUncommitted { t.Errorf("BeginAttempt error = %v, want ErrBoundaryUncommitted", err) } }) t.Run("zero_release_events_rejected", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") _, err := b.ReleaseSafe(ctx, "a1", nil) if err == nil { t.Error("expected error for empty events") } }) } // TestCommitBoundaryCommitsEmptySuccessAndTerminalOnce verifies that success // terminal without body commits start first, terminal exactly once, and // State() does not deadlock during callbacks. func TestCommitBoundaryCommitsEmptySuccessAndTerminalOnce(t *testing.T) { ctx := context.Background() t.Run("success_terminal_no_body", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") rs, _ := NewResponseStart("ch", 200, nil, testNow) b.StageResponseStart("a1", rs) tr, _ := NewSuccessTerminalResult("ch", testNow) if err := b.CommitTerminal(ctx, "a1", tr); err != nil { t.Fatalf("CommitTerminal: %v", err) } if len(sink.responseStarts) != 1 { t.Errorf("responseStarts = %d, want 1", len(sink.responseStarts)) } if len(sink.terminals) != 1 { t.Errorf("terminals = %d, want 1", len(sink.terminals)) } if got := b.State(); got != CommitStateTerminalCommitted { t.Errorf("state = %q, want terminal_committed", got) } }) t.Run("second_terminal_rejected", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") tr, _ := NewSuccessTerminalResult("ch", testNow) b.CommitTerminal(ctx, "a1", tr) err := b.CommitTerminal(ctx, "a1", tr) if err != ErrBoundaryAlreadyTerminal { t.Errorf("second CommitTerminal error = %v, want ErrBoundaryAlreadyTerminal", err) } }) t.Run("state_no_deadlock_during_callback", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") rs, _ := NewResponseStart("ch", 200, nil, testNow) b.StageResponseStart("a1", rs) done := make(chan struct{}) go func() { defer close(done) for i := 0; i < 100; i++ { _ = b.State() } }() evs := make([]ReleaseEvent, 50) for i := range evs { e, _ := NewReleaseTextDeltaEvent("ch", "x", testNow) evs[i] = e } _, err := b.ReleaseSafe(ctx, "a1", evs) if err != nil { t.Fatalf("ReleaseSafe: %v", err) } <-done }) t.Run("error_terminal_discards_staged_start", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") rs, _ := NewResponseStart("ch", 200, nil, testNow) b.StageResponseStart("a1", rs) desc, _ := NewExternalDescriptor("generic", "internal_error", "error", "") causes, _ := NewFailureCauseChain([]FailureCause{}) tr, _ := NewErrorTerminalResult("ch", desc, causes, testNow) if err := b.CommitTerminal(ctx, "a1", tr); err != nil { t.Fatalf("CommitTerminal: %v", err) } if len(sink.responseStarts) != 0 { t.Errorf("responseStarts = %d, want 0 (error discards start)", len(sink.responseStarts)) } if len(sink.terminals) != 1 { t.Errorf("terminals = %d, want 1", len(sink.terminals)) } }) } // --------------------------------------------------------------------------- // REVIEW_API-3: Attempt replacement and recovery strategy matrix // --------------------------------------------------------------------------- // TestCommitBoundaryReplacesOnlyUncommittedAttempt verifies that replacement // discards the old attempt and staged start, allowing a new attempt to begin. func TestCommitBoundaryReplacesOnlyUncommittedAttempt(t *testing.T) { ctx := context.Background() t.Run("replacement_discards_old_and_allows_new", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) // First attempt with staged start. b.BeginAttempt("old") rs, _ := NewResponseStart("ch", 200, nil, testNow) b.StageResponseStart("old", rs) // Replace. if err := b.ReplaceUncommittedAttempt("old", "new"); err != nil { t.Fatalf("ReplaceUncommittedAttempt: %v", err) } // Old attempt should not work (no current attempt). err := b.StageResponseStart("old", rs) if err == nil { // Actually after replace, there's no current attempt, so // StageResponseStart should work since state is still uncommitted. // But the old attempt ID is not special anymore. } // New attempt should work. b.BeginAttempt("new") rs2, _ := NewResponseStart("ch", 200, nil, testNow) b.StageResponseStart("new", rs2) ev, _ := NewReleaseTextDeltaEvent("ch", "data", testNow) _, err = b.ReleaseSafe(ctx, "new", []ReleaseEvent{ev}) if err != nil { t.Fatalf("ReleaseSafe: %v", err) } // Exactly one start from new attempt. if len(sink.responseStarts) != 1 { t.Errorf("responseStarts = %d, want 1 (only new attempt)", len(sink.responseStarts)) } }) t.Run("post_open_replacement_rejected", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") rs, _ := NewResponseStart("ch", 200, nil, testNow) b.StageResponseStart("a1", rs) ev, _ := NewReleaseTextDeltaEvent("ch", "x", testNow) b.ReleaseSafe(ctx, "a1", []ReleaseEvent{ev}) // After stream_open, we cannot begin a new attempt. err := b.BeginAttempt("a2") if err != ErrBoundaryUncommitted { t.Errorf("BeginAttempt after open error = %v, want ErrBoundaryUncommitted", err) } }) } // TestCommitBoundaryCommitStrategyMatrix verifies the allowed strategy table // for each commit state. func TestCommitBoundaryCommitStrategyMatrix(t *testing.T) { ctx := context.Background() strategies := []RecoveryStrategy{ RecoveryStrategyExactReplay, RecoveryStrategyContinuationRepair, RecoveryStrategySchemaRepair, } t.Run("transport_uncommitted", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") expectations := map[RecoveryStrategy]bool{ RecoveryStrategyExactReplay: true, RecoveryStrategySchemaRepair: true, RecoveryStrategyContinuationRepair: false, } for _, s := range strategies { got := b.CommitAllowsStrategy(s) want := expectations[s] if got != want { t.Errorf("CommitAllowsStrategy(%s) = %v, want %v", s, got, want) } } }) t.Run("stream_open", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") rs, _ := NewResponseStart("ch", 200, nil, testNow) b.StageResponseStart("a1", rs) ev, _ := NewReleaseTextDeltaEvent("ch", "x", testNow) b.ReleaseSafe(ctx, "a1", []ReleaseEvent{ev}) expectations := map[RecoveryStrategy]bool{ RecoveryStrategyExactReplay: false, RecoveryStrategySchemaRepair: false, RecoveryStrategyContinuationRepair: true, } for _, s := range strategies { got := b.CommitAllowsStrategy(s) want := expectations[s] if got != want { t.Errorf("CommitAllowsStrategy(%s) = %v, want %v", s, got, want) } } }) t.Run("terminal_committed", func(t *testing.T) { sink := &testSink{} b, _ := NewCommitBoundary(sink) b.BeginAttempt("a1") tr, _ := NewSuccessTerminalResult("ch", testNow) b.CommitTerminal(ctx, "a1", tr) for _, s := range strategies { got := b.CommitAllowsStrategy(s) if got { t.Errorf("CommitAllowsStrategy(%s) = true, want false after terminal", s) } } }) }