relay-flow 0.2.0-alpha → 0.2.2-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 +171 -26
- package/cmd/relay-flow/beads_composition_test.go +451 -0
- package/cmd/relay-flow/commands_test.go +593 -15
- package/cmd/relay-flow/main.go +356 -119
- package/cmd/relay-flow/scenario_test.go +246 -35
- package/cmd/relay-flow/serve.go +5 -2
- package/examples/beads-workflow.yaml +74 -0
- package/examples/default-story-workflow.yaml +88 -0
- package/go.mod +2 -1
- package/go.sum +2 -0
- package/internal/config/config.go +13 -2
- package/internal/config/merge_test.go +18 -0
- package/internal/execution/goworkflows/activities.go +136 -87
- package/internal/execution/goworkflows/end_feedback_test.go +124 -0
- package/internal/execution/goworkflows/engine.go +41 -8
- package/internal/execution/goworkflows/engine_test.go +100 -22
- package/internal/execution/goworkflows/fakes_test.go +63 -25
- package/internal/execution/goworkflows/interpreter.go +45 -30
- package/internal/execution/goworkflows/mailbox_test.go +85 -0
- package/internal/execution/goworkflows/node_runtime_integration_test.go +12 -6
- package/internal/execution/goworkflows/node_runtime_test.go +72 -23
- package/internal/execution/goworkflows/recovery_test.go +7 -7
- package/internal/execution/goworkflows/report_contract_fixture_test.go +31 -0
- package/internal/execution/goworkflows/retry_log_test.go +11 -11
- package/internal/harness/contract_test.go +15 -0
- package/internal/harness/factory.go +25 -3
- package/internal/harness/harness.go +30 -4
- package/internal/harness/opencode/opencode.go +125 -10
- package/internal/harness/opencode/opencode_test.go +183 -0
- package/internal/harness/opencode/repo_setup.go +361 -0
- package/internal/harness/plugin_selection_test.go +5 -5
- package/internal/paths/paths.go +18 -16
- package/internal/recover/recover.go +11 -6
- package/internal/repo/repo.go +13 -0
- package/internal/repo/service.go +10 -0
- package/internal/repo/service_test.go +55 -6
- package/internal/router/router.go +3 -2
- package/internal/router/router_test.go +87 -0
- package/internal/run/manager.go +14 -1
- package/internal/run/run.go +6 -4
- package/internal/run/run_manager_test.go +21 -1
- package/internal/runner/contract_test.go +64 -26
- package/internal/runner/orca/orca.go +30 -54
- package/internal/runner/orca/orca_test.go +143 -4
- package/internal/runner/orca/orcacli/orcacli.go +5 -0
- package/internal/runner/orca/orcacli/orcacli_test.go +3 -0
- package/internal/runner/orca/orcacli/testdata/strict-orca.sh +2 -0
- package/internal/runner/runner.go +15 -8
- package/internal/task/auth_test.go +48 -0
- package/internal/task/beads/bdcli/bdcli.go +323 -0
- package/internal/task/beads/bdcli/bdcli_test.go +297 -0
- package/internal/task/beads/bdcli/testdata/array.json +1 -0
- package/internal/task/beads/bdcli/testdata/children.json +1 -0
- package/internal/task/beads/bdcli/testdata/claimed.json +1 -0
- package/internal/task/beads/bdcli/testdata/commented.json +1 -0
- package/internal/task/beads/bdcli/testdata/comments.json +1 -0
- package/internal/task/beads/bdcli/testdata/created.json +1 -0
- package/internal/task/beads/bdcli/testdata/object.json +1 -0
- package/internal/task/beads/bdcli/testdata/ready.json +1 -0
- package/internal/task/beads/bdcli/testdata/show.json +1 -0
- package/internal/task/beads/bdcli/testdata/strict-bd.sh +149 -0
- package/internal/task/beads/bdcli/testdata/updated.json +1 -0
- package/internal/task/beads/beads.go +840 -0
- package/internal/task/beads/beads_test.go +609 -0
- package/internal/task/beads/comments_test.go +242 -0
- package/internal/task/beads/config_compatibility_test.go +163 -0
- package/internal/task/beads/lifecycle_inheritance_test.go +168 -0
- package/internal/task/beads/repo_composition_test.go +232 -0
- package/internal/task/beads/runtime_config_test.go +81 -0
- package/internal/task/beads/status_compatibility_test.go +233 -0
- package/internal/task/beads/status_test.go +257 -0
- package/internal/task/beads/testdata/strict-bd-repo.sh +27 -0
- package/internal/task/beads/validation_test.go +110 -0
- package/internal/task/contract_test.go +12 -0
- package/internal/task/factory.go +52 -3
- package/internal/task/jira/auth.go +209 -0
- package/internal/task/jira/auth_test.go +160 -0
- package/internal/task/jira/effects_test.go +39 -0
- package/internal/task/jira/filters_test.go +96 -16
- package/internal/task/jira/helpers_test.go +29 -19
- package/internal/task/jira/jira.go +263 -90
- package/internal/task/jira/lifecycle_inheritance_test.go +172 -0
- package/internal/task/jira/normalize.go +32 -14
- package/internal/task/jira/rest/adf.go +165 -0
- package/internal/task/jira/rest/adf_test.go +60 -0
- package/internal/task/jira/rest/client.go +573 -0
- package/internal/task/jira/rest/client_test.go +381 -0
- package/internal/task/jira/templates_test.go +118 -0
- package/internal/task/jira/transition_defaults_test.go +22 -18
- package/internal/task/jira/validation_test.go +1 -1
- package/internal/task/task.go +32 -0
- package/internal/workflow/report_test.go +45 -0
- package/internal/workflow/workflow.go +9 -6
- package/internal/workflow/workflow_test.go +14 -12
- package/package.json +2 -1
- package/internal/task/jira/acli/acli.go +0 -306
- package/internal/task/jira/acli/acli_test.go +0 -208
- package/internal/task/jira/acli/testdata/acli_comments.json +0 -55
- package/internal/task/jira/acli/testdata/search_invalid_assignee.txt +0 -1
- package/internal/task/jira/acli/testdata/search_invalid_status.txt +0 -1
- /package/internal/task/{jira/acli/testdata/search_success.json → beads/bdcli/testdata/empty.json} +0 -0
- /package/internal/task/jira/testdata/{acli_search.json → jira_search_issues.json} +0 -0
|
@@ -114,6 +114,17 @@ func (a *adapter) EnsureEnvironment(ctx context.Context, spec runner.RunSpec) (r
|
|
|
114
114
|
return env, err
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
func (a *adapter) SetEnvironmentStatus(ctx context.Context, env runner.Environment, status string) error {
|
|
118
|
+
slog.Debug("orca call", "op", "set-environment-status", "envID", env.ID, "status", status)
|
|
119
|
+
err := a.cli.SetWorktreeStatus(ctx, env.ID, status)
|
|
120
|
+
if err != nil {
|
|
121
|
+
slog.Info("orca outcome", "op", "set-environment-status", "envID", env.ID, "status", status, "result", "error", "error", sanitizeErr(err))
|
|
122
|
+
} else {
|
|
123
|
+
slog.Info("orca outcome", "op", "set-environment-status", "envID", env.ID, "status", status, "result", "ok")
|
|
124
|
+
}
|
|
125
|
+
return err
|
|
126
|
+
}
|
|
127
|
+
|
|
117
128
|
// ensureEnvironment is the unlogged body factored out so the public method
|
|
118
129
|
// can emit one outcome line (created/exists/error) without duplicate
|
|
119
130
|
// logging on the inner re-reads.
|
|
@@ -183,26 +194,6 @@ func primaryBranch(w *orcacli.Worktree) string {
|
|
|
183
194
|
|
|
184
195
|
// --- Terminals ---
|
|
185
196
|
|
|
186
|
-
// FindTerminal returns the terminal titled exactly title in the environment
|
|
187
|
-
// when it is live and usable; stale/disconnected records are treated as
|
|
188
|
-
// absent.
|
|
189
|
-
func (a *adapter) FindTerminal(ctx context.Context, env runner.Environment, title string) (runner.Terminal, bool, error) {
|
|
190
|
-
slog.Debug("orca call", "op", "find-terminal", "title", title, "envID", env.ID)
|
|
191
|
-
terms, err := a.cli.ListTerminals(ctx, "id:"+env.ID)
|
|
192
|
-
if err != nil {
|
|
193
|
-
slog.Info("orca outcome", "op", "find-terminal", "title", title, "result", "error", "error", sanitizeErr(err))
|
|
194
|
-
return runner.Terminal{}, false, err
|
|
195
|
-
}
|
|
196
|
-
for _, t := range terms {
|
|
197
|
-
if t.Title == title && t.Connected {
|
|
198
|
-
slog.Info("orca outcome", "op", "find-terminal", "title", title, "result", "found")
|
|
199
|
-
return runner.Terminal{ID: t.Handle, Title: t.Title}, true, nil
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
slog.Info("orca outcome", "op", "find-terminal", "title", title, "result", "absent")
|
|
203
|
-
return runner.Terminal{}, false, nil
|
|
204
|
-
}
|
|
205
|
-
|
|
206
197
|
// CloseTerminal closes one terminal by handle.
|
|
207
198
|
func (a *adapter) CloseTerminal(ctx context.Context, terminal runner.Terminal) error {
|
|
208
199
|
slog.Debug("orca call", "op", "close-terminal", "title", terminal.Title, "handle", terminal.ID)
|
|
@@ -215,20 +206,29 @@ func (a *adapter) CloseTerminal(ctx context.Context, terminal runner.Terminal) e
|
|
|
215
206
|
return err
|
|
216
207
|
}
|
|
217
208
|
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
func (a *adapter)
|
|
209
|
+
// FindTerminal addresses a persisted handle directly. Normal execution never
|
|
210
|
+
// lists terminals or rediscovers by title.
|
|
211
|
+
func (a *adapter) FindTerminal(ctx context.Context, terminal runner.Terminal) (runner.Terminal, bool, error) {
|
|
212
|
+
slog.Debug("orca call", "op", "find-terminal", "title", terminal.Title, "handle", terminal.ID)
|
|
213
|
+
if terminal.ID == "" {
|
|
214
|
+
slog.Info("orca outcome", "op", "find-terminal", "title", terminal.Title, "result", "absent")
|
|
215
|
+
return runner.Terminal{}, false, nil
|
|
216
|
+
}
|
|
221
217
|
t, err := a.cli.ShowTerminal(ctx, terminal.ID)
|
|
222
218
|
if errors.Is(err, orcacli.ErrTerminalUnavailable) {
|
|
219
|
+
slog.Info("orca outcome", "op", "find-terminal", "title", terminal.Title, "result", "absent")
|
|
223
220
|
return runner.Terminal{}, false, nil
|
|
224
221
|
}
|
|
225
222
|
if err != nil {
|
|
223
|
+
slog.Info("orca outcome", "op", "find-terminal", "title", terminal.Title, "result", "error", "error", sanitizeErr(err))
|
|
226
224
|
return runner.Terminal{}, false, err
|
|
227
225
|
}
|
|
228
226
|
if !t.Connected {
|
|
227
|
+
slog.Info("orca outcome", "op", "find-terminal", "title", terminal.Title, "result", "absent")
|
|
229
228
|
return runner.Terminal{}, false, nil
|
|
230
229
|
}
|
|
231
|
-
|
|
230
|
+
slog.Info("orca outcome", "op", "find-terminal", "title", terminal.Title, "result", "found")
|
|
231
|
+
return runner.Terminal{ID: t.Handle, Title: terminal.Title}, true, nil
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
func (a *adapter) SendTerminal(ctx context.Context, terminal runner.Terminal, text string) error {
|
|
@@ -242,51 +242,27 @@ func (a *adapter) CreateTerminal(ctx context.Context, env runner.Environment, ti
|
|
|
242
242
|
if err != nil {
|
|
243
243
|
return runner.Terminal{}, err
|
|
244
244
|
}
|
|
245
|
-
if commandResumesSession(command) {
|
|
246
|
-
t, showErr := a.cli.ShowTerminal(ctx, handle)
|
|
247
|
-
if errors.Is(showErr, orcacli.ErrTerminalUnavailable) || (showErr == nil && !t.Connected) {
|
|
248
|
-
_ = a.cli.CloseTerminal(ctx, handle)
|
|
249
|
-
return runner.Terminal{}, runner.ErrSessionUnavailable
|
|
250
|
-
}
|
|
251
|
-
if showErr != nil {
|
|
252
|
-
return runner.Terminal{}, showErr
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
245
|
return runner.Terminal{ID: handle, Title: title}, nil
|
|
256
246
|
}
|
|
257
247
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return true
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return false
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// EnsureTerminal is idempotent: it returns the live terminal with the stable
|
|
268
|
-
// title when present, otherwise creates one running command. The title
|
|
269
|
-
// contains only <ticket>:<node>; visit metadata lives in the command's
|
|
270
|
-
// environment, never the title.
|
|
271
|
-
func (a *adapter) EnsureTerminal(ctx context.Context, env runner.Environment, title string, command runner.Command) (runner.Terminal, error) {
|
|
248
|
+
// EnsureTerminal checks the persisted terminal, then creates a replacement
|
|
249
|
+
// only when that terminal is unavailable. The title is used only for creation.
|
|
250
|
+
func (a *adapter) EnsureTerminal(ctx context.Context, env runner.Environment, stored runner.Terminal, title string, command runner.Command) (runner.Terminal, error) {
|
|
272
251
|
slog.Debug("orca call", "op", "ensure-terminal", "title", title, "envID", env.ID)
|
|
273
|
-
if t, ok, err := a.FindTerminal(ctx,
|
|
252
|
+
if t, ok, err := a.FindTerminal(ctx, stored); err != nil {
|
|
274
253
|
slog.Info("orca outcome", "op", "ensure-terminal", "title", title, "result", "error", "error", sanitizeErr(err))
|
|
275
254
|
return runner.Terminal{}, err
|
|
276
255
|
} else if ok {
|
|
277
256
|
slog.Info("orca outcome", "op", "ensure-terminal", "title", title, "result", "exists")
|
|
278
257
|
return t, nil
|
|
279
258
|
}
|
|
280
|
-
|
|
281
|
-
// it after the ticket).
|
|
282
|
-
name := strings.SplitN(title, ":", 2)[0]
|
|
283
|
-
handle, err := a.cli.CreateTerminal(ctx, name, title, shellCommand(command))
|
|
259
|
+
terminal, err := a.CreateTerminal(ctx, env, title, command)
|
|
284
260
|
if err != nil {
|
|
285
261
|
slog.Info("orca outcome", "op", "ensure-terminal", "title", title, "result", "error", "error", sanitizeErr(err))
|
|
286
262
|
return runner.Terminal{}, err
|
|
287
263
|
}
|
|
288
264
|
slog.Info("orca outcome", "op", "ensure-terminal", "title", title, "result", "created")
|
|
289
|
-
return
|
|
265
|
+
return terminal, nil
|
|
290
266
|
}
|
|
291
267
|
|
|
292
268
|
// sanitizeErr strips the leading "orca [args...]:" prefix from orcacli
|
|
@@ -41,9 +41,14 @@ func TestSanitizeErr(t *testing.T) {
|
|
|
41
41
|
type fakeCLI struct {
|
|
42
42
|
repos []orcacli.Repo
|
|
43
43
|
worktrees []orcacli.Worktree
|
|
44
|
+
terminals map[string]orcacli.Terminal
|
|
44
45
|
|
|
45
46
|
createdBaseBranch string
|
|
46
47
|
createdParent string
|
|
48
|
+
status string
|
|
49
|
+
showHandles []string
|
|
50
|
+
createCommands []string
|
|
51
|
+
createN int
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
func (f *fakeCLI) ListRepos(context.Context) ([]orcacli.Repo, error) { return f.repos, nil }
|
|
@@ -62,16 +67,32 @@ func (f *fakeCLI) CreateWorktree(_ context.Context, ticketKey, repoID, parentWor
|
|
|
62
67
|
})
|
|
63
68
|
return nil
|
|
64
69
|
}
|
|
70
|
+
func (f *fakeCLI) SetWorktreeStatus(_ context.Context, _, status string) error {
|
|
71
|
+
f.status = status
|
|
72
|
+
return nil
|
|
73
|
+
}
|
|
65
74
|
func (f *fakeCLI) DeleteWorktree(context.Context, string) error { return nil }
|
|
66
|
-
func (f *fakeCLI) ShowTerminal(context.Context, string) (orcacli.Terminal, error) {
|
|
67
|
-
|
|
75
|
+
func (f *fakeCLI) ShowTerminal(_ context.Context, handle string) (orcacli.Terminal, error) {
|
|
76
|
+
f.showHandles = append(f.showHandles, handle)
|
|
77
|
+
t, ok := f.terminals[handle]
|
|
78
|
+
if !ok {
|
|
79
|
+
return orcacli.Terminal{}, orcacli.ErrTerminalUnavailable
|
|
80
|
+
}
|
|
81
|
+
return t, nil
|
|
68
82
|
}
|
|
69
83
|
func (f *fakeCLI) SendTerminal(context.Context, string, string) error { return nil }
|
|
70
84
|
func (f *fakeCLI) ListTerminals(context.Context, string) ([]orcacli.Terminal, error) {
|
|
71
85
|
return nil, nil
|
|
72
86
|
}
|
|
73
|
-
func (f *fakeCLI) CreateTerminal(context.Context, string,
|
|
74
|
-
|
|
87
|
+
func (f *fakeCLI) CreateTerminal(_ context.Context, _ string, title, command string) (string, error) {
|
|
88
|
+
f.createN++
|
|
89
|
+
f.createCommands = append(f.createCommands, command)
|
|
90
|
+
handle := "term-created-" + string(rune('0'+f.createN))
|
|
91
|
+
if f.terminals == nil {
|
|
92
|
+
f.terminals = map[string]orcacli.Terminal{}
|
|
93
|
+
}
|
|
94
|
+
f.terminals[handle] = orcacli.Terminal{Handle: handle, Title: title, Connected: true}
|
|
95
|
+
return handle, nil
|
|
75
96
|
}
|
|
76
97
|
func (f *fakeCLI) CloseTerminal(context.Context, string) error { return nil }
|
|
77
98
|
|
|
@@ -122,6 +143,124 @@ func TestEnsureEnvironment_BaseRefOverride(t *testing.T) {
|
|
|
122
143
|
}
|
|
123
144
|
}
|
|
124
145
|
|
|
146
|
+
func TestSetEnvironmentStatus(t *testing.T) {
|
|
147
|
+
fx := &fakeCLI{}
|
|
148
|
+
a, err := New(fx, config.RawValues{})
|
|
149
|
+
if err != nil {
|
|
150
|
+
t.Fatal(err)
|
|
151
|
+
}
|
|
152
|
+
if err := a.SetEnvironmentStatus(context.Background(), runner.Environment{ID: "wt-PAY-1"}, runner.WorkspaceStatusInReview); err != nil {
|
|
153
|
+
t.Fatal(err)
|
|
154
|
+
}
|
|
155
|
+
if fx.status != runner.WorkspaceStatusInReview {
|
|
156
|
+
t.Fatalf("status = %q, want %q", fx.status, runner.WorkspaceStatusInReview)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func TestFindTerminalUsesPersistedID(t *testing.T) {
|
|
161
|
+
fx := &fakeCLI{terminals: map[string]orcacli.Terminal{
|
|
162
|
+
"term-stored": {Handle: "term-stored", Title: "PAY-1:implement", Connected: true},
|
|
163
|
+
}}
|
|
164
|
+
a, err := New(fx, config.RawValues{})
|
|
165
|
+
if err != nil {
|
|
166
|
+
t.Fatal(err)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
got, ok, err := a.FindTerminal(context.Background(), runner.Terminal{ID: "term-stored", Title: "PAY-1:implement"})
|
|
170
|
+
if err != nil || !ok || got.ID != "term-stored" {
|
|
171
|
+
t.Fatalf("FindTerminal = %+v, %v, %v", got, ok, err)
|
|
172
|
+
}
|
|
173
|
+
if len(fx.showHandles) != 1 || fx.showHandles[0] != "term-stored" {
|
|
174
|
+
t.Fatalf("ShowTerminal handles = %v, want [term-stored]", fx.showHandles)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func TestFindTerminalReturnsOnlyLiveUsable(t *testing.T) {
|
|
179
|
+
fx := &fakeCLI{terminals: map[string]orcacli.Terminal{
|
|
180
|
+
"term-dead": {Handle: "term-dead", Title: "PAY-1:implement", Connected: false},
|
|
181
|
+
}}
|
|
182
|
+
a, err := New(fx, config.RawValues{})
|
|
183
|
+
if err != nil {
|
|
184
|
+
t.Fatal(err)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if _, ok, err := a.FindTerminal(context.Background(), runner.Terminal{}); err != nil || ok {
|
|
188
|
+
t.Fatalf("FindTerminal(empty) ok=%v err=%v, want absent", ok, err)
|
|
189
|
+
}
|
|
190
|
+
if _, ok, err := a.FindTerminal(context.Background(), runner.Terminal{ID: "term-dead"}); err != nil || ok {
|
|
191
|
+
t.Fatalf("FindTerminal(dead) ok=%v err=%v, want absent", ok, err)
|
|
192
|
+
}
|
|
193
|
+
if len(fx.showHandles) != 1 || fx.showHandles[0] != "term-dead" {
|
|
194
|
+
t.Fatalf("ShowTerminal handles = %v, want [term-dead]", fx.showHandles)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func TestEnsureTerminalFindsBeforeCreate(t *testing.T) {
|
|
199
|
+
fx := &fakeCLI{terminals: map[string]orcacli.Terminal{
|
|
200
|
+
"term-stored": {Handle: "term-stored", Title: "PAY-1:implement", Connected: true},
|
|
201
|
+
}}
|
|
202
|
+
a, err := New(fx, config.RawValues{})
|
|
203
|
+
if err != nil {
|
|
204
|
+
t.Fatal(err)
|
|
205
|
+
}
|
|
206
|
+
stored := runner.Terminal{ID: "term-stored", Title: "PAY-1:implement"}
|
|
207
|
+
|
|
208
|
+
got, err := a.EnsureTerminal(context.Background(), runner.Environment{ID: "wt-PAY-1"}, stored, "PAY-1:implement", runner.Command{Executable: "opencode"})
|
|
209
|
+
if err != nil {
|
|
210
|
+
t.Fatal(err)
|
|
211
|
+
}
|
|
212
|
+
if got.ID != stored.ID || fx.createN != 0 {
|
|
213
|
+
t.Fatalf("EnsureTerminal = %+v, creates=%d; want stored terminal and no create", got, fx.createN)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func TestEnsureTerminalCreatesWhenStoredTerminalUnavailable(t *testing.T) {
|
|
218
|
+
fx := &fakeCLI{}
|
|
219
|
+
a, err := New(fx, config.RawValues{})
|
|
220
|
+
if err != nil {
|
|
221
|
+
t.Fatal(err)
|
|
222
|
+
}
|
|
223
|
+
command := runner.Command{Executable: "custom-harness", Args: []string{"--session", "opaque", "--prompt", "work", "--agent", "review"}}
|
|
224
|
+
|
|
225
|
+
got, err := a.EnsureTerminal(context.Background(), runner.Environment{ID: "wt-PAY-1"}, runner.Terminal{ID: "term-stale"}, "PAY-1:implement", command)
|
|
226
|
+
if err != nil {
|
|
227
|
+
t.Fatal(err)
|
|
228
|
+
}
|
|
229
|
+
if got.ID != "term-created-1" || fx.createN != 1 {
|
|
230
|
+
t.Fatalf("EnsureTerminal = %+v, creates=%d", got, fx.createN)
|
|
231
|
+
}
|
|
232
|
+
if len(fx.showHandles) != 1 || fx.showHandles[0] != "term-stale" {
|
|
233
|
+
t.Fatalf("ShowTerminal handles = %v, want [term-stale]", fx.showHandles)
|
|
234
|
+
}
|
|
235
|
+
if len(fx.createCommands) != 1 || fx.createCommands[0] != shellCommand(command) {
|
|
236
|
+
t.Fatalf("CreateTerminal commands = %v, want opaque command unchanged", fx.createCommands)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
func TestCreateTerminalAlwaysCreatesAndTreatsCommandAsOpaque(t *testing.T) {
|
|
241
|
+
fx := &fakeCLI{}
|
|
242
|
+
a, err := New(fx, config.RawValues{})
|
|
243
|
+
if err != nil {
|
|
244
|
+
t.Fatal(err)
|
|
245
|
+
}
|
|
246
|
+
command := runner.Command{Executable: "custom-harness", Args: []string{"--session", "opaque"}}
|
|
247
|
+
|
|
248
|
+
first, err := a.CreateTerminal(context.Background(), runner.Environment{ID: "wt-PAY-1"}, "PAY-1:implement", command)
|
|
249
|
+
if err != nil {
|
|
250
|
+
t.Fatal(err)
|
|
251
|
+
}
|
|
252
|
+
second, err := a.CreateTerminal(context.Background(), runner.Environment{ID: "wt-PAY-1"}, "PAY-1:implement", command)
|
|
253
|
+
if err != nil {
|
|
254
|
+
t.Fatal(err)
|
|
255
|
+
}
|
|
256
|
+
if first.ID == second.ID || fx.createN != 2 {
|
|
257
|
+
t.Fatalf("CreateTerminal IDs = %q, %q; creates=%d", first.ID, second.ID, fx.createN)
|
|
258
|
+
}
|
|
259
|
+
if len(fx.showHandles) != 0 {
|
|
260
|
+
t.Fatalf("CreateTerminal parsed resume syntax and inspected terminals: %v", fx.showHandles)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
125
264
|
// An existing ticket branch must win even over a configured baseRef. Passing
|
|
126
265
|
// any other base would make Orca hit its branch-name collision behavior.
|
|
127
266
|
func TestEnsureEnvironment_ExistingTicketBranchAvoidsCollision(t *testing.T) {
|
|
@@ -45,6 +45,7 @@ type Client interface {
|
|
|
45
45
|
ListRepos(ctx context.Context) ([]Repo, error)
|
|
46
46
|
ListWorktrees(ctx context.Context) ([]Worktree, error)
|
|
47
47
|
CreateWorktree(ctx context.Context, ticketKey, repoID, parentWorktreeID, baseBranch string) error
|
|
48
|
+
SetWorktreeStatus(ctx context.Context, worktreeID, status string) error
|
|
48
49
|
DeleteWorktree(ctx context.Context, worktreeID string) error
|
|
49
50
|
ShowTerminal(ctx context.Context, handle string) (Terminal, error)
|
|
50
51
|
SendTerminal(ctx context.Context, handle, text string) error
|
|
@@ -90,6 +91,10 @@ func (CLI) CreateWorktree(ctx context.Context, ticketKey, repoID, parentWorktree
|
|
|
90
91
|
"--parent-worktree", "worktree:"+parentWorktreeID, "--base-branch", baseBranch, "--json")
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
func (CLI) SetWorktreeStatus(ctx context.Context, worktreeID, status string) error {
|
|
95
|
+
return run(ctx, "worktree", "set", "--worktree", "id:"+worktreeID, "--workspace-status", status, "--json")
|
|
96
|
+
}
|
|
97
|
+
|
|
93
98
|
// FindExistingBranch returns the first local or remote-tracking branch whose
|
|
94
99
|
// short ref contains ticketKey. The returned ref is suitable for Orca's
|
|
95
100
|
// --base-branch argument.
|
|
@@ -33,6 +33,9 @@ func TestCLIContractsAgainstCapturedRealOutput(t *testing.T) {
|
|
|
33
33
|
if err := cli.CreateWorktree(ctx, "PAY-101", "repo-1", "wt-main", "origin/alice/PAY-101"); err != nil {
|
|
34
34
|
t.Fatal(err)
|
|
35
35
|
}
|
|
36
|
+
if err := cli.SetWorktreeStatus(ctx, "wt-PAY-101", "in-review"); err != nil {
|
|
37
|
+
t.Fatal(err)
|
|
38
|
+
}
|
|
36
39
|
if err := cli.DeleteWorktree(ctx, "wt-PAY-101"); err != nil {
|
|
37
40
|
t.Fatal(err)
|
|
38
41
|
}
|
|
@@ -8,6 +8,8 @@ elif [ "$#" -eq 3 ] && [ "$1" = worktree ] && [ "$2" = list ] && [ "$3" = --json
|
|
|
8
8
|
fixture=worktree-list.json
|
|
9
9
|
elif [ "$#" -eq 11 ] && [ "$1" = worktree ] && [ "$2" = create ] && [ "$3" = --name ] && [ "$4" = PAY-101 ] && [ "$5" = --repo ] && [ "$6" = id:repo-1 ] && [ "$7" = --parent-worktree ] && [ "$8" = worktree:wt-main ] && [ "$9" = --base-branch ] && [ "${10}" = origin/alice/PAY-101 ] && [ "${11}" = --json ]; then
|
|
10
10
|
fixture=worktree-create.json
|
|
11
|
+
elif [ "$#" -eq 7 ] && [ "$1" = worktree ] && [ "$2" = set ] && [ "$3" = --worktree ] && [ "$4" = id:wt-PAY-101 ] && [ "$5" = --workspace-status ] && [ "$6" = in-review ] && [ "$7" = --json ]; then
|
|
12
|
+
fixture=worktree-create.json
|
|
11
13
|
elif [ "$#" -eq 5 ] && [ "$1" = worktree ] && [ "$2" = rm ] && [ "$3" = --worktree ] && [ "$4" = id:wt-PAY-101 ] && [ "$5" = --json ]; then
|
|
12
14
|
fixture=worktree-remove.json
|
|
13
15
|
elif [ "$#" -eq 6 ] && [ "$1" = terminal ] && [ "$2" = list ] && [ "$3" = --worktree ] && [ "$4" = id:wt-PAY-101 ] && [ "$5" = --include-visual-layouts ] && [ "$6" = --json ]; then
|
|
@@ -13,6 +13,12 @@ import (
|
|
|
13
13
|
|
|
14
14
|
var ErrSessionUnavailable = errors.New("stored session unavailable")
|
|
15
15
|
|
|
16
|
+
const (
|
|
17
|
+
WorkspaceStatusInProgress = "in-progress"
|
|
18
|
+
WorkspaceStatusInReview = "in-review"
|
|
19
|
+
WorkspaceStatusCompleted = "completed"
|
|
20
|
+
)
|
|
21
|
+
|
|
16
22
|
type RepoCandidate struct {
|
|
17
23
|
Name string `json:"name"`
|
|
18
24
|
Path string `json:"path"`
|
|
@@ -42,10 +48,11 @@ type RunSpec struct {
|
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
// Runner owns ticket-scoped environments, terminals, and liveness.
|
|
45
|
-
// FindTerminal
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
51
|
+
// FindTerminal checks a persisted terminal handle against the external runner
|
|
52
|
+
// and returns only a live usable terminal. CloseTerminals closes agent terminals
|
|
53
|
+
// while preserving the environment/workspace; CleanupRun removes all
|
|
54
|
+
// runner-owned run resources at end. Both reconstruct identity from RunSpec
|
|
55
|
+
// without SQLite IDs.
|
|
49
56
|
//
|
|
50
57
|
// Terminal titles are stable and minimal: only "<ticket>:<node>" — never
|
|
51
58
|
// nodeVisitID, workflow name, agent name, or other changing metadata.
|
|
@@ -53,12 +60,12 @@ type Runner interface {
|
|
|
53
60
|
DiscoverRepos(ctx context.Context) ([]RepoCandidate, error)
|
|
54
61
|
ValidateRepo(ctx context.Context, name, path string) error
|
|
55
62
|
EnsureEnvironment(ctx context.Context, spec RunSpec) (Environment, error)
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
SetEnvironmentStatus(ctx context.Context, env Environment, status string) error
|
|
64
|
+
FindTerminal(ctx context.Context, terminal Terminal) (Terminal, bool, error)
|
|
58
65
|
CreateTerminal(ctx context.Context, env Environment, title string, command Command) (Terminal, error)
|
|
59
|
-
|
|
66
|
+
EnsureTerminal(ctx context.Context, env Environment, terminal Terminal, title string, command Command) (Terminal, error)
|
|
67
|
+
SendTerminal(ctx context.Context, terminal Terminal, text string) error
|
|
60
68
|
CloseTerminal(ctx context.Context, terminal Terminal) error
|
|
61
|
-
EnsureTerminal(ctx context.Context, env Environment, title string, command Command) (Terminal, error)
|
|
62
69
|
CloseTerminals(ctx context.Context, spec RunSpec) error
|
|
63
70
|
CleanupRun(ctx context.Context, spec RunSpec) error
|
|
64
71
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package task_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"io"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
"sync"
|
|
10
|
+
"testing"
|
|
11
|
+
|
|
12
|
+
"github.com/rajpopat27/relay-flow/internal/config"
|
|
13
|
+
"github.com/rajpopat27/relay-flow/internal/task"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const alternateAuthPlugin = "alternate-auth-format-test"
|
|
17
|
+
|
|
18
|
+
var registerAlternateAuth sync.Once
|
|
19
|
+
|
|
20
|
+
func TestAlternativeTaskPluginOwnsCredentialsFormat(t *testing.T) {
|
|
21
|
+
registerAlternateAuth.Do(func() {
|
|
22
|
+
task.Register(alternateAuthPlugin, task.Factory{
|
|
23
|
+
RequiredRepoKeys: func() []string { return nil },
|
|
24
|
+
TaskScopeKey: func(_, _ config.RawValues) (string, error) { return "alternate", nil },
|
|
25
|
+
Auth: func(_ context.Context, _ []string, stdin io.Reader) error {
|
|
26
|
+
key, err := io.ReadAll(stdin)
|
|
27
|
+
if err != nil {
|
|
28
|
+
return err
|
|
29
|
+
}
|
|
30
|
+
return os.WriteFile(filepath.Join(os.Getenv("RELAY_FLOW_HOME"), "credentials.yaml"),
|
|
31
|
+
[]byte("alternateApiKey: "+strings.TrimSpace(string(key))+"\n"), 0o600)
|
|
32
|
+
},
|
|
33
|
+
New: func(context.Context, task.RepoSpec) (task.System, error) { return nil, nil },
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
root := t.TempDir()
|
|
37
|
+
t.Setenv("RELAY_FLOW_HOME", root)
|
|
38
|
+
if err := task.Auth(context.Background(), alternateAuthPlugin, nil, strings.NewReader("different-secret")); err != nil {
|
|
39
|
+
t.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
raw, err := os.ReadFile(filepath.Join(root, "credentials.yaml"))
|
|
42
|
+
if err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
if string(raw) != "alternateApiKey: different-secret\n" {
|
|
46
|
+
t.Fatalf("alternate credentials = %q", raw)
|
|
47
|
+
}
|
|
48
|
+
}
|