test(01-02): add failing tests then implement session + state
- 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>
This commit is contained in:
182
state_test.go
Normal file
182
state_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
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:])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user