iop/packages/go/streamgate/stream_release_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

598 lines
19 KiB
Go

package streamgate
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
)
// testNow is a deterministic timestamp used in stream releaser tests.
var releaserNow = time.Date(2026, 7, 24, 12, 0, 0, 0, time.UTC)
// testCanceller records cancel attempts and can be programmed to fail.
type testCanceller struct {
cancelCount int
cancelErr error
}
func (c *testCanceller) CancelAttempt(ctx context.Context, attemptID string) error {
c.cancelCount++
return c.cancelErr
}
// setupReleaser creates a StreamReleaser with a rolling_window EvidenceTail,
// CommitBoundary, and canceller. The tail is configured so that appending
// enough events triggers an epoch via the rolling threshold.
//
// evidenceRunes controls the rolling threshold; maxBuffer is the hard limit.
func setupReleaser(t *testing.T, channel string, evidenceRunes, maxBuffer int) (*StreamReleaser, *testSink, *testCanceller) {
t.Helper()
// Create a rolling_window plan with a low threshold so a few events
// trigger an epoch quickly.
kinds := []EventKind{EventKindTextDelta, EventKindReasoningDelta}
var req FilterHoldRequirement
if maxBuffer > 0 {
var err error
req, err = NewFilterHoldRequirementRollingWithMaxBuffer(channel, kinds, evidenceRunes, maxBuffer)
if err != nil {
t.Fatalf("NewFilterHoldRequirementRollingWithMaxBuffer: %v", err)
}
} else {
var err error
req, err = NewFilterHoldRequirementRolling(channel, kinds, evidenceRunes)
if err != nil {
t.Fatalf("NewFilterHoldRequirementRolling: %v", err)
}
}
binding, err := NewFilterHoldBinding("f1", req, true)
if err != nil {
t.Fatalf("NewFilterHoldBinding: %v", err)
}
plan, err := NewEvidencePlan([]FilterHoldBinding{binding})
if err != nil {
t.Fatalf("NewEvidencePlan: %v", err)
}
tail, err := NewEvidenceTail(plan)
if err != nil {
t.Fatalf("NewEvidenceTail: %v", err)
}
sink := &testSink{}
boundary, err := NewCommitBoundary(sink)
if err != nil {
t.Fatalf("NewCommitBoundary: %v", err)
}
canceller := &testCanceller{}
releaser, err := NewStreamReleaser(tail, boundary, canceller)
if err != nil {
t.Fatalf("NewStreamReleaser: %v", err)
}
return releaser, sink, canceller
}
// appendDataEvents creates and appends text_delta events to the tail until
// an epoch is triggered (rolling threshold reached). Returns the epoch.
func appendDataEvents(t *testing.T, tail *EvidenceTail, channel string, count int) EvidenceEpoch {
t.Helper()
var epoch EvidenceEpoch
for i := 0; i < count; i++ {
ne, err := NewTextDeltaEvent(channel, "chunk", releaserNow)
if err != nil {
t.Fatalf("NewTextDeltaEvent: %v", err)
}
e, signal, err := tail.Append(ne)
if err != nil {
t.Fatalf("Append: %v", err)
}
if signal == EvidenceTailSignalThreshold {
epoch = e
}
}
return epoch
}
// createTextReleaseEvent creates a ReleaseEvent of kind text_delta.
func createTextReleaseEvent(channel, text string) ReleaseEvent {
ev, err := NewReleaseTextDeltaEvent(channel, text, releaserNow)
if err != nil {
panic(err)
}
return ev
}
// createSuccessTerminalResult creates a TerminalResult of kind success.
func createSuccessTerminalResult(channel string) TerminalResult {
tr, err := NewSuccessTerminalResult(channel, releaserNow)
if err != nil {
panic(err)
}
return tr
}
// createErrorTerminalResult creates an error TerminalResult.
func createErrorTerminalResult(channel string) TerminalResult {
desc, err := NewExternalDescriptor("generic", "internal_error", "an_internal_error", "")
if err != nil {
panic(fmt.Sprintf("createErrorTerminalResult NewExternalDescriptor failed: %v", err))
}
causes, err := NewFailureCauseChain([]FailureCause{})
if err != nil {
panic(fmt.Sprintf("createErrorTerminalResult NewFailureCauseChain failed: %v", err))
}
tr, err := NewErrorTerminalResult(channel, desc, causes, releaserNow)
if err != nil {
panic(fmt.Sprintf("createErrorTerminalResult NewErrorTerminalResult failed: %v", err))
}
return tr
}
// ---------------------------------------------------------------------------
// REVIEW_API-2: Evidence prepare/confirm with sink progress
// ---------------------------------------------------------------------------
// TestStreamReleaserConfirmsExactlySinkProgress verifies zero/partial/full
// sink progress where confirmed cursor matches sink success count.
func TestStreamReleaserConfirmsExactlySinkProgress(t *testing.T) {
ctx := context.Background()
t.Run("zero_sink_progress", func(t *testing.T) {
releaser, sink, _ := setupReleaser(t, "ch-zero", 2, 100)
// Begin attempt and stage response start on boundary.
releaser.boundary.BeginAttempt("a1")
rs, _ := NewResponseStart("ch-zero", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("a1", rs)
// Append data events to trigger epoch.
epoch := appendDataEvents(t, releaser.tail, "ch-zero", 3)
// All releases fail.
sink.failAll = true
prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
if err == nil {
t.Error("expected error from ReleaseEpoch")
}
if prog.ReleasedEvents() != 0 {
t.Errorf("ReleasedEvents = %d, want 0 (all events failed)", prog.ReleasedEvents())
}
})
t.Run("partial_sink_progress", func(t *testing.T) {
releaser, sink, _ := setupReleaser(t, "ch-partial", 2, 100)
releaser.boundary.BeginAttempt("a1")
rs, _ := NewResponseStart("ch-partial", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("a1", rs)
epoch := appendDataEvents(t, releaser.tail, "ch-partial", 5)
// 2 successes, then fail.
sink.failAfterSuccesses = 2
prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
if err == nil {
t.Error("expected partial error")
}
if prog.ReleasedEvents() != 2 {
t.Errorf("ReleasedEvents = %d, want 2", prog.ReleasedEvents())
}
})
t.Run("partial_sink_progress_two_succeed_then_fail", func(t *testing.T) {
releaser, sink, _ := setupReleaser(t, "ch-partial2", 2, 100)
releaser.boundary.BeginAttempt("a1")
rs, _ := NewResponseStart("ch-partial2", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("a1", rs)
epoch := appendDataEvents(t, releaser.tail, "ch-partial2", 5)
// 2 successes, then fail.
sink.failAfterSuccesses = 2
prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
if err == nil {
t.Error("expected partial error")
}
if prog.ReleasedEvents() != 2 {
t.Errorf("ReleasedEvents = %d, want 2", prog.ReleasedEvents())
}
})
t.Run("full_sink_progress", func(t *testing.T) {
releaser, sink, _ := setupReleaser(t, "ch-full", 2, 100)
releaser.boundary.BeginAttempt("a1")
rs, _ := NewResponseStart("ch-full", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("a1", rs)
epoch := appendDataEvents(t, releaser.tail, "ch-full", 3)
prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
if err != nil {
t.Fatalf("ReleaseEpoch: %v", err)
}
if prog.ReleasedEvents() != 3 {
t.Errorf("ReleasedEvents = %d, want 3", prog.ReleasedEvents())
}
// Cursor should advance by 3.
if got := releaser.tail.CommittedCursor("ch-full"); got != 3 {
t.Errorf("committed cursor = %d, want 3", got)
}
// Sink should have received 3 releases + 1 start.
if len(sink.releases) != 3 {
t.Errorf("sink releases = %d, want 3", len(sink.releases))
}
if len(sink.responseStarts) != 1 {
t.Errorf("sink responseStarts = %d, want 1", len(sink.responseStarts))
}
})
}
// TestStreamReleaserTerminalBeforeEvidenceExposesOneOutcome verifies that the
// pass path exposes start/output+success terminal and error path exposes
// only error terminal without releasing pending/start.
func TestStreamReleaserTerminalBeforeEvidenceExposesOneOutcome(t *testing.T) {
ctx := context.Background()
t.Run("pass_exposes_start_and_terminal", func(t *testing.T) {
releaser, sink, _ := setupReleaser(t, "ch-pass", 2, 100)
releaser.boundary.BeginAttempt("a1")
rs, _ := NewResponseStart("ch-pass", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("a1", rs)
// Create and release an epoch.
epoch := appendDataEvents(t, releaser.tail, "ch-pass", 2)
prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
if err != nil {
t.Fatalf("ReleaseEpoch: %v", err)
}
if prog.ReleasedEvents() != 2 {
t.Errorf("ReleasedEvents = %d, want 2", prog.ReleasedEvents())
}
// Terminal success.
tr := createSuccessTerminalResult("ch-pass")
prog2, err := releaser.ReleaseTerminalEpoch(ctx, "a1", epoch.ID(), tr)
if err != nil {
t.Fatalf("ReleaseTerminalEpoch: %v", err)
}
_ = prog2
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 len(sink.releases) != 2 {
t.Errorf("releases = %d, want 2", len(sink.releases))
}
})
t.Run("error_exposes_only_error_terminal", func(t *testing.T) {
releaser, sink, canceller := setupReleaser(t, "ch-err", 2, 100)
releaser.boundary.BeginAttempt("a2")
rs, _ := NewResponseStart("ch-err", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("a2", rs)
// Create some pending events but don't release them.
appendDataEvents(t, releaser.tail, "ch-err", 2)
// Error terminal without releasing events.
tr := createErrorTerminalResult("ch-err")
prog, err := releaser.ReleaseTerminalEpoch(ctx, "a2", 999, tr)
if err != nil {
t.Fatalf("ReleaseTerminalEpoch: %v", err)
}
_ = prog
// Should have called cancel.
if canceller.cancelCount != 1 {
t.Errorf("cancelCount = %d, want 1", canceller.cancelCount)
}
if len(sink.responseStarts) != 0 {
t.Errorf("responseStarts = %d, want 0 (error path)", len(sink.responseStarts))
}
if len(sink.terminals) != 1 {
t.Errorf("terminals = %d, want 1", len(sink.terminals))
}
if len(sink.releases) != 0 {
t.Errorf("releases = %d, want 0 (no pending release)", len(sink.releases))
}
})
}
// TestStreamReleaserDoesNotDuplicatePreparedRelease verifies that repeated
// epoch calls do not duplicate downstream events.
func TestStreamReleaserDoesNotDuplicatePreparedRelease(t *testing.T) {
ctx := context.Background()
releaser, sink, _ := setupReleaser(t, "ch-dup", 2, 100)
releaser.boundary.BeginAttempt("a1")
rs, _ := NewResponseStart("ch-dup", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("a1", rs)
// First epoch.
epoch1 := appendDataEvents(t, releaser.tail, "ch-dup", 2)
prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch1.ID())
if err != nil {
t.Fatalf("ReleaseEpoch 1: %v", err)
}
if prog.ReleasedEvents() != 2 {
t.Errorf("first ReleasedEvents = %d, want 2", prog.ReleasedEvents())
}
// Second epoch (more events to trigger another).
epoch2 := appendDataEvents(t, releaser.tail, "ch-dup", 2)
prog2, err := releaser.ReleaseEpoch(ctx, "a1", epoch2.ID())
if err != nil {
t.Fatalf("ReleaseEpoch 2: %v", err)
}
if prog2.ReleasedEvents() != 2 {
t.Errorf("second ReleasedEvents = %d, want 2", prog2.ReleasedEvents())
}
// Total should be 4 releases (2+2), no duplicates.
if len(sink.releases) != 4 {
t.Errorf("total releases = %d, want 4 (2+2)", len(sink.releases))
}
}
// ---------------------------------------------------------------------------
// REVIEW_API-3: Attempt replacement, idle and overflow
// ---------------------------------------------------------------------------
// TestStreamReleaserReplacementResetsTailGeneration verifies that boundary
// attempt and tail pending/prepared token are replaced together.
func TestStreamReleaserReplacementResetsTailGeneration(t *testing.T) {
ctx := context.Background()
releaser, _, _ := setupReleaser(t, "ch-rep", 2, 100)
// Begin attempt and stage start.
releaser.boundary.BeginAttempt("old")
rs, _ := NewResponseStart("ch-rep", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("old", rs)
// Append events.
appendDataEvents(t, releaser.tail, "ch-rep", 2)
// Replace.
err := releaser.ReplaceUncommittedAttempt(ctx, "old", "new")
if err != nil {
t.Fatalf("ReplaceUncommittedAttempt: %v", err)
}
// Tail cursor should be 0 after replace.
if got := releaser.tail.CommittedCursor("ch-rep"); got != 0 {
t.Errorf("tail cursor after replace = %d, want 0", got)
}
// Boundary attempt should be cleared.
if got := releaser.boundary.CurrentAttempt(); got != "" {
t.Errorf("boundary attempt after replace = %q, want empty", got)
}
}
// TestStreamReleaserIdleReleasesNoPending verifies that idle error results
// in zero release/start and correct cancel→terminal ordering.
func TestStreamReleaserIdleReleasesNoPending(t *testing.T) {
ctx := context.Background()
releaser, sink, canceller := setupReleaser(t, "ch-idle", 2, 100)
releaser.boundary.BeginAttempt("a1")
rs, _ := NewResponseStart("ch-idle", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("a1", rs)
// Append some events to create pending state.
appendDataEvents(t, releaser.tail, "ch-idle", 2)
// FailPending.
tr := createErrorTerminalResult("ch-idle")
prog, err := releaser.FailPending(ctx, "a1", tr)
if err != nil {
t.Fatalf("FailPending: %v", err)
}
if prog.ReleasedEvents() != 0 {
t.Errorf("ReleasedEvents = %d, want 0", prog.ReleasedEvents())
}
// No responses or releases.
if len(sink.responseStarts) != 0 {
t.Errorf("responseStarts = %d, want 0", len(sink.responseStarts))
}
if len(sink.releases) != 0 {
t.Errorf("releases = %d, want 0", len(sink.releases))
}
// Cancel should have been called.
if canceller.cancelCount != 1 {
t.Errorf("cancelCount = %d, want 1", canceller.cancelCount)
}
if len(sink.terminals) != 1 {
t.Errorf("terminals = %d, want 1", len(sink.terminals))
}
}
// TestStreamReleaserOverflowCancelsBeforeSingleTerminal verifies that S19
// overflow results in no partial release, and terminal exactly once.
func TestStreamReleaserOverflowCancelsBeforeSingleTerminal(t *testing.T) {
ctx := context.Background()
// Use very small buffer to trigger overflow.
// rolling_window with evidence 1 and maxBuffer 10 (minimum).
releaser, sink, canceller := setupReleaser(t, "ch-overflow", 1, 10)
// Append events, expecting overflow signal.
overflowed := false
for i := 0; i < 50; i++ {
ne, err := NewTextDeltaEvent("ch-overflow", "chunk", releaserNow)
if err != nil {
t.Fatalf("NewTextDeltaEvent: %v", err)
}
_, signal, err := releaser.tail.Append(ne)
if err != nil {
t.Fatalf("Append: %v", err)
}
if signal == EvidenceTailSignalBufferOverflow {
overflowed = true
break
}
}
if !overflowed {
t.Skip("expected overflow signal not generated with current configuration")
}
// FailPending for overflow recovery.
tr := createErrorTerminalResult("ch-overflow")
prog, err := releaser.FailPending(ctx, "a1", tr)
if err != nil {
t.Fatalf("FailPending: %v", err)
}
if prog.ReleasedEvents() != 0 {
t.Errorf("ReleasedEvents = %d, want 0 (overflow)", prog.ReleasedEvents())
}
if len(sink.terminals) != 1 {
t.Errorf("terminals = %d, want 1 (exactly once)", len(sink.terminals))
}
_ = canceller
}
// TestStreamReleaserCancelFailureAppendsBoundedCause verifies that when
// cancel fails, the cause is appended to the terminal result.
func TestStreamReleaserCancelFailureAppendsBoundedCause(t *testing.T) {
ctx := context.Background()
releaser, sink, canceller := setupReleaser(t, "ch-cancel-fail", 2, 100)
releaser.boundary.BeginAttempt("a1")
rs, _ := NewResponseStart("ch-cancel-fail", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("a1", rs)
// Program canceller to fail.
canceller.cancelErr = errors.New("test: cancel failed")
appendDataEvents(t, releaser.tail, "ch-cancel-fail", 2)
tr := createErrorTerminalResult("ch-cancel-fail")
prog, err := releaser.FailPending(ctx, "a1", tr)
if err != nil {
t.Fatalf("FailPending: %v", err)
}
_ = prog
// Terminal should still be committed exactly once.
if len(sink.terminals) != 1 {
t.Errorf("terminals = %d, want 1", len(sink.terminals))
}
// The terminal result should include the cancel failure cause.
lastTerminal := sink.terminals[len(sink.terminals)-1]
causes := lastTerminal.FailureCauses()
if causes.Len() == 0 {
t.Error("expected failure causes to include cancel failure")
} else {
found := false
for i := 0; i < causes.Len(); i++ {
c, _ := causes.At(i)
if c.Code() == "attempt_cancel_failed" {
found = true
break
}
}
if !found {
t.Error("expected 'attempt_cancel_failed' cause code")
}
}
}
// TestStreamReleaserConcurrentEpochsAreSerialized verifies that concurrent
// release calls through the releaser serializer do not cause data races
// (the tail itself is not thread-safe, so appends must be serialized).
func TestStreamReleaserConcurrentEpochsAreSerialized(t *testing.T) {
ctx := context.Background()
releaser, _, _ := setupReleaser(t, "ch-conc", 2, 100)
releaser.boundary.BeginAttempt("a1")
rs, _ := NewResponseStart("ch-conc", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("a1", rs)
// Pre-create all epochs sequentially (tail is not thread-safe).
epochs := make([]EvidenceEpoch, 5)
for i := 0; i < 5; i++ {
epochs[i] = appendDataEvents(t, releaser.tail, "ch-conc", 2)
}
var wg sync.WaitGroup
var successful int64
var errors int64
for i := 0; i < 5; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
_, err := releaser.ReleaseEpoch(ctx, "a1", epochs[idx].ID())
if err == nil {
atomic.AddInt64(&successful, 1)
} else {
atomic.AddInt64(&errors, 1)
}
}(i)
}
wg.Wait()
// All events should have been processed without race.
total := atomic.LoadInt64(&successful) + atomic.LoadInt64(&errors)
if total != 5 {
t.Errorf("total results = %d, want 5", total)
}
}
// TestStreamReleaserTerminalAfterFullReleaseSkipsSecondaryRelease verifies
// that after an epoch is fully released and confirmed, ReleaseTerminalEpoch
// does not attempt another release.
func TestStreamReleaserTerminalAfterFullReleaseSkipsSecondaryRelease(t *testing.T) {
ctx := context.Background()
releaser, sink, _ := setupReleaser(t, "ch-skip", 2, 100)
releaser.boundary.BeginAttempt("a1")
rs, _ := NewResponseStart("ch-skip", 200, nil, releaserNow)
releaser.boundary.StageResponseStart("a1", rs)
// Release epoch fully.
epoch := appendDataEvents(t, releaser.tail, "ch-skip", 2)
prog, err := releaser.ReleaseEpoch(ctx, "a1", epoch.ID())
if err != nil {
t.Fatalf("ReleaseEpoch: %v", err)
}
if prog.ReleasedEvents() != 2 {
t.Errorf("ReleasedEvents = %d, want 2", prog.ReleasedEvents())
}
// Terminal after full release.
tr := createSuccessTerminalResult("ch-skip")
prog2, err := releaser.ReleaseTerminalEpoch(ctx, "a1", epoch.ID(), tr)
if err != nil {
t.Fatalf("ReleaseTerminalEpoch: %v", err)
}
_ = prog2
// Should have 2 releases + 1 start, no extra.
if len(sink.releases) != 2 {
t.Errorf("releases = %d, want 2 (no secondary release)", len(sink.releases))
}
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))
}
}