Codex uses the osc_title_stability idle strategy, but it draws its
progress in the pane body ('Working … esc to interrupt'), not the OSC
title. The title goes stable mid-turn, so ~2s later the classifier
declared codex idle while it was still working. Add a thinking-promoter
pattern ((?i)esc to interrupt) to the codex built-in preset; classify()
checks promoter regexes against the rendered screen before the
title-stability verdict, so codex stays in thinking until the turn's
in-progress footer actually disappears.
Resolves the [CODEX IDLE] TODO item.
132 lines
3.7 KiB
Go
132 lines
3.7 KiB
Go
package preset
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadUsesBuiltInDefaultsWithoutWritingConfig(t *testing.T) {
|
|
configHome := filepath.Join(t.TempDir(), "config")
|
|
t.Setenv("XDG_CONFIG_HOME", configHome)
|
|
|
|
set, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(configHome, "patterm")); !os.IsNotExist(err) {
|
|
t.Fatalf("Load created config dir or unexpected stat error: %v", err)
|
|
}
|
|
if len(set.Agents) != 3 {
|
|
t.Fatalf("agents len = %d, want 3", len(set.Agents))
|
|
}
|
|
claude := presetByName(set.Agents, "claude")
|
|
if claude == nil {
|
|
t.Fatal("missing built-in claude preset")
|
|
}
|
|
if claude.IdleDetection == nil || len(claude.IdleDetection.PermissionPatterns) == 0 {
|
|
t.Fatalf("built-in claude missing permission patterns: %+v", claude.IdleDetection)
|
|
}
|
|
codex := presetByName(set.Agents, "codex")
|
|
if codex == nil {
|
|
t.Fatal("missing built-in codex preset")
|
|
}
|
|
if codex.IdleDetection == nil || len(codex.IdleDetection.ThinkingPatterns) == 0 {
|
|
t.Fatalf("built-in codex missing thinking patterns: %+v", codex.IdleDetection)
|
|
}
|
|
}
|
|
|
|
func TestLoadMergesUserOverlayIntoBuiltInPreset(t *testing.T) {
|
|
configHome := t.TempDir()
|
|
t.Setenv("XDG_CONFIG_HOME", configHome)
|
|
dir := filepath.Join(configHome, "patterm", "presets", "agents")
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeFile(t, filepath.Join(dir, "claude.json"), `{
|
|
"name": "claude",
|
|
"argv": ["claude", "--model", "sonnet"],
|
|
"idle_detection": { "idle_threshold_ms": 3500 }
|
|
}`)
|
|
|
|
set, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
claude := presetByName(set.Agents, "claude")
|
|
if claude == nil {
|
|
t.Fatal("missing claude preset")
|
|
}
|
|
if got := claude.Argv; len(got) != 3 || got[0] != "claude" || got[2] != "sonnet" {
|
|
t.Fatalf("argv = %#v", got)
|
|
}
|
|
if claude.IdleDetection.IdleThresholdMS != 3500 {
|
|
t.Fatalf("idle threshold = %d", claude.IdleDetection.IdleThresholdMS)
|
|
}
|
|
if len(claude.IdleDetection.PermissionPatterns) == 0 {
|
|
t.Fatalf("permission patterns were not inherited: %+v", claude.IdleDetection)
|
|
}
|
|
if claude.MCPInjection == nil || claude.MCPInjection.Kind != "flag" {
|
|
t.Fatalf("mcp injection was not inherited: %+v", claude.MCPInjection)
|
|
}
|
|
}
|
|
|
|
func TestLoadCanDisableBuiltInPreset(t *testing.T) {
|
|
configHome := t.TempDir()
|
|
t.Setenv("XDG_CONFIG_HOME", configHome)
|
|
dir := filepath.Join(configHome, "patterm", "presets", "agents")
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeFile(t, filepath.Join(dir, "opencode.json"), `{"name":"opencode","disabled":true}`)
|
|
|
|
set, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if presetByName(set.Agents, "opencode") != nil {
|
|
t.Fatal("opencode preset was not disabled")
|
|
}
|
|
if presetByName(set.Agents, "claude") == nil || presetByName(set.Agents, "codex") == nil {
|
|
t.Fatalf("other built-ins missing: %+v", set.Agents)
|
|
}
|
|
}
|
|
|
|
func TestLoadAddsCustomUserPreset(t *testing.T) {
|
|
configHome := t.TempDir()
|
|
t.Setenv("XDG_CONFIG_HOME", configHome)
|
|
dir := filepath.Join(configHome, "patterm", "presets", "processes")
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeFile(t, filepath.Join(dir, "test.json"), `{"name":"test","argv":["go","test","./..."]}`)
|
|
|
|
set, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
proc := presetByName(set.Processes, "test")
|
|
if proc == nil {
|
|
t.Fatal("missing custom process preset")
|
|
}
|
|
if proc.Kind != KindCommand {
|
|
t.Fatalf("kind = %q", proc.Kind)
|
|
}
|
|
}
|
|
|
|
func presetByName(ps []*Preset, name string) *Preset {
|
|
for _, p := range ps {
|
|
if p.Name == name {
|
|
return p
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeFile(t *testing.T, path, body string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|