190 lines
5.5 KiB
Go
190 lines
5.5 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestRelayManager_DefaultTimeout(t *testing.T) {
|
|
rm := NewRelayManager("")
|
|
|
|
if rm.timeout != relayTimeout {
|
|
t.Fatalf("rm.timeout = %v, want %v", rm.timeout, relayTimeout)
|
|
}
|
|
}
|
|
|
|
func TestRelayManager_RelayContextHonorsConfiguredTimeout(t *testing.T) {
|
|
rm := NewRelayManager("")
|
|
rm.SetTimeout(3 * time.Second)
|
|
|
|
ctx, cancel := rm.relayContext(context.Background())
|
|
defer cancel()
|
|
|
|
deadline, ok := ctx.Deadline()
|
|
if !ok {
|
|
t.Fatal("expected relay context deadline")
|
|
}
|
|
if remaining := time.Until(deadline); remaining <= 0 || remaining > 3*time.Second {
|
|
t.Fatalf("time until deadline = %v, want within (0, 3s]", remaining)
|
|
}
|
|
}
|
|
|
|
func TestRelayManager_RelayContextDisablesTimeoutAtZero(t *testing.T) {
|
|
rm := NewRelayManager("")
|
|
rm.SetTimeout(0)
|
|
|
|
baseCtx := context.Background()
|
|
ctx, cancel := rm.relayContext(baseCtx)
|
|
defer cancel()
|
|
|
|
if ctx != baseCtx {
|
|
t.Fatal("expected relayContext to return the original context when timeout is disabled")
|
|
}
|
|
if _, ok := ctx.Deadline(); ok {
|
|
t.Fatal("expected no deadline when timeout is disabled")
|
|
}
|
|
}
|
|
|
|
func TestHandleRelay_ReturnsPartialOnTimeout(t *testing.T) {
|
|
e := newTestEngine()
|
|
session := newControllableSession("relay-session")
|
|
e.agent = &controllableAgent{nextSession: session}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
|
|
type relayResult struct {
|
|
resp string
|
|
err error
|
|
}
|
|
done := make(chan relayResult, 1)
|
|
go func() {
|
|
resp, err := e.HandleRelay(ctx, "source", "chat-1", "hello")
|
|
done <- relayResult{resp: resp, err: err}
|
|
}()
|
|
|
|
session.events <- Event{Type: EventText, Content: "partial response", SessionID: "relay-session"}
|
|
time.Sleep(40 * time.Millisecond)
|
|
// After timeout, HandleRelay consumes the next event from the channel to
|
|
// unblock the for-range loop, then checks ctx.Err() and spawns the drain
|
|
// goroutine. We need two events: one to unblock HandleRelay, and one
|
|
// EventResult for the drain goroutine to close the session cleanly.
|
|
session.events <- Event{Type: EventThinking, Content: "still working"}
|
|
session.events <- Event{Type: EventResult, Content: "done"}
|
|
|
|
got := <-done
|
|
if got.err != nil {
|
|
t.Fatalf("HandleRelay() error = %v, want nil", got.err)
|
|
}
|
|
if got.resp != "partial response" {
|
|
t.Fatalf("HandleRelay() response = %q, want %q", got.resp, "partial response")
|
|
}
|
|
|
|
// Wait for the background drain goroutine to close the session.
|
|
select {
|
|
case <-session.closed:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("background drain goroutine did not close the session")
|
|
}
|
|
}
|
|
|
|
func TestHandleRelay_TimeoutWithoutTextReturnsContextError(t *testing.T) {
|
|
e := newTestEngine()
|
|
session := newControllableSession("relay-session")
|
|
e.agent = &controllableAgent{nextSession: session}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
|
|
type relayResult struct {
|
|
resp string
|
|
err error
|
|
}
|
|
done := make(chan relayResult, 1)
|
|
go func() {
|
|
resp, err := e.HandleRelay(ctx, "source", "chat-1", "hello")
|
|
done <- relayResult{resp: resp, err: err}
|
|
}()
|
|
|
|
time.Sleep(40 * time.Millisecond)
|
|
// One event to unblock HandleRelay's for-range, one for the drain goroutine.
|
|
session.events <- Event{Type: EventThinking, Content: "still working"}
|
|
session.events <- Event{Type: EventResult, Content: "done"}
|
|
|
|
got := <-done
|
|
if got.resp != "" {
|
|
t.Fatalf("HandleRelay() response = %q, want empty", got.resp)
|
|
}
|
|
if !errors.Is(got.err, context.DeadlineExceeded) {
|
|
t.Fatalf("HandleRelay() error = %v, want context deadline exceeded", got.err)
|
|
}
|
|
|
|
select {
|
|
case <-session.closed:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("background drain goroutine did not close the session")
|
|
}
|
|
}
|
|
|
|
// relayFallbackAgent fails the first StartSession call (simulating a corrupt
|
|
// resume) and returns freshSession on the second call (fresh start).
|
|
type relayFallbackAgent struct {
|
|
callCount int
|
|
freshSession AgentSession
|
|
}
|
|
|
|
func (a *relayFallbackAgent) Name() string { return "fallback" }
|
|
func (a *relayFallbackAgent) StartSession(_ context.Context, sessionID string) (AgentSession, error) {
|
|
a.callCount++
|
|
if a.callCount == 1 && sessionID != "" {
|
|
return nil, fmt.Errorf("simulated resume failure")
|
|
}
|
|
return a.freshSession, nil
|
|
}
|
|
func (a *relayFallbackAgent) ListSessions(_ context.Context) ([]AgentSessionInfo, error) {
|
|
return nil, nil
|
|
}
|
|
func (a *relayFallbackAgent) Stop() error { return nil }
|
|
|
|
func TestHandleRelay_ResumeFailureFallsBackToFreshSession(t *testing.T) {
|
|
e := newTestEngine()
|
|
freshSession := newControllableSession("fresh-session")
|
|
|
|
e.agent = &relayFallbackAgent{freshSession: freshSession}
|
|
|
|
// Pre-set a stale session ID so that the first StartSession tries to resume.
|
|
relaySessionKey := "relay:source:chat-1"
|
|
sess := e.sessions.GetOrCreateActive(relaySessionKey)
|
|
sess.SetAgentSessionID("stale-id", "fallback")
|
|
e.sessions.Save()
|
|
|
|
ctx := context.Background()
|
|
done := make(chan string, 1)
|
|
go func() {
|
|
resp, err := e.HandleRelay(ctx, "source", "chat-1", "hello")
|
|
if err != nil {
|
|
done <- "error: " + err.Error()
|
|
return
|
|
}
|
|
done <- resp
|
|
}()
|
|
|
|
// The fresh session should receive the message and respond.
|
|
freshSession.events <- Event{Type: EventResult, Content: "recovered", SessionID: "fresh-session"}
|
|
|
|
got := <-done
|
|
if got != "recovered" {
|
|
t.Fatalf("HandleRelay() = %q, want %q", got, "recovered")
|
|
}
|
|
|
|
// Session should be closed after EventResult.
|
|
select {
|
|
case <-freshSession.closed:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("session was not closed after EventResult")
|
|
}
|
|
}
|