Files
orbit/sdk/go/cmd/orbit-workflow-load/verify_test.go

211 lines
10 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
"sync"
"testing"
"time"
)
func testID(n int) string { return fmt.Sprintf("%032x", n) }
func testConfig() config {
return config{QueueID: testID(1), Requests: 1, Concurrency: 1, Workers: 1, Capacity: 2, Timeout: time.Second, RunTimeout: 90 * time.Second, PollInterval: 100 * time.Millisecond}
}
type proofAPI struct {
workflowAPI
instance orbit.WorkflowInstance
steps orbit.WorkflowSteps
tasks map[string]orbit.Task
attempts map[string]orbit.AttemptPage
sources map[string]orbit.TaskSource
}
func (a *proofAPI) GetWorkflow(context.Context, string) (orbit.WorkflowInstance, error) {
return a.instance, nil
}
func (a *proofAPI) GetWorkflowSteps(context.Context, string) (orbit.WorkflowSteps, error) {
return a.steps, nil
}
func (a *proofAPI) GetTask(_ context.Context, id string) (orbit.Task, error) { return a.tasks[id], nil }
func (a *proofAPI) ListTaskAttempts(_ context.Context, id string, _ orbit.HistoryPageRequest) (orbit.AttemptPage, error) {
return a.attempts[id], nil
}
func (a *proofAPI) GetTaskSource(_ context.Context, id string) (orbit.TaskSource, error) {
return a.sources[id], nil
}
func (a *proofAPI) StartWorkflow(context.Context, string, orbit.StartWorkflowRequest) (orbit.WorkflowStartResult, error) {
return orbit.WorkflowStartResult{Instance: a.instance}, nil
}
func proofFixture() (*proofAPI, workflowAdmission) {
c := testConfig()
start := time.Date(2026, 9, 20, 0, 0, 0, 0, time.UTC)
end := start.Add(3 * time.Second)
raw := json.RawMessage(`{"run_id":"run","sequence":0}`)
instance := orbit.WorkflowInstance{ID: testID(2), NamespaceID: testID(3), DefinitionID: testID(4), DefinitionVersionID: testID(5), Version: "v1", State: "SUCCEEDED", Revision: 4, Input: raw, Output: json.RawMessage(`{"left":{"run_id":"run","sequence":0},"right":{"run_id":"run","sequence":0}}`), BusinessKey: businessKey("run", 0), DeadlineAt: start.Add(c.RunTimeout), CreatedAt: start, CompletedAt: &end}
api := &proofAPI{instance: instance, steps: orbit.WorkflowSteps{InstanceID: instance.ID, Revision: instance.Revision}, tasks: map[string]orbit.Task{}, attempts: map[string]orbit.AttemptPage{}, sources: map[string]orbit.TaskSource{}}
for i, key := range []string{"pair", "left", "right"} {
activation := testID(10 + i)
step := orbit.WorkflowStepStatus{ID: testID(20 + i), Key: key, Kind: "TASK", State: "SUCCEEDED", Revision: 3, ActivationID: &activation, ActivationSequence: 1}
if i == 0 {
step.Kind = "PARALLEL"
api.steps.Steps = append(api.steps.Steps, step)
continue
}
taskID := testID(30 + i)
step.TaskID = &taskID
api.steps.Steps = append(api.steps.Steps, step)
authorized, completed := start.Add(time.Second), start.Add(2*time.Second)
task := orbit.Task{ID: taskID, NamespaceID: instance.NamespaceID, QueueID: c.QueueID, Handler: handlerName("run"), HandlerVersion: "v1", PayloadVersion: "v1", State: orbit.TaskSucceeded, Payload: raw, Result: raw, RecoverySafety: orbit.RecoveryReconcileOnly, Retry: orbit.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: uint64(c.RunTimeout.Milliseconds())}, AttemptCount: 1, CreatedAt: start, AvailableAt: start, DeadlineAt: instance.DeadlineAt, CompletedAt: &completed}
api.tasks[taskID] = task
api.attempts[taskID] = orbit.AttemptPage{Items: []orbit.AttemptSummary{{ID: testID(40 + i), TaskID: taskID, State: orbit.AttemptSucceeded, Epoch: 1, CreatedAt: start, AuthorizedAt: &authorized, EndedAt: &completed}}}
api.sources[taskID] = orbit.TaskSource{TaskID: taskID, Kind: "WORKFLOW", Workflow: &orbit.TaskWorkflowSource{WorkflowID: instance.ID, DefinitionID: instance.DefinitionID, DefinitionVersionID: instance.DefinitionVersionID, Version: "v1", StepID: step.ID, StepKey: key, StepKind: "TASK", ActivationID: activation, ActivationSequence: 1}}
}
return api, workflowAdmission{sequence: 0, instance: instance}
}
func TestWorkflowProofComplete(t *testing.T) {
api, admission := proofFixture()
cfg := testConfig()
duration, proofs, code := verifyWorkflow(context.Background(), cfg, api, "run", admission.instance.DefinitionID, admission)
if code != "" || duration != 3*time.Second || len(proofs) != 2 || proofs[0].taskID == proofs[1].taskID {
t.Fatalf("valid proof rejected %s %#v", code, proofs)
}
log := newInvocationLog(1)
handler := log.handler(context.Background(), cfg, "run")
var wg sync.WaitGroup
for _, p := range proofs {
wg.Add(1)
go func(p verifiedTask) {
defer wg.Done()
_, err := handler.Handle(context.Background(), orbit.WorkerExecution{Task: api.tasks[p.taskID], AttemptID: p.attemptID, Epoch: 1})
if err != nil {
t.Error(err)
}
}(p)
}
wg.Wait()
r := newReport(cfg, "run")
log.summarize(&r, proofs)
if r.HandlerCalls != 2 || r.DuplicateInvocations != 0 || r.UnconfirmedInvocations != 0 || len(r.finish().Errors) != 0 {
t.Fatalf("call proof mismatch %#v", r)
}
}
func TestWorkflowProofRejectsWrongIdentitiesAndHistory(t *testing.T) {
for name, change := range map[string]func(*proofAPI){
"version substitution": func(a *proofAPI) { a.instance.DefinitionVersionID = testID(80) },
"changed input": func(a *proofAPI) { a.instance.Input = json.RawMessage(`{"run_id":"run"}`) },
"wrong steps owner": func(a *proofAPI) { a.steps.InstanceID = testID(80) },
"missing join": func(a *proofAPI) { a.steps.Steps = a.steps.Steps[1:] },
"duplicate branch": func(a *proofAPI) { a.steps.Steps[2] = a.steps.Steps[1] },
"wrong branch name": func(a *proofAPI) { a.steps.Steps[2].Key = "third" },
"reused task": func(a *proofAPI) { a.steps.Steps[2].TaskID = a.steps.Steps[1].TaskID },
"reused activation": func(a *proofAPI) { a.steps.Steps[2].ActivationID = a.steps.Steps[1].ActivationID },
"wrong task policy": func(a *proofAPI) { v := a.tasks[testID(31)]; v.Retry.MaxAttempts = 2; a.tasks[v.ID] = v },
"wrong task queue": func(a *proofAPI) { v := a.tasks[testID(31)]; v.QueueID = testID(80); a.tasks[v.ID] = v },
"missing result sequence": func(a *proofAPI) {
v := a.tasks[testID(31)]
v.Result = json.RawMessage(`{"run_id":"run"}`)
a.tasks[v.ID] = v
},
"uncertain task": func(a *proofAPI) { v := a.tasks[testID(31)]; v.EffectUncertain = true; a.tasks[v.ID] = v },
"wrong task deadline": func(a *proofAPI) {
v := a.tasks[testID(31)]
v.DeadlineAt = v.DeadlineAt.Add(time.Second)
a.tasks[v.ID] = v
},
"extra attempt": func(a *proofAPI) {
v := a.attempts[testID(31)]
v.Items = append(v.Items, v.Items[0])
a.attempts[testID(31)] = v
},
"missing authorization": func(a *proofAPI) { a.attempts[testID(31)].Items[0].AuthorizedAt = nil },
"wrong epoch": func(a *proofAPI) { a.attempts[testID(31)].Items[0].Epoch = 2 },
"wrong attempt time": func(a *proofAPI) {
v := a.instance.CreatedAt.Add(-time.Second)
a.attempts[testID(31)].Items[0].EndedAt = &v
},
"wrong source task": func(a *proofAPI) { v := a.sources[testID(31)]; v.TaskID = testID(80); a.sources[testID(31)] = v },
"wrong source branch": func(a *proofAPI) { a.sources[testID(31)].Workflow.StepKey = "right" },
"wrong source kind": func(a *proofAPI) { a.sources[testID(31)].Workflow.StepKind = "POLL" },
"wrong output zero": func(a *proofAPI) {
a.instance.Output = json.RawMessage(`{"left":{"run_id":"run"},"right":{"run_id":"run"}}`)
},
} {
t.Run(name, func(t *testing.T) {
a, admission := proofFixture()
change(a)
_, proofs, code := verifyWorkflow(context.Background(), testConfig(), a, "run", admission.instance.DefinitionID, admission)
if code == "" || len(proofs) != 0 {
t.Fatalf("bad evidence counted %s %#v", code, proofs)
}
})
}
}
func TestStartBindsPublishedVersion(t *testing.T) {
a, _ := proofFixture()
c := testConfig()
results := submitWorkflows(context.Background(), c, a, "run", a.instance.DefinitionID, a.instance.DefinitionVersionID, a.instance.NamespaceID)
if len(results) != 1 || results[0].code != "" {
t.Fatalf("valid start rejected %#v", results)
}
results = submitWorkflows(context.Background(), c, a, "run", a.instance.DefinitionID, testID(80), a.instance.NamespaceID)
if len(results) != 1 || results[0].code != "INVALID_RECEIPT" {
t.Fatal("unpublished version accepted")
}
}
func TestDuplicateInvocationCannotCountAsSecondBranch(t *testing.T) {
a, _ := proofFixture()
c := testConfig()
log := newInvocationLog(1)
h := log.handler(context.Background(), c, "run")
proof := verifiedTask{sequence: 0, taskID: testID(31), attemptID: testID(41)}
for i := 0; i < 2; i++ {
_, _ = h.Handle(context.Background(), orbit.WorkerExecution{Task: a.tasks[proof.taskID], AttemptID: proof.attemptID, Epoch: 1})
}
r := newReport(c, "run")
log.summarize(&r, []verifiedTask{proof, {sequence: 0, taskID: testID(32), attemptID: testID(42)}})
if r.HandlerCalls != 2 || r.DuplicateInvocations != 1 || r.Errors["LOCAL_PROOF_MISMATCH"] != 1 {
t.Fatalf("duplicate filled missing branch %#v", r)
}
r = newReport(c, "run")
log.summarize(&r, nil)
if r.UnconfirmedInvocations != 2 {
t.Fatal("unverified calls disappeared")
}
}
func TestStrictOutputKeepsSequenceZeroRequired(t *testing.T) {
for _, raw := range []string{
`{"left":{"run_id":"run"},"right":{"run_id":"run","sequence":0}}`,
`{"left":{"run_id":"run","sequence":null},"right":{"run_id":"run","sequence":0}}`,
`{"left":{"run_id":"run","sequence":0,"sequence":0},"right":{"run_id":"run","sequence":0}}`,
`{"left":{"run_id":"run","Sequence":0},"right":{"run_id":"run","sequence":0}}`,
`{"left":{"run_id":"run","sequence":0},"right":{"run_id":"run","sequence":0},"right":{"run_id":"run","sequence":0}}`,
} {
if matchesOutput(json.RawMessage(raw), "run", 0) {
t.Fatalf("invalid zero payload accepted %s", raw)
}
}
}
func TestSpecificationRetainsConfiguredTimeout(t *testing.T) {
c := testConfig()
spec := workflowSpec(c.QueueID, handlerName("run"), c.RunTimeout)
if spec.TimeoutMS != 90000 || spec.Body.Steps[0].Parallel.Branches[0].Body.Steps[0].Task.TimeoutMS != 90000 {
t.Fatal("run timeout silently capped")
}
var stored orbit.WorkflowSpecification
raw, _ := json.Marshal(spec)
if err := json.Unmarshal(raw, &stored); err != nil {
t.Fatal(err)
}
if !sameSpecification(stored, spec) {
t.Fatal("serialized specification rejected")
}
stored.TimeoutMS = 30000
if sameSpecification(stored, spec) {
t.Fatal("changed published spec accepted")
}
}