chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,584 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/ai"
|
||||
"go-micro.dev/v6/store"
|
||||
)
|
||||
|
||||
// appendStep returns a step that appends its name to the carried data,
|
||||
// so a run's path is visible in the final State.
|
||||
func appendStep(name string) Step {
|
||||
return Step{Name: name, Run: func(_ context.Context, in State) (State, error) {
|
||||
s := in.String()
|
||||
if s != "" {
|
||||
s += ","
|
||||
}
|
||||
in.Data = []byte(s + name)
|
||||
return in, nil
|
||||
}}
|
||||
}
|
||||
|
||||
func TestFlowStepsRunInOrder(t *testing.T) {
|
||||
f := New("seq",
|
||||
WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "seq")),
|
||||
Steps(appendStep("a"), appendStep("b"), appendStep("c")),
|
||||
)
|
||||
if err := f.Execute(context.Background(), ""); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
res := f.Results()
|
||||
if len(res) != 1 || res[0].Answer != "a,b,c" {
|
||||
t.Fatalf("steps ran out of order: %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
// A run that fails mid-way is persisted at the failing step and resumes
|
||||
// there — without re-running the completed steps.
|
||||
func TestFlowCheckpointResume(t *testing.T) {
|
||||
mem := store.NewMemoryStore()
|
||||
var firstCalls, fixed int
|
||||
|
||||
steps := []Step{
|
||||
{Name: "first", Run: func(_ context.Context, in State) (State, error) {
|
||||
firstCalls++
|
||||
in.Data = []byte("first-done")
|
||||
return in, nil
|
||||
}},
|
||||
{Name: "flaky", Run: func(_ context.Context, in State) (State, error) {
|
||||
if fixed == 0 {
|
||||
return in, errors.New("dependency unavailable")
|
||||
}
|
||||
in.Data = []byte("flaky-done")
|
||||
return in, nil
|
||||
}},
|
||||
}
|
||||
|
||||
f := New("resumable", WithCheckpoint(StoreCheckpoint(mem, "resumable")), Steps(steps...))
|
||||
|
||||
// First run fails at "flaky".
|
||||
if err := f.Execute(context.Background(), "start"); err == nil {
|
||||
t.Fatal("expected the run to fail at the flaky step")
|
||||
}
|
||||
|
||||
pend, _ := f.Pending(context.Background())
|
||||
if len(pend) != 1 {
|
||||
t.Fatalf("expected 1 pending run, got %d", len(pend))
|
||||
}
|
||||
if pend[0].State.Stage != "flaky" {
|
||||
t.Fatalf("run should be checkpointed at the flaky step, got stage %q", pend[0].State.Stage)
|
||||
}
|
||||
runID := pend[0].ID
|
||||
|
||||
// The dependency recovers; resume continues from where it stopped.
|
||||
fixed = 1
|
||||
if err := f.Resume(context.Background(), runID); err != nil {
|
||||
t.Fatalf("Resume: %v", err)
|
||||
}
|
||||
if firstCalls != 1 {
|
||||
t.Errorf("completed step should not re-run on resume; first called %d times", firstCalls)
|
||||
}
|
||||
if pend, _ := f.Pending(context.Background()); len(pend) != 0 {
|
||||
t.Errorf("expected no pending runs after a successful resume, got %d", len(pend))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowStepContextIncludesRunInfo(t *testing.T) {
|
||||
var got ai.RunInfo
|
||||
step := Step{Name: "inspect", Run: func(ctx context.Context, in State) (State, error) {
|
||||
var ok bool
|
||||
got, ok = ai.RunInfoFrom(ctx)
|
||||
if !ok {
|
||||
t.Fatal("RunInfo missing from step context")
|
||||
}
|
||||
in.Data = []byte("ok")
|
||||
return in, nil
|
||||
}}
|
||||
|
||||
mem := store.NewMemoryStore()
|
||||
f := New("correlated",
|
||||
WithCheckpoint(StoreCheckpoint(mem, "correlated")),
|
||||
Steps(step),
|
||||
)
|
||||
ctx := ai.WithRunInfo(context.Background(), ai.RunInfo{RunID: "agent-run-1", Agent: "planner"})
|
||||
if err := f.Execute(ctx, "start"); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
if got.Agent != "correlated" {
|
||||
t.Fatalf("RunInfo.Agent = %q, want correlated", got.Agent)
|
||||
}
|
||||
if got.RunID == "" {
|
||||
t.Fatal("RunInfo.RunID is empty")
|
||||
}
|
||||
if got.Flow != "correlated" {
|
||||
t.Fatalf("RunInfo.Flow = %q, want correlated", got.Flow)
|
||||
}
|
||||
if got.ParentID != "agent-run-1" {
|
||||
t.Fatalf("RunInfo.ParentID = %q, want agent-run-1", got.ParentID)
|
||||
}
|
||||
if got.Step != "inspect" {
|
||||
t.Fatalf("RunInfo.Step = %q, want inspect", got.Step)
|
||||
}
|
||||
runs, err := StoreCheckpoint(mem, "correlated").List(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(runs) != 1 || runs[0].ParentID != "agent-run-1" {
|
||||
t.Fatalf("persisted parent id = %+v, want agent-run-1", runs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowResumePendingResumesOldestRunsUntilFailure(t *testing.T) {
|
||||
mem := store.NewMemoryStore()
|
||||
ctx := context.Background()
|
||||
var calls int
|
||||
step := Step{Name: "work", Run: func(_ context.Context, in State) (State, error) {
|
||||
calls++
|
||||
if in.String() == "block" {
|
||||
return in, errors.New("still blocked")
|
||||
}
|
||||
in.Data = []byte(in.String() + "-done")
|
||||
return in, nil
|
||||
}}
|
||||
f := New("resume-pending", WithCheckpoint(StoreCheckpoint(mem, "resume-pending")), Steps(step))
|
||||
|
||||
base := time.Date(2026, 6, 24, 12, 0, 0, 0, time.UTC)
|
||||
runs := []Run{
|
||||
{
|
||||
ID: "run-ok",
|
||||
Flow: "resume-pending",
|
||||
State: State{Stage: "work", Data: []byte("ok")},
|
||||
Steps: []StepRecord{{Name: "work", Status: "failed", Error: "temporary"}},
|
||||
Status: "failed",
|
||||
Started: base,
|
||||
},
|
||||
{
|
||||
ID: "run-blocked",
|
||||
Flow: "resume-pending",
|
||||
State: State{Stage: "work", Data: []byte("block")},
|
||||
Steps: []StepRecord{{Name: "work", Status: "failed", Error: "temporary"}},
|
||||
Status: "failed",
|
||||
Started: base.Add(time.Minute),
|
||||
},
|
||||
{
|
||||
ID: "run-later",
|
||||
Flow: "resume-pending",
|
||||
State: State{Stage: "work", Data: []byte("later")},
|
||||
Steps: []StepRecord{{Name: "work", Status: "failed", Error: "temporary"}},
|
||||
Status: "failed",
|
||||
Started: base.Add(2 * time.Minute),
|
||||
},
|
||||
}
|
||||
for _, run := range runs {
|
||||
if err := f.checkpoint.Save(ctx, run); err != nil {
|
||||
t.Fatalf("Save(%s): %v", run.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
failedRun, err := f.ResumePending(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected ResumePending to stop at the blocked run")
|
||||
}
|
||||
if failedRun != "run-blocked" {
|
||||
t.Fatalf("failed run = %q, want run-blocked", failedRun)
|
||||
}
|
||||
if calls != 2 {
|
||||
t.Fatalf("ResumePending should stop before later runs; got %d calls", calls)
|
||||
}
|
||||
|
||||
run, ok, err := f.checkpoint.Load(ctx, "run-ok")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("Load(run-ok) ok=%v err=%v", ok, err)
|
||||
}
|
||||
if run.Status != "done" || run.State.String() != "ok-done" {
|
||||
t.Fatalf("run-ok not resumed successfully: %+v", run)
|
||||
}
|
||||
run, ok, err = f.checkpoint.Load(ctx, "run-later")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("Load(run-later) ok=%v err=%v", ok, err)
|
||||
}
|
||||
if run.Status != "failed" {
|
||||
t.Fatalf("run-later should not be resumed after a failure, got %+v", run)
|
||||
}
|
||||
}
|
||||
|
||||
// A flow-level Retry re-runs a failing step until it succeeds.
|
||||
func TestFlowStepRetry(t *testing.T) {
|
||||
var attempts int
|
||||
step := Step{Name: "transient", Run: func(_ context.Context, in State) (State, error) {
|
||||
attempts++
|
||||
if attempts < 3 {
|
||||
return in, errors.New("transient")
|
||||
}
|
||||
in.Data = []byte("ok")
|
||||
return in, nil
|
||||
}}
|
||||
|
||||
f := New("retrying",
|
||||
WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "retrying")),
|
||||
Retry(2), // up to 3 tries
|
||||
Steps(step),
|
||||
)
|
||||
if err := f.Execute(context.Background(), ""); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
if attempts != 3 {
|
||||
t.Errorf("want 3 attempts with Retry(2), got %d", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
// A per-step Retry overrides the flow default.
|
||||
func TestFlowStepRetryOverride(t *testing.T) {
|
||||
var attempts int
|
||||
step := Step{Name: "capped", Retry: 1, Run: func(_ context.Context, in State) (State, error) {
|
||||
attempts++
|
||||
return in, errors.New("always fails")
|
||||
}}
|
||||
|
||||
f := New("override",
|
||||
WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "override")),
|
||||
Retry(5), // would be 6 tries; the step's Retry:1 caps it at 2
|
||||
Steps(step),
|
||||
)
|
||||
_ = f.Execute(context.Background(), "")
|
||||
if attempts != 2 {
|
||||
t.Errorf("per-step Retry(1) should cap tries at 2, got %d", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowStepRetryBackoffWaitsBetweenAttempts(t *testing.T) {
|
||||
var attempts int
|
||||
step := Step{Name: "transient", Run: func(_ context.Context, in State) (State, error) {
|
||||
attempts++
|
||||
if attempts == 1 {
|
||||
return in, errors.New("transient")
|
||||
}
|
||||
in.Data = []byte("ok")
|
||||
return in, nil
|
||||
}}
|
||||
|
||||
f := New("retry-backoff",
|
||||
WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "retry-backoff")),
|
||||
Retry(1),
|
||||
RetryBackoff(10*time.Millisecond),
|
||||
Steps(step),
|
||||
)
|
||||
start := time.Now()
|
||||
if err := f.Execute(context.Background(), ""); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
if attempts != 2 {
|
||||
t.Fatalf("want 2 attempts, got %d", attempts)
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed < 10*time.Millisecond {
|
||||
t.Fatalf("retry backoff was not observed; elapsed %s", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowTimeoutStopsRetryBackoff(t *testing.T) {
|
||||
var attempts int
|
||||
step := Step{Name: "slow", Run: func(_ context.Context, in State) (State, error) {
|
||||
attempts++
|
||||
return in, errors.New("transient")
|
||||
}}
|
||||
|
||||
f := New("timeout-backoff",
|
||||
WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "timeout-backoff")),
|
||||
Timeout(20*time.Millisecond),
|
||||
Retry(1),
|
||||
RetryBackoff(time.Hour),
|
||||
Steps(step),
|
||||
)
|
||||
err := f.Execute(context.Background(), "")
|
||||
if err == nil {
|
||||
t.Fatal("expected the timed-out run to fail")
|
||||
}
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Errorf("want a context deadline error, got %v", err)
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Errorf("timeout should stop during backoff before retrying, got %d attempts", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowTimeoutRespectsExistingDeadline(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
|
||||
defer cancel()
|
||||
|
||||
f := New("existing-deadline", Timeout(time.Millisecond))
|
||||
got, stop := f.withTimeout(ctx)
|
||||
defer stop()
|
||||
|
||||
wantDeadline, _ := ctx.Deadline()
|
||||
gotDeadline, ok := got.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("expected the existing deadline to remain set")
|
||||
}
|
||||
if !gotDeadline.Equal(wantDeadline) {
|
||||
t.Fatalf("deadline = %v, want existing deadline %v", gotDeadline, wantDeadline)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowStepRetryBackoffStopsOnCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var attempts int
|
||||
step := Step{Name: "cancelbackoff", Run: func(_ context.Context, in State) (State, error) {
|
||||
attempts++
|
||||
cancel()
|
||||
return in, errors.New("transient")
|
||||
}}
|
||||
|
||||
f := New("cancel-backoff",
|
||||
WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "cancel-backoff")),
|
||||
Retry(1),
|
||||
RetryBackoff(time.Hour),
|
||||
Steps(step),
|
||||
)
|
||||
err := f.Execute(ctx, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected the canceled run to fail")
|
||||
}
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("want a context.Canceled error, got %v", err)
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Errorf("cancellation should stop during backoff before retrying, got %d attempts", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
// A canceled run stops retrying immediately instead of burning the whole
|
||||
// retry budget, and surfaces the context error.
|
||||
func TestFlowStepRetryStopsOnCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var attempts int
|
||||
step := Step{Name: "cancelaware", Run: func(_ context.Context, in State) (State, error) {
|
||||
attempts++
|
||||
cancel() // the run is canceled while this step is in flight
|
||||
return in, errors.New("transient")
|
||||
}}
|
||||
|
||||
f := New("cancelretry",
|
||||
WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "cancelretry")),
|
||||
Retry(5), // would be 6 tries without the cancellation check
|
||||
Steps(step),
|
||||
)
|
||||
|
||||
err := f.Execute(ctx, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected the canceled run to fail")
|
||||
}
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("want a context.Canceled error, got %v", err)
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Errorf("cancellation should stop retries after the first attempt, got %d", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowStepNamesMustBeUnique(t *testing.T) {
|
||||
step := Step{Name: "work", Run: func(_ context.Context, in State) (State, error) {
|
||||
return in, nil
|
||||
}}
|
||||
f := New("duplicate-steps",
|
||||
WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "duplicate-steps")),
|
||||
Steps(step, step),
|
||||
)
|
||||
|
||||
err := f.Execute(context.Background(), "")
|
||||
if err == nil {
|
||||
t.Fatal("expected duplicate step names to fail validation")
|
||||
}
|
||||
if got, want := err.Error(), `flow: duplicate step name "work"`; got != want {
|
||||
t.Fatalf("error = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowStepNamesMustBeNonEmpty(t *testing.T) {
|
||||
f := New("empty-step-name",
|
||||
WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "empty-step-name")),
|
||||
Steps(Step{Name: "", Run: func(_ context.Context, in State) (State, error) {
|
||||
return in, nil
|
||||
}}),
|
||||
)
|
||||
|
||||
err := f.Execute(context.Background(), "")
|
||||
if err == nil {
|
||||
t.Fatal("expected an empty step name to fail validation")
|
||||
}
|
||||
if got, want := err.Error(), "flow: step 0 has an empty name"; got != want {
|
||||
t.Fatalf("error = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// A step with no Run function is reported as a configuration error rather
|
||||
// than panicking the run.
|
||||
func TestFlowStepNilRun(t *testing.T) {
|
||||
f := New("nilstep",
|
||||
WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "nilstep")),
|
||||
Steps(Step{Name: "missing"}),
|
||||
)
|
||||
err := f.Execute(context.Background(), "")
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for a step with no Run function")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreCheckpointListReturnsRunsInStartedOrder(t *testing.T) {
|
||||
cp := StoreCheckpoint(store.NewMemoryStore(), "ordered")
|
||||
ctx := context.Background()
|
||||
base := time.Date(2026, 6, 24, 12, 0, 0, 0, time.UTC)
|
||||
runs := []Run{
|
||||
{ID: "run-c", Flow: "ordered", Status: "failed", Started: base.Add(2 * time.Minute)},
|
||||
{ID: "run-a", Flow: "ordered", Status: "failed", Started: base},
|
||||
{ID: "run-b", Flow: "ordered", Status: "failed", Started: base.Add(time.Minute)},
|
||||
}
|
||||
for _, run := range runs {
|
||||
if err := cp.Save(ctx, run); err != nil {
|
||||
t.Fatalf("Save(%s): %v", run.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
got, err := cp.List(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("List returned %d runs, want 3", len(got))
|
||||
}
|
||||
want := []string{"run-a", "run-b", "run-c"}
|
||||
for i, id := range want {
|
||||
if got[i].ID != id {
|
||||
t.Fatalf("run %d = %q, want %q (all runs: %+v)", i, got[i].ID, id, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreCheckpointHonorsCanceledContext(t *testing.T) {
|
||||
cp := StoreCheckpoint(store.NewMemoryStore(), "canceled")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
run := Run{ID: "canceled", Started: time.Now()}
|
||||
|
||||
if err := cp.Save(ctx, run); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Save error = %v, want context.Canceled", err)
|
||||
}
|
||||
if _, ok, err := cp.Load(ctx, run.ID); !errors.Is(err, context.Canceled) || ok {
|
||||
t.Fatalf("Load ok, error = %v, %v; want false, context.Canceled", ok, err)
|
||||
}
|
||||
if err := cp.Delete(ctx, run.ID); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Delete error = %v, want context.Canceled", err)
|
||||
}
|
||||
if _, err := cp.List(ctx); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("List error = %v, want context.Canceled", err)
|
||||
}
|
||||
}
|
||||
|
||||
type failingCheckpoint struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (c failingCheckpoint) Save(context.Context, Run) error { return c.err }
|
||||
func (c failingCheckpoint) Load(context.Context, string) (Run, bool, error) {
|
||||
return Run{}, false, c.err
|
||||
}
|
||||
func (c failingCheckpoint) Delete(context.Context, string) error { return c.err }
|
||||
func (c failingCheckpoint) List(context.Context) ([]Run, error) { return nil, c.err }
|
||||
|
||||
func TestFlowCheckpointSaveFailureStopsRun(t *testing.T) {
|
||||
checkpointErr := errors.New("checkpoint unavailable")
|
||||
var ran bool
|
||||
f := New("checkpoint-fails",
|
||||
WithCheckpoint(failingCheckpoint{err: checkpointErr}),
|
||||
Steps(Step{Name: "work", Run: func(_ context.Context, in State) (State, error) {
|
||||
ran = true
|
||||
return in, nil
|
||||
}}),
|
||||
)
|
||||
|
||||
err := f.Execute(context.Background(), "start")
|
||||
if !errors.Is(err, checkpointErr) {
|
||||
t.Fatalf("Execute error = %v, want checkpoint error", err)
|
||||
}
|
||||
if ran {
|
||||
t.Fatal("step ran even though the in-progress checkpoint failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowDeleteOnSuccessFailureIsReturned(t *testing.T) {
|
||||
checkpointErr := errors.New("delete unavailable")
|
||||
cp := &deleteFailCheckpoint{Checkpoint: StoreCheckpoint(store.NewMemoryStore(), "delete-fails"), err: checkpointErr}
|
||||
f := New("delete-fails",
|
||||
WithCheckpoint(cp),
|
||||
DeleteOnSuccess(),
|
||||
Steps(appendStep("work")),
|
||||
)
|
||||
|
||||
err := f.Execute(context.Background(), "")
|
||||
if !errors.Is(err, checkpointErr) {
|
||||
t.Fatalf("Execute error = %v, want delete error", err)
|
||||
}
|
||||
}
|
||||
|
||||
type deleteFailCheckpoint struct {
|
||||
Checkpoint
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *deleteFailCheckpoint) Delete(context.Context, string) error { return c.err }
|
||||
|
||||
func TestStateSetScan(t *testing.T) {
|
||||
var s State
|
||||
type payload struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := s.Set(payload{Email: "a@b.com"}); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
var got payload
|
||||
if err := s.Scan(&got); err != nil {
|
||||
t.Fatalf("Scan: %v", err)
|
||||
}
|
||||
if got.Email != "a@b.com" {
|
||||
t.Errorf("round-trip failed: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowFailureRecordsErrorKind(t *testing.T) {
|
||||
cp := StoreCheckpoint(store.NewMemoryStore(), "failure-kind")
|
||||
f := New("failure-kind",
|
||||
WithCheckpoint(cp),
|
||||
Steps(Step{Name: "limited", Run: func(_ context.Context, in State) (State, error) {
|
||||
return in, errors.New("rate limit exceeded")
|
||||
}}),
|
||||
)
|
||||
|
||||
err := f.Execute(context.Background(), "payload")
|
||||
if err == nil {
|
||||
t.Fatal("Execute error = nil, want failure")
|
||||
}
|
||||
|
||||
runs, listErr := cp.List(context.Background())
|
||||
if listErr != nil {
|
||||
t.Fatalf("List: %v", listErr)
|
||||
}
|
||||
if len(runs) != 1 {
|
||||
t.Fatalf("runs = %d, want 1", len(runs))
|
||||
}
|
||||
if got := runs[0].Steps[0].ErrorKind; got != string(ai.ErrorKindRateLimited) {
|
||||
t.Fatalf("step error kind = %q, want %q", got, ai.ErrorKindRateLimited)
|
||||
}
|
||||
|
||||
results := f.Results()
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("results = %d, want 1", len(results))
|
||||
}
|
||||
if got := results[0].ErrorKind; got != string(ai.ErrorKindRateLimited) {
|
||||
t.Fatalf("result error kind = %q, want %q", got, ai.ErrorKindRateLimited)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user