relay-flow 0.2.9-alpha → 0.2.11-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 -5
- package/cmd/relay-flow/main.go +224 -23
- package/cmd/relay-flow/observability_test.go +204 -0
- package/cmd/relay-flow/render.go +629 -0
- package/cmd/relay-flow/serve.go +32 -1
- package/internal/execution/goworkflows/activities.go +9 -0
- package/internal/execution/goworkflows/engine.go +15 -0
- package/internal/execution/goworkflows/interpreter.go +86 -3
- package/internal/execution/goworkflows/projection.go +16 -0
- package/internal/execution/projection/detail_test.go +259 -0
- package/internal/execution/projection/projection.go +290 -6
- package/internal/execution/projection/projection_test.go +3 -2
- package/internal/execution/temporal/activities.go +9 -0
- package/internal/execution/temporal/engine.go +13 -0
- package/internal/execution/temporal/interpreter.go +131 -3
- package/internal/execution/temporal/operations.go +174 -0
- package/internal/execution/temporal/operations_test.go +36 -0
- package/internal/harness/opencode/opencode_test.go +1 -1
- package/internal/harness/opencode/repo_setup.go +1 -1
- package/internal/run/detail.go +277 -0
- package/internal/run/detail_test.go +71 -0
- package/internal/run/run.go +4 -0
- package/internal/server/client.go +39 -0
- package/internal/server/observability.go +109 -0
- package/internal/server/observability_test.go +60 -0
- package/internal/server/server.go +36 -0
- package/package.json +1 -1
|
@@ -211,6 +211,7 @@ func (e *Engine) registerActivities() error {
|
|
|
211
211
|
a.CompleteMailbox,
|
|
212
212
|
a.ProjectionUpdateNodeRuntimeVisit,
|
|
213
213
|
a.ProjectionRecordProcessedReport,
|
|
214
|
+
a.ProjectionUpsertStep,
|
|
214
215
|
a.ProjectionUpdateNode,
|
|
215
216
|
a.ProjectionUpdateState,
|
|
216
217
|
a.ProjectionUpdateRetry,
|
|
@@ -502,6 +503,20 @@ func (e *Engine) FindRunByTicket(ctx context.Context, ticket string) (run.Run, e
|
|
|
502
503
|
return e.runs.findByTicket(ctx, ticket)
|
|
503
504
|
}
|
|
504
505
|
|
|
506
|
+
func (e *Engine) GetRunDetail(ctx context.Context, id run.ID) (run.RunDetail, error) {
|
|
507
|
+
detail, err := e.runs.getDetail(ctx, id)
|
|
508
|
+
if err != nil {
|
|
509
|
+
return run.RunDetail{}, err
|
|
510
|
+
}
|
|
511
|
+
// Static definitions are used only to add explicit pending display rows;
|
|
512
|
+
// the projection remains a cache and never influences execution.
|
|
513
|
+
if wf, wfErr := e.workflowOf(ctx, id); wfErr == nil {
|
|
514
|
+
detail.AddPendingNodes(*wf)
|
|
515
|
+
detail.DeriveInspectionFields(time.Now().UTC())
|
|
516
|
+
}
|
|
517
|
+
return detail, nil
|
|
518
|
+
}
|
|
519
|
+
|
|
505
520
|
func (e *Engine) ListRuns(ctx context.Context, filter run.Filter) ([]run.Run, error) {
|
|
506
521
|
return e.runs.list(ctx, filter)
|
|
507
522
|
}
|
|
@@ -142,6 +142,23 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
142
142
|
return err
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// The step timeline is a derived display projection. Its sequence is
|
|
146
|
+
// deterministic for replay and is never consulted for routing.
|
|
147
|
+
stepSequence := int64(0)
|
|
148
|
+
target, err := wf.StartTarget()
|
|
149
|
+
if err != nil {
|
|
150
|
+
return err
|
|
151
|
+
}
|
|
152
|
+
startStarted := goworkflow.Now(ctx).UTC()
|
|
153
|
+
if _, err := retryLoop(ctx, start.ID, a, work, "start",
|
|
154
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
155
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep,
|
|
156
|
+
run.StepEntry{RunID: start.ID, Sequence: stepSequence, Node: "start", NodeType: "lifecycle",
|
|
157
|
+
Status: run.StepRunning, StartedAt: &startStarted, Depth: 1})
|
|
158
|
+
}); err != nil {
|
|
159
|
+
return err
|
|
160
|
+
}
|
|
161
|
+
|
|
145
162
|
// Process the reserved start taskConfig (parent target).
|
|
146
163
|
startNode := wf.Nodes["start"]
|
|
147
164
|
if _, err := retryLoop(ctx, start.ID, a, work, "start",
|
|
@@ -160,13 +177,21 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
160
177
|
return err
|
|
161
178
|
}
|
|
162
179
|
|
|
163
|
-
|
|
164
|
-
if err
|
|
180
|
+
startFinished := goworkflow.Now(ctx).UTC()
|
|
181
|
+
if _, err := retryLoop(ctx, start.ID, a, work, "start",
|
|
182
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
183
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep,
|
|
184
|
+
run.StepEntry{RunID: start.ID, Sequence: stepSequence, Node: "start", NodeType: "lifecycle",
|
|
185
|
+
Status: run.StepSucceeded, StartedAt: &startStarted, FinishedAt: &startFinished,
|
|
186
|
+
Route: target, Depth: 1})
|
|
187
|
+
}); err != nil {
|
|
165
188
|
return err
|
|
166
189
|
}
|
|
167
190
|
|
|
168
191
|
current := target
|
|
169
192
|
seenReportIDs := map[string]bool{}
|
|
193
|
+
lastStepByNode := map[string]int64{}
|
|
194
|
+
lastDepthByNode := map[string]int{}
|
|
170
195
|
for current != "end" {
|
|
171
196
|
node := wf.Nodes[current]
|
|
172
197
|
|
|
@@ -178,6 +203,27 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
178
203
|
return err
|
|
179
204
|
}
|
|
180
205
|
visitID := run.NodeVisitID(visit)
|
|
206
|
+
stepSequence++
|
|
207
|
+
stepParent := int64(0)
|
|
208
|
+
stepDepth := 1
|
|
209
|
+
if previous, ok := lastStepByNode[current]; ok {
|
|
210
|
+
stepParent = previous
|
|
211
|
+
stepDepth = lastDepthByNode[current] + 1
|
|
212
|
+
}
|
|
213
|
+
stepStarted := goworkflow.Now(ctx).UTC()
|
|
214
|
+
if _, err := retryLoop(ctx, start.ID, a, work, current,
|
|
215
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
216
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep,
|
|
217
|
+
run.StepEntry{RunID: start.ID, Sequence: stepSequence, Node: current,
|
|
218
|
+
NodeVisitID: visitID, NodeType: string(node.Type), Status: run.StepRunning,
|
|
219
|
+
StartedAt: &stepStarted, ParentSequence: stepParent, Depth: stepDepth,
|
|
220
|
+
Runtime: node.Agent})
|
|
221
|
+
}); err != nil {
|
|
222
|
+
return err
|
|
223
|
+
}
|
|
224
|
+
lastStepByNode[current] = stepSequence
|
|
225
|
+
lastDepthByNode[current] = stepDepth
|
|
226
|
+
|
|
181
227
|
runtime, err := retryLoop(ctx, start.ID, a, work, current,
|
|
182
228
|
func(ctx2 goworkflow.Context) goworkflow.Future[NodeRuntime] {
|
|
183
229
|
return goworkflow.ExecuteActivity[NodeRuntime](ctx2, noNativeRetries,
|
|
@@ -328,6 +374,23 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
328
374
|
}); err != nil {
|
|
329
375
|
return err
|
|
330
376
|
}
|
|
377
|
+
stepStatus := run.StepSucceeded
|
|
378
|
+
stepMessage := report.Summary.Completed
|
|
379
|
+
if report.Status == workflow.OutcomeFailure {
|
|
380
|
+
stepStatus = run.StepFailed
|
|
381
|
+
stepMessage = report.Summary.IssuesDiscovered
|
|
382
|
+
}
|
|
383
|
+
stepFinished := goworkflow.Now(ctx).UTC()
|
|
384
|
+
if _, err := retryLoop(ctx, start.ID, a, work, current,
|
|
385
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
386
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep,
|
|
387
|
+
run.StepEntry{RunID: start.ID, Sequence: stepSequence, Node: current,
|
|
388
|
+
NodeVisitID: visitID, NodeType: string(node.Type), Status: stepStatus,
|
|
389
|
+
StartedAt: &stepStarted, FinishedAt: &stepFinished, Message: stepMessage,
|
|
390
|
+
Route: report.NextStep, Runtime: node.Agent, ParentSequence: stepParent, Depth: stepDepth})
|
|
391
|
+
}); err != nil {
|
|
392
|
+
return err
|
|
393
|
+
}
|
|
331
394
|
|
|
332
395
|
// Ordered transition: summary -> feedback (selected next only) ->
|
|
333
396
|
// complete current -> process next node.
|
|
@@ -389,7 +452,19 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
389
452
|
}
|
|
390
453
|
|
|
391
454
|
// end: apply end task config, then optional runner cleanup, then mark
|
|
392
|
-
// the run completed.
|
|
455
|
+
// the run completed. Record the lifecycle row before setup so a lost
|
|
456
|
+
// terminal upsert can still be finalized by the authoritative completed
|
|
457
|
+
// state update.
|
|
458
|
+
stepSequence++
|
|
459
|
+
endStarted := goworkflow.Now(ctx).UTC()
|
|
460
|
+
endStep := run.StepEntry{RunID: start.ID, Sequence: stepSequence, Node: "end", NodeType: "lifecycle",
|
|
461
|
+
Status: run.StepRunning, StartedAt: &endStarted, Depth: 1}
|
|
462
|
+
if _, err := retryLoop(ctx, start.ID, a, work, "end",
|
|
463
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
464
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep, endStep)
|
|
465
|
+
}); err != nil {
|
|
466
|
+
return err
|
|
467
|
+
}
|
|
393
468
|
endNode := wf.Nodes["end"]
|
|
394
469
|
if _, err := retryLoop(ctx, start.ID, a, work, "end",
|
|
395
470
|
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
@@ -424,6 +499,14 @@ func (a *Activities) runGraph(ctx goworkflow.Context, start run.Start) error {
|
|
|
424
499
|
return err
|
|
425
500
|
}
|
|
426
501
|
}
|
|
502
|
+
endFinished := goworkflow.Now(ctx).UTC()
|
|
503
|
+
endStep.Status, endStep.FinishedAt = run.StepSucceeded, &endFinished
|
|
504
|
+
if _, err := retryLoop(ctx, start.ID, a, work, "end",
|
|
505
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
506
|
+
return goworkflow.ExecuteActivity[struct{}](ctx2, noNativeRetries, a.ProjectionUpsertStep, endStep)
|
|
507
|
+
}); err != nil {
|
|
508
|
+
return err
|
|
509
|
+
}
|
|
427
510
|
if _, err := retryLoop(ctx, start.ID, a, work, "",
|
|
428
511
|
func(ctx2 goworkflow.Context) goworkflow.Future[struct{}] {
|
|
429
512
|
now := goworkflow.Now(ctx).UTC()
|
|
@@ -54,6 +54,18 @@ func (p *RunProjection) updateNode(ctx context.Context, id run.ID, state run.Sta
|
|
|
54
54
|
return p.shared().UpdateNode(ctx, id, state, node, visit)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
func (p *RunProjection) upsertStep(ctx context.Context, step run.StepEntry) error {
|
|
58
|
+
return p.shared().UpsertStep(ctx, step)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func (p *RunProjection) listSteps(ctx context.Context, id run.ID) ([]run.StepEntry, error) {
|
|
62
|
+
return p.shared().ListSteps(ctx, id)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func (p *RunProjection) getDetail(ctx context.Context, id run.ID) (run.RunDetail, error) {
|
|
66
|
+
return p.shared().GetDetail(ctx, id)
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
func (p *RunProjection) getNodeRuntime(ctx context.Context, id run.ID, node string) (NodeRuntime, error) {
|
|
58
70
|
return p.shared().GetNodeRuntime(ctx, id, node)
|
|
59
71
|
}
|
|
@@ -102,6 +114,10 @@ func (p *RunProjection) get(ctx context.Context, id run.ID) (run.Run, error) {
|
|
|
102
114
|
return p.shared().Get(ctx, id)
|
|
103
115
|
}
|
|
104
116
|
|
|
117
|
+
func (p *RunProjection) detail(ctx context.Context, id run.ID) (run.RunDetail, error) {
|
|
118
|
+
return p.getDetail(ctx, id)
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
func (p *RunProjection) findByTicket(ctx context.Context, ticket string) (run.Run, error) {
|
|
106
122
|
return p.shared().FindByTicket(ctx, ticket)
|
|
107
123
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
package projection_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"testing"
|
|
6
|
+
"time"
|
|
7
|
+
|
|
8
|
+
"github.com/rajpopat27/relay-flow/internal/run"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func TestLegacyNullCreationAndDurationRemainUnavailable(t *testing.T) {
|
|
12
|
+
ctx := context.Background()
|
|
13
|
+
p, db := openProjection(t)
|
|
14
|
+
now := time.Now().UTC()
|
|
15
|
+
if _, err := db.Exec(`INSERT INTO relay_runs (id, repo, workflow, ticket_id, ticket_key, state, started_at, updated_at) VALUES (?, 'repo', 'workflow', 'T-LEGACY', 'T-LEGACY', 'waiting', ?, ?)`, "legacy-null", now, now); err != nil {
|
|
16
|
+
t.Fatal(err)
|
|
17
|
+
}
|
|
18
|
+
detail, err := p.GetDetail(ctx, "legacy-null")
|
|
19
|
+
if err != nil {
|
|
20
|
+
t.Fatal(err)
|
|
21
|
+
}
|
|
22
|
+
if detail.CreatedAt != nil {
|
|
23
|
+
t.Fatalf("legacy CreatedAt = %v, want nil", detail.CreatedAt)
|
|
24
|
+
}
|
|
25
|
+
if err := p.UpsertStep(ctx, run.StepEntry{RunID: "legacy-null", Sequence: 1, Node: "coding", Status: run.StepRunning}); err != nil {
|
|
26
|
+
t.Fatal(err)
|
|
27
|
+
}
|
|
28
|
+
steps, err := p.ListSteps(ctx, "legacy-null")
|
|
29
|
+
if err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
if len(steps) != 1 || steps[0].DurationKnown {
|
|
33
|
+
t.Fatalf("legacy unknown duration = %#v", steps)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func TestStepProjectionIsIdempotentAndDerivesDetail(t *testing.T) {
|
|
38
|
+
ctx := context.Background()
|
|
39
|
+
p, _ := openProjection(t)
|
|
40
|
+
start := projectionStart("detail-run", "PAY-DETAIL")
|
|
41
|
+
started := time.Now().UTC().Add(-2 * time.Minute)
|
|
42
|
+
if err := p.InsertStart(ctx, start, started); err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
finished := started.Add(time.Minute)
|
|
46
|
+
step := run.StepEntry{
|
|
47
|
+
RunID: start.ID, Sequence: 1, Node: "coding", NodeVisitID: "visit-1", NodeType: "agent",
|
|
48
|
+
Status: run.StepSucceeded, StartedAt: &started, FinishedAt: &finished,
|
|
49
|
+
Message: "report accepted", Route: "end", Runtime: "coder", Resource: "harness",
|
|
50
|
+
}
|
|
51
|
+
if err := p.UpsertStep(ctx, step); err != nil {
|
|
52
|
+
t.Fatal(err)
|
|
53
|
+
}
|
|
54
|
+
if err := p.UpsertStep(ctx, step); err != nil {
|
|
55
|
+
t.Fatal(err)
|
|
56
|
+
}
|
|
57
|
+
steps, err := p.ListSteps(ctx, start.ID)
|
|
58
|
+
if err != nil {
|
|
59
|
+
t.Fatal(err)
|
|
60
|
+
}
|
|
61
|
+
if len(steps) != 1 || steps[0].NodeVisitID != step.NodeVisitID || steps[0].Route != "end" || steps[0].Duration <= 0 {
|
|
62
|
+
t.Fatalf("steps = %#v", steps)
|
|
63
|
+
}
|
|
64
|
+
detail, err := p.GetDetail(ctx, start.ID)
|
|
65
|
+
if err != nil {
|
|
66
|
+
t.Fatal(err)
|
|
67
|
+
}
|
|
68
|
+
if detail.Progress.Completed != 1 || detail.Progress.Total != 1 || detail.ResourcesDuration.Harness <= 0 {
|
|
69
|
+
t.Fatalf("detail = %#v", detail)
|
|
70
|
+
}
|
|
71
|
+
if detail.Conditions.Completed {
|
|
72
|
+
t.Fatal("starting run was reported completed")
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func TestStepUpsertDoesNotRegressTerminalVisit(t *testing.T) {
|
|
77
|
+
ctx := context.Background()
|
|
78
|
+
p, _ := openProjection(t)
|
|
79
|
+
start := projectionStart("monotonic-run", "PAY-MONOTONIC")
|
|
80
|
+
if err := p.InsertStart(ctx, start, time.Now().UTC()); err != nil {
|
|
81
|
+
t.Fatal(err)
|
|
82
|
+
}
|
|
83
|
+
finished := time.Now().UTC()
|
|
84
|
+
terminal := run.StepEntry{RunID: start.ID, Sequence: 1, Node: "coding", NodeVisitID: "visit-1", Status: run.StepSucceeded, FinishedAt: &finished, Message: "accepted", Route: "end"}
|
|
85
|
+
if err := p.UpsertStep(ctx, terminal); err != nil {
|
|
86
|
+
t.Fatal(err)
|
|
87
|
+
}
|
|
88
|
+
stale := terminal
|
|
89
|
+
stale.Status, stale.Message, stale.Route = run.StepRunning, "stale retry", "coding"
|
|
90
|
+
if err := p.UpsertStep(ctx, stale); err != nil {
|
|
91
|
+
t.Fatal(err)
|
|
92
|
+
}
|
|
93
|
+
steps, err := p.ListSteps(ctx, start.ID)
|
|
94
|
+
if err != nil {
|
|
95
|
+
t.Fatal(err)
|
|
96
|
+
}
|
|
97
|
+
if len(steps) != 1 || steps[0].Status != run.StepSucceeded || steps[0].Message != "accepted" || steps[0].Route != "end" {
|
|
98
|
+
t.Fatalf("stale write regressed terminal step: %#v", steps)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func TestBranchLoopVisitsRemainDistinctAndNested(t *testing.T) {
|
|
103
|
+
ctx := context.Background()
|
|
104
|
+
p, _ := openProjection(t)
|
|
105
|
+
start := projectionStart("branch-loop", "PAY-BRANCH")
|
|
106
|
+
if err := p.InsertStart(ctx, start, time.Now().UTC()); err != nil {
|
|
107
|
+
t.Fatal(err)
|
|
108
|
+
}
|
|
109
|
+
steps := []run.StepEntry{
|
|
110
|
+
{RunID: start.ID, Sequence: 1, Node: "coding", NodeVisitID: "visit-coding-1", Status: run.StepSucceeded, Depth: 1},
|
|
111
|
+
{RunID: start.ID, Sequence: 2, Node: "review", NodeVisitID: "visit-review-1", Status: run.StepSucceeded, Depth: 1},
|
|
112
|
+
{RunID: start.ID, Sequence: 3, Node: "coding", NodeVisitID: "visit-coding-2", ParentSequence: 1, Status: run.StepWaiting, Depth: 2},
|
|
113
|
+
}
|
|
114
|
+
for _, step := range steps {
|
|
115
|
+
if err := p.UpsertStep(ctx, step); err != nil {
|
|
116
|
+
t.Fatal(err)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
got, err := p.ListSteps(ctx, start.ID)
|
|
120
|
+
if err != nil {
|
|
121
|
+
t.Fatal(err)
|
|
122
|
+
}
|
|
123
|
+
if len(got) != len(steps) || got[2].NodeVisitID != "visit-coding-2" || got[2].ParentSequence != 1 || got[2].Depth != 2 {
|
|
124
|
+
t.Fatalf("branch/loop steps = %#v", got)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
func TestStepProjectionFailureDoesNotBlockAuthoritativeRunState(t *testing.T) {
|
|
129
|
+
ctx := context.Background()
|
|
130
|
+
p, db := openProjection(t)
|
|
131
|
+
start := projectionStart("projection-failure", "PAY-PROJECTION")
|
|
132
|
+
if err := p.InsertStart(ctx, start, time.Now().UTC()); err != nil {
|
|
133
|
+
t.Fatal(err)
|
|
134
|
+
}
|
|
135
|
+
if _, err := db.Exec(`DROP TABLE relay_run_steps`); err != nil {
|
|
136
|
+
t.Fatal(err)
|
|
137
|
+
}
|
|
138
|
+
if err := p.UpdateState(ctx, start.ID, run.StateWaiting, "", nil); err != nil {
|
|
139
|
+
t.Fatalf("authoritative state update failed with missing display table: %v", err)
|
|
140
|
+
}
|
|
141
|
+
got, err := p.Get(ctx, start.ID)
|
|
142
|
+
if err != nil {
|
|
143
|
+
t.Fatal(err)
|
|
144
|
+
}
|
|
145
|
+
if got.State != run.StateWaiting {
|
|
146
|
+
t.Fatalf("state = %s, want waiting", got.State)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func TestTerminalStepTimingIsStableAcrossLaterRunFinalization(t *testing.T) {
|
|
151
|
+
ctx := context.Background()
|
|
152
|
+
p, _ := openProjection(t)
|
|
153
|
+
start := projectionStart("stable-terminal", "PAY-STABLE")
|
|
154
|
+
t0 := time.Now().UTC().Add(-2 * time.Minute)
|
|
155
|
+
t1 := t0.Add(20 * time.Second)
|
|
156
|
+
if err := p.InsertStart(ctx, start, t0); err != nil {
|
|
157
|
+
t.Fatal(err)
|
|
158
|
+
}
|
|
159
|
+
if err := p.UpsertStep(ctx, run.StepEntry{RunID: start.ID, Sequence: 1, Node: "coding", Status: run.StepSucceeded, StartedAt: &t0, FinishedAt: &t1}); err != nil {
|
|
160
|
+
t.Fatal(err)
|
|
161
|
+
}
|
|
162
|
+
before, err := p.ListSteps(ctx, start.ID)
|
|
163
|
+
if err != nil {
|
|
164
|
+
t.Fatal(err)
|
|
165
|
+
}
|
|
166
|
+
if len(before) != 1 || before[0].FinishedAt == nil || before[0].Duration <= 0 {
|
|
167
|
+
t.Fatalf("initial terminal step = %#v", before)
|
|
168
|
+
}
|
|
169
|
+
finished := t1.Add(time.Minute)
|
|
170
|
+
if err := p.UpdateState(ctx, start.ID, run.StateCanceled, "canceled", &finished); err != nil {
|
|
171
|
+
t.Fatal(err)
|
|
172
|
+
}
|
|
173
|
+
if err := p.UpdateState(ctx, start.ID, run.StateCanceled, "canceled", &finished); err != nil {
|
|
174
|
+
t.Fatal(err)
|
|
175
|
+
}
|
|
176
|
+
after, err := p.ListSteps(ctx, start.ID)
|
|
177
|
+
if err != nil {
|
|
178
|
+
t.Fatal(err)
|
|
179
|
+
}
|
|
180
|
+
if len(after) != 1 || !after[0].FinishedAt.Equal(*before[0].FinishedAt) || after[0].Duration != before[0].Duration {
|
|
181
|
+
t.Fatalf("terminal timing changed after run finalization: before=%#v after=%#v", before, after)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func TestCompletedRunFinalizesActiveStepWhenFinalUpsertIsMissing(t *testing.T) {
|
|
186
|
+
ctx := context.Background()
|
|
187
|
+
p, _ := openProjection(t)
|
|
188
|
+
start := projectionStart("complete-detail", "PAY-COMPLETE-DETAIL")
|
|
189
|
+
started := time.Now().UTC().Add(-time.Minute)
|
|
190
|
+
if err := p.InsertStart(ctx, start, started); err != nil {
|
|
191
|
+
t.Fatal(err)
|
|
192
|
+
}
|
|
193
|
+
if err := p.UpsertStep(ctx, run.StepEntry{RunID: start.ID, Sequence: 1, Node: "end", Status: run.StepRunning, StartedAt: &started}); err != nil {
|
|
194
|
+
t.Fatal(err)
|
|
195
|
+
}
|
|
196
|
+
finished := time.Now().UTC()
|
|
197
|
+
if err := p.UpdateState(ctx, start.ID, run.StateCompleted, "", &finished); err != nil {
|
|
198
|
+
t.Fatal(err)
|
|
199
|
+
}
|
|
200
|
+
steps, err := p.ListSteps(ctx, start.ID)
|
|
201
|
+
if err != nil {
|
|
202
|
+
t.Fatal(err)
|
|
203
|
+
}
|
|
204
|
+
if len(steps) != 1 || steps[0].Status != run.StepSucceeded || steps[0].FinishedAt == nil || steps[0].Duration <= 0 {
|
|
205
|
+
t.Fatalf("completed step = %#v", steps)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func TestCanceledRunFinalizesCurrentStepTiming(t *testing.T) {
|
|
210
|
+
ctx := context.Background()
|
|
211
|
+
p, _ := openProjection(t)
|
|
212
|
+
start := projectionStart("cancel-detail", "PAY-CANCEL-DETAIL")
|
|
213
|
+
started := time.Now().UTC().Add(-time.Minute)
|
|
214
|
+
if err := p.InsertStart(ctx, start, started); err != nil {
|
|
215
|
+
t.Fatal(err)
|
|
216
|
+
}
|
|
217
|
+
if err := p.UpsertStep(ctx, run.StepEntry{RunID: start.ID, Sequence: 1, Node: "coding", Status: run.StepRunning, StartedAt: &started}); err != nil {
|
|
218
|
+
t.Fatal(err)
|
|
219
|
+
}
|
|
220
|
+
if err := p.UpdateState(ctx, start.ID, run.StateCanceling, "operator canceled", nil); err != nil {
|
|
221
|
+
t.Fatal(err)
|
|
222
|
+
}
|
|
223
|
+
finished := time.Now().UTC()
|
|
224
|
+
if err := p.UpdateState(ctx, start.ID, run.StateCanceled, "operator canceled", &finished); err != nil {
|
|
225
|
+
t.Fatal(err)
|
|
226
|
+
}
|
|
227
|
+
steps, err := p.ListSteps(ctx, start.ID)
|
|
228
|
+
if err != nil {
|
|
229
|
+
t.Fatal(err)
|
|
230
|
+
}
|
|
231
|
+
if len(steps) != 1 || steps[0].Status != run.StepCanceled || steps[0].FinishedAt == nil || steps[0].Duration <= 0 {
|
|
232
|
+
t.Fatalf("canceled step = %#v", steps)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
func TestSetupConflictMarksCurrentStepBlocked(t *testing.T) {
|
|
237
|
+
ctx := context.Background()
|
|
238
|
+
p, _ := openProjection(t)
|
|
239
|
+
start := projectionStart("setup-conflict", "PAY-SETUP")
|
|
240
|
+
if err := p.InsertStart(ctx, start, time.Now().UTC()); err != nil {
|
|
241
|
+
t.Fatal(err)
|
|
242
|
+
}
|
|
243
|
+
if err := p.UpsertStep(ctx, run.StepEntry{RunID: start.ID, Sequence: 1, Node: "coding", Status: run.StepRunning}); err != nil {
|
|
244
|
+
t.Fatal(err)
|
|
245
|
+
}
|
|
246
|
+
if err := p.UpdateState(ctx, start.ID, run.StateBlocked, "runner setup conflict", nil); err != nil {
|
|
247
|
+
t.Fatal(err)
|
|
248
|
+
}
|
|
249
|
+
steps, err := p.ListSteps(ctx, start.ID)
|
|
250
|
+
if err != nil {
|
|
251
|
+
t.Fatal(err)
|
|
252
|
+
}
|
|
253
|
+
if len(steps) != 1 || steps[0].Status != run.StepBlocked || steps[0].Message != "runner setup conflict" {
|
|
254
|
+
t.Fatalf("blocked setup step = %#v", steps)
|
|
255
|
+
}
|
|
256
|
+
if steps[0].DurationKnown {
|
|
257
|
+
t.Fatal("unknown setup duration was fabricated as known")
|
|
258
|
+
}
|
|
259
|
+
}
|