ScuttleBot

scuttlebot / cmd / codex-relay / main_test.go
Source Blame History 312 lines
50baf1a… lmata 1 package main
50baf1a… lmata 2
50baf1a… lmata 3 import (
50baf1a… lmata 4 "bytes"
f3c383e… noreply 5 "fmt"
f3c383e… noreply 6 "os"
50baf1a… lmata 7 "path/filepath"
50baf1a… lmata 8 "strings"
50baf1a… lmata 9 "testing"
50baf1a… lmata 10 "time"
50baf1a… lmata 11 )
50baf1a… lmata 12
50baf1a… lmata 13 func TestFilterMessages(t *testing.T) {
50baf1a… lmata 14 t.Helper()
50baf1a… lmata 15
50baf1a… lmata 16 base := time.Date(2026, 3, 31, 21, 0, 0, 0, time.FixedZone("CST", -6*60*60))
50baf1a… lmata 17 since := base.Add(-time.Second)
50baf1a… lmata 18 nick := "codex-scuttlebot-1234"
50baf1a… lmata 19
50baf1a… lmata 20 messages := []message{
24a217e… lmata 21 {Nick: "bridge", Text: "[glengoolie] hello", At: base},
24a217e… lmata 22 {Nick: "glengoolie", Text: "ambient chat", At: base.Add(time.Second)},
24a217e… lmata 23 {Nick: "codex-otherrepo-9999", Text: "status post", At: base.Add(2 * time.Second)},
24a217e… lmata 24 {Nick: "glengoolie", Text: nick + ": check README.md", At: base.Add(3 * time.Second)},
24a217e… lmata 25 {Nick: "glengoolie", Text: nick + ": and inspect bridge.go", At: base.Add(4 * time.Second)},
50baf1a… lmata 26 }
50baf1a… lmata 27
cefe27d… lmata 28 got, newest := filterMessages(messages, since, nick, "worker")
50baf1a… lmata 29 if len(got) != 2 {
50baf1a… lmata 30 t.Fatalf("len(filterMessages) = %d, want 2", len(got))
50baf1a… lmata 31 }
50baf1a… lmata 32 if got[0].Text != nick+": check README.md" {
50baf1a… lmata 33 t.Fatalf("first injected message = %q", got[0].Text)
50baf1a… lmata 34 }
50baf1a… lmata 35 if got[1].Text != nick+": and inspect bridge.go" {
50baf1a… lmata 36 t.Fatalf("second injected message = %q", got[1].Text)
50baf1a… lmata 37 }
50baf1a… lmata 38 if !newest.Equal(base.Add(4 * time.Second)) {
50baf1a… lmata 39 t.Fatalf("newest = %s", newest)
50baf1a… lmata 40 }
50baf1a… lmata 41 }
50baf1a… lmata 42
50baf1a… lmata 43 func TestTargetCWD(t *testing.T) {
50baf1a… lmata 44 t.Helper()
50baf1a… lmata 45
50baf1a… lmata 46 cwd, err := filepath.Abs(".")
50baf1a… lmata 47 if err != nil {
50baf1a… lmata 48 t.Fatal(err)
50baf1a… lmata 49 }
50baf1a… lmata 50
50baf1a… lmata 51 got, err := targetCWD([]string{"--cd", "../.."})
50baf1a… lmata 52 if err != nil {
50baf1a… lmata 53 t.Fatal(err)
50baf1a… lmata 54 }
50baf1a… lmata 55 want := filepath.Clean(filepath.Join(cwd, "../.."))
50baf1a… lmata 56 if got != want {
50baf1a… lmata 57 t.Fatalf("targetCWD = %q, want %q", got, want)
50baf1a… lmata 58 }
50baf1a… lmata 59 }
50baf1a… lmata 60
50baf1a… lmata 61 func TestRelayStateShouldInterruptOnlyWhenRecentlyBusy(t *testing.T) {
50baf1a… lmata 62 t.Helper()
50baf1a… lmata 63
50baf1a… lmata 64 var state relayState
50baf1a… lmata 65 now := time.Date(2026, 3, 31, 21, 47, 0, 0, time.UTC)
50baf1a… lmata 66 state.observeOutput([]byte("Working (1s • esc to interrupt)"), now)
50baf1a… lmata 67
50baf1a… lmata 68 if !state.shouldInterrupt(now.Add(defaultBusyWindow / 2)) {
50baf1a… lmata 69 t.Fatal("shouldInterrupt = false, want true for recent busy session")
50baf1a… lmata 70 }
50baf1a… lmata 71 if state.shouldInterrupt(now.Add(defaultBusyWindow + time.Millisecond)) {
50baf1a… lmata 72 t.Fatal("shouldInterrupt = true, want false after busy window expires")
50baf1a… lmata 73 }
50baf1a… lmata 74 }
50baf1a… lmata 75
50baf1a… lmata 76 func TestInjectMessagesIdleSkipsCtrlCAndSubmits(t *testing.T) {
50baf1a… lmata 77 t.Helper()
50baf1a… lmata 78
50baf1a… lmata 79 var writer bytes.Buffer
50baf1a… lmata 80 cfg := config{
50baf1a… lmata 81 Nick: "codex-scuttlebot-1234",
50baf1a… lmata 82 InterruptOnMessage: true,
50baf1a… lmata 83 }
50baf1a… lmata 84 state := &relayState{}
50baf1a… lmata 85 batch := []message{{
50baf1a… lmata 86 Nick: "glengoolie",
50baf1a… lmata 87 Text: "codex-scuttlebot-1234: check README.md",
50baf1a… lmata 88 }}
50baf1a… lmata 89
1d3caa2… lmata 90 if err := injectMessages(&writer, cfg, state, "#general", batch); err != nil {
50baf1a… lmata 91 t.Fatal(err)
50baf1a… lmata 92 }
50baf1a… lmata 93
1d3caa2… lmata 94 want := "[IRC operator messages]\n[general] glengoolie: check README.md\n\r"
50baf1a… lmata 95 if writer.String() != want {
50baf1a… lmata 96 t.Fatalf("injectMessages idle = %q, want %q", writer.String(), want)
50baf1a… lmata 97 }
50baf1a… lmata 98 }
50baf1a… lmata 99
50baf1a… lmata 100 func TestInjectMessagesBusySendsCtrlCBeforeSubmit(t *testing.T) {
50baf1a… lmata 101 t.Helper()
50baf1a… lmata 102
50baf1a… lmata 103 var writer bytes.Buffer
50baf1a… lmata 104 cfg := config{
50baf1a… lmata 105 Nick: "codex-scuttlebot-1234",
50baf1a… lmata 106 InterruptOnMessage: true,
50baf1a… lmata 107 }
50baf1a… lmata 108 state := &relayState{}
50baf1a… lmata 109 state.observeOutput([]byte("Working (2s • esc to interrupt)"), time.Now())
50baf1a… lmata 110 batch := []message{{
50baf1a… lmata 111 Nick: "glengoolie",
50baf1a… lmata 112 Text: "codex-scuttlebot-1234: stop and re-read bridge.go",
50baf1a… lmata 113 }}
50baf1a… lmata 114
1d3caa2… lmata 115 if err := injectMessages(&writer, cfg, state, "#general", batch); err != nil {
50baf1a… lmata 116 t.Fatal(err)
50baf1a… lmata 117 }
50baf1a… lmata 118
1d3caa2… lmata 119 want := string([]byte{3}) + "[IRC operator messages]\n[general] glengoolie: stop and re-read bridge.go\n\r"
50baf1a… lmata 120 if writer.String() != want {
50baf1a… lmata 121 t.Fatalf("injectMessages busy = %q, want %q", writer.String(), want)
50baf1a… lmata 122 }
50baf1a… lmata 123 }
50baf1a… lmata 124
50baf1a… lmata 125 func TestSummarizeFunctionCallExecCommandRedactsSecrets(t *testing.T) {
50baf1a… lmata 126 t.Helper()
50baf1a… lmata 127
50baf1a… lmata 128 msg := summarizeFunctionCall("exec_command", `{"cmd":"cd /repo && curl -H \"Authorization: Bearer d2f5565f5f34fe6ea81d3cba6c20117f032180e3cf4aa401\" http://localhost:8080/v1/status"}`)
50baf1a… lmata 129 if !strings.HasPrefix(msg, "› curl") {
50baf1a… lmata 130 t.Fatalf("summarizeFunctionCall prefix = %q", msg)
50baf1a… lmata 131 }
50baf1a… lmata 132 if strings.Contains(msg, "d2f5565f5f34fe6ea81d3cba6c20117f032180e3cf4aa401") {
50baf1a… lmata 133 t.Fatalf("summarizeFunctionCall leaked token: %q", msg)
50baf1a… lmata 134 }
50baf1a… lmata 135 if !strings.Contains(msg, "[redacted]") {
50baf1a… lmata 136 t.Fatalf("summarizeFunctionCall did not redact secret: %q", msg)
50baf1a… lmata 137 }
50baf1a… lmata 138 }
50baf1a… lmata 139
50baf1a… lmata 140 func TestSummarizeCustomToolCallApplyPatch(t *testing.T) {
50baf1a… lmata 141 t.Helper()
50baf1a… lmata 142
50baf1a… lmata 143 patch := strings.Join([]string{
50baf1a… lmata 144 "*** Begin Patch",
50baf1a… lmata 145 "*** Update File: cmd/codex-relay/main.go",
50baf1a… lmata 146 "*** Add File: glengoolie.tmp",
50baf1a… lmata 147 "*** End Patch",
50baf1a… lmata 148 }, "\n")
50baf1a… lmata 149
50baf1a… lmata 150 got := summarizeCustomToolCall("apply_patch", patch)
50baf1a… lmata 151 want := "patch 2 files: cmd/codex-relay/main.go, glengoolie.tmp"
50baf1a… lmata 152 if got != want {
50baf1a… lmata 153 t.Fatalf("summarizeCustomToolCall = %q, want %q", got, want)
50baf1a… lmata 154 }
50baf1a… lmata 155 }
50baf1a… lmata 156
50baf1a… lmata 157 func TestSessionMessagesFunctionCallAndAssistant(t *testing.T) {
50baf1a… lmata 158 t.Helper()
50baf1a… lmata 159
50baf1a… lmata 160 fnLine := []byte(`{"type":"response_item","payload":{"type":"function_call","name":"exec_command","arguments":"{\"cmd\":\"pwd\"}"}}`)
67e0178… lmata 161 got := sessionMessages(fnLine, false)
f3c383e… noreply 162 if len(got) != 1 || got[0].Text != "› pwd" {
50baf1a… lmata 163 t.Fatalf("sessionMessages function_call = %#v", got)
50baf1a… lmata 164 }
50baf1a… lmata 165
50baf1a… lmata 166 msgLine := []byte(`{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"one line\nsecond line"}]}}`)
67e0178… lmata 167 got = sessionMessages(msgLine, false)
f3c383e… noreply 168 if len(got) != 2 || got[0].Text != "one line" || got[1].Text != "second line" {
50baf1a… lmata 169 t.Fatalf("sessionMessages assistant = %#v", got)
67e0178… lmata 170 }
67e0178… lmata 171 }
67e0178… lmata 172
67e0178… lmata 173 func TestSessionMessagesReasoning(t *testing.T) {
67e0178… lmata 174 line := []byte(`{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"reasoning","text":"thinking hard"},{"type":"output_text","text":"done"}]}}`)
67e0178… lmata 175
67e0178… lmata 176 // reasoning off — only output_text
67e0178… lmata 177 got := sessionMessages(line, false)
f3c383e… noreply 178 if len(got) != 1 || got[0].Text != "done" {
67e0178… lmata 179 t.Fatalf("mirrorReasoning=false: got %#v", got)
67e0178… lmata 180 }
67e0178… lmata 181
67e0178… lmata 182 // reasoning on — both, reasoning prefixed
67e0178… lmata 183 got = sessionMessages(line, true)
f3c383e… noreply 184 if len(got) != 2 || got[0].Text != "💭 thinking hard" || got[1].Text != "done" {
67e0178… lmata 185 t.Fatalf("mirrorReasoning=true: got %#v", got)
50baf1a… lmata 186 }
50baf1a… lmata 187 }
50baf1a… lmata 188
50baf1a… lmata 189 func TestExplicitThreadID(t *testing.T) {
50baf1a… lmata 190 t.Helper()
50baf1a… lmata 191
50baf1a… lmata 192 got := explicitThreadID([]string{"resume", "019d45e1-8328-7261-9a02-5c4304e07724"})
50baf1a… lmata 193 want := "019d45e1-8328-7261-9a02-5c4304e07724"
50baf1a… lmata 194 if got != want {
50baf1a… lmata 195 t.Fatalf("explicitThreadID = %q, want %q", got, want)
f3c383e… noreply 196 }
f3c383e… noreply 197 }
f3c383e… noreply 198
f3c383e… noreply 199 func writeSessionFile(t *testing.T, dir, uuid, cwd, timestamp string) string {
f3c383e… noreply 200 t.Helper()
f3c383e… noreply 201 content := fmt.Sprintf(`{"type":"session_meta","payload":{"id":"%s","timestamp":"%s","cwd":"%s"}}`, uuid, timestamp, cwd)
f3c383e… noreply 202 name := fmt.Sprintf("rollout-%s-%s.jsonl", strings.ReplaceAll(timestamp[:19], ":", "-"), uuid)
f3c383e… noreply 203 path := filepath.Join(dir, name)
f3c383e… noreply 204 if err := os.WriteFile(path, []byte(content+"\n"), 0644); err != nil {
f3c383e… noreply 205 t.Fatal(err)
f3c383e… noreply 206 }
f3c383e… noreply 207 return path
f3c383e… noreply 208 }
f3c383e… noreply 209
f3c383e… noreply 210 func TestFindLatestSessionPathSkipsPreExisting(t *testing.T) {
f3c383e… noreply 211 t.Helper()
f3c383e… noreply 212
f3c383e… noreply 213 root := t.TempDir()
f3c383e… noreply 214 dateDir := filepath.Join(root, "2026", "04", "04")
f3c383e… noreply 215 if err := os.MkdirAll(dateDir, 0755); err != nil {
f3c383e… noreply 216 t.Fatal(err)
f3c383e… noreply 217 }
f3c383e… noreply 218
f3c383e… noreply 219 cwd := "/home/user/project"
f3c383e… noreply 220
f3c383e… noreply 221 // Create a pre-existing session file.
f3c383e… noreply 222 oldPath := writeSessionFile(t, dateDir,
f3c383e… noreply 223 "aaaa-aaaa-aaaa-aaaa", cwd, "2026-04-04T10:00:00Z")
f3c383e… noreply 224
f3c383e… noreply 225 // Snapshot includes the old file.
f3c383e… noreply 226 preExisting := map[string]struct{}{oldPath: {}}
f3c383e… noreply 227
f3c383e… noreply 228 // Create a new session file (not in snapshot).
f3c383e… noreply 229 newPath := writeSessionFile(t, dateDir,
f3c383e… noreply 230 "bbbb-bbbb-bbbb-bbbb", cwd, "2026-04-04T10:00:01Z")
f3c383e… noreply 231
f3c383e… noreply 232 notBefore, _ := time.Parse(time.RFC3339, "2026-04-04T09:59:58Z")
f3c383e… noreply 233 got, err := findLatestSessionPath(root, cwd, notBefore, preExisting)
f3c383e… noreply 234 if err != nil {
f3c383e… noreply 235 t.Fatalf("findLatestSessionPath error: %v", err)
f3c383e… noreply 236 }
f3c383e… noreply 237 if got != newPath {
f3c383e… noreply 238 t.Fatalf("findLatestSessionPath = %q, want %q", got, newPath)
f3c383e… noreply 239 }
f3c383e… noreply 240 }
f3c383e… noreply 241
f3c383e… noreply 242 func TestFindLatestSessionPathPicksOldestNew(t *testing.T) {
f3c383e… noreply 243 t.Helper()
f3c383e… noreply 244
f3c383e… noreply 245 root := t.TempDir()
f3c383e… noreply 246 dateDir := filepath.Join(root, "2026", "04", "04")
f3c383e… noreply 247 if err := os.MkdirAll(dateDir, 0755); err != nil {
f3c383e… noreply 248 t.Fatal(err)
f3c383e… noreply 249 }
f3c383e… noreply 250
f3c383e… noreply 251 cwd := "/home/user/project"
f3c383e… noreply 252
f3c383e… noreply 253 // Two new sessions in the same CWD, no pre-existing.
f3c383e… noreply 254 earlyPath := writeSessionFile(t, dateDir,
f3c383e… noreply 255 "cccc-cccc-cccc-cccc", cwd, "2026-04-04T10:00:01Z")
f3c383e… noreply 256 _ = writeSessionFile(t, dateDir,
f3c383e… noreply 257 "dddd-dddd-dddd-dddd", cwd, "2026-04-04T10:00:02Z")
f3c383e… noreply 258
f3c383e… noreply 259 notBefore, _ := time.Parse(time.RFC3339, "2026-04-04T10:00:00Z")
f3c383e… noreply 260 got, err := findLatestSessionPath(root, cwd, notBefore, map[string]struct{}{})
f3c383e… noreply 261 if err != nil {
f3c383e… noreply 262 t.Fatalf("findLatestSessionPath error: %v", err)
f3c383e… noreply 263 }
f3c383e… noreply 264 if got != earlyPath {
f3c383e… noreply 265 t.Fatalf("findLatestSessionPath = %q, want oldest %q", got, earlyPath)
f3c383e… noreply 266 }
f3c383e… noreply 267 }
f3c383e… noreply 268
f3c383e… noreply 269 func TestFindLatestSessionPathNilPreExistingAllowsAll(t *testing.T) {
f3c383e… noreply 270 t.Helper()
f3c383e… noreply 271
f3c383e… noreply 272 root := t.TempDir()
f3c383e… noreply 273 dateDir := filepath.Join(root, "2026", "04", "04")
f3c383e… noreply 274 if err := os.MkdirAll(dateDir, 0755); err != nil {
f3c383e… noreply 275 t.Fatal(err)
f3c383e… noreply 276 }
f3c383e… noreply 277
f3c383e… noreply 278 cwd := "/home/user/project"
f3c383e… noreply 279
f3c383e… noreply 280 // Single file — nil preExisting (reconnect path) should find it.
f3c383e… noreply 281 path := writeSessionFile(t, dateDir,
f3c383e… noreply 282 "eeee-eeee-eeee-eeee", cwd, "2026-04-04T10:00:00Z")
f3c383e… noreply 283
f3c383e… noreply 284 got, err := findLatestSessionPath(root, cwd, time.Time{}, nil)
f3c383e… noreply 285 if err != nil {
f3c383e… noreply 286 t.Fatalf("findLatestSessionPath error: %v", err)
f3c383e… noreply 287 }
f3c383e… noreply 288 if got != path {
f3c383e… noreply 289 t.Fatalf("findLatestSessionPath = %q, want %q", got, path)
f3c383e… noreply 290 }
f3c383e… noreply 291 }
f3c383e… noreply 292
f3c383e… noreply 293 func TestSnapshotSessionFiles(t *testing.T) {
f3c383e… noreply 294 t.Helper()
f3c383e… noreply 295
f3c383e… noreply 296 root := t.TempDir()
f3c383e… noreply 297 dateDir := filepath.Join(root, "2026", "04", "04")
f3c383e… noreply 298 if err := os.MkdirAll(dateDir, 0755); err != nil {
f3c383e… noreply 299 t.Fatal(err)
f3c383e… noreply 300 }
f3c383e… noreply 301
f3c383e… noreply 302 path := writeSessionFile(t, dateDir,
f3c383e… noreply 303 "ffff-ffff-ffff-ffff", "/tmp", "2026-04-04T10:00:00Z")
f3c383e… noreply 304
f3c383e… noreply 305 snap := snapshotSessionFiles(root)
f3c383e… noreply 306 if _, ok := snap[path]; !ok {
f3c383e… noreply 307 t.Fatalf("snapshotSessionFiles missing %q", path)
f3c383e… noreply 308 }
f3c383e… noreply 309 if len(snap) != 1 {
f3c383e… noreply 310 t.Fatalf("snapshotSessionFiles len = %d, want 1", len(snap))
50baf1a… lmata 311 }
50baf1a… lmata 312 }

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button