feat: expose workflow state events and persist signal transitions

This commit is contained in:
2026-09-18 09:27:45 +08:00
parent 082898e705
commit a4d9900e2c
34 changed files with 1499 additions and 27 deletions

View File

@@ -0,0 +1,80 @@
package orbit
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
)
func TestIntegrationWorkflowEventsSDK(t *testing.T) {
api, queue, _ := integrationWorkerClient(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
operation := workerTestID(t)
spec := workflowSDKSpec(queue, "events."+operation)
spec.Body = WorkflowBlock{Steps: []WorkflowStep{
{Key: "receipt", Kind: "SIGNAL", Signal: &WorkflowSignal{Name: "done", PayloadVersion: "v1", Correlation: WorkflowValue{Kind: "LITERAL", Literal: json.RawMessage(`"` + operation + `"`)}, PayloadType: spec.InputType, TimeoutMS: 60000}},
{Key: "delay", Kind: "TIMER", Timer: &WorkflowTimer{DelayMS: 60000}},
}, OutputType: WorkflowType{Kind: "NULL"}, Output: WorkflowRef("delay")}
v := startWorkflowSDK(t, ctx, api, spec)
sent, err := api.SendWorkflowSignal(ctx, v.ID, operation, SendWorkflowSignalRequest{Name: "done", PayloadVersion: "v1", TargetStepKey: "receipt", TargetActivationSequence: 1, CorrelationKey: operation, Payload: workflowExact, Reason: "verify timeline"})
if err != nil {
t.Fatal(err)
}
waitWorkflowStepSDK(t, ctx, api, v.ID, "receipt", func(d WorkflowStepDetail) bool { return d.Step.State == "SUCCEEDED" })
current, err := api.GetWorkflow(ctx, v.ID)
if err != nil {
t.Fatal(err)
}
if _, err = api.ControlWorkflow(ctx, v.ID, operation, WorkflowControlRequest{Operation: "CANCEL", ExpectedControlRevision: current.ControlRevision, Reason: "timeline cleanup"}); err != nil {
t.Fatal(err)
}
waitWorkflowSDK(t, ctx, api, v.ID, "CANCELLED")
seen := map[string]bool{}
types := map[string]bool{}
cursor := ""
pages := 0
for {
page, err := api.ListWorkflowEvents(ctx, v.ID, HistoryPageRequest{Limit: 2, Cursor: cursor})
if err != nil {
t.Fatal(err)
}
pages++
raw, _ := json.Marshal(page)
if strings.Contains(string(raw), `"payload"`) || strings.Contains(string(raw), `9007199254740993`) {
t.Fatal("payload in summaries")
}
for _, e := range page.Items {
if seen[e.ID] || e.WorkflowID != v.ID || e.StepKey == "$root" {
t.Fatal("event identity", e)
}
seen[e.ID] = true
types[e.Type] = true
detail, err := api.GetWorkflowEvent(ctx, v.ID, e.ID)
if err != nil || detail.WorkflowEventSummary != e {
t.Fatal("detail", detail, err)
}
if e.AggregateKind == "workflow_signal" && (e.AggregateID != sent.Signal.ID || e.State != "CONSUMED" || e.StateScope != "SIGNAL") {
t.Fatal("signal transition", e)
}
}
if page.NextCursor == "" {
break
}
cursor = page.NextCursor
if pages > 20 {
t.Fatal("unbounded page repetition")
}
}
for _, kind := range []string{"workflow.started", "workflow.step.changed", "workflow.signal.accepted", "workflow.signal.changed", "workflow.controlled", "workflow.stopped"} {
if !types[kind] {
t.Fatal("event missing", kind, types)
}
}
if len(seen) < 7 || pages < 4 {
t.Fatal("incomplete timeline", len(seen), pages)
}
t.Logf("real HTTP event timeline: %d immutable events over %d pages; signal transition and cancelled workflow retained", len(seen), pages)
}