When Claude requests a tool permission approval, the last JSONL entry is type=assistant with stop_reason=tool_use. Previously this was always classified as Working. Now, if the tool_use entry is older than 10s with no new activity, it's classified as NeedsInput. Also fix vmux label to accept fuzzy match queries (not just session UUIDs).
204 lines
4.7 KiB
Go
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"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
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:])
|
|
}
|
|
}
|