118 lines
4.0 KiB
Go
118 lines
4.0 KiB
Go
package app
|
|
|
|
import (
|
|
"regexp"
|
|
"testing"
|
|
)
|
|
|
|
func mustCompile(t *testing.T, p string) *regexp.Regexp {
|
|
t.Helper()
|
|
re, err := regexp.Compile(p)
|
|
if err != nil {
|
|
t.Fatalf("regex %q: %v", p, err)
|
|
}
|
|
return re
|
|
}
|
|
|
|
func TestClassifyOutputActivity(t *testing.T) {
|
|
cfg := &resolvedIdleDetection{strategy: StrategyOutputActivity, idleThresholdMS: 2000}
|
|
|
|
cases := []struct {
|
|
name string
|
|
idleMS int64
|
|
want IdleState
|
|
}{
|
|
{"fresh-spawn no writes", 0, StateWorking},
|
|
{"recent activity", 500, StateWorking},
|
|
{"under threshold", 1999, StateWorking},
|
|
{"at threshold", 2000, StateIdle},
|
|
{"over threshold", 5000, StateIdle},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, _ := classify(cfg, false, false, tc.idleMS, 0, "", nil, nil)
|
|
if got != tc.want {
|
|
t.Fatalf("got %q want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyTitleStability(t *testing.T) {
|
|
cfg := &resolvedIdleDetection{strategy: StrategyOSCTitleStability, idleThresholdMS: 2000}
|
|
// Title change recent → working.
|
|
if got, _ := classify(cfg, false, false, 9999, 500, "step 3", nil, nil); got != StateWorking {
|
|
t.Fatalf("recent title change: got %q", got)
|
|
}
|
|
// Title stable past threshold → idle.
|
|
if got, _ := classify(cfg, false, false, 9999, 5000, "step 3", nil, nil); got != StateIdle {
|
|
t.Fatalf("stable title: got %q", got)
|
|
}
|
|
// No title yet: fall back to output activity.
|
|
if got, _ := classify(cfg, false, false, 100, 0, "", nil, nil); got != StateWorking {
|
|
t.Fatalf("no title yet, recent output: got %q", got)
|
|
}
|
|
if got, _ := classify(cfg, false, false, 5000, 0, "", nil, nil); got != StateIdle {
|
|
t.Fatalf("no title yet, output idle: got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestClassifyTitleStatus(t *testing.T) {
|
|
cfg := &resolvedIdleDetection{
|
|
strategy: StrategyOSCTitleStatus,
|
|
idleThresholdMS: 2000,
|
|
titleStatusMap: map[string]IdleState{
|
|
"thinking": StateThinking,
|
|
"permission": StatePermission,
|
|
"error": StateError,
|
|
},
|
|
}
|
|
if got, _ := classify(cfg, false, false, 9999, 500, "Thinking…", nil, nil); got != StateThinking {
|
|
t.Fatalf("thinking title: got %q", got)
|
|
}
|
|
if got, _ := classify(cfg, false, false, 9999, 500, "Waiting for permission", nil, nil); got != StatePermission {
|
|
t.Fatalf("permission title: got %q", got)
|
|
}
|
|
// No match in map → fall back to stability.
|
|
if got, _ := classify(cfg, false, false, 9999, 5000, "ready", nil, nil); got != StateIdle {
|
|
t.Fatalf("unmatched title, stable: got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestClassifyPromoterRegex(t *testing.T) {
|
|
cfg := &resolvedIdleDetection{
|
|
strategy: StrategyOutputActivity,
|
|
idleThresholdMS: 2000,
|
|
permissionRegexes: []*regexp.Regexp{mustCompile(t, `Approve\?`)},
|
|
errorRegexes: []*regexp.Regexp{mustCompile(t, `panic:`)},
|
|
thinkingRegexes: []*regexp.Regexp{mustCompile(t, `Thinking`)},
|
|
}
|
|
// Permission promoter beats idle.
|
|
if got, _ := classify(cfg, false, false, 5000, 0, "", []byte("Approve? [y/n]"), nil); got != StatePermission {
|
|
t.Fatalf("permission promoter: got %q", got)
|
|
}
|
|
// Error trumps permission.
|
|
if got, _ := classify(cfg, false, false, 5000, 0, "", []byte("panic: bad\nApprove?"), nil); got != StateError {
|
|
t.Fatalf("error promoter beats permission: got %q", got)
|
|
}
|
|
// Thinking promoter on idle output.
|
|
if got, _ := classify(cfg, false, false, 5000, 0, "", []byte("Thinking…"), nil); got != StateThinking {
|
|
t.Fatalf("thinking promoter: got %q", got)
|
|
}
|
|
// Rendered-screen prompts still promote even when the raw tail no
|
|
// longer contains the original prompt bytes.
|
|
if got, _ := classify(cfg, false, false, 100, 0, "", []byte("Calling patterm..."), []byte("Approve? [y/n]")); got != StatePermission {
|
|
t.Fatalf("screen permission promoter: got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestClassifyExitTerminal(t *testing.T) {
|
|
cfg := &resolvedIdleDetection{strategy: StrategyOutputActivity, idleThresholdMS: 2000}
|
|
if got, _ := classify(cfg, true, true, 0, 0, "", nil, nil); got != StateError {
|
|
t.Fatalf("non-zero exit: got %q", got)
|
|
}
|
|
if got, _ := classify(cfg, true, false, 0, 0, "", nil, nil); got != StateIdle {
|
|
t.Fatalf("clean exit: got %q", got)
|
|
}
|
|
}
|