relay-flow 0.2.3-alpha → 0.2.4-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -10
- package/cmd/relay-flow/main.go +2 -0
- package/cmd/relay-flow/pi_wiring_test.go +64 -0
- package/cmd/relay-flow/serve.go +2 -0
- package/examples/beads-workflow.yaml +3 -3
- package/examples/config-reference.yaml +144 -0
- package/examples/minimal-beads-task-workflow.yaml +34 -0
- package/examples/minimal-jira-task-workflow.yaml +68 -0
- package/examples/workflow-reference.yaml +111 -0
- package/internal/harness/opencode/opencode_test.go +1 -1
- package/internal/harness/opencode/repo_setup.go +1 -1
- package/internal/harness/pi/config_test.go +46 -0
- package/internal/harness/pi/lifecycle_test.go +69 -0
- package/internal/harness/pi/pi.go +261 -0
- package/internal/harness/pi/pi_test.go +322 -0
- package/internal/harness/pi/prompt_test.go +121 -0
- package/internal/harness/pi/testdata/pi-0.84.1/capture.json +126 -0
- package/internal/harness/pi/testdata/pi-0.84.1/noninteractive-output.txt +11 -0
- package/internal/harness/pi/testdata/pi-0.84.1/tui-output-sanitized.txt +17 -0
- package/internal/harness/pi/validation_test.go +168 -0
- package/internal/runner/herdr/baseref.go +60 -0
- package/internal/runner/herdr/herdr.go +578 -0
- package/internal/runner/herdr/herdr_test.go +688 -0
- package/internal/runner/herdr/herdrcli/contract.go +128 -0
- package/internal/runner/herdr/herdrcli/exec.go +68 -0
- package/internal/runner/herdr/herdrcli/herdrcli_test.go +274 -0
- package/internal/runner/herdr/herdrcli/live_test.go +132 -0
- package/internal/runner/herdr/herdrcli/operations.go +281 -0
- package/internal/runner/herdr/herdrcli/response.go +116 -0
- package/internal/runner/herdr/herdrcli/testdata/empty-panes.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/empty-tabs.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/error-not-git-worktree.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/error-pane-not-found.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/error-workspace-not-found.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/error-worktree-not-found.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/malformed.json +1 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-close.json +6 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-get.json +25 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-list.json +45 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-process-info-shell.json +22 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-process-info.json +23 -0
- package/internal/runner/herdr/herdrcli/testdata/pane-rename.json +23 -0
- package/internal/runner/herdr/herdrcli/testdata/snapshot.json +213 -0
- package/internal/runner/herdr/herdrcli/testdata/strict-herdr.sh +175 -0
- package/internal/runner/herdr/herdrcli/testdata/tab-create.json +31 -0
- package/internal/runner/herdr/herdrcli/testdata/tab-list.json +26 -0
- package/internal/runner/herdr/herdrcli/testdata/workspace-close.json +6 -0
- package/internal/runner/herdr/herdrcli/testdata/worktree-create.json +58 -0
- package/internal/runner/herdr/herdrcli/testdata/worktree-list.json +35 -0
- package/internal/runner/herdr/herdrcli/testdata/worktree-open.json +59 -0
- package/internal/task/beads/beads.go +0 -16
- package/internal/task/beads/config_compatibility_test.go +7 -8
- package/internal/task/jira/filters_test.go +32 -6
- package/internal/task/jira/jira.go +27 -17
- package/package.json +1 -1
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
package herdr
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
"sync"
|
|
11
|
+
"testing"
|
|
12
|
+
|
|
13
|
+
"github.com/rajpopat27/relay-flow/internal/config"
|
|
14
|
+
"github.com/rajpopat27/relay-flow/internal/runner"
|
|
15
|
+
"github.com/rajpopat27/relay-flow/internal/runner/herdr/herdrcli"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// fakeClient is the typed CLI seam used for adapter decision tests. Command
|
|
19
|
+
// shapes themselves are covered by the strict executable tests in herdrcli.
|
|
20
|
+
type fakeClient struct {
|
|
21
|
+
mu sync.Mutex
|
|
22
|
+
|
|
23
|
+
snapshot herdrcli.Snapshot
|
|
24
|
+
snapshotErr error
|
|
25
|
+
|
|
26
|
+
listing herdrcli.WorktreeListing
|
|
27
|
+
listingErr error
|
|
28
|
+
|
|
29
|
+
openWorkspace herdrcli.Workspace
|
|
30
|
+
openErr error
|
|
31
|
+
|
|
32
|
+
createWorkspace herdrcli.Workspace
|
|
33
|
+
createErr error
|
|
34
|
+
|
|
35
|
+
tabs []herdrcli.Tab
|
|
36
|
+
tabsErr error
|
|
37
|
+
|
|
38
|
+
panes []herdrcli.Pane
|
|
39
|
+
panesErr error
|
|
40
|
+
|
|
41
|
+
pane herdrcli.Pane
|
|
42
|
+
paneErr error
|
|
43
|
+
|
|
44
|
+
info herdrcli.ProcessInfo
|
|
45
|
+
infoErr error
|
|
46
|
+
|
|
47
|
+
createdTab herdrcli.Tab
|
|
48
|
+
createdPane herdrcli.Pane
|
|
49
|
+
createTabErr error
|
|
50
|
+
|
|
51
|
+
renameErr error
|
|
52
|
+
runErr error
|
|
53
|
+
closePaneErr error
|
|
54
|
+
closeWorkspaceErr error
|
|
55
|
+
|
|
56
|
+
openCalls []worktreeCall
|
|
57
|
+
createCalls []worktreeCall
|
|
58
|
+
tabCreateCalls []tabCreateCall
|
|
59
|
+
renameCalls []renameCall
|
|
60
|
+
runCalls []runCall
|
|
61
|
+
closedPanes []string
|
|
62
|
+
closedWorkspaces []string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type worktreeCall struct{ RepoPath, Branch, Base, Label string }
|
|
66
|
+
type tabCreateCall struct{ WorkspaceID, CWD, Label string }
|
|
67
|
+
type renameCall struct{ PaneID, Label string }
|
|
68
|
+
type runCall struct{ PaneID, Command string }
|
|
69
|
+
|
|
70
|
+
var _ herdrcli.Client = (*fakeClient)(nil)
|
|
71
|
+
|
|
72
|
+
func (f *fakeClient) Snapshot(context.Context) (herdrcli.Snapshot, error) {
|
|
73
|
+
f.mu.Lock()
|
|
74
|
+
defer f.mu.Unlock()
|
|
75
|
+
return f.snapshot, f.snapshotErr
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func (f *fakeClient) WorktreeList(_ context.Context, _ string) (herdrcli.WorktreeListing, error) {
|
|
79
|
+
f.mu.Lock()
|
|
80
|
+
defer f.mu.Unlock()
|
|
81
|
+
return f.listing, f.listingErr
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func (f *fakeClient) WorktreeCreate(_ context.Context, repoPath, branch, base, label string) (herdrcli.Workspace, error) {
|
|
85
|
+
f.mu.Lock()
|
|
86
|
+
defer f.mu.Unlock()
|
|
87
|
+
f.createCalls = append(f.createCalls, worktreeCall{RepoPath: repoPath, Branch: branch, Base: base, Label: label})
|
|
88
|
+
return f.createWorkspace, f.createErr
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func (f *fakeClient) WorktreeOpen(_ context.Context, repoPath, branch, label string) (herdrcli.Workspace, error) {
|
|
92
|
+
f.mu.Lock()
|
|
93
|
+
defer f.mu.Unlock()
|
|
94
|
+
f.openCalls = append(f.openCalls, worktreeCall{RepoPath: repoPath, Branch: branch, Label: label})
|
|
95
|
+
return f.openWorkspace, f.openErr
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func (f *fakeClient) CreateTab(_ context.Context, workspaceID, cwd, label string) (herdrcli.Tab, herdrcli.Pane, error) {
|
|
99
|
+
f.mu.Lock()
|
|
100
|
+
defer f.mu.Unlock()
|
|
101
|
+
f.tabCreateCalls = append(f.tabCreateCalls, tabCreateCall{WorkspaceID: workspaceID, CWD: cwd, Label: label})
|
|
102
|
+
return f.createdTab, f.createdPane, f.createTabErr
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func (f *fakeClient) ListTabs(context.Context, string) ([]herdrcli.Tab, error) {
|
|
106
|
+
f.mu.Lock()
|
|
107
|
+
defer f.mu.Unlock()
|
|
108
|
+
return f.tabs, f.tabsErr
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func (f *fakeClient) ListPanes(context.Context, string) ([]herdrcli.Pane, error) {
|
|
112
|
+
f.mu.Lock()
|
|
113
|
+
defer f.mu.Unlock()
|
|
114
|
+
return f.panes, f.panesErr
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func (f *fakeClient) GetPane(context.Context, string) (herdrcli.Pane, error) {
|
|
118
|
+
f.mu.Lock()
|
|
119
|
+
defer f.mu.Unlock()
|
|
120
|
+
return f.pane, f.paneErr
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func (f *fakeClient) ProcessInfo(context.Context, string) (herdrcli.ProcessInfo, error) {
|
|
124
|
+
f.mu.Lock()
|
|
125
|
+
defer f.mu.Unlock()
|
|
126
|
+
return f.info, f.infoErr
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func (f *fakeClient) RenamePane(_ context.Context, paneID, label string) error {
|
|
130
|
+
f.mu.Lock()
|
|
131
|
+
defer f.mu.Unlock()
|
|
132
|
+
f.renameCalls = append(f.renameCalls, renameCall{PaneID: paneID, Label: label})
|
|
133
|
+
return f.renameErr
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func (f *fakeClient) RunPane(_ context.Context, paneID, command string) error {
|
|
137
|
+
f.mu.Lock()
|
|
138
|
+
defer f.mu.Unlock()
|
|
139
|
+
f.runCalls = append(f.runCalls, runCall{PaneID: paneID, Command: command})
|
|
140
|
+
return f.runErr
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func (f *fakeClient) ClosePane(_ context.Context, paneID string) error {
|
|
144
|
+
f.mu.Lock()
|
|
145
|
+
defer f.mu.Unlock()
|
|
146
|
+
f.closedPanes = append(f.closedPanes, paneID)
|
|
147
|
+
return f.closePaneErr
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func (f *fakeClient) CloseWorkspace(_ context.Context, workspaceID string) error {
|
|
151
|
+
f.mu.Lock()
|
|
152
|
+
defer f.mu.Unlock()
|
|
153
|
+
f.closedWorkspaces = append(f.closedWorkspaces, workspaceID)
|
|
154
|
+
return f.closeWorkspaceErr
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func uint32Pointer(v uint32) *uint32 { return &v }
|
|
158
|
+
|
|
159
|
+
const (
|
|
160
|
+
repoPath = "/work/payments"
|
|
161
|
+
checkoutPath = "/work/worktrees/payments/pay-101"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
func ticketWorkspaceValue() herdrcli.Workspace {
|
|
165
|
+
return herdrcli.Workspace{
|
|
166
|
+
ID: "w2",
|
|
167
|
+
Label: "PAY-101",
|
|
168
|
+
Worktree: herdrcli.WorkspaceWorktree{
|
|
169
|
+
CheckoutPath: checkoutPath,
|
|
170
|
+
RepoName: "payments",
|
|
171
|
+
RepoRoot: repoPath,
|
|
172
|
+
IsLinked: true,
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func runSpec() runner.RunSpec {
|
|
178
|
+
return runner.RunSpec{RunID: "run-1", RepoName: "payments", RepoPath: repoPath, TicketKey: "PAY-101"}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func liveProcessInfo(paneID string) herdrcli.ProcessInfo {
|
|
182
|
+
return herdrcli.ProcessInfo{
|
|
183
|
+
PaneID: paneID,
|
|
184
|
+
ShellPID: uint32Pointer(100),
|
|
185
|
+
ForegroundProcesses: []herdrcli.ForegroundProcess{{PID: 200, Name: "opencode"}},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func shellProcessInfo(paneID string) herdrcli.ProcessInfo {
|
|
190
|
+
return herdrcli.ProcessInfo{
|
|
191
|
+
PaneID: paneID,
|
|
192
|
+
ShellPID: uint32Pointer(100),
|
|
193
|
+
ForegroundProcesses: []herdrcli.ForegroundProcess{{PID: 100, Name: "zsh"}},
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func nodeCommand(runID string) runner.Command {
|
|
198
|
+
return runner.Command{
|
|
199
|
+
Executable: "opencode",
|
|
200
|
+
Args: []string{"--agent", "build", "--prompt", "line one\nline two"},
|
|
201
|
+
Env: map[string]string{"RELAY_FLOW_RUN_ID": runID, "RELAY_FLOW_NODE": "coding"},
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- Construction ---
|
|
206
|
+
|
|
207
|
+
func TestNewRejectsUnknownRunnerConfigKeys(t *testing.T) {
|
|
208
|
+
if _, err := New(config.RawValues{"session": "relay-flow"}); err != nil {
|
|
209
|
+
t.Fatalf("New(valid) = %v", err)
|
|
210
|
+
}
|
|
211
|
+
if _, err := New(config.RawValues{"workspace": "w1"}); err == nil {
|
|
212
|
+
t.Fatal("New accepted an unknown runnerConfig key")
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- Repos ---
|
|
217
|
+
|
|
218
|
+
func TestDiscoverReposDeduplicatesRepositoryRoots(t *testing.T) {
|
|
219
|
+
cli := &fakeClient{snapshot: herdrcli.Snapshot{Workspaces: []herdrcli.Workspace{
|
|
220
|
+
{ID: "w1", Label: "payments", Worktree: herdrcli.WorkspaceWorktree{CheckoutPath: repoPath, RepoName: "payments", RepoRoot: repoPath}},
|
|
221
|
+
ticketWorkspaceValue(),
|
|
222
|
+
{ID: "w3", Label: "billing", Worktree: herdrcli.WorkspaceWorktree{CheckoutPath: "/work/billing", RepoName: "billing", RepoRoot: "/work/billing"}},
|
|
223
|
+
{ID: "w4", Label: "scratch"},
|
|
224
|
+
}}}
|
|
225
|
+
got, err := newAdapter(cli).DiscoverRepos(context.Background())
|
|
226
|
+
if err != nil {
|
|
227
|
+
t.Fatal(err)
|
|
228
|
+
}
|
|
229
|
+
want := []runner.RepoCandidate{
|
|
230
|
+
{Name: "billing", Path: "/work/billing"},
|
|
231
|
+
{Name: "payments", Path: repoPath},
|
|
232
|
+
}
|
|
233
|
+
if len(got) != len(want) {
|
|
234
|
+
t.Fatalf("DiscoverRepos = %+v, want %+v", got, want)
|
|
235
|
+
}
|
|
236
|
+
for i := range want {
|
|
237
|
+
if got[i] != want[i] {
|
|
238
|
+
t.Fatalf("DiscoverRepos[%d] = %+v, want %+v", i, got[i], want[i])
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
func TestValidateRepoAcceptsRepositoryRootAndRejectsInnerPaths(t *testing.T) {
|
|
244
|
+
cli := &fakeClient{listing: herdrcli.WorktreeListing{
|
|
245
|
+
Source: herdrcli.WorktreeSource{RepoName: "payments", RepoRoot: repoPath, SourceCheckoutPath: repoPath},
|
|
246
|
+
}}
|
|
247
|
+
a := newAdapter(cli)
|
|
248
|
+
if err := a.ValidateRepo(context.Background(), "payments", repoPath); err != nil {
|
|
249
|
+
t.Fatalf("ValidateRepo(root) = %v", err)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
err := a.ValidateRepo(context.Background(), "payments", repoPath+"/internal/api")
|
|
253
|
+
if err == nil {
|
|
254
|
+
t.Fatal("ValidateRepo accepted a path inside the repository")
|
|
255
|
+
}
|
|
256
|
+
if !strings.Contains(err.Error(), repoPath) {
|
|
257
|
+
t.Fatalf("ValidateRepo error = %v, want the repository root in the message", err)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
func TestValidateRepoRejectsNonGitPath(t *testing.T) {
|
|
262
|
+
cli := &fakeClient{listingErr: herdrcli.ErrNotGitWorktree}
|
|
263
|
+
err := newAdapter(cli).ValidateRepo(context.Background(), "payments", "/work/not-a-repo")
|
|
264
|
+
if !errors.Is(err, herdrcli.ErrNotGitWorktree) {
|
|
265
|
+
t.Fatalf("ValidateRepo(non-git) = %v, want ErrNotGitWorktree", err)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func TestValidateRepoCreatesNothing(t *testing.T) {
|
|
270
|
+
cli := &fakeClient{listing: herdrcli.WorktreeListing{
|
|
271
|
+
Source: herdrcli.WorktreeSource{RepoRoot: repoPath, SourceCheckoutPath: repoPath},
|
|
272
|
+
}}
|
|
273
|
+
if err := newAdapter(cli).ValidateRepo(context.Background(), "payments", repoPath); err != nil {
|
|
274
|
+
t.Fatal(err)
|
|
275
|
+
}
|
|
276
|
+
if len(cli.createCalls) != 0 || len(cli.openCalls) != 0 || len(cli.tabCreateCalls) != 0 {
|
|
277
|
+
t.Fatalf("ValidateRepo created Herdr resources: %+v %+v %+v", cli.createCalls, cli.openCalls, cli.tabCreateCalls)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// --- Environment ---
|
|
282
|
+
|
|
283
|
+
func TestEnsureEnvironmentReusesExistingTicketWorktree(t *testing.T) {
|
|
284
|
+
cli := &fakeClient{openWorkspace: ticketWorkspaceValue()}
|
|
285
|
+
env, err := newAdapter(cli).EnsureEnvironment(context.Background(), runSpec())
|
|
286
|
+
if err != nil {
|
|
287
|
+
t.Fatal(err)
|
|
288
|
+
}
|
|
289
|
+
if env != (runner.Environment{ID: "w2", Path: checkoutPath}) {
|
|
290
|
+
t.Fatalf("EnsureEnvironment = %+v", env)
|
|
291
|
+
}
|
|
292
|
+
if len(cli.createCalls) != 0 {
|
|
293
|
+
t.Fatalf("worktree create calls = %+v, want none for an existing checkout", cli.createCalls)
|
|
294
|
+
}
|
|
295
|
+
if len(cli.openCalls) != 1 || cli.openCalls[0].Branch != "PAY-101" || cli.openCalls[0].Label != "PAY-101" {
|
|
296
|
+
t.Fatalf("worktree open calls = %+v", cli.openCalls)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
func TestEnsureEnvironmentCreatesTicketWorktreeFromOriginBase(t *testing.T) {
|
|
301
|
+
repo := newGitRepo(t, "main")
|
|
302
|
+
git(t, repo, "remote", "add", "origin", repo)
|
|
303
|
+
git(t, repo, "update-ref", "refs/remotes/origin/main", "HEAD")
|
|
304
|
+
git(t, repo, "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main")
|
|
305
|
+
|
|
306
|
+
cli := &fakeClient{
|
|
307
|
+
openErr: herdrcli.ErrWorktreeNotFound,
|
|
308
|
+
createWorkspace: ticketWorkspaceValue(),
|
|
309
|
+
}
|
|
310
|
+
spec := runSpec()
|
|
311
|
+
spec.RepoPath = repo
|
|
312
|
+
env, err := newAdapter(cli).EnsureEnvironment(context.Background(), spec)
|
|
313
|
+
if err != nil {
|
|
314
|
+
t.Fatal(err)
|
|
315
|
+
}
|
|
316
|
+
if env.ID != "w2" || env.Path != checkoutPath {
|
|
317
|
+
t.Fatalf("EnsureEnvironment = %+v", env)
|
|
318
|
+
}
|
|
319
|
+
if len(cli.createCalls) != 1 {
|
|
320
|
+
t.Fatalf("worktree create calls = %+v", cli.createCalls)
|
|
321
|
+
}
|
|
322
|
+
call := cli.createCalls[0]
|
|
323
|
+
if call.Branch != "PAY-101" || call.Label != "PAY-101" || call.Base != "origin/main" {
|
|
324
|
+
t.Fatalf("worktree create call = %+v, want ticket branch based on origin/main", call)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func TestEnsureEnvironmentPropagatesOpenFailures(t *testing.T) {
|
|
329
|
+
cli := &fakeClient{openErr: errors.New("herdr server unavailable")}
|
|
330
|
+
if _, err := newAdapter(cli).EnsureEnvironment(context.Background(), runSpec()); err == nil {
|
|
331
|
+
t.Fatal("EnsureEnvironment hid a transport failure")
|
|
332
|
+
}
|
|
333
|
+
if len(cli.createCalls) != 0 {
|
|
334
|
+
t.Fatalf("worktree create calls = %+v, want none after a transport failure", cli.createCalls)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
func TestSetEnvironmentStatusIsSuccessfulNoOp(t *testing.T) {
|
|
339
|
+
cli := &fakeClient{}
|
|
340
|
+
err := newAdapter(cli).SetEnvironmentStatus(context.Background(), runner.Environment{ID: "w2"}, runner.WorkspaceStatusInProgress)
|
|
341
|
+
if err != nil {
|
|
342
|
+
t.Fatalf("SetEnvironmentStatus = %v", err)
|
|
343
|
+
}
|
|
344
|
+
if len(cli.closedWorkspaces) != 0 || len(cli.createCalls) != 0 {
|
|
345
|
+
t.Fatal("SetEnvironmentStatus touched Herdr state")
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// --- Base ref ladder ---
|
|
350
|
+
|
|
351
|
+
func TestOriginBaseRefLadder(t *testing.T) {
|
|
352
|
+
t.Run("origin/HEAD", func(t *testing.T) {
|
|
353
|
+
repo := newGitRepo(t, "trunk")
|
|
354
|
+
git(t, repo, "remote", "add", "origin", repo)
|
|
355
|
+
git(t, repo, "update-ref", "refs/remotes/origin/trunk", "HEAD")
|
|
356
|
+
git(t, repo, "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/trunk")
|
|
357
|
+
assertBaseRef(t, repo, "origin/trunk")
|
|
358
|
+
})
|
|
359
|
+
t.Run("origin/main", func(t *testing.T) {
|
|
360
|
+
repo := newGitRepo(t, "work")
|
|
361
|
+
git(t, repo, "remote", "add", "origin", repo)
|
|
362
|
+
git(t, repo, "update-ref", "refs/remotes/origin/main", "HEAD")
|
|
363
|
+
assertBaseRef(t, repo, "origin/main")
|
|
364
|
+
})
|
|
365
|
+
t.Run("origin/master", func(t *testing.T) {
|
|
366
|
+
repo := newGitRepo(t, "work")
|
|
367
|
+
git(t, repo, "remote", "add", "origin", repo)
|
|
368
|
+
git(t, repo, "update-ref", "refs/remotes/origin/master", "HEAD")
|
|
369
|
+
assertBaseRef(t, repo, "origin/master")
|
|
370
|
+
})
|
|
371
|
+
t.Run("local main without origin", func(t *testing.T) {
|
|
372
|
+
assertBaseRef(t, newGitRepo(t, "main"), "main")
|
|
373
|
+
})
|
|
374
|
+
t.Run("local master without origin", func(t *testing.T) {
|
|
375
|
+
assertBaseRef(t, newGitRepo(t, "master"), "master")
|
|
376
|
+
})
|
|
377
|
+
t.Run("no usable base", func(t *testing.T) {
|
|
378
|
+
repo := newGitRepo(t, "wip")
|
|
379
|
+
if _, err := originBaseRef(repo); err == nil {
|
|
380
|
+
t.Fatal("originBaseRef accepted a repository with no origin or main/master")
|
|
381
|
+
} else if !strings.Contains(err.Error(), "remote set-head") {
|
|
382
|
+
t.Fatalf("originBaseRef error = %v, want actionable guidance", err)
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
func assertBaseRef(t *testing.T, repo, want string) {
|
|
388
|
+
t.Helper()
|
|
389
|
+
got, err := originBaseRef(repo)
|
|
390
|
+
if err != nil {
|
|
391
|
+
t.Fatal(err)
|
|
392
|
+
}
|
|
393
|
+
if got != want {
|
|
394
|
+
t.Fatalf("originBaseRef = %q, want %q", got, want)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
func newGitRepo(t *testing.T, branch string) string {
|
|
399
|
+
t.Helper()
|
|
400
|
+
dir := t.TempDir()
|
|
401
|
+
git(t, dir, "init", "-q", "-b", branch, ".")
|
|
402
|
+
git(t, dir, "config", "user.email", "test@relay-flow.test")
|
|
403
|
+
git(t, dir, "config", "user.name", "test")
|
|
404
|
+
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("x\n"), 0o644); err != nil {
|
|
405
|
+
t.Fatal(err)
|
|
406
|
+
}
|
|
407
|
+
git(t, dir, "add", "-A")
|
|
408
|
+
git(t, dir, "commit", "-qm", "init")
|
|
409
|
+
return dir
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
func git(t *testing.T, dir string, args ...string) {
|
|
413
|
+
t.Helper()
|
|
414
|
+
cmd := exec.Command("git", args...)
|
|
415
|
+
cmd.Dir = dir
|
|
416
|
+
if out, err := cmd.CombinedOutput(); err != nil {
|
|
417
|
+
t.Fatalf("git %v: %v: %s", args, err, out)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// --- Terminals ---
|
|
422
|
+
|
|
423
|
+
func TestCreateTerminalUsesPublicPaneIDAndStableLabel(t *testing.T) {
|
|
424
|
+
cli := &fakeClient{
|
|
425
|
+
createdTab: herdrcli.Tab{ID: "w2:t2", WorkspaceID: "w2", Label: "PAY-101:coding"},
|
|
426
|
+
createdPane: herdrcli.Pane{ID: "w2:p2", TerminalID: "term_ephemeral", WorkspaceID: "w2", TabID: "w2:t2"},
|
|
427
|
+
}
|
|
428
|
+
env := runner.Environment{ID: "w2", Path: checkoutPath}
|
|
429
|
+
terminal, err := newAdapter(cli).CreateTerminal(context.Background(), env, "PAY-101:coding", nodeCommand("run-1"))
|
|
430
|
+
if err != nil {
|
|
431
|
+
t.Fatal(err)
|
|
432
|
+
}
|
|
433
|
+
if terminal != (runner.Terminal{ID: "w2:p2", Title: "PAY-101:coding"}) {
|
|
434
|
+
t.Fatalf("CreateTerminal = %+v, want the public pane ID", terminal)
|
|
435
|
+
}
|
|
436
|
+
if len(cli.tabCreateCalls) != 1 || cli.tabCreateCalls[0] != (tabCreateCall{WorkspaceID: "w2", CWD: checkoutPath, Label: "PAY-101:coding"}) {
|
|
437
|
+
t.Fatalf("tab create calls = %+v, want the worktree checkout as cwd", cli.tabCreateCalls)
|
|
438
|
+
}
|
|
439
|
+
if len(cli.renameCalls) != 1 || cli.renameCalls[0].Label != "PAY-101:coding" {
|
|
440
|
+
t.Fatalf("rename calls = %+v", cli.renameCalls)
|
|
441
|
+
}
|
|
442
|
+
if len(cli.runCalls) != 1 {
|
|
443
|
+
t.Fatalf("run calls = %+v", cli.runCalls)
|
|
444
|
+
}
|
|
445
|
+
command := cli.runCalls[0].Command
|
|
446
|
+
for _, want := range []string{"RELAY_FLOW_RUN_ID='run-1'", "RELAY_FLOW_NODE='coding'", "'opencode'", "'line one\nline two'"} {
|
|
447
|
+
if !strings.Contains(command, want) {
|
|
448
|
+
t.Fatalf("command %q missing %q", command, want)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
for _, forbidden := range []string{"nodeVisit", "run-1:coding", "--env"} {
|
|
452
|
+
if strings.Contains(cli.tabCreateCalls[0].Label, forbidden) {
|
|
453
|
+
t.Fatalf("label %q contains %q", cli.tabCreateCalls[0].Label, forbidden)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
func TestFindTerminalAcceptsLiveAgentAndRejectsRestoredShell(t *testing.T) {
|
|
459
|
+
live := &fakeClient{
|
|
460
|
+
pane: herdrcli.Pane{ID: "w2:p2", TerminalID: "term_1", WorkspaceID: "w2", Label: "PAY-101:coding"},
|
|
461
|
+
info: liveProcessInfo("w2:p2"),
|
|
462
|
+
}
|
|
463
|
+
terminal, ok, err := newAdapter(live).FindTerminal(context.Background(), runner.Terminal{ID: "w2:p2", Title: "PAY-101:coding"})
|
|
464
|
+
if err != nil || !ok || terminal.ID != "w2:p2" {
|
|
465
|
+
t.Fatalf("FindTerminal(live) = %+v, %v, %v", terminal, ok, err)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
restored := &fakeClient{
|
|
469
|
+
pane: herdrcli.Pane{ID: "w2:p2", TerminalID: "term_after_restart", WorkspaceID: "w2", Label: "PAY-101:coding"},
|
|
470
|
+
info: shellProcessInfo("w2:p2"),
|
|
471
|
+
}
|
|
472
|
+
if _, ok, err := newAdapter(restored).FindTerminal(context.Background(), runner.Terminal{ID: "w2:p2"}); err != nil || ok {
|
|
473
|
+
t.Fatalf("FindTerminal(restored shell) = %v, %v, want absent", ok, err)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
func TestFindTerminalDistinguishesMissingPaneFromTransportFailure(t *testing.T) {
|
|
478
|
+
missing := &fakeClient{paneErr: herdrcli.ErrPaneNotFound}
|
|
479
|
+
if _, ok, err := newAdapter(missing).FindTerminal(context.Background(), runner.Terminal{ID: "w2:p9"}); err != nil || ok {
|
|
480
|
+
t.Fatalf("FindTerminal(missing) = %v, %v, want absent without error", ok, err)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
broken := &fakeClient{paneErr: errors.New("herdr server unavailable")}
|
|
484
|
+
if _, ok, err := newAdapter(broken).FindTerminal(context.Background(), runner.Terminal{ID: "w2:p2"}); err == nil || ok {
|
|
485
|
+
t.Fatalf("FindTerminal(transport failure) = %v, %v, want the error to propagate", ok, err)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
func TestEnsureTerminalReusesLivePaneWithoutRelaunching(t *testing.T) {
|
|
490
|
+
cli := &fakeClient{
|
|
491
|
+
pane: herdrcli.Pane{ID: "w2:p2", TerminalID: "term_1", WorkspaceID: "w2", Label: "PAY-101:coding"},
|
|
492
|
+
info: liveProcessInfo("w2:p2"),
|
|
493
|
+
}
|
|
494
|
+
env := runner.Environment{ID: "w2", Path: checkoutPath}
|
|
495
|
+
stored := runner.Terminal{ID: "w2:p2", Title: "PAY-101:coding"}
|
|
496
|
+
got, err := newAdapter(cli).EnsureTerminal(context.Background(), env, stored, "PAY-101:coding", nodeCommand("run-1"))
|
|
497
|
+
if err != nil || got.ID != "w2:p2" {
|
|
498
|
+
t.Fatalf("EnsureTerminal = %+v, %v", got, err)
|
|
499
|
+
}
|
|
500
|
+
if len(cli.tabCreateCalls) != 0 || len(cli.runCalls) != 0 || len(cli.closedPanes) != 0 {
|
|
501
|
+
t.Fatal("EnsureTerminal disturbed a live pane")
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
func TestEnsureTerminalReplacesMissingPane(t *testing.T) {
|
|
506
|
+
cli := &fakeClient{
|
|
507
|
+
paneErr: herdrcli.ErrPaneNotFound,
|
|
508
|
+
createdTab: herdrcli.Tab{ID: "w2:t3", WorkspaceID: "w2", Label: "PAY-101:coding"},
|
|
509
|
+
createdPane: herdrcli.Pane{ID: "w2:p3", TerminalID: "term_new", WorkspaceID: "w2", TabID: "w2:t3"},
|
|
510
|
+
}
|
|
511
|
+
env := runner.Environment{ID: "w2", Path: checkoutPath}
|
|
512
|
+
stored := runner.Terminal{ID: "w2:p2", Title: "PAY-101:coding"}
|
|
513
|
+
got, err := newAdapter(cli).EnsureTerminal(context.Background(), env, stored, "PAY-101:coding", nodeCommand("run-1"))
|
|
514
|
+
if err != nil || got.ID != "w2:p3" {
|
|
515
|
+
t.Fatalf("EnsureTerminal = %+v, %v", got, err)
|
|
516
|
+
}
|
|
517
|
+
if len(cli.tabCreateCalls) != 1 {
|
|
518
|
+
t.Fatalf("tab create calls = %+v, want exactly one replacement", cli.tabCreateCalls)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
func TestEnsureTerminalRecoversLostCreateThroughTabLabel(t *testing.T) {
|
|
523
|
+
cli := &fakeClient{
|
|
524
|
+
paneErr: herdrcli.ErrPaneNotFound,
|
|
525
|
+
tabs: []herdrcli.Tab{{ID: "w2:t2", WorkspaceID: "w2", Label: "PAY-101:coding"}},
|
|
526
|
+
panes: []herdrcli.Pane{{ID: "w2:p2", WorkspaceID: "w2", TabID: "w2:t2"}},
|
|
527
|
+
}
|
|
528
|
+
env := runner.Environment{ID: "w2", Path: checkoutPath}
|
|
529
|
+
got, err := newAdapter(cli).EnsureTerminal(context.Background(), env, runner.Terminal{}, "PAY-101:coding", nodeCommand("run-2"))
|
|
530
|
+
if err != nil || got.ID != "w2:p2" {
|
|
531
|
+
t.Fatalf("EnsureTerminal = %+v, %v", got, err)
|
|
532
|
+
}
|
|
533
|
+
if len(cli.tabCreateCalls) != 0 {
|
|
534
|
+
t.Fatalf("tab create calls = %+v, want no duplicate pane", cli.tabCreateCalls)
|
|
535
|
+
}
|
|
536
|
+
if len(cli.renameCalls) != 1 || cli.renameCalls[0].PaneID != "w2:p2" {
|
|
537
|
+
t.Fatalf("rename calls = %+v, want the recovered pane labelled", cli.renameCalls)
|
|
538
|
+
}
|
|
539
|
+
if len(cli.runCalls) != 1 || !strings.Contains(cli.runCalls[0].Command, "RELAY_FLOW_RUN_ID='run-2'") {
|
|
540
|
+
t.Fatalf("run calls = %+v, want the current run's environment", cli.runCalls)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
func TestEnsureTerminalRecoversLabelledPaneWithoutRenaming(t *testing.T) {
|
|
545
|
+
cli := &fakeClient{
|
|
546
|
+
paneErr: herdrcli.ErrPaneNotFound,
|
|
547
|
+
tabs: []herdrcli.Tab{{ID: "w2:t2", WorkspaceID: "w2", Label: "PAY-101:coding"}},
|
|
548
|
+
panes: []herdrcli.Pane{{ID: "w2:p2", WorkspaceID: "w2", TabID: "w2:t2", Label: "PAY-101:coding"}},
|
|
549
|
+
}
|
|
550
|
+
env := runner.Environment{ID: "w2", Path: checkoutPath}
|
|
551
|
+
got, err := newAdapter(cli).EnsureTerminal(context.Background(), env, runner.Terminal{}, "PAY-101:coding", nodeCommand("run-2"))
|
|
552
|
+
if err != nil || got.ID != "w2:p2" {
|
|
553
|
+
t.Fatalf("EnsureTerminal = %+v, %v", got, err)
|
|
554
|
+
}
|
|
555
|
+
if len(cli.renameCalls) != 0 {
|
|
556
|
+
t.Fatalf("rename calls = %+v, want none for an already labelled pane", cli.renameCalls)
|
|
557
|
+
}
|
|
558
|
+
if len(cli.runCalls) != 1 {
|
|
559
|
+
t.Fatalf("run calls = %+v", cli.runCalls)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// A pane left by an earlier run must be relaunched with the current run's
|
|
564
|
+
// environment: Herdr binds environment to the launched command, not the tab.
|
|
565
|
+
func TestAdoptedPaneNeverInheritsPreviousRunEnvironment(t *testing.T) {
|
|
566
|
+
cli := &fakeClient{
|
|
567
|
+
paneErr: herdrcli.ErrPaneNotFound,
|
|
568
|
+
tabs: []herdrcli.Tab{{ID: "w2:t2", WorkspaceID: "w2", Label: "PAY-101:coding"}},
|
|
569
|
+
panes: []herdrcli.Pane{{ID: "w2:p2", WorkspaceID: "w2", TabID: "w2:t2", Label: "PAY-101:coding"}},
|
|
570
|
+
}
|
|
571
|
+
env := runner.Environment{ID: "w2", Path: checkoutPath}
|
|
572
|
+
if _, err := newAdapter(cli).EnsureTerminal(context.Background(), env, runner.Terminal{}, "PAY-101:coding", nodeCommand("run-9")); err != nil {
|
|
573
|
+
t.Fatal(err)
|
|
574
|
+
}
|
|
575
|
+
command := cli.runCalls[0].Command
|
|
576
|
+
if !strings.Contains(command, "RELAY_FLOW_RUN_ID='run-9'") || strings.Contains(command, "run-1") {
|
|
577
|
+
t.Fatalf("command %q must carry only the current run identity", command)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
func TestSendTerminalForwardsMultilineTextUnchanged(t *testing.T) {
|
|
582
|
+
cli := &fakeClient{}
|
|
583
|
+
text := "feedback line one\nfeedback line two\n"
|
|
584
|
+
if err := newAdapter(cli).SendTerminal(context.Background(), runner.Terminal{ID: "w2:p2"}, text); err != nil {
|
|
585
|
+
t.Fatal(err)
|
|
586
|
+
}
|
|
587
|
+
if len(cli.runCalls) != 1 || cli.runCalls[0].Command != text {
|
|
588
|
+
t.Fatalf("run calls = %+v, want the exact text", cli.runCalls)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
func TestCloseTerminalIsIdempotentForMissingPane(t *testing.T) {
|
|
593
|
+
cli := &fakeClient{closePaneErr: herdrcli.ErrPaneNotFound}
|
|
594
|
+
if err := newAdapter(cli).CloseTerminal(context.Background(), runner.Terminal{ID: "w2:p9"}); err != nil {
|
|
595
|
+
t.Fatalf("CloseTerminal(missing) = %v, want nil", err)
|
|
596
|
+
}
|
|
597
|
+
cli.closePaneErr = errors.New("herdr server unavailable")
|
|
598
|
+
if err := newAdapter(cli).CloseTerminal(context.Background(), runner.Terminal{ID: "w2:p2"}); err == nil {
|
|
599
|
+
t.Fatal("CloseTerminal hid a transport failure")
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// --- Cleanup ---
|
|
604
|
+
|
|
605
|
+
func openTicketWorktreeClient() *fakeClient {
|
|
606
|
+
return &fakeClient{
|
|
607
|
+
listing: herdrcli.WorktreeListing{
|
|
608
|
+
Source: herdrcli.WorktreeSource{RepoName: "payments", RepoRoot: repoPath, SourceCheckoutPath: repoPath},
|
|
609
|
+
Worktrees: []herdrcli.Worktree{
|
|
610
|
+
{Path: repoPath, Branch: "main", OpenWorkspaceID: "w1"},
|
|
611
|
+
{Path: checkoutPath, Branch: "PAY-101", IsLinked: true, OpenWorkspaceID: "w2"},
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
tabs: []herdrcli.Tab{
|
|
615
|
+
{ID: "w2:t1", WorkspaceID: "w2", Label: "1"},
|
|
616
|
+
{ID: "w2:t2", WorkspaceID: "w2", Label: "PAY-101:coding"},
|
|
617
|
+
},
|
|
618
|
+
panes: []herdrcli.Pane{
|
|
619
|
+
{ID: "w2:p1", WorkspaceID: "w2", TabID: "w2:t1"},
|
|
620
|
+
{ID: "w2:p2", WorkspaceID: "w2", TabID: "w2:t2", Label: "PAY-101:coding"},
|
|
621
|
+
{ID: "w2:p3", WorkspaceID: "w2", TabID: "w2:t3", Label: "PAY-202:review"},
|
|
622
|
+
},
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
func TestCloseTerminalsClosesOnlyTicketPanesAndKeepsWorkspace(t *testing.T) {
|
|
627
|
+
cli := openTicketWorktreeClient()
|
|
628
|
+
if err := newAdapter(cli).CloseTerminals(context.Background(), runSpec()); err != nil {
|
|
629
|
+
t.Fatal(err)
|
|
630
|
+
}
|
|
631
|
+
if len(cli.closedPanes) != 1 || cli.closedPanes[0] != "w2:p2" {
|
|
632
|
+
t.Fatalf("closed panes = %v, want only the ticket node pane", cli.closedPanes)
|
|
633
|
+
}
|
|
634
|
+
if len(cli.closedWorkspaces) != 0 {
|
|
635
|
+
t.Fatalf("closed workspaces = %v, want the workspace preserved", cli.closedWorkspaces)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
func TestCleanupRunClosesTicketWorkspaceAndKeepsWorktree(t *testing.T) {
|
|
640
|
+
cli := openTicketWorktreeClient()
|
|
641
|
+
if err := newAdapter(cli).CleanupRun(context.Background(), runSpec()); err != nil {
|
|
642
|
+
t.Fatal(err)
|
|
643
|
+
}
|
|
644
|
+
if len(cli.closedPanes) != 1 || cli.closedPanes[0] != "w2:p2" {
|
|
645
|
+
t.Fatalf("closed panes = %v", cli.closedPanes)
|
|
646
|
+
}
|
|
647
|
+
if len(cli.closedWorkspaces) != 1 || cli.closedWorkspaces[0] != "w2" {
|
|
648
|
+
t.Fatalf("closed workspaces = %v, want the ticket workspace", cli.closedWorkspaces)
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
func TestCleanupRollsForwardWhenTicketWorktreeIsGone(t *testing.T) {
|
|
653
|
+
cases := map[string]*fakeClient{
|
|
654
|
+
"repository is not a git work tree": {listingErr: herdrcli.ErrNotGitWorktree},
|
|
655
|
+
"ticket checkout removed": {listing: herdrcli.WorktreeListing{
|
|
656
|
+
Source: herdrcli.WorktreeSource{RepoRoot: repoPath},
|
|
657
|
+
Worktrees: []herdrcli.Worktree{{Path: repoPath, Branch: "main", OpenWorkspaceID: "w1"}},
|
|
658
|
+
}},
|
|
659
|
+
"ticket workspace closed": {listing: herdrcli.WorktreeListing{
|
|
660
|
+
Source: herdrcli.WorktreeSource{RepoRoot: repoPath},
|
|
661
|
+
Worktrees: []herdrcli.Worktree{{Path: checkoutPath, Branch: "PAY-101", IsLinked: true}},
|
|
662
|
+
}},
|
|
663
|
+
}
|
|
664
|
+
for name, cli := range cases {
|
|
665
|
+
t.Run(name, func(t *testing.T) {
|
|
666
|
+
a := newAdapter(cli)
|
|
667
|
+
if err := a.CloseTerminals(context.Background(), runSpec()); err != nil {
|
|
668
|
+
t.Fatalf("CloseTerminals = %v, want roll-forward success", err)
|
|
669
|
+
}
|
|
670
|
+
if err := a.CleanupRun(context.Background(), runSpec()); err != nil {
|
|
671
|
+
t.Fatalf("CleanupRun = %v, want roll-forward success", err)
|
|
672
|
+
}
|
|
673
|
+
if len(cli.closedPanes) != 0 || len(cli.closedWorkspaces) != 0 {
|
|
674
|
+
t.Fatal("cleanup touched Herdr state for an absent environment")
|
|
675
|
+
}
|
|
676
|
+
})
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
func TestCleanupPropagatesTransportFailures(t *testing.T) {
|
|
681
|
+
cli := &fakeClient{listingErr: errors.New("herdr server unavailable")}
|
|
682
|
+
if err := newAdapter(cli).CloseTerminals(context.Background(), runSpec()); err == nil {
|
|
683
|
+
t.Fatal("CloseTerminals hid a transport failure")
|
|
684
|
+
}
|
|
685
|
+
if err := newAdapter(cli).CleanupRun(context.Background(), runSpec()); err == nil {
|
|
686
|
+
t.Fatal("CleanupRun hid a transport failure")
|
|
687
|
+
}
|
|
688
|
+
}
|