relay-flow 0.2.7-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 +76 -37
- package/cmd/relay-flow/commands_test.go +199 -19
- package/cmd/relay-flow/main.go +183 -144
- package/cmd/relay-flow/onboarding.go +344 -0
- package/cmd/relay-flow/onboarding_test.go +515 -0
- package/cmd/relay-flow/repo_registration.go +454 -0
- package/cmd/relay-flow/serve.go +5 -2
- package/examples/config-reference.yaml +7 -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 +47 -3
- package/internal/repo/service_test.go +36 -1
- 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 +65 -0
- package/internal/server/client.go +40 -2
- package/internal/server/fixture_test.go +27 -16
- package/internal/server/server.go +80 -4
- package/internal/task/beads/beads.go +7 -2
- package/internal/task/beads/beads_test.go +13 -0
- package/internal/task/factory.go +9 -7
- package/internal/task/jira/auth_test.go +9 -1
- package/internal/task/jira/filters_test.go +2 -2
- package/internal/task/jira/helpers_test.go +13 -0
- package/internal/task/jira/jira.go +233 -44
- package/internal/task/jira/lifecycle_inheritance_test.go +5 -5
- package/internal/task/jira/rest/client.go +73 -31
- package/internal/task/jira/rest/client_test.go +25 -0
- package/internal/task/jira/status_defaults_test.go +134 -0
- package/internal/task/jira/templates_test.go +2 -2
- package/internal/task/jira/testdata/jira_search_issues.json +4 -4
- package/internal/task/jira/transition_defaults_test.go +8 -12
- package/internal/task/jira/validation_test.go +3 -1
- package/internal/task/registration.go +59 -0
- package/internal/workflow/workflow.go +4 -1
- package/internal/workflow/workflow_test.go +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,13 +24,13 @@ same `relay-flow-plugin` package with host-specific entrypoints.
|
|
|
24
24
|
**OpenCode**
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
|
-
opencode plugin relay-flow-plugin@0.2.
|
|
27
|
+
opencode plugin relay-flow-plugin@0.2.9-alpha
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
**Pi**
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
|
-
pi install npm:relay-flow-plugin@0.2.
|
|
33
|
+
pi install npm:relay-flow-plugin@0.2.9-alpha
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Pi loads the package's `pi.ts` extension from its manifest. Do not add
|
|
@@ -38,39 +38,40 @@ Pi loads the package's `pi.ts` extension from its manifest. Do not add
|
|
|
38
38
|
|
|
39
39
|
### 3. Initialize relay-flow
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
uses Jira, Orca, OpenCode, and the default embedded executor:
|
|
41
|
+
For a first-time interactive setup, run:
|
|
43
42
|
|
|
44
43
|
```sh
|
|
45
|
-
relay-flow init
|
|
46
|
-
--task-plugin jira \
|
|
47
|
-
--runner-plugin orca \
|
|
48
|
-
--harness-plugin opencode
|
|
44
|
+
relay-flow init
|
|
49
45
|
```
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
Relay-flow selects the task system, runner, harness, and durable executor,
|
|
48
|
+
runs the selected task plugin's authentication flow inline, and then asks
|
|
49
|
+
whether repositories should be registered now. Choosing no completes setup
|
|
50
|
+
without starting a server. Choosing yes starts (or reuses) the background
|
|
51
|
+
server, waits for its Unix socket, and opens the existing repository
|
|
52
|
+
registration form. After registration, the server remains running; stop it
|
|
53
|
+
with `relay-flow stop`.
|
|
56
54
|
|
|
57
|
-
|
|
55
|
+
The equivalent non-interactive setup keeps the independent commands available
|
|
56
|
+
for scripts and existing installations. This example uses Jira, Orca,
|
|
57
|
+
OpenCode, and the default embedded executor:
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
|
+
relay-flow init \
|
|
61
|
+
--task-plugin jira \
|
|
62
|
+
--runner-plugin orca \
|
|
63
|
+
--harness-plugin opencode
|
|
60
64
|
relay-flow task auth
|
|
65
|
+
relay-flow serve --background
|
|
61
66
|
```
|
|
62
67
|
|
|
63
|
-
For Beads, skip
|
|
68
|
+
For Beads, skip `task auth` and initialize/authenticate the Beads workspace
|
|
64
69
|
with `bd` and, when needed, Dolt. See [Beads task system](#beads-task-system)
|
|
65
|
-
below.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```sh
|
|
70
|
-
relay-flow serve --background
|
|
71
|
-
```
|
|
70
|
+
below. The default executor is `goworkflows` with SQLite. To use Temporal
|
|
71
|
+
instead, add `--executor-plugin temporal`, `--temporal-address <host:port>`,
|
|
72
|
+
and `--temporal-namespace <name>` to the command.
|
|
72
73
|
|
|
73
|
-
###
|
|
74
|
+
### 4. Register a repository later
|
|
74
75
|
|
|
75
76
|
The repository must already exist in the selected runner. For Orca:
|
|
76
77
|
|
|
@@ -81,9 +82,11 @@ relay-flow repo register
|
|
|
81
82
|
|
|
82
83
|
For Herdr, register the repository path directly; relay-flow creates ticket
|
|
83
84
|
worktrees lazily. The interactive registration asks for the task-system values
|
|
84
|
-
required by the selected task plugin.
|
|
85
|
+
required by the selected task plugin. `repo register` remains the standalone
|
|
86
|
+
escape hatch for adding repositories after initialization and requires a
|
|
87
|
+
running `relay-flow serve --background`.
|
|
85
88
|
|
|
86
|
-
###
|
|
89
|
+
### 5. Submit a workflow
|
|
87
90
|
|
|
88
91
|
```sh
|
|
89
92
|
relay-flow workflow submit --file examples/minimal-jira-task-workflow.yaml
|
|
@@ -154,7 +157,7 @@ OpenCode plugin configuration uses both entrypoints. The server entrypoint is li
|
|
|
154
157
|
```json
|
|
155
158
|
{
|
|
156
159
|
"$schema": "https://opencode.ai/config.json",
|
|
157
|
-
"plugin": ["relay-flow-plugin@0.2.
|
|
160
|
+
"plugin": ["relay-flow-plugin@0.2.9-alpha"]
|
|
158
161
|
}
|
|
159
162
|
```
|
|
160
163
|
|
|
@@ -163,7 +166,7 @@ The native HITL approval entrypoint is listed in `.opencode/tui.json`:
|
|
|
163
166
|
```json
|
|
164
167
|
{
|
|
165
168
|
"$schema": "https://opencode.ai/tui.json",
|
|
166
|
-
"plugin": ["relay-flow-plugin@0.2.
|
|
169
|
+
"plugin": ["relay-flow-plugin@0.2.9-alpha"]
|
|
167
170
|
}
|
|
168
171
|
```
|
|
169
172
|
|
|
@@ -179,7 +182,7 @@ Pi plugin: install the same published package manually in Pi's global package
|
|
|
179
182
|
settings before starting a Pi harness session:
|
|
180
183
|
|
|
181
184
|
```sh
|
|
182
|
-
pi install npm:relay-flow-plugin@0.2.
|
|
185
|
+
pi install npm:relay-flow-plugin@0.2.9-alpha
|
|
183
186
|
```
|
|
184
187
|
|
|
185
188
|
Relay-flow does not install or configure the package automatically. Pi resolves
|
|
@@ -199,10 +202,30 @@ internal and is never part of either plugin payload.
|
|
|
199
202
|
|
|
200
203
|
```sh
|
|
201
204
|
relay-flow init
|
|
202
|
-
relay-flow task auth
|
|
203
205
|
```
|
|
204
206
|
|
|
205
|
-
`init` selects the task system, runner, harness,
|
|
207
|
+
On a new interactive home, `init` selects the task system, runner, harness,
|
|
208
|
+
and durable executor, runs the selected task plug-in's authentication flow,
|
|
209
|
+
and asks whether to register repositories. It writes machine config and the
|
|
210
|
+
selected execution backend only after authentication succeeds. A skipped
|
|
211
|
+
repository setup does not start the server; selecting it starts or reuses the
|
|
212
|
+
background server, waits for socket readiness, and invokes the same
|
|
213
|
+
`repo register` form described below. The server remains running after a
|
|
214
|
+
successful guided registration.
|
|
215
|
+
|
|
216
|
+
For scripts and existing installations, keep the independent sequence:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
relay-flow init --task-plugin jira --runner-plugin orca --harness-plugin opencode
|
|
220
|
+
relay-flow task auth --site https://company.atlassian.net --email you@example.com --token "$JIRA_API_TOKEN"
|
|
221
|
+
relay-flow serve --background
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`task auth` delegates to the selected task plug-in. Jira prompts for its site,
|
|
225
|
+
email, and masked API token when flags are omitted, validates `/myself`, and
|
|
226
|
+
owns the system-wide `credentials.yaml`. A normal init rerun refuses existing
|
|
227
|
+
state. `relay-flow init --force` updates safe stopped instances while
|
|
228
|
+
preserving durable and repo state; it does not launch repository onboarding.
|
|
206
229
|
|
|
207
230
|
For Beads, select the plugin explicitly when scripting setup:
|
|
208
231
|
|
|
@@ -234,9 +257,11 @@ workflows/<name>.yaml 0644 submitted workflow definitions
|
|
|
234
257
|
|
|
235
258
|
### Register a repo
|
|
236
259
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
260
|
+
The guided first-run flow can start the server and open repository registration
|
|
261
|
+
for you. `repo register` remains a server-backed standalone command for later
|
|
262
|
+
additions: if no server is running, start it with
|
|
263
|
+
`relay-flow serve --background` (or rerun interactive `relay-flow init` for a
|
|
264
|
+
new home).
|
|
240
265
|
|
|
241
266
|
```sh
|
|
242
267
|
relay-flow serve --background
|
|
@@ -255,11 +280,23 @@ orca repo add --path /work/payments # displayName becomes "payments"
|
|
|
255
280
|
relay-flow repo register
|
|
256
281
|
```
|
|
257
282
|
|
|
258
|
-
Shows a multi-select titled `Select repositories`; use Space to select Orca repos and Enter to confirm. Enter the Jira project once. Each repo is registered sequentially with its Orca name/path and a Jira component derived from that repo name. Earlier registrations remain if a later one fails.
|
|
283
|
+
Shows a multi-select titled `Select repositories`; use Space to select Orca repos and Enter to confirm. Enter the Jira project once, then choose the repository's start, work, and end statuses from the statuses discovered for that project. Each repo is registered sequentially with its Orca name/path and a Jira component derived from that repo name. Earlier registrations remain if a later one fails.
|
|
259
284
|
|
|
260
285
|
Each Jira poll uses REST v3 enhanced search and requests linked-issue status with the candidate fields. Tickets with any unfinished inward `Blocks` issue are filtered before routing; no per-ticket blocker lookup is made.
|
|
261
286
|
|
|
262
|
-
For scripted Jira registration,
|
|
287
|
+
For scripted Jira registration, pass all three repository status defaults explicitly:
|
|
288
|
+
|
|
289
|
+
```sh
|
|
290
|
+
relay-flow repo register \
|
|
291
|
+
--name payments \
|
|
292
|
+
--path /work/payments \
|
|
293
|
+
--set project=PAY \
|
|
294
|
+
--set statusDefaults.start="Open" \
|
|
295
|
+
--set statusDefaults.work="Working" \
|
|
296
|
+
--set statusDefaults.end="Closed"
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The values must be statuses available to the Jira project. Component is always derived from `--name` and cannot be overridden. Registration is rejected while another repo already holds the same canonical task scope.
|
|
263
300
|
|
|
264
301
|
### Beads task system
|
|
265
302
|
|
|
@@ -357,8 +394,9 @@ Task, runner, harness, and durable executor plugins are selected machine-wide. A
|
|
|
357
394
|
|
|
358
395
|
### Run
|
|
359
396
|
|
|
360
|
-
The
|
|
361
|
-
|
|
397
|
+
The guided first-run flow starts or reuses the server before `repo register`.
|
|
398
|
+
Outside that flow, `repo register` and `workflow submit` require the server to
|
|
399
|
+
already be running; the same process polls repos and drives runs.
|
|
362
400
|
|
|
363
401
|
```sh
|
|
364
402
|
relay-flow serve # normal start; requires an initialized database
|
|
@@ -443,6 +481,7 @@ Rules enforced at submit:
|
|
|
443
481
|
- The graph must be fully reachable from `start`; unknown fields are rejected; `runnerPlugin`/`harnessPlugin`/`closeOn`/legacy `tasks`/`runner` keys are rejected.
|
|
444
482
|
- Only `agent` and `hitl` nodes receive mailbox subtasks; `start` and `end` never do.
|
|
445
483
|
- `cleanupRunnerOnEnd` is the only workflow cleanup knob and takes priority over terminal retention after `end`; the word `terminal` refers to runner terminals only.
|
|
484
|
+
- `nudgePrompt` supports `{{taskSystem}}`, `{{ticket}}`, `{{workflow}}`, `{{repo}}`, `{{node}}`, `{{mailbox}}`, and `{{nextSteps}}`; unsupported variables are rejected at submission.
|
|
446
485
|
|
|
447
486
|
### Task-config merge
|
|
448
487
|
|
|
@@ -631,19 +631,45 @@ func insertRun(t *testing.T, path, id, state string) {
|
|
|
631
631
|
// 8.4: fully-flagged repo register never prompts and produces the same
|
|
632
632
|
// repo entry the interactive run would post.
|
|
633
633
|
type registerServer struct {
|
|
634
|
-
ackServer
|
|
635
|
-
fields
|
|
636
|
-
gotInput
|
|
637
|
-
gotInputs
|
|
638
|
-
successful
|
|
639
|
-
failName
|
|
640
|
-
calls
|
|
634
|
+
ackServer // embed for the unreachable stubs
|
|
635
|
+
fields []string
|
|
636
|
+
gotInput *repo.RegisterInput
|
|
637
|
+
gotInputs []repo.RegisterInput
|
|
638
|
+
successful []repo.RegisterInput
|
|
639
|
+
failName string
|
|
640
|
+
calls int
|
|
641
|
+
candidates []runner.RepoCandidate
|
|
642
|
+
ensureCalls []runner.RepoCandidate
|
|
643
|
+
registered []repo.Info
|
|
641
644
|
}
|
|
642
645
|
|
|
643
|
-
func (s *registerServer)
|
|
646
|
+
func (s *registerServer) TaskRegistrationFields(context.Context, config.RawValues) ([]task.RegistrationField, error) {
|
|
644
647
|
s.calls++
|
|
645
|
-
|
|
648
|
+
out := make([]task.RegistrationField, 0, len(s.fields))
|
|
649
|
+
for _, field := range s.fields {
|
|
650
|
+
out = append(out, task.RegistrationField{Key: field, Title: field, Derived: field == "component"})
|
|
651
|
+
}
|
|
652
|
+
return out, nil
|
|
653
|
+
}
|
|
654
|
+
func (s *registerServer) DiscoverRepos(context.Context) ([]runner.RepoCandidate, error) {
|
|
655
|
+
return append([]runner.RepoCandidate(nil), s.candidates...), nil
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
func (s *registerServer) EnsureRepo(_ context.Context, candidate runner.RepoCandidate) error {
|
|
659
|
+
s.ensureCalls = append(s.ensureCalls, candidate)
|
|
660
|
+
for _, existing := range s.candidates {
|
|
661
|
+
if existing.Path == candidate.Path {
|
|
662
|
+
return nil
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
s.candidates = append(s.candidates, candidate)
|
|
666
|
+
return nil
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
func (s *registerServer) ListRepos(context.Context) ([]repo.Info, error) {
|
|
670
|
+
return append([]repo.Info(nil), s.registered...), nil
|
|
646
671
|
}
|
|
672
|
+
|
|
647
673
|
func (s *registerServer) RegisterRepo(_ context.Context, in repo.RegisterInput) (repo.Info, error) {
|
|
648
674
|
s.calls++
|
|
649
675
|
cp := in
|
|
@@ -653,7 +679,9 @@ func (s *registerServer) RegisterRepo(_ context.Context, in repo.RegisterInput)
|
|
|
653
679
|
return repo.Info{}, errors.New("registration failed")
|
|
654
680
|
}
|
|
655
681
|
s.successful = append(s.successful, cp)
|
|
656
|
-
|
|
682
|
+
info := repo.Info{Name: in.Name, Path: in.Path, TaskConfig: in.TaskConfig}
|
|
683
|
+
s.registered = append(s.registered, info)
|
|
684
|
+
return info, nil
|
|
657
685
|
}
|
|
658
686
|
|
|
659
687
|
func serveRegister(t *testing.T, home string, fields []string) *registerServer {
|
|
@@ -767,7 +795,152 @@ func TestRepoRegisterFlagsNonInteractive(t *testing.T) {
|
|
|
767
795
|
}
|
|
768
796
|
}
|
|
769
797
|
|
|
770
|
-
func
|
|
798
|
+
func TestRegistrationSelectRequiresExplicitChoiceWithoutDefault(t *testing.T) {
|
|
799
|
+
field := task.RegistrationField{Key: "statusDefaults.start", Options: []string{"Open", "Working"}}
|
|
800
|
+
options := registrationSelectOptions(field)
|
|
801
|
+
if len(options) != 3 {
|
|
802
|
+
t.Fatalf("options = %d, want empty choice plus two statuses", len(options))
|
|
803
|
+
}
|
|
804
|
+
var selected string
|
|
805
|
+
selectField := huh.NewSelect[string]().Options(options...).Value(&selected)
|
|
806
|
+
var out bytes.Buffer
|
|
807
|
+
if err := selectField.RunAccessible(&out, strings.NewReader("1\n")); err != nil {
|
|
808
|
+
t.Fatal(err)
|
|
809
|
+
}
|
|
810
|
+
if selected != "" {
|
|
811
|
+
t.Fatalf("empty choice selected %q, want empty value", selected)
|
|
812
|
+
}
|
|
813
|
+
if err := selectField.RunAccessible(&out, strings.NewReader("2\n")); err != nil {
|
|
814
|
+
t.Fatal(err)
|
|
815
|
+
}
|
|
816
|
+
if selected != "Open" {
|
|
817
|
+
t.Fatalf("explicit status selection = %q, want Open", selected)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
func TestRegistrationTaskConfigBuildsNestedStatusDefaults(t *testing.T) {
|
|
822
|
+
registration := task.Registration{Fields: []task.RegistrationField{
|
|
823
|
+
{Key: "project", Title: "Project"},
|
|
824
|
+
{Key: "component", Title: "Component", Derived: true},
|
|
825
|
+
{Key: "statusDefaults.start", Title: "Start", Options: []string{"Open", "Working", "Closed"}},
|
|
826
|
+
{Key: "statusDefaults.work", Title: "Work", Options: []string{"Open", "Working", "Closed"}},
|
|
827
|
+
{Key: "statusDefaults.end", Title: "End", Options: []string{"Open", "Working", "Closed"}},
|
|
828
|
+
}}
|
|
829
|
+
got, err := registrationTaskConfig(registration, kvFlags{
|
|
830
|
+
"project": "PAY",
|
|
831
|
+
"statusDefaults.start": "Open",
|
|
832
|
+
"statusDefaults.work": "Working",
|
|
833
|
+
"statusDefaults.end": "Closed",
|
|
834
|
+
}, "payments")
|
|
835
|
+
if err != nil {
|
|
836
|
+
t.Fatal(err)
|
|
837
|
+
}
|
|
838
|
+
defaults, ok := got["statusDefaults"].(map[string]any)
|
|
839
|
+
if !ok || defaults["start"] != "Open" || defaults["work"] != "Working" || defaults["end"] != "Closed" {
|
|
840
|
+
t.Fatalf("nested statusDefaults = %#v", got["statusDefaults"])
|
|
841
|
+
}
|
|
842
|
+
for _, tc := range []struct {
|
|
843
|
+
name string
|
|
844
|
+
sets kvFlags
|
|
845
|
+
want string
|
|
846
|
+
}{
|
|
847
|
+
{name: "invalid status", sets: kvFlags{"project": "PAY", "statusDefaults.start": "Missing"}, want: "not one of the available choices"},
|
|
848
|
+
{name: "derived component", sets: kvFlags{"project": "PAY", "component": "other"}, want: "derived"},
|
|
849
|
+
} {
|
|
850
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
851
|
+
if _, err := registrationTaskConfig(registration, tc.sets, "payments"); err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
852
|
+
t.Fatalf("registration error = %v, want %q", err, tc.want)
|
|
853
|
+
}
|
|
854
|
+
})
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
func TestRepoAddValidationRejectsDuplicateNormalizedPathAndName(t *testing.T) {
|
|
859
|
+
candidates := []runner.RepoCandidate{{Name: "payments", Path: "/work/payments"}}
|
|
860
|
+
if err := validateAddedRepo(runner.RepoCandidate{Name: "new", Path: "/work/payments/."}, candidates, nil); err == nil || !strings.Contains(err.Error(), "already discovered") {
|
|
861
|
+
t.Fatalf("duplicate discovered path error = %v", err)
|
|
862
|
+
}
|
|
863
|
+
registered := []repo.Info{{Name: "checkout", Path: "/work/checkout"}}
|
|
864
|
+
if err := validateAddedRepo(runner.RepoCandidate{Name: " checkout ", Path: "/work/new"}, candidates, registered); err == nil || !strings.Contains(err.Error(), "already registered") {
|
|
865
|
+
t.Fatalf("duplicate registered name error = %v", err)
|
|
866
|
+
}
|
|
867
|
+
for _, candidate := range []runner.RepoCandidate{{Name: "", Path: "/work/new"}, {Name: "new", Path: ""}} {
|
|
868
|
+
if err := validateAddedRepo(candidate, nil, nil); err == nil {
|
|
869
|
+
t.Fatalf("validateAddedRepo(%+v) accepted missing input", candidate)
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
func TestInteractiveRepoSelectionPreservesSelectionsAndAddsMultiple(t *testing.T) {
|
|
875
|
+
home := t.TempDir()
|
|
876
|
+
deps := serveRegister(t, home, nil)
|
|
877
|
+
deps.candidates = []runner.RepoCandidate{{Name: "existing", Path: "/work/existing"}}
|
|
878
|
+
client := server.NewClient(filepath.Join(home, ".relay-flow", "server.sock"))
|
|
879
|
+
|
|
880
|
+
selectionSteps := [][]int{{0, addRepositorySelection}, {0, 1, addRepositorySelection}, {0, 1, 2}}
|
|
881
|
+
addSteps := []runner.RepoCandidate{
|
|
882
|
+
{Name: "new-one", Path: filepath.Join(t.TempDir(), "new-one")},
|
|
883
|
+
{Name: "new-two", Path: filepath.Join(t.TempDir(), "new-two")},
|
|
884
|
+
}
|
|
885
|
+
selectionCalls := 0
|
|
886
|
+
addCalls := 0
|
|
887
|
+
candidates, selected, err := selectReposInteractiveWithPrompts(
|
|
888
|
+
context.Background(), client, deps.candidates, nil,
|
|
889
|
+
func(got []runner.RepoCandidate, selected []int) ([]int, error) {
|
|
890
|
+
wantSelected := [][]int{nil, {0, 1}, {0, 1, 2}}[selectionCalls]
|
|
891
|
+
if fmt.Sprint(selected) != fmt.Sprint(wantSelected) {
|
|
892
|
+
t.Fatalf("selection %d started with %v, want %v", selectionCalls, selected, wantSelected)
|
|
893
|
+
}
|
|
894
|
+
if selectionCalls >= len(selectionSteps) {
|
|
895
|
+
t.Fatalf("unexpected selection call %d with candidates %+v", selectionCalls, got)
|
|
896
|
+
}
|
|
897
|
+
step := append([]int(nil), selectionSteps[selectionCalls]...)
|
|
898
|
+
selectionCalls++
|
|
899
|
+
return step, nil
|
|
900
|
+
},
|
|
901
|
+
func() (runner.RepoCandidate, error) {
|
|
902
|
+
if addCalls >= len(addSteps) {
|
|
903
|
+
return runner.RepoCandidate{}, errors.New("unexpected add prompt")
|
|
904
|
+
}
|
|
905
|
+
step := addSteps[addCalls]
|
|
906
|
+
addCalls++
|
|
907
|
+
return step, nil
|
|
908
|
+
},
|
|
909
|
+
)
|
|
910
|
+
if err != nil {
|
|
911
|
+
t.Fatal(err)
|
|
912
|
+
}
|
|
913
|
+
if selectionCalls != 3 || addCalls != 2 || len(deps.ensureCalls) != 2 {
|
|
914
|
+
t.Fatalf("selection calls=%d add calls=%d ensure calls=%v", selectionCalls, addCalls, deps.ensureCalls)
|
|
915
|
+
}
|
|
916
|
+
if len(candidates) != 3 || len(selected) != 3 || fmt.Sprint(selected) != "[0 1 2]" {
|
|
917
|
+
t.Fatalf("final candidates=%+v selected=%v", candidates, selected)
|
|
918
|
+
}
|
|
919
|
+
if candidates[1].Name != "new-one" || candidates[2].Name != "new-two" {
|
|
920
|
+
t.Fatalf("final candidate names=%+v", candidates)
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
func TestRepoSelectionOptionsIncludeVisibleAddAction(t *testing.T) {
|
|
925
|
+
options := repoSelectionOptions([]runner.RepoCandidate{{Name: "payments", Path: "/work/payments"}})
|
|
926
|
+
if len(options) != 2 {
|
|
927
|
+
t.Fatalf("options = %d, want candidate plus Add action", len(options))
|
|
928
|
+
}
|
|
929
|
+
var selected []int
|
|
930
|
+
field := repoMultiSelect(options, &selected)
|
|
931
|
+
var out bytes.Buffer
|
|
932
|
+
if err := field.RunAccessible(&out, strings.NewReader("2\n")); err != nil {
|
|
933
|
+
t.Fatal(err)
|
|
934
|
+
}
|
|
935
|
+
if len(selected) != 1 || selected[0] != addRepositorySelection {
|
|
936
|
+
t.Fatalf("selected = %v, want Add action sentinel", selected)
|
|
937
|
+
}
|
|
938
|
+
if !strings.Contains(out.String(), "Add repository") {
|
|
939
|
+
t.Fatalf("output %q missing Add repository", out.String())
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
func TestRepoMultiSelectAndPluginMapping(t *testing.T) {
|
|
771
944
|
selected := []int{0, 1}
|
|
772
945
|
field := repoMultiSelect([]huh.Option[int]{
|
|
773
946
|
huh.NewOption("payments", 0),
|
|
@@ -783,9 +956,10 @@ func TestRepoMultiSelectAndSharedJiraMapping(t *testing.T) {
|
|
|
783
956
|
if fmt.Sprint(selected) != "[0 1]" {
|
|
784
957
|
t.Fatalf("selected = %v, want [0 1]", selected)
|
|
785
958
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
959
|
+
registration := task.Registration{Fields: []task.RegistrationField{
|
|
960
|
+
{Key: "project", Title: "Project"},
|
|
961
|
+
{Key: "component", Title: "Component", Derived: true},
|
|
962
|
+
}}
|
|
789
963
|
|
|
790
964
|
home := t.TempDir()
|
|
791
965
|
deps := serveRegister(t, home, []string{"project", "component"})
|
|
@@ -794,8 +968,8 @@ func TestRepoMultiSelectAndSharedJiraMapping(t *testing.T) {
|
|
|
794
968
|
{Name: "payments", Path: "/srv/payments"},
|
|
795
969
|
{Name: "checkout", Path: "/srv/checkout"},
|
|
796
970
|
}
|
|
797
|
-
if err :=
|
|
798
|
-
|
|
971
|
+
if err := registerSelectedReposDynamic(context.Background(), client, candidates, selected,
|
|
972
|
+
registration, kvFlags{"project": "PAY"}); err != nil {
|
|
799
973
|
t.Fatal(err)
|
|
800
974
|
}
|
|
801
975
|
if len(deps.successful) != 2 {
|
|
@@ -818,8 +992,12 @@ func TestRepoRegistrationPartialFailureKeepsPriorSuccess(t *testing.T) {
|
|
|
818
992
|
{Name: "checkout", Path: "/srv/checkout"},
|
|
819
993
|
{Name: "later", Path: "/srv/later"},
|
|
820
994
|
}
|
|
821
|
-
|
|
822
|
-
|
|
995
|
+
registration := task.Registration{Fields: []task.RegistrationField{
|
|
996
|
+
{Key: "project", Title: "Project"},
|
|
997
|
+
{Key: "component", Title: "Component", Derived: true},
|
|
998
|
+
}}
|
|
999
|
+
err := registerSelectedReposDynamic(context.Background(), client, candidates, []int{0, 1, 2},
|
|
1000
|
+
registration, kvFlags{"project": "PAY"})
|
|
823
1001
|
if err == nil || !strings.Contains(err.Error(), "checkout") {
|
|
824
1002
|
t.Fatalf("partial failure = %v, want failed repo name", err)
|
|
825
1003
|
}
|
|
@@ -1012,7 +1190,9 @@ func (s *ackServer) CancelRun(context.Context, string, string) error { panic("un
|
|
|
1012
1190
|
func (s *ackServer) DiscoverRepos(context.Context) ([]runner.RepoCandidate, error) {
|
|
1013
1191
|
panic("unreachable")
|
|
1014
1192
|
}
|
|
1015
|
-
func (s *ackServer)
|
|
1193
|
+
func (s *ackServer) TaskRegistrationFields(context.Context, config.RawValues) ([]task.RegistrationField, error) {
|
|
1194
|
+
panic("unreachable")
|
|
1195
|
+
}
|
|
1016
1196
|
func (s *ackServer) RegisterRepo(context.Context, repo.RegisterInput) (repo.Info, error) {
|
|
1017
1197
|
panic("unreachable")
|
|
1018
1198
|
}
|