iop/packages/go/streamgate/runtime_test.go
toki c1c1e19678 feat(streamgate): 요청 런타임 수명주기를 구현한다
증거 평가와 복구를 하나의 요청 루프로 수렴시켜 OpenAI ingress 재구성과 stream release를 일관되게 처리한다.
2026-07-26 21:03:39 +09:00

418 lines
13 KiB
Go

package streamgate
import (
"context"
"errors"
"sync"
"testing"
"time"
)
type sliceEventSource struct {
mu sync.Mutex
events []NormalizedEvent
index int
}
func newSliceEventSource(events []NormalizedEvent) *sliceEventSource {
return &sliceEventSource{events: events}
}
func (s *sliceEventSource) NextEvent(ctx context.Context) (NormalizedEvent, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.index >= len(s.events) {
return NormalizedEvent{}, errors.New("EOF")
}
ev := s.events[s.index]
s.index++
return ev, nil
}
type fixtureController struct {
mu sync.Mutex
abortCount int
}
func (c *fixtureController) AbortAttempt(ctx context.Context) error {
c.mu.Lock()
defer c.mu.Unlock()
c.abortCount++
return nil
}
type fixtureDispatcher struct {
mu sync.Mutex
handler func(ctx context.Context, request RebuiltRequest) (AttemptBinding, error)
dispatched []RebuiltRequest
}
func (d *fixtureDispatcher) DispatchAttempt(ctx context.Context, request RebuiltRequest) (AttemptBinding, error) {
d.mu.Lock()
d.dispatched = append(d.dispatched, request)
h := d.handler
d.mu.Unlock()
if h != nil {
return h(ctx, request)
}
return AttemptBinding{}, errors.New("dispatcher error")
}
type fixtureRebuilder struct {
mu sync.Mutex
planCount int
}
func (r *fixtureRebuilder) RebuildRequest(ctx context.Context, snapshot RecoveryRequestSnapshotRef, plan RecoveryPlan) (RebuiltRequestDraft, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.planCount++
return NewRebuiltRequestDraftWithIdempotency(plan.PlanID(), plan.IdempotencyKey(), "req-ref-1", "ep", "fam", 10, 20, 100, 5, nil)
}
type fixtureSink struct {
mu sync.Mutex
starts []ResponseStart
events []ReleaseEvent
terminals []TerminalResult
state CommitState
}
func newFixtureSink() *fixtureSink {
return &fixtureSink{state: CommitStateTransportUncommitted}
}
func (s *fixtureSink) CommitResponseStart(ctx context.Context, rs ResponseStart) (CommitState, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.starts = append(s.starts, rs)
s.state = CommitStateStreamOpen
return CommitStateStreamOpen, nil
}
func (s *fixtureSink) Release(ctx context.Context, ev ReleaseEvent) (CommitState, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.events = append(s.events, ev)
s.state = CommitStateStreamOpen
return CommitStateStreamOpen, nil
}
func (s *fixtureSink) 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
}
type customMockFilter struct {
id string
holdReq *FilterHoldRequirement
appliesFn func(FilterContext) bool
evaluateFn func(context.Context, FilterContext, EvidenceBatch) (FilterDecision, error)
}
func (m *customMockFilter) ID() string { return m.id }
func (m *customMockFilter) Applies(c FilterContext) bool {
if m.appliesFn != nil {
return m.appliesFn(c)
}
return true
}
func (m *customMockFilter) HoldRequirement(c FilterContext) FilterHoldRequirement {
if m.holdReq != nil {
return *m.holdReq
}
req, _ := NewFilterHoldRequirementRolling("default", []EventKind{EventKindTextDelta, EventKindTerminal}, 1)
return req
}
func (m *customMockFilter) Evaluate(ctx context.Context, fc FilterContext, batch EvidenceBatch) (FilterDecision, error) {
if m.evaluateFn != nil {
return m.evaluateFn(ctx, fc, batch)
}
fp := FixedFingerprint{1}
ev, _ := NewSanitizedEvidence(EventKindTextDelta, "default", "rule1", "desc", fp, 1, 0, FilterOutcomeKindEvaluated, time.Now())
return NewFilterDecision(FilterDecisionKindPass, "consumer1", m.id, "rule1", ev, nil)
}
func createTestRuntimeSnapshot(t *testing.T, regs []FilterRegistration, disp AttemptDispatcher, rebuilder RequestRebuilder, sink ReleaseSink) RequestRuntimeSnapshot {
regSnap, err := NewFilterRegistrySnapshot("gen-1", regs, nil)
if err != nil {
t.Fatalf("NewFilterRegistrySnapshot: %v", err)
}
snapRef, err := NewRecoveryRequestSnapshotRef("snap.ref.1", 100, 200, 1024)
if err != nil {
t.Fatalf("NewRecoveryRequestSnapshotRef: %v", err)
}
opts := DefaultRuntimeOptions()
snap, err := NewRequestRuntimeSnapshot(
"req-123",
"gen-1",
"test",
"ep",
"fam",
opts,
regSnap,
nil,
snapRef,
disp,
rebuilder,
nil,
sink,
)
if err != nil {
t.Fatalf("NewRequestRuntimeSnapshot: %v", err)
}
return snap
}
func TestRequestRuntimeDisabledAndPass(t *testing.T) {
sink := newFixtureSink()
disp := &fixtureDispatcher{}
rebuilder := &fixtureRebuilder{}
passF := &customMockFilter{id: "pass-filter"}
regPass, err := NewFilterRegistration(passF, "cap1", true, FilterEnforcementBlocking, 5*time.Second, 10)
if err != nil {
t.Fatalf("NewFilterRegistration pass: %v", err)
}
disabledF := &customMockFilter{id: "disabled-filter"}
regDisabled, err := NewFilterRegistration(disabledF, "cap2", false, FilterEnforcementBlocking, 5*time.Second, 10)
if err != nil {
t.Fatalf("NewFilterRegistration disabled: %v", err)
}
snap := createTestRuntimeSnapshot(t, []FilterRegistration{regPass, regDisabled}, disp, rebuilder, sink)
rsEv, _ := NewResponseStartEvent("default", 200, map[string]string{"content-type": "text/event-stream"}, time.Now())
txtEv, _ := NewTextDeltaEvent("default", "hello world", time.Now())
termEv, _ := NewTerminalEvent("default", time.Now())
src := newSliceEventSource([]NormalizedEvent{rsEv, txtEv, termEv})
ctrl := &fixtureController{}
initialBinding, err := NewAttemptBinding("att-1", "gpt-4", "openai", "primary", src, ctrl)
if err != nil {
t.Fatalf("NewAttemptBinding: %v", err)
}
rt, err := NewRequestRuntime(snap, "group-a", initialBinding)
if err != nil {
t.Fatalf("NewRequestRuntime: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := rt.Run(ctx); err != nil {
t.Fatalf("Run: %v", err)
}
sink.mu.Lock()
defer sink.mu.Unlock()
if len(sink.starts) != 1 {
t.Errorf("expected 1 response start committed, got %d", len(sink.starts))
}
if len(sink.events) != 1 {
t.Errorf("expected 1 release event, got %d", len(sink.events))
}
if len(sink.terminals) != 1 {
t.Errorf("expected 1 terminal result, got %d", len(sink.terminals))
}
}
func TestRequestRuntimeRecoveryLifecycle(t *testing.T) {
sink := newFixtureSink()
rebuilder := &fixtureRebuilder{}
disp := &fixtureDispatcher{}
filterID := "violating-filter"
var evalCount int
var mu sync.Mutex
vF := &customMockFilter{
id: filterID,
evaluateFn: func(ctx context.Context, fc FilterContext, batch EvidenceBatch) (FilterDecision, error) {
mu.Lock()
count := evalCount
evalCount++
mu.Unlock()
fp := FixedFingerprint{1}
ev, _ := NewSanitizedEvidence(EventKindTextDelta, "default", "rule1", "desc", fp, 1, 0, FilterOutcomeKindEvaluated, time.Now())
if count == 0 {
dir, _ := NewRecoveryDirectiveExact("req-ref-1")
intent, _ := NewRecoveryIntent(RecoveryStrategyExactReplay, dir, "rule_violation", 10)
return NewFilterDecision(FilterDecisionKindViolation, "consumer1", filterID, "rule1", ev, &intent)
}
return NewFilterDecision(FilterDecisionKindPass, "consumer1", filterID, "rule1", ev, nil)
},
}
reg, err := NewFilterRegistration(vF, "cap1", true, FilterEnforcementBlocking, 5*time.Second, 10)
if err != nil {
t.Fatalf("NewFilterRegistration: %v", err)
}
// Prepare second attempt source
txtEv2, _ := NewTextDeltaEvent("default", "recovered text", time.Now())
termEv2, _ := NewTerminalEvent("default", time.Now())
src2 := newSliceEventSource([]NormalizedEvent{txtEv2, termEv2})
ctrl2 := &fixtureController{}
disp.handler = func(ctx context.Context, request RebuiltRequest) (AttemptBinding, error) {
return NewAttemptBinding("att-2", "gpt-4", "openai", "primary", src2, ctrl2)
}
snap := createTestRuntimeSnapshot(t, []FilterRegistration{reg}, disp, rebuilder, sink)
txtEv1, _ := NewTextDeltaEvent("default", "violating text", time.Now())
termEv1, _ := NewTerminalEvent("default", time.Now())
src1 := newSliceEventSource([]NormalizedEvent{txtEv1, termEv1})
ctrl1 := &fixtureController{}
initialBinding, err := NewAttemptBinding("att-1", "gpt-4", "openai", "primary", src1, ctrl1)
if err != nil {
t.Fatalf("NewAttemptBinding: %v", err)
}
rt, err := NewRequestRuntime(snap, "group-a", initialBinding)
if err != nil {
t.Fatalf("NewRequestRuntime: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := rt.Run(ctx); err != nil {
t.Fatalf("Run: %v", err)
}
sink.mu.Lock()
defer sink.mu.Unlock()
if len(sink.terminals) != 1 {
t.Fatalf("expected exactly 1 terminal result, got %d", len(sink.terminals))
}
if !sink.terminals[0].Success() {
t.Errorf("expected successful terminal result after recovery")
}
}
func TestRequestRuntimeFailureMatrix(t *testing.T) {
t.Run("FatalViolation", func(t *testing.T) {
sink := newFixtureSink()
disp := &fixtureDispatcher{}
rebuilder := &fixtureRebuilder{}
fatalF := &customMockFilter{
id: "fatal-filter",
evaluateFn: func(ctx context.Context, fc FilterContext, batch EvidenceBatch) (FilterDecision, error) {
fp := FixedFingerprint{1}
ev, _ := NewSanitizedEvidence(EventKindTextDelta, "default", "rule1", "fatal desc", fp, 1, 0, FilterOutcomeKindEvaluated, time.Now())
return NewFilterDecision(FilterDecisionKindFatal, "consumer1", "fatal-filter", "rule1", ev, nil)
},
}
reg, err := NewFilterRegistration(fatalF, "cap1", true, FilterEnforcementBlocking, 5*time.Second, 10)
if err != nil {
t.Fatalf("NewFilterRegistration: %v", err)
}
snap := createTestRuntimeSnapshot(t, []FilterRegistration{reg}, disp, rebuilder, sink)
txtEv, _ := NewTextDeltaEvent("default", "bad text", time.Now())
src := newSliceEventSource([]NormalizedEvent{txtEv})
ctrl := &fixtureController{}
initialBinding, _ := NewAttemptBinding("att-1", "gpt-4", "openai", "primary", src, ctrl)
rt, _ := NewRequestRuntime(snap, "group-a", initialBinding)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = rt.Run(ctx)
sink.mu.Lock()
defer sink.mu.Unlock()
if len(sink.terminals) != 1 {
t.Fatalf("expected 1 terminal result for fatal violation, got %d", len(sink.terminals))
}
if sink.terminals[0].Success() {
t.Errorf("expected error terminal result for fatal violation")
}
})
t.Run("ExhaustedRecoveryBudget", func(t *testing.T) {
sink := newFixtureSink()
disp := &fixtureDispatcher{}
rebuilder := &fixtureRebuilder{}
// Always violate filter
vF := &customMockFilter{
id: "violating-filter",
evaluateFn: func(ctx context.Context, fc FilterContext, batch EvidenceBatch) (FilterDecision, error) {
fp := FixedFingerprint{1}
ev, _ := NewSanitizedEvidence(EventKindTextDelta, "default", "rule1", "desc", fp, 1, 0, FilterOutcomeKindEvaluated, time.Now())
dir, _ := NewRecoveryDirectiveExact("req-ref-1")
intent, _ := NewRecoveryIntent(RecoveryStrategyExactReplay, dir, "rule_violation", 10)
return NewFilterDecision(FilterDecisionKindViolation, "consumer1", "violating-filter", "rule1", ev, &intent)
},
}
reg, _ := NewFilterRegistration(vF, "cap1", true, FilterEnforcementBlocking, 5*time.Second, 10)
disp.handler = func(ctx context.Context, request RebuiltRequest) (AttemptBinding, error) {
txtEv, _ := NewTextDeltaEvent("default", "violating text", time.Now())
src := newSliceEventSource([]NormalizedEvent{txtEv})
ctrl := &fixtureController{}
return NewAttemptBinding("att-next", "gpt-4", "openai", "primary", src, ctrl)
}
snap := createTestRuntimeSnapshot(t, []FilterRegistration{reg}, disp, rebuilder, sink)
txtEv, _ := NewTextDeltaEvent("default", "violating text 1", time.Now())
src := newSliceEventSource([]NormalizedEvent{txtEv})
ctrl := &fixtureController{}
initialBinding, _ := NewAttemptBinding("att-1", "gpt-4", "openai", "primary", src, ctrl)
rt, _ := NewRequestRuntime(snap, "group-a", initialBinding)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_ = rt.Run(ctx)
sink.mu.Lock()
defer sink.mu.Unlock()
if len(sink.terminals) != 1 {
t.Fatalf("expected single terminal result on budget exhaustion, got %d", len(sink.terminals))
}
if sink.terminals[0].Success() {
t.Errorf("expected error terminal result on budget exhaustion")
}
})
t.Run("CallerCancellation", func(t *testing.T) {
sink := newFixtureSink()
disp := &fixtureDispatcher{}
rebuilder := &fixtureRebuilder{}
passF := &customMockFilter{id: "pass-filter"}
reg, _ := NewFilterRegistration(passF, "cap1", true, FilterEnforcementBlocking, 5*time.Second, 10)
snap := createTestRuntimeSnapshot(t, []FilterRegistration{reg}, disp, rebuilder, sink)
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
txtEv, _ := NewTextDeltaEvent("default", "text", time.Now())
src := newSliceEventSource([]NormalizedEvent{txtEv})
ctrl := &fixtureController{}
initialBinding, _ := NewAttemptBinding("att-1", "gpt-4", "openai", "primary", src, ctrl)
rt, _ := NewRequestRuntime(snap, "group-a", initialBinding)
err := rt.Run(ctx)
if !errors.Is(err, context.Canceled) {
t.Errorf("expected context.Canceled, got %v", err)
}
})
}