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.
@@ -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: config.RawValues{"project": "PAY", "component": "api"},
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: config.RawValues{"project": "PAY", "component": "api"},
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
- // 3.5: Jira transition defaults per specs/workflow-definition "Jira
14
- // transition defaults are deterministic": omitted transitions default to
15
- // parent In Progress at start, mailbox In Progress at agent/HITL work
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. The end effective config carries no
132
- // transition; the adapter must still default the parent to Done. Run
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] != "To Do" || fake.taskTransitions[1] != "To Do" {
160
- t.Fatalf("mailbox transitions = %v, want two To Do transitions", fake.taskTransitions)
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 "To Do" is not available for PAY-102`)}
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 = config.RawValues{"project": "PAY", "component": "api"}
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-flow",
3
- "version": "0.2.7-alpha",
3
+ "version": "0.2.8-alpha",
4
4
  "description": "Graph-based agent workflow engine — tracker-agnostic, pluggable runners",
5
5
  "bin": {
6
6
  "relay-flow": "./bin/relay-flow.js",