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
@@ -1,259 +0,0 @@
1
- package jira
2
-
3
- import (
4
- "strings"
5
- "testing"
6
-
7
- "github.com/rajpopat27/relay-flow/internal/acli"
8
- "github.com/rajpopat27/relay-flow/internal/config"
9
- "github.com/rajpopat27/relay-flow/internal/tasks"
10
- )
11
-
12
- var testNodes = map[string]config.Node{
13
- "coding": {Agent: "build", When: "In Progress", OnSuccess: "reviewing", OnFailure: "coding"},
14
- "reviewing": {Agent: "build", When: "In Review", OnSuccess: "done", OnFailure: "coding"},
15
- "done": {When: "Done"},
16
- }
17
-
18
- func TestClaimLabel(t *testing.T) {
19
- if got := claimLabel("xyzTaskFlow"); got != "wf:xyzTaskFlow" {
20
- t.Errorf("claimLabel = %q", got)
21
- }
22
- }
23
-
24
- func TestBuildJQL(t *testing.T) {
25
- cfg := JiraConfig{Query: "project = xyz", IssueTypes: []string{"Task"}}
26
- j, err := newJira(cfg, "wf", testNodes, "Jane Doe", "repo:xyz", nil)
27
- if err != nil {
28
- t.Fatalf("%v", err)
29
- }
30
- want := `(project = xyz) AND issuetype IN ("Task") AND component = "repo:xyz" AND assignee = "Jane Doe" ORDER BY updated`
31
- if j.jql != want {
32
- t.Errorf("jql =\n %q\nwant\n %q", j.jql, want)
33
- }
34
- }
35
-
36
- func TestBuildJQLCentralizedSkipsAssignee(t *testing.T) {
37
- cfg := JiraConfig{Query: "project = xyz", IssueTypes: []string{"Task", "Story"}, AssigneeIsAgent: true}
38
- j, err := newJira(cfg, "wf", testNodes, "", "repo:xyz", nil)
39
- if err != nil {
40
- t.Fatalf("%v", err)
41
- }
42
- if strings.Contains(j.jql, "assignee") {
43
- t.Errorf("assigneeIsAgent must skip assignee clause: %q", j.jql)
44
- }
45
- if !strings.Contains(j.jql, `"Task", "Story"`) {
46
- t.Errorf("issueTypes list: %q", j.jql)
47
- }
48
- }
49
-
50
- func TestConfigValidation(t *testing.T) {
51
- base := map[string]any{"query": "project = xyz", "issueTypes": []any{"Task"}}
52
- goodAny, err := unmarshalConfig(base)
53
- if err != nil {
54
- t.Fatalf("valid config rejected: %v", err)
55
- }
56
- good := goodAny.(JiraConfig)
57
- if good.Query != "project = xyz" || len(good.IssueTypes) != 1 {
58
- t.Errorf("%+v", good)
59
- }
60
-
61
- bad := []struct {
62
- name string
63
- mut func(map[string]any)
64
- want string
65
- }{
66
- {"empty query", func(m map[string]any) { m["query"] = "" }, "query"},
67
- {"no issueTypes", func(m map[string]any) { delete(m, "issueTypes") }, "issueTypes"},
68
- {"ORDER BY in query", func(m map[string]any) { m["query"] = "project = xyz ORDER BY rank" }, "ORDER BY"},
69
- {"issuetype in query", func(m map[string]any) { m["query"] = "project = xyz AND issuetype = Bug" }, "issuetype"},
70
- {"assignee in query", func(m map[string]any) { m["query"] = "project = xyz AND assignee = bob" }, "assignee"},
71
- {"unknown field", func(m map[string]any) { m["bogus"] = 1 }, "bogus"},
72
- }
73
- for _, tc := range bad {
74
- t.Run(tc.name, func(t *testing.T) {
75
- m := map[string]any{"query": "project = xyz", "issueTypes": []any{"Task"}}
76
- tc.mut(m)
77
- if _, err := unmarshalConfig(m); err == nil || !strings.Contains(err.Error(), tc.want) {
78
- t.Errorf("err = %v, want mention %q", err, tc.want)
79
- }
80
- })
81
- }
82
- }
83
-
84
- type fakeAcli struct {
85
- tickets []acli.Ticket
86
- labelsAdded []string
87
- transitions []string
88
- comments []string
89
- views map[string]acli.Ticket
90
- transitionErr error
91
- }
92
-
93
- func (f *fakeAcli) Search(jql string) ([]acli.Ticket, error) { return f.tickets, nil }
94
- func (f *fakeAcli) AddLabel(key string, existing []string, label string) error {
95
- f.labelsAdded = append(f.labelsAdded, key+":"+label)
96
- return nil
97
- }
98
- func (f *fakeAcli) Transition(key, status string) error {
99
- f.transitions = append(f.transitions, key+":"+status)
100
- return f.transitionErr
101
- }
102
- func (f *fakeAcli) Comment(key, body string) error {
103
- f.comments = append(f.comments, key+":"+body)
104
- return nil
105
- }
106
- func (f *fakeAcli) View(key string) (acli.Ticket, error) {
107
- t, ok := f.views[key]
108
- if !ok {
109
- t = acli.Ticket{Key: key}
110
- }
111
- return t, nil
112
- }
113
-
114
- func mustJira(t *testing.T, ac aclier) *jiraTasks {
115
- t.Helper()
116
- j, err := newJira(JiraConfig{Query: "project = xyz", IssueTypes: []string{"Task"}}, "wf", testNodes, "", "repo:xyz", ac)
117
- if err != nil {
118
- t.Fatalf("%v", err)
119
- }
120
- return j
121
- }
122
-
123
- func TestListMapsStateToNode(t *testing.T) {
124
- fa := &fakeAcli{tickets: []acli.Ticket{
125
- {Key: "XYZ-1", Summary: "s", Status: "In Progress"},
126
- {Key: "XYZ-2", Summary: "s", Status: "In Review", Labels: []string{"wf:otherFlow"}},
127
- {Key: "XYZ-3", Summary: "s", Status: "Backlog"},
128
- }}
129
- j := mustJira(t, fa)
130
- got, err := j.List()
131
- if err != nil {
132
- t.Fatalf("%v", err)
133
- }
134
- if len(got) != 3 {
135
- t.Fatalf("len = %d", len(got))
136
- }
137
- if got[0].Node != "coding" || got[0].ClaimedBy != "" {
138
- t.Errorf("t1 = %+v", got[0])
139
- }
140
- if got[1].Node != "reviewing" || got[1].ClaimedBy != "otherFlow" {
141
- t.Errorf("t2 = %+v", got[1])
142
- }
143
- if got[2].Node != "" {
144
- t.Errorf("unmapped state must yield empty Node: %+v", got[2])
145
- }
146
- }
147
-
148
- func TestClaim(t *testing.T) {
149
- fa := &fakeAcli{views: map[string]acli.Ticket{"XYZ-1": {Key: "XYZ-1", Labels: []string{"existing"}}}}
150
- j := mustJira(t, fa)
151
- err := j.Claim(tasksTicket("XYZ-1"))
152
- if err != nil {
153
- t.Fatalf("%v", err)
154
- }
155
- if len(fa.labelsAdded) != 1 || fa.labelsAdded[0] != "XYZ-1:wf:wf" {
156
- t.Errorf("labelsAdded = %v", fa.labelsAdded)
157
- }
158
- }
159
-
160
- func TestReportTransitions(t *testing.T) {
161
- fa := &fakeAcli{views: map[string]acli.Ticket{
162
- "XYZ-1": {Key: "XYZ-1", Status: "In Progress"},
163
- }}
164
- j := mustJira(t, fa)
165
- err := j.Report(tasksTicket("XYZ-1"), "success", "reviewing", "did the thing")
166
- if err != nil {
167
- t.Fatalf("%v", err)
168
- }
169
- if len(fa.transitions) != 1 || fa.transitions[0] != "XYZ-1:In Review" {
170
- t.Errorf("transitions = %v", fa.transitions)
171
- }
172
- if len(fa.comments) != 1 || !strings.Contains(fa.comments[0], "did the thing") {
173
- t.Errorf("comments = %v", fa.comments)
174
- }
175
- }
176
-
177
- func TestReportSelfLoopCommentsOnly(t *testing.T) {
178
- fa := &fakeAcli{views: map[string]acli.Ticket{
179
- "XYZ-1": {Key: "XYZ-1", Status: "In Progress"},
180
- }}
181
- j := mustJira(t, fa)
182
- // coding onFailure = coding: target state == current state.
183
- err := j.Report(tasksTicket("XYZ-1"), "failure", "coding", "still broken")
184
- if err != nil {
185
- t.Fatalf("%v", err)
186
- }
187
- if len(fa.transitions) != 0 {
188
- t.Errorf("self-loop must not transition: %v", fa.transitions)
189
- }
190
- if len(fa.comments) != 1 {
191
- t.Errorf("self-loop must comment: %v", fa.comments)
192
- }
193
- }
194
-
195
- func TestReportUnknownTargetNode(t *testing.T) {
196
- fa := &fakeAcli{views: map[string]acli.Ticket{"XYZ-1": {Key: "XYZ-1", Status: "In Progress"}}}
197
- j := mustJira(t, fa)
198
- if err := j.Report(tasksTicket("XYZ-1"), "success", "nowhere", "x"); err == nil ||
199
- !strings.Contains(err.Error(), "unknown node") {
200
- t.Errorf("err = %v", err)
201
- }
202
- }
203
-
204
- func TestProjectKeyFromQuery(t *testing.T) {
205
- got, err := ProjectKeyFromQuery("project = xyz AND foo = bar")
206
- if err != nil || got != "xyz" {
207
- t.Errorf("got %q err %v", got, err)
208
- }
209
- got, err = ProjectKeyFromQuery(`PROJECT = "ABC"`)
210
- if err != nil || got != "ABC" {
211
- t.Errorf("got %q err %v", got, err)
212
- }
213
- if _, err = ProjectKeyFromQuery("foo = bar"); err == nil {
214
- t.Error("expected error for missing project")
215
- }
216
- }
217
-
218
- func TestValidateStates(t *testing.T) {
219
- fa := &fakeValidator{good: map[string]bool{"In Progress": true, "In Review": true}}
220
- bad, err := ValidateStates(fa, testNodes, "xyz")
221
- if err != nil {
222
- t.Fatalf("%v", err)
223
- }
224
- if len(bad) != 1 || bad[0] != "Done" {
225
- t.Errorf("bad = %v", bad)
226
- }
227
- }
228
-
229
- type fakeValidator struct{ good map[string]bool }
230
-
231
- func (f *fakeValidator) ValidateStatus(projectKey, status string) error {
232
- if f.good[status] {
233
- return nil
234
- }
235
- return errInvalidStatus
236
- }
237
-
238
- var errInvalidStatus = errorString("invalid status")
239
-
240
- type errorString string
241
-
242
- func (e errorString) Error() string { return string(e) }
243
-
244
- func tasksTicket(key string) tasks.Ticket { return tasks.Ticket{Key: key} }
245
-
246
- func TestReportTransitionFailurePropagates(t *testing.T) {
247
- fa := &fakeAcli{
248
- views: map[string]acli.Ticket{"XYZ-1": {Key: "XYZ-1", Status: "In Progress"}},
249
- transitionErr: errorString("No allowed transitions found for given status"),
250
- }
251
- j := mustJira(t, fa)
252
- err := j.Report(tasks.Ticket{Key: "XYZ-1"}, "success", "reviewing", "x")
253
- if err == nil || !strings.Contains(err.Error(), "No allowed transitions") {
254
- t.Errorf("transition failure must propagate, got %v", err)
255
- }
256
- if len(fa.comments) != 0 {
257
- t.Errorf("no comment on failed transition: %v", fa.comments)
258
- }
259
- }
@@ -1,16 +0,0 @@
1
- package jira
2
-
3
- import (
4
- "strings"
5
- "testing"
6
- )
7
-
8
- func TestDistributedJQLHasAssignee(t *testing.T) {
9
- j, err := newJira(JiraConfig{Query: "project = XYZ", IssueTypes: []string{"Task"}}, "wf", testNodes, "Jane Doe", "xyz-repo", nil)
10
- if err != nil {
11
- t.Fatalf("%v", err)
12
- }
13
- if !strings.Contains(j.jql, `assignee = "Jane Doe"`) {
14
- t.Errorf("distributed JQL missing assignee: %q", j.jql)
15
- }
16
- }
@@ -1,90 +0,0 @@
1
- // Package tasks defines the ticket-system plug point. Jira ships built in
2
- // (internal/tasks/jira); beads/linear/github adapters register the same
3
- // way via init() — the database/sql driver pattern: import to register,
4
- // then resolve by type name from the workflow YAML's `tasks.type`.
5
- package tasks
6
-
7
- import (
8
- "fmt"
9
- "sort"
10
- "sync"
11
-
12
- "github.com/rajpopat27/relay-flow/internal/config"
13
- )
14
-
15
- // Ticket is one unit of work as seen by the daemon. Adapters fill Node by
16
- // reverse-mapping the ticket's tracker state through the workflow's node
17
- // `when` values ("" = unmapped state), and ClaimedBy from claim labels
18
- // ("" = unclaimed).
19
- type Ticket struct {
20
- Key string
21
- Summary string
22
- Node string
23
- ClaimedBy string
24
- }
25
-
26
- // Tasks is the tracker adapter interface: list candidate tickets, claim
27
- // one, report an outcome. One List call per poll cycle per workflow.
28
- type Tasks interface {
29
- List() ([]Ticket, error)
30
- Claim(t Ticket) error
31
- // Report records the agent outcome: transitions the ticket to the
32
- // target node's tracker state (adapter resolves it) and posts the
33
- // summary. Self-loop (target state == current) → comment only.
34
- Report(t Ticket, outcome, targetNode, summary string) error
35
- }
36
-
37
- // Factory builds an adapter instance for one submitted workflow.
38
- type Factory struct {
39
- // UnmarshalConfig strictly decodes the YAML's tasks.config map into
40
- // the adapter's own config struct.
41
- UnmarshalConfig func(map[string]any) (any, error)
42
- // New builds the adapter. wfName is the workflow identity (claim
43
- // labels); nodes carry the `when` state map; assignee is the machine
44
- // user's tracker identity ("" when the adapter doesn't need it);
45
- // repoName is the repo's display name (tracker-side component/label
46
- // scoping; "" when the adapter doesn't scope by repo).
47
- New func(cfg any, wfName string, nodes map[string]config.Node, assignee, repoName string) (Tasks, error)
48
- }
49
-
50
- var (
51
- mu sync.RWMutex
52
- factories = map[string]Factory{}
53
- )
54
-
55
- // Register makes an adapter available under `type: <name>`. Called from
56
- // adapter init(); panics on duplicate names (programmer error).
57
- func Register(name string, f Factory) {
58
- mu.Lock()
59
- defer mu.Unlock()
60
- if _, dup := factories[name]; dup {
61
- panic("tasks: duplicate adapter registration " + name)
62
- }
63
- factories[name] = f
64
- }
65
-
66
- // New resolves a tasks adapter by type name and builds an instance.
67
- func New(typeName string, rawCfg map[string]any, wfName string, nodes map[string]config.Node, assignee, repoName string) (Tasks, error) {
68
- mu.RLock()
69
- f, ok := factories[typeName]
70
- mu.RUnlock()
71
- if !ok {
72
- return nil, fmt.Errorf("unknown tasks type %q (registered: %v)", typeName, registered())
73
- }
74
- cfg, err := f.UnmarshalConfig(rawCfg)
75
- if err != nil {
76
- return nil, fmt.Errorf("tasks type %q config: %w", typeName, err)
77
- }
78
- return f.New(cfg, wfName, nodes, assignee, repoName)
79
- }
80
-
81
- func registered() []string {
82
- mu.RLock()
83
- defer mu.RUnlock()
84
- names := make([]string, 0, len(factories))
85
- for n := range factories {
86
- names = append(names, n)
87
- }
88
- sort.Strings(names)
89
- return names
90
- }
@@ -1,91 +0,0 @@
1
- package tasks
2
-
3
- import (
4
- "strings"
5
- "testing"
6
-
7
- "github.com/rajpopat27/relay-flow/internal/config"
8
- )
9
-
10
- func testNodes() map[string]config.Node {
11
- return map[string]config.Node{
12
- "coding": {Agent: "build", When: "In Progress", OnSuccess: "reviewing", OnFailure: "coding"},
13
- "reviewing": {Agent: "build", When: "In Review", OnSuccess: "done", OnFailure: "coding"},
14
- "done": {When: "Done"},
15
- }
16
- }
17
-
18
- type fakeTasks struct {
19
- listed []Ticket
20
- claims []string
21
- reports []string
22
- }
23
-
24
- func (f *fakeTasks) List() ([]Ticket, error) { return f.listed, nil }
25
- func (f *fakeTasks) Claim(t Ticket) error {
26
- f.claims = append(f.claims, t.Key)
27
- return nil
28
- }
29
- func (f *fakeTasks) Report(t Ticket, outcome, targetNode, summary string) error {
30
- f.reports = append(f.reports, t.Key+":"+outcome+":"+targetNode)
31
- return nil
32
- }
33
-
34
- func TestRegistryNew(t *testing.T) {
35
- fake := &fakeTasks{}
36
- Register("fake", Factory{
37
- UnmarshalConfig: func(m map[string]any) (any, error) {
38
- if m["bad"] != nil {
39
- return nil, errFakeConfig
40
- }
41
- return m, nil
42
- },
43
- New: func(cfg any, wfName string, nodes map[string]config.Node, assignee, repoName string) (Tasks, error) {
44
- if repoName != "repo:xyz" {
45
- t.Errorf("repoName = %q", repoName)
46
- }
47
- if wfName == "" {
48
- t.Error("wfName empty")
49
- }
50
- if nodes["coding"].When != "In Progress" {
51
- t.Error("nodes not passed")
52
- }
53
- return fake, nil
54
- },
55
- })
56
-
57
- tk, err := New("fake", map[string]any{"k": "v"}, "xyzFlow", testNodes(), "Jane Doe", "repo:xyz")
58
- if err != nil {
59
- t.Fatalf("%v", err)
60
- }
61
- tk.Claim(Ticket{Key: "XYZ-1"})
62
- if len(fake.claims) != 1 || fake.claims[0] != "XYZ-1" {
63
- t.Errorf("claims = %v", fake.claims)
64
- }
65
-
66
- if _, err := New("nonexistent-adapter", nil, "w", nil, "", ""); err == nil ||
67
- !strings.Contains(err.Error(), "unknown tasks type") || !strings.Contains(err.Error(), "fake") {
68
- t.Errorf("unknown adapter error = %v", err)
69
- }
70
-
71
- if _, err := New("fake", map[string]any{"bad": true}, "w", testNodes(), "", ""); err == nil || !strings.Contains(err.Error(), "bad config") {
72
- t.Errorf("config unmarshal err = %v", err)
73
- }
74
- }
75
-
76
- var errFakeConfig = errorString("bad config")
77
-
78
- type errorString string
79
-
80
- func (e errorString) Error() string { return string(e) }
81
-
82
- func TestDuplicateRegisterPanics(t *testing.T) {
83
- defer func() {
84
- if recover() == nil {
85
- t.Fatal("expected panic on duplicate registration")
86
- }
87
- }()
88
- f := Factory{}
89
- Register("dup", f)
90
- Register("dup", f)
91
- }