ScuttleBot

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

Keyboard Shortcuts

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