relay-flow 0.2.7-alpha → 0.2.8-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 +19 -7
- package/cmd/relay-flow/commands_test.go +82 -11
- package/cmd/relay-flow/main.go +18 -86
- package/cmd/relay-flow/repo_registration.go +235 -0
- package/cmd/relay-flow/serve.go +2 -2
- package/examples/config-reference.yaml +7 -0
- package/internal/harness/opencode/opencode_test.go +1 -1
- package/internal/harness/opencode/repo_setup.go +1 -1
- package/internal/repo/service.go +13 -0
- package/internal/repo/service_test.go +14 -1
- package/internal/server/api_test.go +46 -0
- package/internal/server/client.go +30 -2
- package/internal/server/fixture_test.go +20 -16
- package/internal/server/server.go +39 -4
- package/internal/task/beads/beads.go +7 -2
- package/internal/task/beads/beads_test.go +13 -0
- package/internal/task/factory.go +9 -7
- package/internal/task/jira/auth_test.go +9 -1
- package/internal/task/jira/filters_test.go +2 -2
- package/internal/task/jira/helpers_test.go +13 -0
- package/internal/task/jira/jira.go +233 -44
- package/internal/task/jira/lifecycle_inheritance_test.go +5 -5
- package/internal/task/jira/rest/client.go +73 -31
- package/internal/task/jira/rest/client_test.go +25 -0
- package/internal/task/jira/status_defaults_test.go +134 -0
- package/internal/task/jira/templates_test.go +2 -2
- package/internal/task/jira/transition_defaults_test.go +8 -12
- package/internal/task/jira/validation_test.go +3 -1
- package/internal/task/registration.go +59 -0
- package/package.json +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
package jira
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"reflect"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/rajpopat27/relay-flow/internal/config"
|
|
10
|
+
"github.com/rajpopat27/relay-flow/internal/task"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func TestStatusRegistrationDefaultsOnlySelectAvailableConventions(t *testing.T) {
|
|
14
|
+
withConventions := statusRegistrationField("statusDefaults.start", "Start", []string{"Open", "In Progress", "Closed"}, defaultStartParentStatus)
|
|
15
|
+
if withConventions.Default != defaultStartParentStatus {
|
|
16
|
+
t.Fatalf("default = %q, want %q", withConventions.Default, defaultStartParentStatus)
|
|
17
|
+
}
|
|
18
|
+
withoutConventions := statusRegistrationField("statusDefaults.end", "End", []string{"Open", "Working", "Closed"}, defaultEndParentStatus)
|
|
19
|
+
if withoutConventions.Default != "" {
|
|
20
|
+
t.Fatalf("missing conventional default = %q, want empty", withoutConventions.Default)
|
|
21
|
+
}
|
|
22
|
+
if !reflect.DeepEqual(withoutConventions.Options, []string{"Open", "Working", "Closed"}) {
|
|
23
|
+
t.Fatalf("options = %v", withoutConventions.Options)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func TestRepositoryStatusDefaultsDriveLifecycleAndMailboxCompletion(t *testing.T) {
|
|
28
|
+
fake := &fakeJira{}
|
|
29
|
+
sys, err := newSystem(context.Background(), &fakeClient{fake: fake}, task.RepoSpec{
|
|
30
|
+
Name: "payments",
|
|
31
|
+
RepoConfig: config.RawValues{
|
|
32
|
+
"project": "PAY",
|
|
33
|
+
"component": "api",
|
|
34
|
+
"statusDefaults": map[string]any{
|
|
35
|
+
"start": "Open",
|
|
36
|
+
"work": "Working",
|
|
37
|
+
"end": "Closed",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
if err != nil {
|
|
42
|
+
t.Fatal(err)
|
|
43
|
+
}
|
|
44
|
+
defaults := task.LifecycleDefaults(sys)
|
|
45
|
+
parent := task.TicketRef{ID: "1", Key: "PAY-101"}
|
|
46
|
+
mailbox := task.Mailbox{ID: "2", Key: "PAY-102", Node: "coding"}
|
|
47
|
+
|
|
48
|
+
if err := sys.ApplyTaskConfig(context.Background(), task.Target{Parent: parent}, defaults.StartDefaults()); err != nil {
|
|
49
|
+
t.Fatal(err)
|
|
50
|
+
}
|
|
51
|
+
if err := sys.ApplyTaskConfig(context.Background(), task.Target{Parent: parent, Mailbox: &mailbox}, defaults.WorkDefaults()); err != nil {
|
|
52
|
+
t.Fatal(err)
|
|
53
|
+
}
|
|
54
|
+
if err := sys.CompleteMailbox(context.Background(), mailbox); err != nil {
|
|
55
|
+
t.Fatal(err)
|
|
56
|
+
}
|
|
57
|
+
if len(fake.parentTransitions) != 1 || fake.parentTransitions[0] != "Open" {
|
|
58
|
+
t.Fatalf("parent transitions = %v, want [Open]", fake.parentTransitions)
|
|
59
|
+
}
|
|
60
|
+
if len(fake.taskTransitions) != 2 || fake.taskTransitions[0] != "Working" || fake.taskTransitions[1] != "Closed" {
|
|
61
|
+
t.Fatalf("mailbox transitions = %v, want [Working Closed]", fake.taskTransitions)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func TestJiraRegistrationRejectsComponentOverride(t *testing.T) {
|
|
66
|
+
err := validateJiraRegistration(context.Background(), task.RepoRegistrationSpec{
|
|
67
|
+
Name: "payments",
|
|
68
|
+
RepoConfig: config.RawValues{
|
|
69
|
+
"project": "PAY",
|
|
70
|
+
"component": "other-component",
|
|
71
|
+
"statusDefaults": map[string]any{
|
|
72
|
+
"start": "Open", "work": "Working", "end": "Closed",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
if err == nil || !strings.Contains(err.Error(), "must match repository name") {
|
|
77
|
+
t.Fatalf("component override error = %v", err)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func TestJiraConstructionRejectsRootStatusDefaults(t *testing.T) {
|
|
82
|
+
_, err := newSystem(context.Background(), &fakeClient{fake: &fakeJira{}}, task.RepoSpec{
|
|
83
|
+
Name: "payments",
|
|
84
|
+
RootConfig: config.RawValues{"statusDefaults": map[string]any{}},
|
|
85
|
+
RepoConfig: testRepoConfig(nil),
|
|
86
|
+
})
|
|
87
|
+
if err == nil || !strings.Contains(err.Error(), "configured per repository") {
|
|
88
|
+
t.Fatalf("root statusDefaults error = %v", err)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func TestJiraValidateConfigRejectsNonRepositoryStatusDefaults(t *testing.T) {
|
|
93
|
+
sys := newSystemWithFake(t, &fakeJira{})
|
|
94
|
+
for _, tc := range []struct {
|
|
95
|
+
name string
|
|
96
|
+
node bool
|
|
97
|
+
}{
|
|
98
|
+
{name: "workflow"},
|
|
99
|
+
{name: "node", node: true},
|
|
100
|
+
} {
|
|
101
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
102
|
+
workflowConfig := config.RawValues{}
|
|
103
|
+
nodeConfig := map[string]config.RawValues{}
|
|
104
|
+
if tc.node {
|
|
105
|
+
nodeConfig["coding"] = config.RawValues{"statusDefaults": map[string]any{}}
|
|
106
|
+
} else {
|
|
107
|
+
workflowConfig["statusDefaults"] = map[string]any{}
|
|
108
|
+
}
|
|
109
|
+
err := sys.ValidateConfig(context.Background(), workflowConfig, nodeConfig)
|
|
110
|
+
if err == nil || !strings.Contains(err.Error(), "configured per repository") {
|
|
111
|
+
t.Fatalf("validation error = %v", err)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func TestJiraConstructionRequiresCompleteRepositoryStatusDefaults(t *testing.T) {
|
|
118
|
+
for _, repoConfig := range []config.RawValues{
|
|
119
|
+
{"project": "PAY", "component": "api"},
|
|
120
|
+
{"project": "PAY", "component": "api", "statusDefaults": map[string]any{}},
|
|
121
|
+
{"project": "PAY", "component": "api", "statusDefaults": map[string]any{
|
|
122
|
+
"start": "Open", "work": "Working",
|
|
123
|
+
}},
|
|
124
|
+
} {
|
|
125
|
+
client := &validationClient{}
|
|
126
|
+
_, err := newSystem(context.Background(), client, task.RepoSpec{Name: "payments", RepoConfig: repoConfig})
|
|
127
|
+
if err == nil {
|
|
128
|
+
t.Fatalf("repo config %#v was accepted without complete status defaults", repoConfig)
|
|
129
|
+
}
|
|
130
|
+
if len(client.statuses) != 0 {
|
|
131
|
+
t.Fatalf("status probes = %v for invalid repository config, want none", client.statuses)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -21,7 +21,7 @@ func TestTaskTextTemplatesExposeValues(t *testing.T) {
|
|
|
21
21
|
"summaryComment": "summary {{sourceNode}}|{{targetNode}}|{{mailbox}}\n{{summaryReport}}",
|
|
22
22
|
"feedbackComment": "feedback {{sourceNode}}|{{targetNode}}|{{mailbox}}\n{{feedbackReport}}",
|
|
23
23
|
}},
|
|
24
|
-
RepoConfig:
|
|
24
|
+
RepoConfig: testRepoConfig(nil),
|
|
25
25
|
})
|
|
26
26
|
if err != nil {
|
|
27
27
|
t.Fatal(err)
|
|
@@ -91,7 +91,7 @@ func TestTaskTemplatesRenderSharedReportContractValues(t *testing.T) {
|
|
|
91
91
|
}
|
|
92
92
|
fixture := fixtures["work"]
|
|
93
93
|
sys, err := newSystem(context.Background(), &fakeClient{fake: &fakeJira{}}, task.RepoSpec{
|
|
94
|
-
Name: "payments", RepoConfig:
|
|
94
|
+
Name: "payments", RepoConfig: testRepoConfig(nil),
|
|
95
95
|
})
|
|
96
96
|
if err != nil {
|
|
97
97
|
t.Fatal(err)
|
|
@@ -10,11 +10,9 @@ import (
|
|
|
10
10
|
"github.com/rajpopat27/relay-flow/internal/task"
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// nodes, and parent Done at end; an omitted work-node parent transition
|
|
17
|
-
// leaves the parent unchanged.
|
|
13
|
+
// Jira repository status defaults are deterministic: omitted transitions use
|
|
14
|
+
// the registered start/work/end values, and an omitted work-node parent
|
|
15
|
+
// transition leaves the parent unchanged.
|
|
18
16
|
//
|
|
19
17
|
// This test is package-local (package jira) so it can drive the adapter's
|
|
20
18
|
// ApplyTaskConfig against a test-local fake client without inventing an
|
|
@@ -128,10 +126,8 @@ func TestEndDefaultParentDone(t *testing.T) {
|
|
|
128
126
|
sys := newSystemWithFake(t, fake)
|
|
129
127
|
parent := task.TicketRef{ID: "1", Key: "PAY-101", Title: "parent"}
|
|
130
128
|
|
|
131
|
-
// end: omitted transitionTo.
|
|
132
|
-
//
|
|
133
|
-
// orchestration merges EndDefaults under the end node config before
|
|
134
|
-
// applying it to the parent target.
|
|
129
|
+
// end: omitted transitionTo. Run orchestration merges the registered
|
|
130
|
+
// EndDefaults under the end node config before applying it to the parent.
|
|
135
131
|
endCfg := config.Merge(sys.(task.LifecycleDefaults).EndDefaults(), config.RawValues{})
|
|
136
132
|
if err := sys.ApplyTaskConfig(context.Background(), task.Target{Parent: parent}, endCfg); err != nil {
|
|
137
133
|
t.Fatalf("ApplyTaskConfig end failed: %v", err)
|
|
@@ -156,8 +152,8 @@ func TestPrepareRestartReopensMailboxesWithoutChangingParent(t *testing.T) {
|
|
|
156
152
|
if err := preparer.PrepareRestart(context.Background(), parent, mailboxes); err != nil {
|
|
157
153
|
t.Fatalf("PrepareRestart failed: %v", err)
|
|
158
154
|
}
|
|
159
|
-
if len(fake.taskTransitions) != 2 || fake.taskTransitions[0] !=
|
|
160
|
-
t.Fatalf("mailbox transitions = %v, want two
|
|
155
|
+
if len(fake.taskTransitions) != 2 || fake.taskTransitions[0] != defaultStartParentStatus || fake.taskTransitions[1] != defaultStartParentStatus {
|
|
156
|
+
t.Fatalf("mailbox transitions = %v, want two %s transitions", fake.taskTransitions, defaultStartParentStatus)
|
|
161
157
|
}
|
|
162
158
|
if len(fake.parentTransitions) != 0 {
|
|
163
159
|
t.Fatalf("parent transitions = %v, want none (start owns parent status)", fake.parentTransitions)
|
|
@@ -165,7 +161,7 @@ func TestPrepareRestartReopensMailboxesWithoutChangingParent(t *testing.T) {
|
|
|
165
161
|
}
|
|
166
162
|
|
|
167
163
|
func TestPrepareRestartPreservesHumanJiraStateOnConflict(t *testing.T) {
|
|
168
|
-
fake := &fakeJira{transitionErr: errors.New(`transition to "
|
|
164
|
+
fake := &fakeJira{transitionErr: errors.New(`transition to "In Progress" is not available for PAY-102`)}
|
|
169
165
|
sys := newSystemWithFake(t, fake)
|
|
170
166
|
preparer := sys.(task.RestartPreparer)
|
|
171
167
|
err := preparer.PrepareRestart(context.Background(), task.TicketRef{Key: "PAY-101"}, []task.Mailbox{
|
|
@@ -88,7 +88,9 @@ func TestValidateConfigProbesWorkflowAssignee(t *testing.T) {
|
|
|
88
88
|
|
|
89
89
|
func taskSpec(root, repo config.RawValues) task.RepoSpec {
|
|
90
90
|
if repo == nil {
|
|
91
|
-
repo =
|
|
91
|
+
repo = testRepoConfig(nil)
|
|
92
|
+
} else {
|
|
93
|
+
repo = config.Merge(testRepoConfig(nil), repo)
|
|
92
94
|
}
|
|
93
95
|
return task.RepoSpec{Name: "payments", RootConfig: root, RepoConfig: repo}
|
|
94
96
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package task
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
|
|
7
|
+
"github.com/rajpopat27/relay-flow/internal/config"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// RegistrationField describes one task-plugin-owned repository-registration
|
|
11
|
+
// value. The CLI only renders this metadata; it does not interpret provider
|
|
12
|
+
// names or status vocabulary. A field with Options is rendered as a select,
|
|
13
|
+
// otherwise it is rendered as free text. Derived fields are filled from the
|
|
14
|
+
// runner candidate name and are never prompted.
|
|
15
|
+
type RegistrationField struct {
|
|
16
|
+
Key string `json:"key"`
|
|
17
|
+
Title string `json:"title"`
|
|
18
|
+
Options []string `json:"options,omitempty"`
|
|
19
|
+
Default string `json:"default,omitempty"`
|
|
20
|
+
Derived bool `json:"derived,omitempty"`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Registration describes the fields needed by a task plugin for one repo
|
|
24
|
+
// registration.
|
|
25
|
+
type Registration struct {
|
|
26
|
+
Fields []RegistrationField `json:"fields"`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// RegistrationFields returns task-plugin-owned registration metadata for the
|
|
30
|
+
// supplied values. Values use the plugin's registration keys (including
|
|
31
|
+
// dotted keys such as statusDefaults.start) and are intentionally opaque to
|
|
32
|
+
// core.
|
|
33
|
+
func RegistrationFields(ctx context.Context, name string, values config.RawValues) ([]RegistrationField, error) {
|
|
34
|
+
f, err := lookup(name)
|
|
35
|
+
if err != nil {
|
|
36
|
+
return nil, err
|
|
37
|
+
}
|
|
38
|
+
if f.RegistrationFields == nil {
|
|
39
|
+
return nil, fmt.Errorf("task plugin %q does not provide repository registration fields", name)
|
|
40
|
+
}
|
|
41
|
+
return f.RegistrationFields(ctx, values)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ValidateRegistration lets a task plugin validate registration values,
|
|
45
|
+
// including remote project/workflow metadata, before the repository is
|
|
46
|
+
// persisted. Plugins that do not need this phase remain unchanged.
|
|
47
|
+
func ValidateRegistration(ctx context.Context, name string, spec RepoRegistrationSpec) error {
|
|
48
|
+
f, err := lookup(name)
|
|
49
|
+
if err != nil {
|
|
50
|
+
return err
|
|
51
|
+
}
|
|
52
|
+
if f.ValidateRegistration == nil {
|
|
53
|
+
return nil
|
|
54
|
+
}
|
|
55
|
+
if err := f.ValidateRegistration(ctx, spec); err != nil {
|
|
56
|
+
return fmt.Errorf("task registration: %w", err)
|
|
57
|
+
}
|
|
58
|
+
return nil
|
|
59
|
+
}
|