relay-flow 0.2.8-alpha → 0.2.10-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 -35
- package/cmd/relay-flow/commands_test.go +117 -8
- package/cmd/relay-flow/main.go +391 -83
- package/cmd/relay-flow/observability_test.go +204 -0
- package/cmd/relay-flow/onboarding.go +344 -0
- package/cmd/relay-flow/onboarding_test.go +515 -0
- package/cmd/relay-flow/render.go +629 -0
- package/cmd/relay-flow/repo_registration.go +219 -0
- package/cmd/relay-flow/serve.go +35 -1
- package/go.mod +12 -8
- package/go.sum +24 -17
- package/internal/execution/goworkflows/activities.go +9 -0
- package/internal/execution/goworkflows/engine.go +15 -0
- package/internal/execution/goworkflows/engine_test.go +3 -3
- package/internal/execution/goworkflows/interpreter.go +86 -3
- package/internal/execution/goworkflows/projection.go +16 -0
- package/internal/execution/projection/detail_test.go +259 -0
- package/internal/execution/projection/projection.go +290 -6
- package/internal/execution/projection/projection_test.go +3 -2
- package/internal/execution/temporal/activities.go +9 -0
- package/internal/execution/temporal/engine.go +13 -0
- package/internal/execution/temporal/interpreter.go +131 -3
- package/internal/execution/temporal/operations.go +174 -0
- package/internal/execution/temporal/operations_test.go +36 -0
- 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/run/detail.go +277 -0
- package/internal/run/detail_test.go +71 -0
- package/internal/run/run.go +4 -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 +49 -0
- package/internal/server/fixture_test.go +7 -0
- package/internal/server/observability.go +109 -0
- package/internal/server/observability_test.go +60 -0
- package/internal/server/server.go +77 -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
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.10-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.10-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.
|
|
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.
|
|
66
73
|
|
|
67
|
-
###
|
|
68
|
-
|
|
69
|
-
```sh
|
|
70
|
-
relay-flow serve --background
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### 6. Register a repository
|
|
74
|
+
### 4. Register a repository later
|
|
74
75
|
|
|
75
76
|
The repository must already exist in the selected runner. For Orca:
|
|
76
77
|
|
|
@@ -81,13 +82,18 @@ 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
|
|
90
93
|
relay-flow workflow list
|
|
94
|
+
relay-flow workflow list --json # stable automation output
|
|
95
|
+
relay-flow run get --ticket PAY-101 # Argo-shaped human inspection view
|
|
96
|
+
relay-flow run get --ticket PAY-101 --json # stable automation output
|
|
91
97
|
```
|
|
92
98
|
|
|
93
99
|
Replace the example workflow with
|
|
@@ -154,7 +160,7 @@ OpenCode plugin configuration uses both entrypoints. The server entrypoint is li
|
|
|
154
160
|
```json
|
|
155
161
|
{
|
|
156
162
|
"$schema": "https://opencode.ai/config.json",
|
|
157
|
-
"plugin": ["relay-flow-plugin@0.2.
|
|
163
|
+
"plugin": ["relay-flow-plugin@0.2.10-alpha"]
|
|
158
164
|
}
|
|
159
165
|
```
|
|
160
166
|
|
|
@@ -163,7 +169,7 @@ The native HITL approval entrypoint is listed in `.opencode/tui.json`:
|
|
|
163
169
|
```json
|
|
164
170
|
{
|
|
165
171
|
"$schema": "https://opencode.ai/tui.json",
|
|
166
|
-
"plugin": ["relay-flow-plugin@0.2.
|
|
172
|
+
"plugin": ["relay-flow-plugin@0.2.10-alpha"]
|
|
167
173
|
}
|
|
168
174
|
```
|
|
169
175
|
|
|
@@ -179,7 +185,7 @@ Pi plugin: install the same published package manually in Pi's global package
|
|
|
179
185
|
settings before starting a Pi harness session:
|
|
180
186
|
|
|
181
187
|
```sh
|
|
182
|
-
pi install npm:relay-flow-plugin@0.2.
|
|
188
|
+
pi install npm:relay-flow-plugin@0.2.10-alpha
|
|
183
189
|
```
|
|
184
190
|
|
|
185
191
|
Relay-flow does not install or configure the package automatically. Pi resolves
|
|
@@ -199,10 +205,30 @@ internal and is never part of either plugin payload.
|
|
|
199
205
|
|
|
200
206
|
```sh
|
|
201
207
|
relay-flow init
|
|
202
|
-
relay-flow task auth
|
|
203
208
|
```
|
|
204
209
|
|
|
205
|
-
`init` selects the task system, runner, harness,
|
|
210
|
+
On a new interactive home, `init` selects the task system, runner, harness,
|
|
211
|
+
and durable executor, runs the selected task plug-in's authentication flow,
|
|
212
|
+
and asks whether to register repositories. It writes machine config and the
|
|
213
|
+
selected execution backend only after authentication succeeds. A skipped
|
|
214
|
+
repository setup does not start the server; selecting it starts or reuses the
|
|
215
|
+
background server, waits for socket readiness, and invokes the same
|
|
216
|
+
`repo register` form described below. The server remains running after a
|
|
217
|
+
successful guided registration.
|
|
218
|
+
|
|
219
|
+
For scripts and existing installations, keep the independent sequence:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
relay-flow init --task-plugin jira --runner-plugin orca --harness-plugin opencode
|
|
223
|
+
relay-flow task auth --site https://company.atlassian.net --email you@example.com --token "$JIRA_API_TOKEN"
|
|
224
|
+
relay-flow serve --background
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`task auth` delegates to the selected task plug-in. Jira prompts for its site,
|
|
228
|
+
email, and masked API token when flags are omitted, validates `/myself`, and
|
|
229
|
+
owns the system-wide `credentials.yaml`. A normal init rerun refuses existing
|
|
230
|
+
state. `relay-flow init --force` updates safe stopped instances while
|
|
231
|
+
preserving durable and repo state; it does not launch repository onboarding.
|
|
206
232
|
|
|
207
233
|
For Beads, select the plugin explicitly when scripting setup:
|
|
208
234
|
|
|
@@ -234,9 +260,11 @@ workflows/<name>.yaml 0644 submitted workflow definitions
|
|
|
234
260
|
|
|
235
261
|
### Register a repo
|
|
236
262
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
263
|
+
The guided first-run flow can start the server and open repository registration
|
|
264
|
+
for you. `repo register` remains a server-backed standalone command for later
|
|
265
|
+
additions: if no server is running, start it with
|
|
266
|
+
`relay-flow serve --background` (or rerun interactive `relay-flow init` for a
|
|
267
|
+
new home).
|
|
240
268
|
|
|
241
269
|
```sh
|
|
242
270
|
relay-flow serve --background
|
|
@@ -369,8 +397,9 @@ Task, runner, harness, and durable executor plugins are selected machine-wide. A
|
|
|
369
397
|
|
|
370
398
|
### Run
|
|
371
399
|
|
|
372
|
-
The
|
|
373
|
-
|
|
400
|
+
The guided first-run flow starts or reuses the server before `repo register`.
|
|
401
|
+
Outside that flow, `repo register` and `workflow submit` require the server to
|
|
402
|
+
already be running; the same process polls repos and drives runs.
|
|
374
403
|
|
|
375
404
|
```sh
|
|
376
405
|
relay-flow serve # normal start; requires an initialized database
|
|
@@ -455,6 +484,7 @@ Rules enforced at submit:
|
|
|
455
484
|
- The graph must be fully reachable from `start`; unknown fields are rejected; `runnerPlugin`/`harnessPlugin`/`closeOn`/legacy `tasks`/`runner` keys are rejected.
|
|
456
485
|
- Only `agent` and `hitl` nodes receive mailbox subtasks; `start` and `end` never do.
|
|
457
486
|
- `cleanupRunnerOnEnd` is the only workflow cleanup knob and takes priority over terminal retention after `end`; the word `terminal` refers to runner terminals only.
|
|
487
|
+
- `nudgePrompt` supports `{{taskSystem}}`, `{{ticket}}`, `{{workflow}}`, `{{repo}}`, `{{node}}`, `{{mailbox}}`, and `{{nextSteps}}`; unsupported variables are rejected at submission.
|
|
458
488
|
|
|
459
489
|
### Task-config merge
|
|
460
490
|
|
|
@@ -651,6 +681,17 @@ One Repo Poller per registered repo (not per workflow) fetches active parent tic
|
|
|
651
681
|
|
|
652
682
|
---
|
|
653
683
|
|
|
684
|
+
## CLI inspection and automation
|
|
685
|
+
|
|
686
|
+
Human-readable workflow and run views are intended for interactive operator
|
|
687
|
+
use. Scripts should always pass `--json`; the JSON output is the stable
|
|
688
|
+
machine-readable interface. `run get --ticket <key>` shows metadata,
|
|
689
|
+
conditions, timing, progress, resource-duration placeholders, and the actual
|
|
690
|
+
node-visit tree, including retries and revisits. Scoped `--help` is available
|
|
691
|
+
at the root, group, and leaf command levels. Build information is available
|
|
692
|
+
without a server through `relay-flow version`, `relay-flow --version`, and
|
|
693
|
+
`relay-flow -v`.
|
|
694
|
+
|
|
654
695
|
## Development
|
|
655
696
|
|
|
656
697
|
```sh
|
|
@@ -631,13 +631,16 @@ 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
646
|
func (s *registerServer) TaskRegistrationFields(context.Context, config.RawValues) ([]task.RegistrationField, error) {
|
|
@@ -648,6 +651,25 @@ func (s *registerServer) TaskRegistrationFields(context.Context, config.RawValue
|
|
|
648
651
|
}
|
|
649
652
|
return out, nil
|
|
650
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
|
|
671
|
+
}
|
|
672
|
+
|
|
651
673
|
func (s *registerServer) RegisterRepo(_ context.Context, in repo.RegisterInput) (repo.Info, error) {
|
|
652
674
|
s.calls++
|
|
653
675
|
cp := in
|
|
@@ -657,7 +679,9 @@ func (s *registerServer) RegisterRepo(_ context.Context, in repo.RegisterInput)
|
|
|
657
679
|
return repo.Info{}, errors.New("registration failed")
|
|
658
680
|
}
|
|
659
681
|
s.successful = append(s.successful, cp)
|
|
660
|
-
|
|
682
|
+
info := repo.Info{Name: in.Name, Path: in.Path, TaskConfig: in.TaskConfig}
|
|
683
|
+
s.registered = append(s.registered, info)
|
|
684
|
+
return info, nil
|
|
661
685
|
}
|
|
662
686
|
|
|
663
687
|
func serveRegister(t *testing.T, home string, fields []string) *registerServer {
|
|
@@ -831,6 +855,91 @@ func TestRegistrationTaskConfigBuildsNestedStatusDefaults(t *testing.T) {
|
|
|
831
855
|
}
|
|
832
856
|
}
|
|
833
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
|
+
|
|
834
943
|
func TestRepoMultiSelectAndPluginMapping(t *testing.T) {
|
|
835
944
|
selected := []int{0, 1}
|
|
836
945
|
field := repoMultiSelect([]huh.Option[int]{
|