iop/packages/go/streamgate/commit_boundary_test.go
toki 8a4f6c55a1 sync: roadmap, skills, test inventory, streamgate package, docs updates
- Update roadmap milestones and phase docs across multiple phases
- Update plan, code-review, create-roadmap, update-roadmap, finalize-task-routing skills
- Update dev-corp-runtime-deploy, dev-runtime-deploy, orchestrate-agent-task-loop skills
- Refactor agent-task-loop dispatch script
- Add streamgate Go package (commit_boundary, evidence_tail, filter_registry, stream_release)
- Add test inventory files (dev, dev-corp, unified)
- Update test smoke tests and rules for dev/dev-corp
- Update docs/edge-local-dev-guide and e2e scripts
- Update inventory-query Go package
- Remove deprecated templates and inventory.yaml files
- Add orchestrate-agent-task-loop tests
2026-07-25 11:41:08 +09:00

440 lines
13 KiB
Go

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)
}
}
})
}