You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package runners_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/jobs/runners"
|
||||
)
|
||||
|
||||
type fakeReaperRepo struct {
|
||||
sessions []runners.ReaperSession
|
||||
listErr error
|
||||
|
||||
mu sync.Mutex
|
||||
marked map[uuid.UUID]string
|
||||
markErr error
|
||||
}
|
||||
|
||||
func (f *fakeReaperRepo) ListSessionsForReaper(_ context.Context, _, _ time.Time) ([]runners.ReaperSession, error) {
|
||||
if f.listErr != nil {
|
||||
return nil, f.listErr
|
||||
}
|
||||
return f.sessions, nil
|
||||
}
|
||||
|
||||
func (f *fakeReaperRepo) MarkSessionTerminatedReason(_ context.Context, id uuid.UUID, reason string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.marked == nil {
|
||||
f.marked = map[uuid.UUID]string{}
|
||||
}
|
||||
f.marked[id] = reason
|
||||
return f.markErr
|
||||
}
|
||||
|
||||
type fakeKiller struct {
|
||||
mu sync.Mutex
|
||||
killed []uuid.UUID
|
||||
}
|
||||
|
||||
func (k *fakeKiller) Kill(_ context.Context, sid uuid.UUID, _ time.Duration) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
k.killed = append(k.killed, sid)
|
||||
}
|
||||
|
||||
func TestAcpSessionReaper_KillsWallClockAndIdle(t *testing.T) {
|
||||
now := time.Now()
|
||||
wallSession := runners.ReaperSession{
|
||||
ID: uuid.New(), UserID: uuid.New(),
|
||||
StartedAt: now.Add(-5 * time.Hour),
|
||||
LastActivityAt: now.Add(-1 * time.Minute), // active, but wall-clock exceeded
|
||||
}
|
||||
idleSession := runners.ReaperSession{
|
||||
ID: uuid.New(), UserID: uuid.New(),
|
||||
StartedAt: now.Add(-10 * time.Minute),
|
||||
LastActivityAt: now.Add(-1 * time.Hour), // idle
|
||||
}
|
||||
repo := &fakeReaperRepo{sessions: []runners.ReaperSession{wallSession, idleSession}}
|
||||
killer := &fakeKiller{}
|
||||
r := runners.NewAcpSessionReaper(repo, killer, nil, runners.AcpSessionReaperConfig{
|
||||
WallClockTimeout: 4 * time.Hour,
|
||||
IdleTimeout: 30 * time.Minute,
|
||||
}, discardLogger())
|
||||
|
||||
r.Run(context.Background())
|
||||
|
||||
assert.ElementsMatch(t, []uuid.UUID{wallSession.ID, idleSession.ID}, killer.killed)
|
||||
assert.Equal(t, "reaper_wall_clock", repo.marked[wallSession.ID])
|
||||
assert.Equal(t, "reaper_idle", repo.marked[idleSession.ID])
|
||||
}
|
||||
|
||||
func TestAcpSessionReaper_PerSessionWallClock(t *testing.T) {
|
||||
now := time.Now()
|
||||
cap30s := int32(30)
|
||||
s := runners.ReaperSession{
|
||||
ID: uuid.New(), UserID: uuid.New(),
|
||||
StartedAt: now.Add(-1 * time.Minute),
|
||||
LastActivityAt: now,
|
||||
MaxWallClockSeconds: &cap30s, // exceeded per-session cap, no global cap set
|
||||
}
|
||||
repo := &fakeReaperRepo{sessions: []runners.ReaperSession{s}}
|
||||
killer := &fakeKiller{}
|
||||
r := runners.NewAcpSessionReaper(repo, killer, nil, runners.AcpSessionReaperConfig{
|
||||
IdleTimeout: time.Hour, // generous idle so only wall-clock triggers
|
||||
}, discardLogger())
|
||||
|
||||
r.Run(context.Background())
|
||||
|
||||
require.Len(t, killer.killed, 1)
|
||||
assert.Equal(t, s.ID, killer.killed[0])
|
||||
assert.Equal(t, "reaper_wall_clock", repo.marked[s.ID])
|
||||
}
|
||||
|
||||
func TestAcpSessionReaper_SkipsWithinThresholds(t *testing.T) {
|
||||
now := time.Now()
|
||||
s := runners.ReaperSession{
|
||||
ID: uuid.New(), UserID: uuid.New(),
|
||||
StartedAt: now.Add(-5 * time.Minute),
|
||||
LastActivityAt: now.Add(-1 * time.Minute),
|
||||
}
|
||||
repo := &fakeReaperRepo{sessions: []runners.ReaperSession{s}}
|
||||
killer := &fakeKiller{}
|
||||
r := runners.NewAcpSessionReaper(repo, killer, nil, runners.AcpSessionReaperConfig{
|
||||
WallClockTimeout: 4 * time.Hour,
|
||||
IdleTimeout: 30 * time.Minute,
|
||||
}, discardLogger())
|
||||
|
||||
r.Run(context.Background())
|
||||
|
||||
assert.Empty(t, killer.killed)
|
||||
assert.Empty(t, repo.marked)
|
||||
}
|
||||
|
||||
func TestAcpSessionReaper_MarkErrorStillKills(t *testing.T) {
|
||||
now := time.Now()
|
||||
s := runners.ReaperSession{
|
||||
ID: uuid.New(), UserID: uuid.New(),
|
||||
StartedAt: now.Add(-5 * time.Hour),
|
||||
LastActivityAt: now,
|
||||
}
|
||||
repo := &fakeReaperRepo{sessions: []runners.ReaperSession{s}, markErr: errors.New("db down")}
|
||||
killer := &fakeKiller{}
|
||||
r := runners.NewAcpSessionReaper(repo, killer, nil, runners.AcpSessionReaperConfig{
|
||||
WallClockTimeout: 4 * time.Hour,
|
||||
IdleTimeout: 30 * time.Minute,
|
||||
}, discardLogger())
|
||||
|
||||
r.Run(context.Background())
|
||||
|
||||
require.Len(t, killer.killed, 1)
|
||||
}
|
||||
|
||||
func TestAcpSessionReaper_ListErrorIsNotFatal(t *testing.T) {
|
||||
repo := &fakeReaperRepo{listErr: errors.New("boom")}
|
||||
killer := &fakeKiller{}
|
||||
r := runners.NewAcpSessionReaper(repo, killer, nil, runners.AcpSessionReaperConfig{
|
||||
IdleTimeout: 30 * time.Minute,
|
||||
}, discardLogger())
|
||||
|
||||
r.Run(context.Background())
|
||||
assert.Empty(t, killer.killed)
|
||||
}
|
||||
Reference in New Issue
Block a user