v0.3.82 2e387ff

lifecycle_test.go RawHistoryFolder

1package swarm
2
3import (
4	"context"
5	"errors"
6	"testing"
7	"time"
8)
9
10func nextSwarmLifecycle(t *testing.T, events <-chan LifecycleEvent) LifecycleEvent {
11	t.Helper()
12	select {
13	case e := <-events:
14		return e
15	case <-time.After(2 * time.Second):
16		t.Fatal("missing lifecycle event")
17		return LifecycleEvent{}
18	}
19}
20
21func TestSubagentLifecycle(t *testing.T) {
22	for _, tc := range []struct {
23		status string
24		err    error
25	}{
26		{"completed", nil}, {"failed", errors.New("failed")}, {"cancelled", context.Canceled}, {"timed_out", context.DeadlineExceeded},
27	} {
28		t.Run(tc.status, func(t *testing.T) {
29			root := t.TempDir()
30			events := make(chan LifecycleEvent, 2)
31			f := New(Config{Root: root, RepoRoot: root, OnLifecycle: func(e LifecycleEvent) { events <- e }, NewRunner: func(*Agent) Runner {
32				return RunnerFunc(func(context.Context, Sink) error { return tc.err })
33			}})
34			f.SetActiveSession("parent")
35			a, err := f.Spawn(context.Background(), "review")
36			if err != nil {
37				t.Fatal(err)
38			}
39			done := make(chan struct{})
40			go func() { a.Wait(); close(done) }()
41			select {
42			case <-done:
43			case <-time.After(2 * time.Second):
44				t.Fatal("runner did not finish")
45			}
46			first, last := nextSwarmLifecycle(t, events), nextSwarmLifecycle(t, events)
47			if first.Event != "subagent_start" || last.Event != "subagent_stop" || last.Status != tc.status || first.AgentRunID == "" || first.AgentRunID != last.AgentRunID {
48				t.Fatalf("events: %+v %+v", first, last)
49			}
50			for _, e := range []LifecycleEvent{first, last} {
51				if e.SessionID != "parent" || e.AgentID != a.ID || e.CWD != root {
52					t.Fatalf("identity: %+v", e)
53				}
54			}
55			if err := f.Stop(a.ID); err != nil {
56				t.Fatal(err)
57			}
58			select {
59			case e := <-events:
60				t.Fatalf("duplicate event: %+v", e)
61			default:
62			}
63		})
64	}
65}
66
67func TestShutdownWaitsForSubagentStop(t *testing.T) {
68	root := t.TempDir()
69	events := make(chan LifecycleEvent, 2)
70	started := make(chan struct{})
71	f := New(Config{Root: root, RepoRoot: root, OnLifecycle: func(e LifecycleEvent) { events <- e }, NewRunner: func(*Agent) Runner {
72		return RunnerFunc(func(ctx context.Context, _ Sink) error { close(started); <-ctx.Done(); return ctx.Err() })
73	}})
74	if _, err := f.Spawn(context.Background(), "wait"); err != nil {
75		t.Fatal(err)
76	}
77	select {
78	case <-started:
79	case <-time.After(time.Second):
80		t.Fatal("runner not started")
81	}
82	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
83	defer cancel()
84	f.Shutdown(ctx)
85	if ctx.Err() != nil {
86		t.Fatal("shutdown did not finish")
87	}
88	if nextSwarmLifecycle(t, events).Event != "subagent_start" {
89		t.Fatal("missing start")
90	}
91	if e := nextSwarmLifecycle(t, events); e.Event != "subagent_stop" || e.Status != "cancelled" {
92		t.Fatalf("stop: %+v", e)
93	}
94}
95
96func TestResumedSubagentHasNewLifecycleRun(t *testing.T) {
97	root := t.TempDir()
98	events := make(chan LifecycleEvent, 4)
99	f := New(Config{Root: root, RepoRoot: root, OnLifecycle: func(e LifecycleEvent) { events <- e }, NewRunner: func(*Agent) Runner {
100		return RunnerFunc(func(context.Context, Sink) error { return nil })
101	}})
102	f.SetActiveSession("original")
103	a, err := f.Spawn(context.Background(), "review")
104	if err != nil {
105		t.Fatal(err)
106	}
107	first := nextSwarmLifecycle(t, events)
108	nextSwarmLifecycle(t, events)
109	a.Wait()
110	f.SetActiveSession("other")
111	resumed, err := f.Resume(context.Background(), a.ID)
112	if err != nil {
113		t.Fatal(err)
114	}
115	start, stop := nextSwarmLifecycle(t, events), nextSwarmLifecycle(t, events)
116	resumed.Wait()
117	if start.Event != "subagent_start" || stop.Event != "subagent_stop" || start.AgentRunID == first.AgentRunID || start.AgentRunID != stop.AgentRunID || start.SessionID != "original" || stop.SessionID != "original" {
118		t.Fatalf("resume: %+v %+v", start, stop)
119	}
120}

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