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.
Files changed (116) hide show
  1. package/README.md +148 -143
  2. package/cmd/relay-flow/commands_test.go +464 -0
  3. package/cmd/relay-flow/main.go +670 -180
  4. package/cmd/relay-flow/scenario_test.go +1135 -0
  5. package/cmd/relay-flow/serve.go +609 -0
  6. package/go.mod +69 -2
  7. package/go.sum +185 -0
  8. package/internal/config/config.go +88 -0
  9. package/internal/config/machine.go +99 -48
  10. package/internal/config/machine_test.go +248 -0
  11. package/internal/config/merge_test.go +118 -0
  12. package/internal/config/writeatomic.go +36 -0
  13. package/internal/config/writeatomic_test.go +98 -0
  14. package/internal/execution/goworkflows/activities.go +490 -0
  15. package/internal/execution/goworkflows/engine.go +487 -0
  16. package/internal/execution/goworkflows/engine_test.go +600 -0
  17. package/internal/execution/goworkflows/fakes_test.go +517 -0
  18. package/internal/execution/goworkflows/interpreter.go +605 -0
  19. package/internal/execution/goworkflows/logging_test.go +154 -0
  20. package/internal/execution/goworkflows/mailbox_test.go +423 -0
  21. package/internal/execution/goworkflows/node_runtime_integration_test.go +127 -0
  22. package/internal/execution/goworkflows/node_runtime_test.go +486 -0
  23. package/internal/execution/goworkflows/projection.go +504 -0
  24. package/internal/execution/goworkflows/recovery_test.go +1092 -0
  25. package/internal/execution/goworkflows/retry_log_test.go +59 -0
  26. package/internal/execution/goworkflows/retry_projection_test.go +98 -0
  27. package/internal/harness/contract_test.go +169 -0
  28. package/internal/harness/factory.go +63 -0
  29. package/internal/harness/harness.go +41 -0
  30. package/internal/harness/opencode/opencode.go +166 -0
  31. package/internal/harness/opencode/opencode_test.go +50 -0
  32. package/internal/harness/plugin_selection_test.go +126 -0
  33. package/internal/identity/identity.go +37 -0
  34. package/internal/logging/logging.go +56 -0
  35. package/internal/logging/logging_test.go +116 -0
  36. package/internal/paths/paths.go +67 -0
  37. package/internal/recover/recover.go +115 -0
  38. package/internal/repo/poller.go +186 -0
  39. package/internal/repo/poller_test.go +327 -0
  40. package/internal/repo/repo.go +119 -0
  41. package/internal/repo/service.go +216 -0
  42. package/internal/repo/service_test.go +298 -0
  43. package/internal/retry/retry.go +118 -0
  44. package/internal/router/router.go +83 -0
  45. package/internal/router/router_test.go +144 -0
  46. package/internal/run/manager.go +108 -0
  47. package/internal/run/run.go +140 -0
  48. package/internal/run/run_identity_test.go +52 -0
  49. package/internal/run/run_manager_test.go +266 -0
  50. package/internal/runner/contract_test.go +221 -0
  51. package/internal/runner/factory.go +65 -0
  52. package/internal/runner/orca/orca.go +363 -170
  53. package/internal/runner/orca/orca_test.go +134 -160
  54. package/internal/runner/orca/orcacli/orcacli.go +215 -0
  55. package/internal/runner/orca/orcacli/orcacli_test.go +154 -0
  56. package/internal/runner/orca/orcacli/testdata/repo-list.json +18 -0
  57. package/internal/runner/orca/orcacli/testdata/strict-orca.sh +30 -0
  58. package/internal/runner/orca/orcacli/testdata/terminal-close.json +12 -0
  59. package/internal/runner/orca/orcacli/testdata/terminal-create.json +18 -0
  60. package/internal/runner/orca/orcacli/testdata/terminal-list.json +51 -0
  61. package/internal/runner/orca/orcacli/testdata/terminal-send.json +1 -0
  62. package/internal/runner/orca/orcacli/testdata/terminal-show.json +1 -0
  63. package/internal/runner/orca/orcacli/testdata/worktree-create.json +22 -0
  64. package/internal/runner/orca/orcacli/testdata/worktree-list.json +31 -0
  65. package/internal/runner/orca/orcacli/testdata/worktree-remove.json +6 -0
  66. package/internal/runner/runner.go +47 -64
  67. package/internal/server/api_test.go +300 -0
  68. package/internal/server/client.go +192 -74
  69. package/internal/server/fixture_test.go +248 -0
  70. package/internal/server/server.go +425 -248
  71. package/internal/server/shutdown_test.go +116 -0
  72. package/internal/task/contract_test.go +223 -0
  73. package/internal/task/factory.go +103 -0
  74. package/internal/task/jira/acli/acli.go +306 -0
  75. package/internal/task/jira/acli/acli_test.go +208 -0
  76. package/internal/task/jira/acli/testdata/acli_comments.json +55 -0
  77. package/internal/task/jira/acli/testdata/search_invalid_assignee.txt +1 -0
  78. package/internal/task/jira/acli/testdata/search_invalid_status.txt +1 -0
  79. package/internal/task/jira/acli/testdata/search_success.json +1 -0
  80. package/internal/task/jira/filters_test.go +234 -0
  81. package/internal/task/jira/helpers_test.go +60 -0
  82. package/internal/task/jira/jira.go +507 -0
  83. package/internal/task/jira/normalize.go +101 -0
  84. package/internal/task/jira/testdata/acli_search.json +120 -0
  85. package/internal/task/jira/transition_defaults_test.go +156 -0
  86. package/internal/task/jira/validation_test.go +94 -0
  87. package/internal/task/task.go +84 -0
  88. package/internal/workflow/report.go +85 -0
  89. package/internal/workflow/report_test.go +259 -0
  90. package/internal/workflow/service.go +142 -0
  91. package/internal/workflow/store.go +136 -0
  92. package/internal/workflow/store_test.go +282 -0
  93. package/internal/workflow/workflow.go +342 -0
  94. package/internal/workflow/workflow_test.go +410 -0
  95. package/package.json +1 -1
  96. package/internal/acli/acli.go +0 -229
  97. package/internal/config/demo_test.go +0 -17
  98. package/internal/config/schema.go +0 -193
  99. package/internal/config/schema_test.go +0 -162
  100. package/internal/daemon/daemon.go +0 -218
  101. package/internal/daemon/daemon_test.go +0 -204
  102. package/internal/discovery/discovery.go +0 -122
  103. package/internal/discovery/discovery_test.go +0 -62
  104. package/internal/opencode/opencode.go +0 -26
  105. package/internal/orcacli/orcacli.go +0 -264
  106. package/internal/runner/orca/README.md +0 -64
  107. package/internal/runner/runner_test.go +0 -64
  108. package/internal/server/server_test.go +0 -195
  109. package/internal/tasks/jira/README.md +0 -69
  110. package/internal/tasks/jira/component_test.go +0 -16
  111. package/internal/tasks/jira/decode.go +0 -24
  112. package/internal/tasks/jira/jira.go +0 -231
  113. package/internal/tasks/jira/jira_test.go +0 -259
  114. package/internal/tasks/jira/jql_test.go +0 -16
  115. package/internal/tasks/tasks.go +0 -90
  116. 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
+ }