v0.1.90 3d7f80e

runner_test.go RawHistoryFolder

1package swarm
2
3import (
4	"strings"
5	"testing"
6)
7
8// TestSwarmAgentArgs locks in the exact flag set the subprocess
9// runner uses to start a swarm agent in daemon mode. Past
10// regressions in this area:
11//
12//   - "--no-sess" instead of "--no-session" (old print-mode runner):
13//     every spawned agent died with "unknown flag" before it could
14//     talk to the model.
15//
16//   - Forgetting --cwd: the child resolved tools against the parent
17//     zot's working directory, defeating the whole point of the
18//     worktree isolation.
19//
20//   - Forgetting --session: a daemon-mode agent without a session
21//     file would lose context between follow-up turns, making
22//     "send another message" mostly useless.
23//
24// The test asserts the load-bearing pieces are present in plausible
25// positions. If a flag is renamed, update both the runner and this
26// test so we notice immediately.
27func TestSwarmAgentArgs(t *testing.T) {
28	args := swarmAgentArgs(swarmAgentArgsOpts{
29		Exe:         "/path/to/zot",
30		Dir:         "/tmp/worktree",
31		SessionPath: "/tmp/state/session.json",
32		InboxPath:   "/tmp/state/in.sock",
33		Task:        "do the thing",
34	})
35	if len(args) < 7 {
36		t.Fatalf("argv unexpectedly short: %v", args)
37	}
38	if args[0] != "/path/to/zot" {
39		t.Fatalf("argv[0] = %q; want the binary path", args[0])
40	}
41	// The task must come last so anything that looks flag-like in
42	// the task body doesn't get interpreted as a flag.
43	if args[len(args)-1] != "do the thing" {
44		t.Fatalf("task should be last positional; got %v", args)
45	}
46
47	mustHave := map[string]string{
48		"--swarm-agent": "/tmp/state/in.sock",
49		"--session":     "/tmp/state/session.json",
50		"--cwd":         "/tmp/worktree",
51	}
52	for flag, value := range mustHave {
53		i := indexOf(args, flag)
54		if i < 0 {
55			t.Errorf("argv missing %q: %v", flag, args)
56			continue
57		}
58		if i+1 >= len(args) || args[i+1] != value {
59			t.Errorf("argv %q value = %q; want %q", flag, safeAt(args, i+1), value)
60		}
61	}
62
63	// Reject prior bad flags explicitly so a future revert is caught.
64	joined := strings.Join(args, " ")
65	for _, bad := range []string{"--print", "--no-sess ", "--no-session"} {
66		if strings.Contains(joined, bad) {
67			t.Fatalf("argv contains stale/wrong flag %q: %s", bad, joined)
68		}
69	}
70}
71
72// TestSwarmAgentArgsEmptyTaskOmitsPositional makes sure that when the
73// agent is being adopted (no fresh task) we don't pass an empty
74// positional which the arg parser would treat as a real prompt.
75func TestSwarmAgentArgsEmptyTaskOmitsPositional(t *testing.T) {
76	args := swarmAgentArgs(swarmAgentArgsOpts{
77		Exe: "/zot", Dir: "/wt", SessionPath: "/s.json", InboxPath: "/in.sock",
78	})
79	for _, a := range args {
80		if a == "" {
81			t.Fatalf("argv contains an empty positional: %v", args)
82		}
83	}
84	// last arg should be a real flag value, not a stray positional
85	if a := args[len(args)-1]; strings.HasPrefix(a, "--") {
86		t.Fatalf("argv ends on a flag with no value: %v", args)
87	}
88}
89
90// TestDefaultChildArgsSpawnIncludesTask pins the spawn shape: a
91// fresh (non-resuming) Agent produces argv that ends with the
92// original task as a positional, so the child runs it as the
93// initial user turn.
94func TestDefaultChildArgsSpawnIncludesTask(t *testing.T) {
95	a := &Agent{Dir: "/wt", Task: "do thing"}
96	args := defaultChildArgs("/zot", a, "/s.json", "/in.sock")
97	if got := args[len(args)-1]; got != "do thing" {
98		t.Fatalf("spawn argv last = %q; want %q\n%v", got, "do thing", args)
99	}
100}
101
102// TestDefaultChildArgsResumeOmitsTask is the regression for the
103// "agent busy; send 'cancel' first" error: when an Agent is being
104// resumed (Resuming==true), the child argv MUST NOT include the
105// original Task as a positional. Otherwise the child fires the task
106// as a fresh user turn on every resume, racing with whatever the
107// user types next via the inbox.
108func TestDefaultChildArgsResumeOmitsTask(t *testing.T) {
109	a := &Agent{Dir: "/wt", Task: "do thing", Resuming: true}
110	args := defaultChildArgs("/zot", a, "/s.json", "/in.sock")
111	for _, v := range args {
112		if v == "do thing" {
113			t.Fatalf("resume argv contains the task; it would re-fire as a duplicate turn\n%v", args)
114		}
115	}
116	// And no trailing positional at all: the last arg should be a
117	// flag value, not a stray empty string.
118	if got := args[len(args)-1]; got == "" {
119		t.Fatalf("resume argv ends with an empty positional: %v", args)
120	}
121	if strings.HasPrefix(args[len(args)-1], "--") {
122		t.Fatalf("resume argv ends on a bare flag (no value): %v", args)
123	}
124}
125
126func indexOf(xs []string, want string) int {
127	for i, x := range xs {
128		if x == want {
129			return i
130		}
131	}
132	return -1
133}
134
135func safeAt(xs []string, i int) string {
136	if i < 0 || i >= len(xs) {
137		return ""
138	}
139	return xs[i]
140}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close