relay-flow 0.2.8-alpha → 0.2.9-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 +62 -35
- package/cmd/relay-flow/commands_test.go +117 -8
- package/cmd/relay-flow/main.go +167 -60
- package/cmd/relay-flow/onboarding.go +344 -0
- package/cmd/relay-flow/onboarding_test.go +515 -0
- package/cmd/relay-flow/repo_registration.go +219 -0
- package/cmd/relay-flow/serve.go +3 -0
- package/go.mod +12 -8
- package/go.sum +24 -17
- package/internal/execution/goworkflows/engine_test.go +3 -3
- package/internal/harness/opencode/opencode_test.go +4 -4
- package/internal/harness/opencode/repo_setup.go +1 -1
- package/internal/harness/pi/prompt_test.go +3 -3
- package/internal/repo/service.go +34 -3
- package/internal/repo/service_test.go +22 -0
- package/internal/runner/herdr/herdr.go +103 -4
- package/internal/runner/herdr/herdr_test.go +72 -2
- package/internal/runner/herdr/herdrcli/contract.go +12 -3
- package/internal/runner/herdr/herdrcli/herdrcli_test.go +17 -2
- package/internal/runner/herdr/herdrcli/operations.go +16 -0
- package/internal/runner/herdr/herdrcli/testdata/strict-herdr.sh +8 -0
- package/internal/runner/herdr/herdrcli/testdata/workspace-create.json +1 -0
- package/internal/runner/orca/orca.go +68 -9
- package/internal/runner/orca/orca_test.go +134 -2
- package/internal/runner/orca/orcacli/orcacli.go +21 -1
- package/internal/runner/orca/orcacli/orcacli_test.go +22 -0
- package/internal/runner/orca/orcacli/testdata/strict-orca.sh +5 -0
- package/internal/runner/runner.go +9 -0
- package/internal/server/api_test.go +19 -0
- package/internal/server/client.go +10 -0
- package/internal/server/fixture_test.go +7 -0
- package/internal/server/server.go +41 -0
- package/internal/task/jira/testdata/jira_search_issues.json +4 -4
- package/internal/workflow/workflow.go +4 -1
- package/internal/workflow/workflow_test.go +4 -4
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ package main
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"fmt"
|
|
6
|
+
"path/filepath"
|
|
6
7
|
"strings"
|
|
7
8
|
|
|
8
9
|
"github.com/charmbracelet/huh"
|
|
@@ -13,6 +14,224 @@ import (
|
|
|
13
14
|
"github.com/rajpopat27/relay-flow/internal/task"
|
|
14
15
|
)
|
|
15
16
|
|
|
17
|
+
const addRepositorySelection = -1
|
|
18
|
+
|
|
19
|
+
func repoSelectionOptions(candidates []runner.RepoCandidate) []huh.Option[int] {
|
|
20
|
+
options := make([]huh.Option[int], 0, len(candidates)+1)
|
|
21
|
+
for i, candidate := range candidates {
|
|
22
|
+
label := strings.TrimSpace(candidate.Name)
|
|
23
|
+
if label == "" {
|
|
24
|
+
label = candidate.Path
|
|
25
|
+
}
|
|
26
|
+
options = append(options, huh.NewOption(label+" ("+candidate.Path+")", i))
|
|
27
|
+
}
|
|
28
|
+
options = append(options, huh.NewOption("[+] Add repository", addRepositorySelection))
|
|
29
|
+
return options
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func canonicalRepoPath(path string) string {
|
|
33
|
+
path = strings.TrimSpace(path)
|
|
34
|
+
if path == "" {
|
|
35
|
+
return ""
|
|
36
|
+
}
|
|
37
|
+
absolute, err := filepath.Abs(path)
|
|
38
|
+
if err != nil {
|
|
39
|
+
return filepath.Clean(path)
|
|
40
|
+
}
|
|
41
|
+
absolute = filepath.Clean(absolute)
|
|
42
|
+
if resolved, err := filepath.EvalSymlinks(absolute); err == nil {
|
|
43
|
+
return filepath.Clean(resolved)
|
|
44
|
+
}
|
|
45
|
+
return absolute
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func selectedRepoPaths(candidates []runner.RepoCandidate, selected []int) []string {
|
|
49
|
+
paths := make([]string, 0, len(selected))
|
|
50
|
+
seen := map[string]bool{}
|
|
51
|
+
for _, index := range selected {
|
|
52
|
+
if index < 0 || index >= len(candidates) {
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
path := canonicalRepoPath(candidates[index].Path)
|
|
56
|
+
if path == "" || seen[path] {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
seen[path] = true
|
|
60
|
+
paths = append(paths, path)
|
|
61
|
+
}
|
|
62
|
+
return paths
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func selectedRepoIndices(candidates []runner.RepoCandidate, paths []string) []int {
|
|
66
|
+
wanted := map[string]bool{}
|
|
67
|
+
for _, path := range paths {
|
|
68
|
+
if path != "" {
|
|
69
|
+
wanted[path] = true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
selected := make([]int, 0, len(wanted))
|
|
73
|
+
for i, candidate := range candidates {
|
|
74
|
+
if wanted[canonicalRepoPath(candidate.Path)] {
|
|
75
|
+
selected = append(selected, i)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return selected
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func candidateAtPath(candidates []runner.RepoCandidate, path string) bool {
|
|
82
|
+
path = canonicalRepoPath(path)
|
|
83
|
+
for _, candidate := range candidates {
|
|
84
|
+
if canonicalRepoPath(candidate.Path) == path {
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func mergeRepoCandidateNames(candidates []runner.RepoCandidate, names map[string]string) []runner.RepoCandidate {
|
|
92
|
+
out := make([]runner.RepoCandidate, len(candidates))
|
|
93
|
+
copy(out, candidates)
|
|
94
|
+
for i := range out {
|
|
95
|
+
if name := names[canonicalRepoPath(out[i].Path)]; name != "" {
|
|
96
|
+
out[i].Name = name
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func validateAddedRepo(candidate runner.RepoCandidate, candidates []runner.RepoCandidate, registered []repo.Info) error {
|
|
103
|
+
candidate.Name = strings.TrimSpace(candidate.Name)
|
|
104
|
+
candidate.Path = canonicalRepoPath(candidate.Path)
|
|
105
|
+
if candidate.Path == "" {
|
|
106
|
+
return fmt.Errorf("repository path is required")
|
|
107
|
+
}
|
|
108
|
+
if candidate.Name == "" {
|
|
109
|
+
return fmt.Errorf("repository name is required")
|
|
110
|
+
}
|
|
111
|
+
for _, existing := range candidates {
|
|
112
|
+
if canonicalRepoPath(existing.Path) == candidate.Path {
|
|
113
|
+
return fmt.Errorf("repository path %q is already discovered as %q", candidate.Path, existing.Name)
|
|
114
|
+
}
|
|
115
|
+
if strings.TrimSpace(existing.Name) == candidate.Name {
|
|
116
|
+
return fmt.Errorf("repository name %q is already discovered", candidate.Name)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for _, existing := range registered {
|
|
120
|
+
if canonicalRepoPath(existing.Path) == candidate.Path {
|
|
121
|
+
return fmt.Errorf("repository path %q is already registered as %q", candidate.Path, existing.Name)
|
|
122
|
+
}
|
|
123
|
+
if strings.TrimSpace(existing.Name) == candidate.Name {
|
|
124
|
+
return fmt.Errorf("repository name %q is already registered", candidate.Name)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func promptAddedRepo() (runner.RepoCandidate, error) {
|
|
131
|
+
var path, name string
|
|
132
|
+
pathInput := huh.NewInput().Title("Repository path").Value(&path).Validate(func(value string) error {
|
|
133
|
+
if strings.TrimSpace(value) == "" {
|
|
134
|
+
return fmt.Errorf("repository path is required")
|
|
135
|
+
}
|
|
136
|
+
return nil
|
|
137
|
+
})
|
|
138
|
+
nameInput := huh.NewInput().Title("Registered name").Value(&name).Validate(func(value string) error {
|
|
139
|
+
if strings.TrimSpace(value) == "" {
|
|
140
|
+
return fmt.Errorf("repository name is required")
|
|
141
|
+
}
|
|
142
|
+
return nil
|
|
143
|
+
})
|
|
144
|
+
if err := huh.NewForm(huh.NewGroup(pathInput, nameInput)).Run(); err != nil {
|
|
145
|
+
return runner.RepoCandidate{}, err
|
|
146
|
+
}
|
|
147
|
+
return runner.RepoCandidate{Name: strings.TrimSpace(name), Path: canonicalRepoPath(path)}, nil
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type repoSelectionPrompt func([]runner.RepoCandidate, []int) ([]int, error)
|
|
151
|
+
type repoAddPrompt func() (runner.RepoCandidate, error)
|
|
152
|
+
|
|
153
|
+
func selectReposInteractive(ctx context.Context, c *server.Client, candidates []runner.RepoCandidate, registered []repo.Info) ([]runner.RepoCandidate, []int, error) {
|
|
154
|
+
return selectReposInteractiveWithPrompts(ctx, c, candidates, registered, promptRepoSelection, promptAddedRepo)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func promptRepoSelection(candidates []runner.RepoCandidate, selected []int) ([]int, error) {
|
|
158
|
+
pick := huh.NewForm(huh.NewGroup(repoMultiSelect(repoSelectionOptions(candidates), &selected)))
|
|
159
|
+
if err := pick.Run(); err != nil {
|
|
160
|
+
return nil, err
|
|
161
|
+
}
|
|
162
|
+
return selected, nil
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func selectReposInteractiveWithPrompts(ctx context.Context, c *server.Client, candidates []runner.RepoCandidate, registered []repo.Info, selectPrompt repoSelectionPrompt, addPrompt repoAddPrompt) ([]runner.RepoCandidate, []int, error) {
|
|
166
|
+
selectedPaths := []string{}
|
|
167
|
+
nameOverrides := map[string]string{}
|
|
168
|
+
addedCandidates := []runner.RepoCandidate{}
|
|
169
|
+
for {
|
|
170
|
+
candidates = mergeRepoCandidateNames(candidates, nameOverrides)
|
|
171
|
+
selected, err := selectPrompt(candidates, selectedRepoIndices(candidates, selectedPaths))
|
|
172
|
+
if err != nil {
|
|
173
|
+
return nil, nil, err
|
|
174
|
+
}
|
|
175
|
+
selectedPaths = selectedRepoPaths(candidates, selected)
|
|
176
|
+
add := false
|
|
177
|
+
for _, value := range selected {
|
|
178
|
+
if value == addRepositorySelection {
|
|
179
|
+
add = true
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if !add {
|
|
184
|
+
if len(selectedPaths) == 0 {
|
|
185
|
+
return nil, nil, fmt.Errorf("select at least one repository")
|
|
186
|
+
}
|
|
187
|
+
return candidates, selectedRepoIndices(candidates, selectedPaths), nil
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
added, err := addPrompt()
|
|
191
|
+
if err != nil {
|
|
192
|
+
return nil, nil, err
|
|
193
|
+
}
|
|
194
|
+
knownCandidates := append(append([]runner.RepoCandidate(nil), candidates...), addedCandidates...)
|
|
195
|
+
if err := validateAddedRepo(added, knownCandidates, registered); err != nil {
|
|
196
|
+
return nil, nil, err
|
|
197
|
+
}
|
|
198
|
+
if err := c.EnsureRepo(ctx, added); err != nil {
|
|
199
|
+
return nil, nil, fmt.Errorf("ensure repository %q: %w", added.Name, err)
|
|
200
|
+
}
|
|
201
|
+
path := canonicalRepoPath(added.Path)
|
|
202
|
+
nameOverrides[path] = added.Name
|
|
203
|
+
addedCandidates = append(addedCandidates, added)
|
|
204
|
+
selectedPaths = append(selectedPaths, path)
|
|
205
|
+
|
|
206
|
+
refreshed, err := c.DiscoverRepos(ctx)
|
|
207
|
+
if err != nil {
|
|
208
|
+
return nil, nil, err
|
|
209
|
+
}
|
|
210
|
+
refreshed = mergeRepoCandidateNames(refreshed, nameOverrides)
|
|
211
|
+
// Preserve selected entries across a runner refresh even if the
|
|
212
|
+
// external runner's listing is briefly eventually consistent.
|
|
213
|
+
for _, old := range candidates {
|
|
214
|
+
oldPath := canonicalRepoPath(old.Path)
|
|
215
|
+
if containsPath(selectedPaths, oldPath) && !candidateAtPath(refreshed, oldPath) {
|
|
216
|
+
refreshed = append(refreshed, old)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if !candidateAtPath(refreshed, path) {
|
|
220
|
+
refreshed = append(refreshed, added)
|
|
221
|
+
}
|
|
222
|
+
candidates = refreshed
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func containsPath(paths []string, want string) bool {
|
|
227
|
+
for _, path := range paths {
|
|
228
|
+
if path == want {
|
|
229
|
+
return true
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
|
|
16
235
|
// loadRepoRegistration asks the selected task plugin for the fields needed by
|
|
17
236
|
// registration. A second pass lets a plugin expose dependent choices after a
|
|
18
237
|
// value such as a project has been supplied.
|
package/cmd/relay-flow/serve.go
CHANGED
|
@@ -565,6 +565,9 @@ func (d *serveDeps) RegisterNodeSession(ctx context.Context, registration runsvc
|
|
|
565
565
|
func (d *serveDeps) DiscoverRepos(ctx context.Context) ([]runner.RepoCandidate, error) {
|
|
566
566
|
return d.repos.Discover(ctx)
|
|
567
567
|
}
|
|
568
|
+
func (d *serveDeps) EnsureRepo(ctx context.Context, candidate runner.RepoCandidate) error {
|
|
569
|
+
return d.repos.EnsureRepo(ctx, candidate.Name, candidate.Path)
|
|
570
|
+
}
|
|
568
571
|
func (d *serveDeps) TaskRegistrationFields(ctx context.Context, values config.RawValues) ([]task.RegistrationField, error) {
|
|
569
572
|
return d.repos.RegistrationFields(ctx, values)
|
|
570
573
|
}
|
package/go.mod
CHANGED
|
@@ -3,6 +3,7 @@ module github.com/rajpopat27/relay-flow
|
|
|
3
3
|
go 1.26.0
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
|
+
github.com/charmbracelet/huh/spinner v0.0.0-20260223110133-9dc45e34a40b
|
|
6
7
|
github.com/cschleiden/go-workflows v1.4.2
|
|
7
8
|
github.com/google/renameio/v2 v2.0.1
|
|
8
9
|
github.com/google/uuid v1.6.0
|
|
@@ -16,6 +17,9 @@ require (
|
|
|
16
17
|
|
|
17
18
|
require (
|
|
18
19
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
20
|
+
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
|
21
|
+
github.com/clipperhouse/stringish v0.1.1 // indirect
|
|
22
|
+
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
|
19
23
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
|
20
24
|
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
|
|
21
25
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
|
@@ -27,15 +31,15 @@ require (
|
|
|
27
31
|
github.com/benbjohnson/clock v1.3.0 // indirect
|
|
28
32
|
github.com/catppuccin/go v0.3.0 // indirect
|
|
29
33
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
|
30
|
-
github.com/charmbracelet/bubbles
|
|
31
|
-
github.com/charmbracelet/bubbletea v1.3.
|
|
32
|
-
github.com/charmbracelet/colorprofile v0.
|
|
34
|
+
github.com/charmbracelet/bubbles v1.0.0 // indirect
|
|
35
|
+
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
|
36
|
+
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
|
33
37
|
github.com/charmbracelet/huh v1.0.0
|
|
34
38
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
|
35
|
-
github.com/charmbracelet/x/ansi v0.
|
|
36
|
-
github.com/charmbracelet/x/cellbuf v0.0.
|
|
39
|
+
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
|
40
|
+
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
|
37
41
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
|
38
|
-
github.com/charmbracelet/x/term v0.2.
|
|
42
|
+
github.com/charmbracelet/x/term v0.2.2 // indirect
|
|
39
43
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
40
44
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
41
45
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
@@ -52,10 +56,10 @@ require (
|
|
|
52
56
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
53
57
|
github.com/jellydator/ttlcache/v3 v3.0.0 // indirect
|
|
54
58
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
|
55
|
-
github.com/lucasb-eyer/go-colorful v1.
|
|
59
|
+
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
|
56
60
|
github.com/mattn/go-isatty v0.0.20
|
|
57
61
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
|
58
|
-
github.com/mattn/go-runewidth v0.0.
|
|
62
|
+
github.com/mattn/go-runewidth v0.0.19 // indirect
|
|
59
63
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
|
60
64
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
|
61
65
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
package/go.sum
CHANGED
|
@@ -14,20 +14,22 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
|
|
|
14
14
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
|
15
15
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
|
16
16
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
17
|
-
github.com/charmbracelet/bubbles
|
|
18
|
-
github.com/charmbracelet/bubbles
|
|
19
|
-
github.com/charmbracelet/bubbletea v1.3.
|
|
20
|
-
github.com/charmbracelet/bubbletea v1.3.
|
|
21
|
-
github.com/charmbracelet/colorprofile v0.
|
|
22
|
-
github.com/charmbracelet/colorprofile v0.
|
|
17
|
+
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
|
18
|
+
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
|
19
|
+
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
|
20
|
+
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
|
21
|
+
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
|
22
|
+
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
|
23
23
|
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
|
|
24
24
|
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
|
25
|
+
github.com/charmbracelet/huh/spinner v0.0.0-20260223110133-9dc45e34a40b h1:deQbW7eR/gYwkXonGX6a1now6H6f8v4kfv0OIKECu0I=
|
|
26
|
+
github.com/charmbracelet/huh/spinner v0.0.0-20260223110133-9dc45e34a40b/go.mod h1:Y68nuKJuC/Q2lmiq18EkHWkVWi2VGLrwaOfOyPKLkkE=
|
|
25
27
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
|
26
28
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
|
27
|
-
github.com/charmbracelet/x/ansi v0.
|
|
28
|
-
github.com/charmbracelet/x/ansi v0.
|
|
29
|
-
github.com/charmbracelet/x/cellbuf v0.0.
|
|
30
|
-
github.com/charmbracelet/x/cellbuf v0.0.
|
|
29
|
+
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
|
30
|
+
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
|
31
|
+
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
|
32
|
+
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
|
31
33
|
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
|
32
34
|
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
|
33
35
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
|
@@ -36,12 +38,18 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payR
|
|
|
36
38
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
|
37
39
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
|
38
40
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
|
39
|
-
github.com/charmbracelet/x/term v0.2.
|
|
40
|
-
github.com/charmbracelet/x/term v0.2.
|
|
41
|
+
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
|
42
|
+
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
|
41
43
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
|
42
44
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
|
43
45
|
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
|
44
46
|
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
|
47
|
+
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
|
48
|
+
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
|
49
|
+
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
|
50
|
+
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
|
51
|
+
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
|
52
|
+
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
|
45
53
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
|
46
54
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
|
47
55
|
github.com/cschleiden/go-workflows v1.4.2 h1:s7wgx3iKvFmwJVzB7wSuUFqbplcpCUGrn8u1J4/T+2c=
|
|
@@ -98,14 +106,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
|
98
106
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
99
107
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|
100
108
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
|
101
|
-
github.com/lucasb-eyer/go-colorful v1.
|
|
102
|
-
github.com/lucasb-eyer/go-colorful v1.
|
|
109
|
+
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
|
110
|
+
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
103
111
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
104
112
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
105
113
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
|
106
114
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
|
107
|
-
github.com/mattn/go-runewidth v0.0.
|
|
108
|
-
github.com/mattn/go-runewidth v0.0.
|
|
115
|
+
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
|
116
|
+
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
|
109
117
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
|
110
118
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
|
111
119
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
|
@@ -124,7 +132,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|
|
124
132
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
125
133
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
126
134
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
127
|
-
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
128
135
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
129
136
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
130
137
|
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
|
@@ -187,7 +187,7 @@ func TestRunBeginsAtStartAndFollowsEntryEdge(t *testing.T) {
|
|
|
187
187
|
|
|
188
188
|
wf := linearWorkflow(false)
|
|
189
189
|
node := wf.Nodes["coding"]
|
|
190
|
-
node.NudgePrompt = "Continue {{ticket}} at {{node}}. Valid next steps: {{nextSteps}}."
|
|
190
|
+
node.NudgePrompt = "Continue {{ticket}} at {{node}} in {{mailbox}}. Valid next steps: {{nextSteps}}."
|
|
191
191
|
wf.Nodes["coding"] = node
|
|
192
192
|
rid, err := startRun(engine, wf)
|
|
193
193
|
if err != nil {
|
|
@@ -221,8 +221,8 @@ func TestRunBeginsAtStartAndFollowsEntryEdge(t *testing.T) {
|
|
|
221
221
|
if call.NudgeTemplate != node.NudgePrompt {
|
|
222
222
|
t.Fatalf("nudge passed to harness = %q, want raw template %q", call.NudgeTemplate, node.NudgePrompt)
|
|
223
223
|
}
|
|
224
|
-
if call.Data.Ticket != "PAY-101" || call.Data.Workflow != wf.Name || call.Data.Repo != "payments" || call.Data.Node != "coding" || call.Data.NextSteps == "" {
|
|
225
|
-
t.Fatalf("nudge prompt data = %+v, want current workflow values", call.Data)
|
|
224
|
+
if call.Data.Ticket != "PAY-101" || call.Data.Workflow != wf.Name || call.Data.Repo != "payments" || call.Data.Node != "coding" || call.Data.Mailbox != "PAY-101-coding" || call.Data.NextSteps == "" {
|
|
225
|
+
t.Fatalf("nudge prompt data = %+v, want current workflow and mailbox values", call.Data)
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// Pre-edge gate: before following the start edge the run ensures the
|
|
@@ -14,7 +14,7 @@ import (
|
|
|
14
14
|
"github.com/rajpopat27/relay-flow/internal/workflow"
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
const configuredPlugin = "relay-flow-plugin@0.2.
|
|
17
|
+
const configuredPlugin = "relay-flow-plugin@0.2.9-alpha"
|
|
18
18
|
|
|
19
19
|
func TestBuildCommandArgv(t *testing.T) {
|
|
20
20
|
t.Setenv("RELAY_FLOW_HOME", "/var/lib/relay-flow-test")
|
|
@@ -71,12 +71,12 @@ func TestRenderPromptTemplatesExposeAllValues(t *testing.T) {
|
|
|
71
71
|
Node: "review", NodeType: workflow.NodeHITL, Agent: "build", NodeDescription: "Review it.",
|
|
72
72
|
NextSteps: "end (when: approved)", Mailbox: "PAY-234",
|
|
73
73
|
}
|
|
74
|
-
nudge := "nudge {{taskSystem}}|{{ticket}}|{{workflow}}|{{repo}}|{{node}}|{{nextSteps}}"
|
|
74
|
+
nudge := "nudge {{taskSystem}}|{{ticket}}|{{workflow}}|{{repo}}|{{node}}|{{mailbox}}|{{nextSteps}}"
|
|
75
75
|
initial, err := h.RenderPrompt(harness.PromptInitial, data, nudge)
|
|
76
76
|
if err != nil {
|
|
77
77
|
t.Fatal(err)
|
|
78
78
|
}
|
|
79
|
-
wantInitial := "initial linear|PAY-101|basicFlow|payments|review|hitl|build|Review it.|end (when: approved)|PAY-234\n\nReturn the complete report directly. Relay-flow will show a native TUI approval dialog after the report is valid. Do not use OpenCode's Question tool for relay-flow approval.\n\nnudge linear|PAY-101|basicFlow|payments|review|end (when: approved)"
|
|
79
|
+
wantInitial := "initial linear|PAY-101|basicFlow|payments|review|hitl|build|Review it.|end (when: approved)|PAY-234\n\nReturn the complete report directly. Relay-flow will show a native TUI approval dialog after the report is valid. Do not use OpenCode's Question tool for relay-flow approval.\n\nnudge linear|PAY-101|basicFlow|payments|review|PAY-234|end (when: approved)"
|
|
80
80
|
if initial != wantInitial {
|
|
81
81
|
t.Fatalf("initial prompt = %q, want %q", initial, wantInitial)
|
|
82
82
|
}
|
|
@@ -84,7 +84,7 @@ func TestRenderPromptTemplatesExposeAllValues(t *testing.T) {
|
|
|
84
84
|
if err != nil {
|
|
85
85
|
t.Fatal(err)
|
|
86
86
|
}
|
|
87
|
-
if want := "feedback PAY-234\n\nReturn the complete report directly. Relay-flow will show a native TUI approval dialog after the report is valid. Do not use OpenCode's Question tool for relay-flow approval.\n\nnudge linear|PAY-101|basicFlow|payments|review|end (when: approved)"; feedback != want {
|
|
87
|
+
if want := "feedback PAY-234\n\nReturn the complete report directly. Relay-flow will show a native TUI approval dialog after the report is valid. Do not use OpenCode's Question tool for relay-flow approval.\n\nnudge linear|PAY-101|basicFlow|payments|review|PAY-234|end (when: approved)"; feedback != want {
|
|
88
88
|
t.Fatalf("feedback prompt = %q, want %q", feedback, want)
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -23,13 +23,13 @@ func TestPiRenderPromptSubstitutesInitialAndFeedbackData(t *testing.T) {
|
|
|
23
23
|
NextSteps: "review (when: ready)",
|
|
24
24
|
Mailbox: "PAY-234",
|
|
25
25
|
}
|
|
26
|
-
nudge := "nudge {{taskSystem}}|{{ticket}}|{{workflow}}|{{repo}}|{{node}}|{{nextSteps}}"
|
|
26
|
+
nudge := "nudge {{taskSystem}}|{{ticket}}|{{workflow}}|{{repo}}|{{node}}|{{mailbox}}|{{nextSteps}}"
|
|
27
27
|
|
|
28
28
|
initial, err := h.RenderPrompt(harness.PromptInitial, data, nudge)
|
|
29
29
|
if err != nil {
|
|
30
30
|
t.Fatalf("RenderPrompt(initial): %v", err)
|
|
31
31
|
}
|
|
32
|
-
wantInitial := "Task system: jira\nUse the jira tools to read the parent ticket PAY-101.\n\nYour mailbox is PAY-234. Read its description and comments for node instructions and feedback.\n\nKeep the summary brief, and make the feedback as detailed and actionable as possible for the next agent.\n\nnudge jira|PAY-101|basicFlow|payments|implement|review (when: ready)"
|
|
32
|
+
wantInitial := "Task system: jira\nUse the jira tools to read the parent ticket PAY-101.\n\nYour mailbox is PAY-234. Read its description and comments for node instructions and feedback.\n\nKeep the summary brief, and make the feedback as detailed and actionable as possible for the next agent.\n\nnudge jira|PAY-101|basicFlow|payments|implement|PAY-234|review (when: ready)"
|
|
33
33
|
if initial != wantInitial {
|
|
34
34
|
t.Fatalf("initial prompt = %q, want %q", initial, wantInitial)
|
|
35
35
|
}
|
|
@@ -38,7 +38,7 @@ func TestPiRenderPromptSubstitutesInitialAndFeedbackData(t *testing.T) {
|
|
|
38
38
|
if err != nil {
|
|
39
39
|
t.Fatalf("RenderPrompt(feedback): %v", err)
|
|
40
40
|
}
|
|
41
|
-
wantFeedback := "New feedback was added to the comments section of your mailbox subtask PAY-234. Read it.\n\nnudge jira|PAY-101|basicFlow|payments|implement|review (when: ready)"
|
|
41
|
+
wantFeedback := "New feedback was added to the comments section of your mailbox subtask PAY-234. Read it.\n\nnudge jira|PAY-101|basicFlow|payments|implement|PAY-234|review (when: ready)"
|
|
42
42
|
if feedback != wantFeedback {
|
|
43
43
|
t.Fatalf("feedback prompt = %q, want %q", feedback, wantFeedback)
|
|
44
44
|
}
|
package/internal/repo/service.go
CHANGED
|
@@ -4,6 +4,7 @@ import (
|
|
|
4
4
|
"context"
|
|
5
5
|
"fmt"
|
|
6
6
|
"path/filepath"
|
|
7
|
+
"strings"
|
|
7
8
|
|
|
8
9
|
"github.com/rajpopat27/relay-flow/internal/config"
|
|
9
10
|
"github.com/rajpopat27/relay-flow/internal/harness"
|
|
@@ -70,6 +71,25 @@ func (s *Service) Discover(ctx context.Context) ([]runner.RepoCandidate, error)
|
|
|
70
71
|
return s.runner.DiscoverRepos(ctx)
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
// EnsureRepo makes the runner resource for a repository available without
|
|
75
|
+
// changing relay-flow's machine configuration. Runner adapters that expose
|
|
76
|
+
// RepoRegistrar perform their idempotent external provisioning; other
|
|
77
|
+
// runners validate the repository through the existing read-only contract.
|
|
78
|
+
func (s *Service) EnsureRepo(ctx context.Context, name, path string) error {
|
|
79
|
+
name = strings.TrimSpace(name)
|
|
80
|
+
path = strings.TrimSpace(path)
|
|
81
|
+
if name == "" {
|
|
82
|
+
return fmt.Errorf("repo: name is required")
|
|
83
|
+
}
|
|
84
|
+
if path == "" {
|
|
85
|
+
return fmt.Errorf("repo %q: path is required", name)
|
|
86
|
+
}
|
|
87
|
+
if registrar, ok := s.runner.(runner.RepoRegistrar); ok {
|
|
88
|
+
return registrar.EnsureRepo(ctx, name, path)
|
|
89
|
+
}
|
|
90
|
+
return s.runner.ValidateRepo(ctx, name, path)
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
// RequiredRepoKeys delegates to the task factory's method of the same name.
|
|
74
94
|
func (s *Service) RequiredRepoKeys() []string {
|
|
75
95
|
keys, err := task.RequiredRepoKeys(s.taskPlugin)
|
|
@@ -95,9 +115,14 @@ type RegisterInput struct {
|
|
|
95
115
|
// connectivity, duplicate names, canonical paths, and the task plugin's
|
|
96
116
|
// registration identity before atomically writing machine config.
|
|
97
117
|
func (s *Service) Register(ctx context.Context, input RegisterInput) (Info, error) {
|
|
118
|
+
input.Name = strings.TrimSpace(input.Name)
|
|
119
|
+
input.Path = strings.TrimSpace(input.Path)
|
|
98
120
|
if input.Name == "" {
|
|
99
121
|
return Info{}, fmt.Errorf("repo: name is required")
|
|
100
122
|
}
|
|
123
|
+
if input.Path == "" {
|
|
124
|
+
return Info{}, fmt.Errorf("repo %q: path is required", input.Name)
|
|
125
|
+
}
|
|
101
126
|
cfg, err := config.LoadMachine(s.cfgPath)
|
|
102
127
|
if err != nil {
|
|
103
128
|
return Info{}, err
|
|
@@ -154,8 +179,10 @@ func (s *Service) Register(ctx context.Context, input RegisterInput) (Info, erro
|
|
|
154
179
|
return Info{}, fmt.Errorf("repo %q: task registration identity already used by repo %q", input.Name, name)
|
|
155
180
|
}
|
|
156
181
|
}
|
|
157
|
-
// Runner validates the repo
|
|
158
|
-
|
|
182
|
+
// Runner validates or provisions the repo, depending on the adapter
|
|
183
|
+
// capability. Provisioning is idempotent so explicit registration and the
|
|
184
|
+
// interactive Add action share one runner-owned path.
|
|
185
|
+
if err := s.EnsureRepo(ctx, input.Name, input.Path); err != nil {
|
|
159
186
|
return Info{}, fmt.Errorf("repo %q: runner validation: %w", input.Name, err)
|
|
160
187
|
}
|
|
161
188
|
// Task-system connectivity: construct the repo-bound System.
|
|
@@ -245,5 +272,9 @@ func canonicalPath(p string) string {
|
|
|
245
272
|
if err != nil {
|
|
246
273
|
return filepath.Clean(p)
|
|
247
274
|
}
|
|
248
|
-
|
|
275
|
+
abs = filepath.Clean(abs)
|
|
276
|
+
if resolved, err := filepath.EvalSymlinks(abs); err == nil {
|
|
277
|
+
return filepath.Clean(resolved)
|
|
278
|
+
}
|
|
279
|
+
return abs
|
|
249
280
|
}
|
|
@@ -23,6 +23,17 @@ type fakeRunnerDiscovery struct {
|
|
|
23
23
|
validErr error
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
type fakeRunnerRegistrar struct {
|
|
27
|
+
*fakeRunnerDiscovery
|
|
28
|
+
ensureErr error
|
|
29
|
+
ensureCalls []runner.RepoCandidate
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func (f *fakeRunnerRegistrar) EnsureRepo(_ context.Context, name, path string) error {
|
|
33
|
+
f.ensureCalls = append(f.ensureCalls, runner.RepoCandidate{Name: name, Path: path})
|
|
34
|
+
return f.ensureErr
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
func (f *fakeRunnerDiscovery) DiscoverRepos(context.Context) ([]runner.RepoCandidate, error) {
|
|
27
38
|
return f.candidates, nil
|
|
28
39
|
}
|
|
@@ -228,6 +239,17 @@ func TestRegisterValidatesRunnerRepo(t *testing.T) {
|
|
|
228
239
|
}
|
|
229
240
|
}
|
|
230
241
|
|
|
242
|
+
func TestRegisterUsesRunnerRepoProvisioningCapability(t *testing.T) {
|
|
243
|
+
rn := &fakeRunnerRegistrar{fakeRunnerDiscovery: &fakeRunnerDiscovery{validErr: errInvalidRepo{}}}
|
|
244
|
+
fx := newServiceFixture(t, rn)
|
|
245
|
+
if _, err := fx.svc.Register(context.Background(), repo.RegisterInput{Name: "payments", Path: "/srv/payments", TaskConfig: config.RawValues{"project": "P", "component": "c"}}); err != nil {
|
|
246
|
+
t.Fatal(err)
|
|
247
|
+
}
|
|
248
|
+
if len(rn.ensureCalls) != 1 || rn.ensureCalls[0] != (runner.RepoCandidate{Name: "payments", Path: "/srv/payments"}) {
|
|
249
|
+
t.Fatalf("EnsureRepo calls = %+v", rn.ensureCalls)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
231
253
|
func TestRegisterValidatesTaskConnectivity(t *testing.T) {
|
|
232
254
|
// The task factory's New is invoked to validate connectivity; its error
|
|
233
255
|
// must reject registration before persisting.
|