Files
vmux/state_test.go
Pierre Martin bab681ca30 feat: stabilize list output with fixed-height preview (5 lines, 300 chars)
Each session always occupies the same number of lines, preventing
visual jitter between watch refreshes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 20:50:28 +01:00

204 lines
4.7 KiB
Go

package main
import (
"testing"
"time"
)
var testNow = time.Date(2026, 3, 23, 12, 0, 30, 0, time.UTC)
func TestDetectState_EndTurnText(t *testing.T) {
msgs := []JSONLMessage{{
Type: "assistant",
Timestamp: "2026-03-23T12:00:00Z",
Message: &MessagePayload{
Role: "assistant",
Content: []ContentBlock{{Type: "text", Text: "Done."}},
StopReason: "end_turn",
},
}}
state := DetectState(msgs, testNow)
if state != NeedsInput {
t.Errorf("expected NeedsInput, got %v", state)
}
}
func TestDetectState_ToolUseAskUser(t *testing.T) {
msgs := []JSONLMessage{{
Type: "assistant",
Timestamp: "2026-03-23T12:00:00Z",
Message: &MessagePayload{
Role: "assistant",
Content: []ContentBlock{
{Type: "text", Text: "Let me ask..."},
{Type: "tool_use", Name: "AskUserQuestion"},
},
StopReason: "tool_use",
},
}}
state := DetectState(msgs, testNow)
if state != NeedsInput {
t.Errorf("expected NeedsInput, got %v", state)
}
}
func TestDetectState_ToolUseOther(t *testing.T) {
// Recent tool_use (< PermissionStallThreshold) → Working
msgs := []JSONLMessage{{
Type: "assistant",
Timestamp: "2026-03-23T12:00:25Z", // 5s before testNow
Message: &MessagePayload{
Role: "assistant",
Content: []ContentBlock{
{Type: "tool_use", Name: "Read"},
},
StopReason: "tool_use",
},
}}
state := DetectState(msgs, testNow)
if state != Working {
t.Errorf("expected Working, got %v", state)
}
}
func TestDetectState_ToolUseStale(t *testing.T) {
// tool_use older than PermissionStallThreshold → permission prompt
msgs := []JSONLMessage{{
Type: "assistant",
Timestamp: "2026-03-23T12:00:10Z", // 20s before testNow
Message: &MessagePayload{
Role: "assistant",
Content: []ContentBlock{
{Type: "tool_use", Name: "Bash"},
},
StopReason: "tool_use",
},
}}
state := DetectState(msgs, testNow)
if state != NeedsInput {
t.Errorf("expected NeedsInput for stale tool_use, got %v", state)
}
}
func TestDetectState_Progress(t *testing.T) {
msgs := []JSONLMessage{{
Type: "progress",
Timestamp: "2026-03-23T12:00:00Z",
Data: &ProgressData{Type: "agent_progress"},
}}
state := DetectState(msgs, testNow)
if state != Working {
t.Errorf("expected Working, got %v", state)
}
}
func TestDetectState_ToolResult(t *testing.T) {
msgs := []JSONLMessage{{
Type: "user",
Timestamp: "2026-03-23T12:00:00Z",
Message: &MessagePayload{
Role: "user",
Content: []ContentBlock{{Type: "tool_result"}},
},
}}
state := DetectState(msgs, testNow)
if state != Working {
t.Errorf("expected Working, got %v", state)
}
}
func TestDetectState_IdleThreshold(t *testing.T) {
// Message timestamp > 60s in the past
msgs := []JSONLMessage{{
Type: "assistant",
Timestamp: "2026-03-23T11:58:00Z", // 2min30s before testNow
Message: &MessagePayload{
Role: "assistant",
Content: []ContentBlock{{Type: "text", Text: "Done."}},
StopReason: "end_turn",
},
}}
state := DetectState(msgs, testNow)
if state != Idle {
t.Errorf("expected Idle, got %v", state)
}
}
func TestDetectState_EmptyMessages(t *testing.T) {
state := DetectState(nil, testNow)
if state != Unknown {
t.Errorf("expected Unknown, got %v", state)
}
}
func TestExtractPreview(t *testing.T) {
msgs := []JSONLMessage{
{
Type: "user",
Message: &MessagePayload{
Role: "user",
Content: []ContentBlock{{Type: "text", Text: "do something"}},
},
},
{
Type: "assistant",
Message: &MessagePayload{
Role: "assistant",
Content: []ContentBlock{{Type: "text", Text: "Voici le resultat\nLigne 2\nLigne 3\nLigne 4\nLigne 5\nLigne 6\nLigne 7"}},
},
},
}
preview := ExtractPreview(msgs)
want := "Voici le resultat\nLigne 2\nLigne 3\nLigne 4\nLigne 5"
if preview != want {
t.Errorf("ExtractPreview = %q, want %q", preview, want)
}
}
func TestExtractPreview_NoAssistant(t *testing.T) {
msgs := []JSONLMessage{{
Type: "user",
Message: &MessagePayload{
Role: "user",
Content: []ContentBlock{{Type: "text", Text: "hello"}},
},
}}
preview := ExtractPreview(msgs)
if preview != "" {
t.Errorf("expected empty preview, got %q", preview)
}
}
func TestExtractPreview_LongText(t *testing.T) {
// Text longer than 300 chars should be truncated
longText := ""
for i := 0; i < 100; i++ {
longText += "abcde "
}
msgs := []JSONLMessage{{
Type: "assistant",
Message: &MessagePayload{
Role: "assistant",
Content: []ContentBlock{{Type: "text", Text: longText}},
},
}}
preview := ExtractPreview(msgs)
if len(preview) > 303 { // 300 + "..."
t.Errorf("preview too long: %d chars", len(preview))
}
if preview[len(preview)-3:] != "..." {
t.Errorf("expected truncation marker '...', got %q", preview[len(preview)-3:])
}
}