Files
kandev/apps/backend/internal/agentctl/server/process/status_tracker_test.go
T
2026-01-25 17:49:53 +00:00

269 lines
7.2 KiB
Go

package process
import (
"sync"
"testing"
"time"
"github.com/tuzig/vt10x"
)
// mockDetector is a test detector that returns configurable states
type mockDetector struct {
state AgentState
acceptTransition bool
}
func (d *mockDetector) DetectState(lines []string, glyphs [][]vt10x.Glyph) AgentState {
return d.state
}
func (d *mockDetector) ShouldAcceptStateChange(currentState, newState AgentState) bool {
return d.acceptTransition
}
func TestStatusTracker_Write(t *testing.T) {
log := newTestLogger(t)
detector := &mockDetector{state: StateUnknown, acceptTransition: true}
config := DefaultStatusTrackerConfig()
tracker := NewStatusTracker("test-session", detector, nil, config, log)
// Write some terminal data
tracker.Write([]byte("Hello, World!"))
// The data should be fed to the terminal - we can't easily verify this
// without exposing internal state, but at least verify no panics
}
func TestStatusTracker_Resize(t *testing.T) {
log := newTestLogger(t)
detector := &mockDetector{state: StateUnknown, acceptTransition: true}
config := StatusTrackerConfig{
Rows: 24,
Cols: 80,
CheckInterval: 100 * time.Millisecond,
}
tracker := NewStatusTracker("test-session", detector, nil, config, log)
// Resize the terminal
tracker.Resize(120, 40)
// Verify config was updated
tracker.mu.Lock()
cols := tracker.config.Cols
rows := tracker.config.Rows
tracker.mu.Unlock()
if cols != 120 {
t.Errorf("Resize() cols = %d, want 120", cols)
}
if rows != 40 {
t.Errorf("Resize() rows = %d, want 40", rows)
}
}
func TestStatusTracker_ShouldCheck(t *testing.T) {
log := newTestLogger(t)
detector := &mockDetector{state: StateUnknown, acceptTransition: true}
config := StatusTrackerConfig{
Rows: 24,
Cols: 80,
CheckInterval: 50 * time.Millisecond,
}
tracker := NewStatusTracker("test-session", detector, nil, config, log)
// Should check immediately after creation
if !tracker.ShouldCheck() {
t.Error("ShouldCheck() should return true initially")
}
// After checking, should not check again immediately
tracker.CheckAndUpdate()
if tracker.ShouldCheck() {
t.Error("ShouldCheck() should return false right after check")
}
// After waiting, should check again
time.Sleep(60 * time.Millisecond)
if !tracker.ShouldCheck() {
t.Error("ShouldCheck() should return true after check interval")
}
}
func TestStatusTracker_CheckAndUpdate_StateChange(t *testing.T) {
log := newTestLogger(t)
detector := &mockDetector{state: StateWorking, acceptTransition: true}
var callbackCalled bool
var callbackState AgentState
var callbackMu sync.Mutex
callback := func(sessionID string, state AgentState) {
callbackMu.Lock()
callbackCalled = true
callbackState = state
callbackMu.Unlock()
}
config := DefaultStatusTrackerConfig()
tracker := NewStatusTracker("test-session", detector, callback, config, log)
// First check should emit the initial state change
state := tracker.CheckAndUpdate()
if state != StateWorking {
t.Errorf("CheckAndUpdate() = %v, want %v", state, StateWorking)
}
callbackMu.Lock()
if !callbackCalled {
t.Error("Callback should have been called on state change")
}
if callbackState != StateWorking {
t.Errorf("Callback received state %v, want %v", callbackState, StateWorking)
}
callbackMu.Unlock()
}
func TestStatusTracker_CheckAndUpdate_NoChangeNoCallback(t *testing.T) {
log := newTestLogger(t)
detector := &mockDetector{state: StateWorking, acceptTransition: true}
callCount := 0
callback := func(sessionID string, state AgentState) {
callCount++
}
config := DefaultStatusTrackerConfig()
tracker := NewStatusTracker("test-session", detector, callback, config, log)
// First check triggers callback
tracker.CheckAndUpdate()
// Second check with same state should not trigger callback
tracker.CheckAndUpdate()
if callCount != 1 {
t.Errorf("Callback called %d times, want 1", callCount)
}
}
func TestStatusTracker_CheckAndUpdate_TransitionRejected(t *testing.T) {
log := newTestLogger(t)
detector := &mockDetector{state: StateWorking, acceptTransition: false}
callCount := 0
callback := func(sessionID string, state AgentState) {
callCount++
}
config := DefaultStatusTrackerConfig()
tracker := NewStatusTracker("test-session", detector, callback, config, log)
// Check - detector rejects the transition
state := tracker.CheckAndUpdate()
// State should remain unknown since transition was rejected
if state != StateUnknown {
t.Errorf("CheckAndUpdate() = %v, want %v (transition rejected)", state, StateUnknown)
}
if callCount != 0 {
t.Errorf("Callback called %d times, want 0 (transition rejected)", callCount)
}
}
func TestStatusTracker_StabilityWindow(t *testing.T) {
log := newTestLogger(t)
detector := &mockDetector{state: StateWorking, acceptTransition: true}
var lastState AgentState
callback := func(sessionID string, state AgentState) {
lastState = state
}
config := StatusTrackerConfig{
Rows: 24,
Cols: 80,
CheckInterval: 10 * time.Millisecond,
StabilityWindow: 50 * time.Millisecond,
}
tracker := NewStatusTracker("test-session", detector, callback, config, log)
// First check - state becomes pending
tracker.CheckAndUpdate()
// State should still be unknown (waiting for stability)
if lastState == StateWorking {
t.Error("State should not change immediately with stability window")
}
// Wait for stability window
time.Sleep(60 * time.Millisecond)
// Now check again - state should be stable
tracker.CheckAndUpdate()
if lastState != StateWorking {
t.Errorf("State should be %v after stability window, got %v", StateWorking, lastState)
}
}
func TestStatusTracker_CurrentState(t *testing.T) {
log := newTestLogger(t)
detector := &mockDetector{state: StateWaitingInput, acceptTransition: true}
config := DefaultStatusTrackerConfig()
tracker := NewStatusTracker("test-session", detector, nil, config, log)
// Initial state
if tracker.CurrentState() != StateUnknown {
t.Errorf("CurrentState() = %v, want %v", tracker.CurrentState(), StateUnknown)
}
// After check
tracker.CheckAndUpdate()
if tracker.CurrentState() != StateWaitingInput {
t.Errorf("CurrentState() = %v, want %v", tracker.CurrentState(), StateWaitingInput)
}
}
func TestDefaultStatusTrackerConfig(t *testing.T) {
config := DefaultStatusTrackerConfig()
if config.Rows != 24 {
t.Errorf("DefaultStatusTrackerConfig().Rows = %d, want 24", config.Rows)
}
if config.Cols != 80 {
t.Errorf("DefaultStatusTrackerConfig().Cols = %d, want 80", config.Cols)
}
if config.CheckInterval != 100*time.Millisecond {
t.Errorf("DefaultStatusTrackerConfig().CheckInterval = %v, want 100ms", config.CheckInterval)
}
if config.StabilityWindow != 0 {
t.Errorf("DefaultStatusTrackerConfig().StabilityWindow = %v, want 0", config.StabilityWindow)
}
}
func TestNoOpDetector(t *testing.T) {
detector := &NoOpDetector{}
// Always returns unknown
state := detector.DetectState(nil, nil)
if state != StateUnknown {
t.Errorf("NoOpDetector.DetectState() = %v, want %v", state, StateUnknown)
}
// Always accepts transitions
if !detector.ShouldAcceptStateChange(StateWorking, StateUnknown) {
t.Error("NoOpDetector.ShouldAcceptStateChange() should always return true")
}
}