relay-flow 0.0.1 → 0.2.0-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 +148 -143
- package/cmd/relay-flow/commands_test.go +464 -0
- package/cmd/relay-flow/main.go +670 -180
- package/cmd/relay-flow/scenario_test.go +1135 -0
- package/cmd/relay-flow/serve.go +609 -0
- package/go.mod +69 -2
- package/go.sum +185 -0
- package/internal/config/config.go +88 -0
- package/internal/config/machine.go +99 -48
- package/internal/config/machine_test.go +248 -0
- package/internal/config/merge_test.go +118 -0
- package/internal/config/writeatomic.go +36 -0
- package/internal/config/writeatomic_test.go +98 -0
- package/internal/execution/goworkflows/activities.go +490 -0
- package/internal/execution/goworkflows/engine.go +487 -0
- package/internal/execution/goworkflows/engine_test.go +600 -0
- package/internal/execution/goworkflows/fakes_test.go +517 -0
- package/internal/execution/goworkflows/interpreter.go +605 -0
- package/internal/execution/goworkflows/logging_test.go +154 -0
- package/internal/execution/goworkflows/mailbox_test.go +423 -0
- package/internal/execution/goworkflows/node_runtime_integration_test.go +127 -0
- package/internal/execution/goworkflows/node_runtime_test.go +486 -0
- package/internal/execution/goworkflows/projection.go +504 -0
- package/internal/execution/goworkflows/recovery_test.go +1092 -0
- package/internal/execution/goworkflows/retry_log_test.go +59 -0
- package/internal/execution/goworkflows/retry_projection_test.go +98 -0
- package/internal/harness/contract_test.go +169 -0
- package/internal/harness/factory.go +63 -0
- package/internal/harness/harness.go +41 -0
- package/internal/harness/opencode/opencode.go +166 -0
- package/internal/harness/opencode/opencode_test.go +50 -0
- package/internal/harness/plugin_selection_test.go +126 -0
- package/internal/identity/identity.go +37 -0
- package/internal/logging/logging.go +56 -0
- package/internal/logging/logging_test.go +116 -0
- package/internal/paths/paths.go +67 -0
- package/internal/recover/recover.go +115 -0
- package/internal/repo/poller.go +186 -0
- package/internal/repo/poller_test.go +327 -0
- package/internal/repo/repo.go +119 -0
- package/internal/repo/service.go +216 -0
- package/internal/repo/service_test.go +298 -0
- package/internal/retry/retry.go +118 -0
- package/internal/router/router.go +83 -0
- package/internal/router/router_test.go +144 -0
- package/internal/run/manager.go +108 -0
- package/internal/run/run.go +140 -0
- package/internal/run/run_identity_test.go +52 -0
- package/internal/run/run_manager_test.go +266 -0
- package/internal/runner/contract_test.go +221 -0
- package/internal/runner/factory.go +65 -0
- package/internal/runner/orca/orca.go +363 -170
- package/internal/runner/orca/orca_test.go +134 -160
- package/internal/runner/orca/orcacli/orcacli.go +215 -0
- package/internal/runner/orca/orcacli/orcacli_test.go +154 -0
- package/internal/runner/orca/orcacli/testdata/repo-list.json +18 -0
- package/internal/runner/orca/orcacli/testdata/strict-orca.sh +30 -0
- package/internal/runner/orca/orcacli/testdata/terminal-close.json +12 -0
- package/internal/runner/orca/orcacli/testdata/terminal-create.json +18 -0
- package/internal/runner/orca/orcacli/testdata/terminal-list.json +51 -0
- package/internal/runner/orca/orcacli/testdata/terminal-send.json +1 -0
- package/internal/runner/orca/orcacli/testdata/terminal-show.json +1 -0
- package/internal/runner/orca/orcacli/testdata/worktree-create.json +22 -0
- package/internal/runner/orca/orcacli/testdata/worktree-list.json +31 -0
- package/internal/runner/orca/orcacli/testdata/worktree-remove.json +6 -0
- package/internal/runner/runner.go +47 -64
- package/internal/server/api_test.go +300 -0
- package/internal/server/client.go +192 -74
- package/internal/server/fixture_test.go +248 -0
- package/internal/server/server.go +425 -248
- package/internal/server/shutdown_test.go +116 -0
- package/internal/task/contract_test.go +223 -0
- package/internal/task/factory.go +103 -0
- package/internal/task/jira/acli/acli.go +306 -0
- package/internal/task/jira/acli/acli_test.go +208 -0
- package/internal/task/jira/acli/testdata/acli_comments.json +55 -0
- package/internal/task/jira/acli/testdata/search_invalid_assignee.txt +1 -0
- package/internal/task/jira/acli/testdata/search_invalid_status.txt +1 -0
- package/internal/task/jira/acli/testdata/search_success.json +1 -0
- package/internal/task/jira/filters_test.go +234 -0
- package/internal/task/jira/helpers_test.go +60 -0
- package/internal/task/jira/jira.go +507 -0
- package/internal/task/jira/normalize.go +101 -0
- package/internal/task/jira/testdata/acli_search.json +120 -0
- package/internal/task/jira/transition_defaults_test.go +156 -0
- package/internal/task/jira/validation_test.go +94 -0
- package/internal/task/task.go +84 -0
- package/internal/workflow/report.go +85 -0
- package/internal/workflow/report_test.go +259 -0
- package/internal/workflow/service.go +142 -0
- package/internal/workflow/store.go +136 -0
- package/internal/workflow/store_test.go +282 -0
- package/internal/workflow/workflow.go +342 -0
- package/internal/workflow/workflow_test.go +410 -0
- package/package.json +1 -1
- package/internal/acli/acli.go +0 -229
- package/internal/config/demo_test.go +0 -17
- package/internal/config/schema.go +0 -193
- package/internal/config/schema_test.go +0 -162
- package/internal/daemon/daemon.go +0 -218
- package/internal/daemon/daemon_test.go +0 -204
- package/internal/discovery/discovery.go +0 -122
- package/internal/discovery/discovery_test.go +0 -62
- package/internal/opencode/opencode.go +0 -26
- package/internal/orcacli/orcacli.go +0 -264
- package/internal/runner/orca/README.md +0 -64
- package/internal/runner/runner_test.go +0 -64
- package/internal/server/server_test.go +0 -195
- package/internal/tasks/jira/README.md +0 -69
- package/internal/tasks/jira/component_test.go +0 -16
- package/internal/tasks/jira/decode.go +0 -24
- package/internal/tasks/jira/jira.go +0 -231
- package/internal/tasks/jira/jira_test.go +0 -259
- package/internal/tasks/jira/jql_test.go +0 -16
- package/internal/tasks/tasks.go +0 -90
- package/internal/tasks/tasks_test.go +0 -91
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
package server_test
|
|
2
|
+
|
|
3
|
+
// Test-local server fixture using the settled seam (d): a thin
|
|
4
|
+
// server.New(deps) http.Handler driven over in-process HTTP. Deps are the
|
|
5
|
+
// documented consumer services (workflow/repo/run/report), faked here.
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"context"
|
|
9
|
+
"net"
|
|
10
|
+
"net/http"
|
|
11
|
+
"os"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"testing"
|
|
14
|
+
|
|
15
|
+
"github.com/rajpopat27/relay-flow/internal/repo"
|
|
16
|
+
"github.com/rajpopat27/relay-flow/internal/run"
|
|
17
|
+
"github.com/rajpopat27/relay-flow/internal/runner"
|
|
18
|
+
"github.com/rajpopat27/relay-flow/internal/server"
|
|
19
|
+
"github.com/rajpopat27/relay-flow/internal/workflow"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
// fakeServices backs the handler with fakes behind the documented service
|
|
23
|
+
// seams. activeWorkflows seeds workflows with an active run (drives 409);
|
|
24
|
+
// failRepos forces a 500; slowReport simulates a long-running report call.
|
|
25
|
+
type fakeServices struct {
|
|
26
|
+
activeWorkflows map[string]bool
|
|
27
|
+
failRepos bool
|
|
28
|
+
slowReport chan struct{}
|
|
29
|
+
workflows map[string]*workflow.Workflow
|
|
30
|
+
repos map[string]repo.Info
|
|
31
|
+
runs []run.Run
|
|
32
|
+
shutdownCh chan struct{}
|
|
33
|
+
registrations []run.NodeRuntimeRegistration
|
|
34
|
+
runtimeAck run.NodeRuntimeRegistrationAck
|
|
35
|
+
processedReports map[string]bool
|
|
36
|
+
submittedReports int
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func (f *fakeServices) SubmitWorkflow(_ context.Context, yaml []byte) (*workflow.Workflow, error) {
|
|
40
|
+
wf, err := workflow.Parse("x", yaml)
|
|
41
|
+
if err != nil {
|
|
42
|
+
return nil, err
|
|
43
|
+
}
|
|
44
|
+
if f.activeWorkflows[wf.Name] {
|
|
45
|
+
return nil, errActiveRun{wf.Name}
|
|
46
|
+
}
|
|
47
|
+
f.workflows[wf.Name] = wf
|
|
48
|
+
return wf, nil
|
|
49
|
+
}
|
|
50
|
+
func (f *fakeServices) GetWorkflow(_ context.Context, name string) (*workflow.Workflow, error) {
|
|
51
|
+
if wf, ok := f.workflows[name]; ok {
|
|
52
|
+
return wf, nil
|
|
53
|
+
}
|
|
54
|
+
return nil, errNotFound{name}
|
|
55
|
+
}
|
|
56
|
+
func (f *fakeServices) ListWorkflows(context.Context) ([]*workflow.Workflow, error) {
|
|
57
|
+
out := []*workflow.Workflow{}
|
|
58
|
+
for _, w := range f.workflows {
|
|
59
|
+
out = append(out, w)
|
|
60
|
+
}
|
|
61
|
+
return out, nil
|
|
62
|
+
}
|
|
63
|
+
func (f *fakeServices) RemoveWorkflow(_ context.Context, name string) error {
|
|
64
|
+
if f.activeWorkflows[name] {
|
|
65
|
+
return errActiveRun{name}
|
|
66
|
+
}
|
|
67
|
+
if _, ok := f.workflows[name]; !ok {
|
|
68
|
+
return errNotFound{name}
|
|
69
|
+
}
|
|
70
|
+
delete(f.workflows, name)
|
|
71
|
+
return nil
|
|
72
|
+
}
|
|
73
|
+
func (f *fakeServices) ListRuns(context.Context, run.Filter) ([]run.Run, error) { return f.runs, nil }
|
|
74
|
+
func (f *fakeServices) GetRunByTicket(_ context.Context, ticket string) (run.Run, error) {
|
|
75
|
+
for _, r := range f.runs {
|
|
76
|
+
if r.Ticket.Key == ticket {
|
|
77
|
+
return r, nil
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return run.Run{}, errNotFound{ticket}
|
|
81
|
+
}
|
|
82
|
+
func (f *fakeServices) CancelRun(_ context.Context, ticket, _ string) error {
|
|
83
|
+
for i, r := range f.runs {
|
|
84
|
+
if r.Ticket.Key == ticket {
|
|
85
|
+
f.runs[i].State = run.StateCanceled
|
|
86
|
+
return nil
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return errNotFound{ticket}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func (f *fakeServices) SubmitReport(ctx context.Context, _ run.ReportRequest) (run.ReportAck, error) {
|
|
93
|
+
f.submittedReports++
|
|
94
|
+
if f.slowReport != nil {
|
|
95
|
+
select {
|
|
96
|
+
case <-f.slowReport:
|
|
97
|
+
case <-ctx.Done():
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return run.ReportAck{Accepted: true}, nil
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func (f *fakeServices) HasProcessedReport(_ context.Context, id run.ID, reportID string) (bool, error) {
|
|
104
|
+
return f.processedReports[string(id)+":"+reportID], nil
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func (f *fakeServices) RegisterNodeSession(_ context.Context, registration run.NodeRuntimeRegistration) (run.NodeRuntimeRegistrationAck, error) {
|
|
108
|
+
f.registrations = append(f.registrations, registration)
|
|
109
|
+
if f.runtimeAck != (run.NodeRuntimeRegistrationAck{}) {
|
|
110
|
+
return f.runtimeAck, nil
|
|
111
|
+
}
|
|
112
|
+
return run.NodeRuntimeRegistrationAck{Accepted: true}, nil
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func (f *fakeServices) GetRepo(_ context.Context, name string) (repo.Info, error) {
|
|
116
|
+
if f.failRepos {
|
|
117
|
+
return repo.Info{}, errUnexpected{}
|
|
118
|
+
}
|
|
119
|
+
if info, ok := f.repos[name]; ok {
|
|
120
|
+
return info, nil
|
|
121
|
+
}
|
|
122
|
+
return repo.Info{}, errNotFound{name}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func (f *fakeServices) ListRepos(context.Context) ([]repo.Info, error) {
|
|
126
|
+
out := []repo.Info{}
|
|
127
|
+
for _, r := range f.repos {
|
|
128
|
+
out = append(out, r)
|
|
129
|
+
}
|
|
130
|
+
return out, nil
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func (f *fakeServices) DiscoverRepos(context.Context) ([]runner.RepoCandidate, error) {
|
|
134
|
+
return []runner.RepoCandidate{{Name: "payments", Path: "/srv/payments"}}, nil
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func (f *fakeServices) TaskFields(context.Context) ([]string, error) {
|
|
138
|
+
return []string{"status", "labels"}, nil
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func (f *fakeServices) RegisterRepo(_ context.Context, input repo.RegisterInput) (repo.Info, error) {
|
|
142
|
+
if f.repos == nil {
|
|
143
|
+
f.repos = map[string]repo.Info{}
|
|
144
|
+
}
|
|
145
|
+
info := repo.Info{Name: input.Name, Path: input.Path, TaskConfig: input.TaskConfig}
|
|
146
|
+
f.repos[input.Name] = info
|
|
147
|
+
return info, nil
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func (f *fakeServices) RemoveRepo(_ context.Context, name string) error {
|
|
151
|
+
if _, ok := f.repos[name]; !ok {
|
|
152
|
+
return errNotFound{name}
|
|
153
|
+
}
|
|
154
|
+
delete(f.repos, name)
|
|
155
|
+
return nil
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func (f *fakeServices) Shutdown(context.Context) error {
|
|
159
|
+
if f.shutdownCh != nil {
|
|
160
|
+
select {
|
|
161
|
+
case <-f.shutdownCh:
|
|
162
|
+
default:
|
|
163
|
+
close(f.shutdownCh)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return nil
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// errActiveRun wraps server.ErrConflict so the handler maps it to 409 per
|
|
170
|
+
// the documented typed-error mapping (docs/structs-methods-interfaces.md
|
|
171
|
+
// routes + 'API responses and status codes are stable').
|
|
172
|
+
type errActiveRun struct{ name string }
|
|
173
|
+
|
|
174
|
+
func (e errActiveRun) Error() string { return "workflow has active run: " + e.name }
|
|
175
|
+
func (e errActiveRun) Unwrap() error { return server.ErrConflict }
|
|
176
|
+
|
|
177
|
+
// errNotFound wraps server.ErrNotFound so the handler maps it to 404.
|
|
178
|
+
type errNotFound struct{ name string }
|
|
179
|
+
|
|
180
|
+
func (e errNotFound) Error() string { return "not found: " + e.name }
|
|
181
|
+
func (e errNotFound) Unwrap() error { return server.ErrNotFound }
|
|
182
|
+
|
|
183
|
+
type errUnexpected struct{}
|
|
184
|
+
|
|
185
|
+
func (errUnexpected) Error() string { return "unexpected failure" }
|
|
186
|
+
|
|
187
|
+
// startHandler builds the http.Handler via server.New(deps) and serves it on
|
|
188
|
+
// an in-process listener; returns a client and a shutdown func.
|
|
189
|
+
func startHandler(t *testing.T, svc *fakeServices) (*http.Client, func()) {
|
|
190
|
+
t.Helper()
|
|
191
|
+
if svc.workflows == nil {
|
|
192
|
+
svc.workflows = map[string]*workflow.Workflow{}
|
|
193
|
+
}
|
|
194
|
+
if svc.repos == nil {
|
|
195
|
+
svc.repos = map[string]repo.Info{}
|
|
196
|
+
}
|
|
197
|
+
h := server.New(svc) // seam (d): thin http.Handler over the services
|
|
198
|
+
|
|
199
|
+
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
200
|
+
if err != nil {
|
|
201
|
+
t.Fatal(err)
|
|
202
|
+
}
|
|
203
|
+
srv := &http.Server{Handler: h}
|
|
204
|
+
go srv.Serve(ln)
|
|
205
|
+
|
|
206
|
+
client := &http.Client{Transport: &http.Transport{
|
|
207
|
+
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
208
|
+
return (&net.Dialer{}).DialContext(ctx, "tcp", ln.Addr().String())
|
|
209
|
+
},
|
|
210
|
+
}}
|
|
211
|
+
cleanup := func() {
|
|
212
|
+
_ = srv.Shutdown(context.Background())
|
|
213
|
+
}
|
|
214
|
+
return client, cleanup
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// startHandlerOnSocket serves the handler on a real Unix socket inside dir so
|
|
218
|
+
// the same dir can be reused across a restart (same-state resume). The server
|
|
219
|
+
// owns socket creation/mode; cleanup shuts down and unlinks the socket.
|
|
220
|
+
func startHandlerOnSocket(t *testing.T, dir string, svc *fakeServices) (*http.Client, func()) {
|
|
221
|
+
t.Helper()
|
|
222
|
+
if svc.workflows == nil {
|
|
223
|
+
svc.workflows = map[string]*workflow.Workflow{}
|
|
224
|
+
}
|
|
225
|
+
if svc.repos == nil {
|
|
226
|
+
svc.repos = map[string]repo.Info{}
|
|
227
|
+
}
|
|
228
|
+
h := server.New(svc)
|
|
229
|
+
sock := filepath.Join(dir, "server.sock")
|
|
230
|
+
_ = os.Remove(sock) // clear any stale socket before (re)binding
|
|
231
|
+
ln, err := net.Listen("unix", sock)
|
|
232
|
+
if err != nil {
|
|
233
|
+
t.Fatal(err)
|
|
234
|
+
}
|
|
235
|
+
srv := &http.Server{Handler: h}
|
|
236
|
+
go srv.Serve(ln)
|
|
237
|
+
client := &http.Client{Transport: &http.Transport{
|
|
238
|
+
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
239
|
+
return (&net.Dialer{}).DialContext(ctx, "unix", sock)
|
|
240
|
+
},
|
|
241
|
+
}}
|
|
242
|
+
cleanup := func() {
|
|
243
|
+
_ = srv.Shutdown(context.Background())
|
|
244
|
+
_ = ln.Close()
|
|
245
|
+
_ = os.Remove(sock)
|
|
246
|
+
}
|
|
247
|
+
return client, cleanup
|
|
248
|
+
}
|