Files
codex-helper/backend/internal/app/runtime_test.go
T

161 lines
4.1 KiB
Go

package app
import (
"context"
"errors"
"net/http/httptest"
"sync"
"testing"
)
type fakeCodexClient struct {
mu sync.Mutex
connected bool
starts int
initializes int
closes int
calls int
initErrors []error
initStarted chan struct{}
initRelease chan struct{}
}
func (f *fakeCodexClient) Start(context.Context) error {
f.mu.Lock()
defer f.mu.Unlock()
f.starts++
f.connected = true
return nil
}
func (f *fakeCodexClient) Initialize(context.Context) error {
f.mu.Lock()
f.initializes++
var err error
if len(f.initErrors) > 0 {
err, f.initErrors = f.initErrors[0], f.initErrors[1:]
}
started, release := f.initStarted, f.initRelease
f.mu.Unlock()
if started != nil {
select {
case started <- struct{}{}:
default:
}
}
if release != nil {
<-release
}
return err
}
func (f *fakeCodexClient) Call(_ context.Context, method string, _ any, out any) error {
f.mu.Lock()
defer f.mu.Unlock()
f.calls++
if method == "account/login/start" {
result := out.(*map[string]any)
*result = map[string]any{"verificationUrl": "https://example.test/device", "userCode": "ABCD-EFGH"}
}
return nil
}
func (f *fakeCodexClient) Close() error {
f.mu.Lock()
defer f.mu.Unlock()
f.closes++
f.connected = false
return nil
}
func (f *fakeCodexClient) Connected() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.connected
}
func (f *fakeCodexClient) counts() (starts, initializes, closes, calls int) {
f.mu.Lock()
defer f.mu.Unlock()
return f.starts, f.initializes, f.closes, f.calls
}
func TestEnsureReadySerializesColdStart(t *testing.T) {
client := &fakeCodexClient{}
rt := &accountRuntime{client: client}
var wg sync.WaitGroup
errs := make(chan error, 8)
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
errs <- rt.ensureReady(context.Background())
}()
}
wg.Wait()
close(errs)
for err := range errs {
if err != nil {
t.Fatal(err)
}
}
starts, initializes, _, _ := client.counts()
if starts != 1 || initializes != 1 {
t.Fatalf("cold starts = %d, initializes = %d; want 1 each", starts, initializes)
}
}
func TestEnsureReadyRetriesAfterInitializeFailure(t *testing.T) {
client := &fakeCodexClient{initErrors: []error{errors.New("handshake failed")}}
rt := &accountRuntime{client: client}
if err := rt.ensureReady(context.Background()); err == nil {
t.Fatal("first initialization unexpectedly succeeded")
}
if err := rt.ensureReady(context.Background()); err != nil {
t.Fatalf("retry failed: %v", err)
}
starts, initializes, closes, _ := client.counts()
if starts != 2 || initializes != 2 || closes != 1 {
t.Fatalf("starts = %d, initializes = %d, closes = %d; want 2, 2, 1", starts, initializes, closes)
}
}
func TestStopWaitsForStartupAndPreventsRestart(t *testing.T) {
started := make(chan struct{}, 1)
release := make(chan struct{})
client := &fakeCodexClient{initStarted: started, initRelease: release}
rt := &accountRuntime{client: client}
readyDone := make(chan error, 1)
go func() { readyDone <- rt.ensureReady(context.Background()) }()
<-started
stopDone := make(chan struct{})
go func() { rt.stop(); close(stopDone) }()
close(release)
if err := <-readyDone; err != nil {
t.Fatalf("startup failed: %v", err)
}
<-stopDone
if err := rt.ensureReady(context.Background()); !errors.Is(err, errRuntimeStopped) {
t.Fatalf("restart error = %v; want stopped", err)
}
starts, initializes, closes, _ := client.counts()
if starts != 1 || initializes != 1 || closes != 1 {
t.Fatalf("starts = %d, initializes = %d, closes = %d; want 1 each", starts, initializes, closes)
}
}
func TestDeviceLoginStartsColdRuntime(t *testing.T) {
client := &fakeCodexClient{}
a := &App{runtimes: map[int64]*accountRuntime{2: {client: client}}}
recorder := httptest.NewRecorder()
request := httptest.NewRequest("POST", "/api/v1/accounts/2/login/device", nil)
a.deviceLogin(recorder, request, 2)
if recorder.Code != 200 {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
}
starts, initializes, _, calls := client.counts()
if starts != 1 || initializes != 1 || calls != 1 {
t.Fatalf("starts = %d, initializes = %d, calls = %d; want 1 each", starts, initializes, calls)
}
}