- TailReadJSONL: reverse-read JSONL without loading entire file - FindSessionForProcess: match PID to most recent JSONL via EncodePath - DetectState: heuristic based on last message stop_reason + tool name - ExtractPreview: extract first 3 lines of last assistant text - All 17 tests pass (session_test.go + state_test.go) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
183 lines
4.1 KiB
Go
183 lines
4.1 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) {
|
|
msgs := []JSONLMessage{{
|
|
Type: "assistant",
|
|
Timestamp: "2026-03-23T12:00:00Z",
|
|
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_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"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
preview := ExtractPreview(msgs)
|
|
want := "Voici le resultat\nLigne 2\nLigne 3"
|
|
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 200 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) > 203 { // 200 + "..."
|
|
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:])
|
|
}
|
|
}
|