From adb9f6762c4231cc45aea4e8d5978b6990652a96 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 14 May 2026 21:38:38 -0500 Subject: [PATCH] test: add workflow determinism validation via workflowcheck and replay tests - Add workflowcheck CI step that gates Docker build on static analysis - Add opt-in replay test suite (RUN_REPLAY_TESTS=1) with Label("replay") - Add testdata/README.md with instructions for capturing history fixtures Co-Authored-By: Claude Sonnet 4.6 --- .../workflows/docker-build-push-worker.yml | 14 ++++ internal/workflows/replay_test.go | 73 +++++++++++++++++++ internal/workflows/testdata/README.md | 29 ++++++++ 3 files changed, 116 insertions(+) create mode 100644 internal/workflows/replay_test.go create mode 100644 internal/workflows/testdata/README.md diff --git a/.github/workflows/docker-build-push-worker.yml b/.github/workflows/docker-build-push-worker.yml index 4dd66b7..e7db8f4 100644 --- a/.github/workflows/docker-build-push-worker.yml +++ b/.github/workflows/docker-build-push-worker.yml @@ -13,7 +13,21 @@ permissions: contents: write jobs: + workflowcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - name: Install workflowcheck + run: go install go.temporal.io/sdk/contrib/tools/workflowcheck@latest + - name: Check workflow determinism + run: workflowcheck ./... + build-and-push: + needs: workflowcheck uses: ./.github/workflows/reusable-docker-build-push.yml with: image_suffix: "-worker" diff --git a/internal/workflows/replay_test.go b/internal/workflows/replay_test.go new file mode 100644 index 0000000..0227f4d --- /dev/null +++ b/internal/workflows/replay_test.go @@ -0,0 +1,73 @@ +package workflows + +import ( + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.temporal.io/sdk/worker" +) + +var _ = Describe("Replay", Label("replay"), func() { + BeforeEach(func() { + if os.Getenv("RUN_REPLAY_TESTS") == "" { + Skip("set RUN_REPLAY_TESTS=1 to run replay tests") + } + }) + + It("SpinUpWorkflow is deterministic against recorded history", func() { + r := worker.NewWorkflowReplayer() + r.RegisterWorkflow(SpinUpWorkflow) + Expect(r.ReplayWorkflowHistoryFromJSONFile(nil, "testdata/spin_up_history.json")). + NotTo(HaveOccurred()) + }) + + It("SpinDownWorkflow is deterministic against recorded history", func() { + r := worker.NewWorkflowReplayer() + r.RegisterWorkflow(SpinDownWorkflow) + Expect(r.ReplayWorkflowHistoryFromJSONFile(nil, "testdata/spin_down_history.json")). + NotTo(HaveOccurred()) + }) + + It("SpinUpIAMWorkflow is deterministic against recorded history", func() { + r := worker.NewWorkflowReplayer() + r.RegisterWorkflow(SpinUpIAMWorkflow) + Expect(r.ReplayWorkflowHistoryFromJSONFile(nil, "testdata/spin_up_iam_history.json")). + NotTo(HaveOccurred()) + }) + + It("SpinDownIAMWorkflow is deterministic against recorded history", func() { + r := worker.NewWorkflowReplayer() + r.RegisterWorkflow(SpinDownIAMWorkflow) + Expect(r.ReplayWorkflowHistoryFromJSONFile(nil, "testdata/spin_down_iam_history.json")). + NotTo(HaveOccurred()) + }) + + It("SpinUpNetworkWorkflow is deterministic against recorded history", func() { + r := worker.NewWorkflowReplayer() + r.RegisterWorkflow(SpinUpNetworkWorkflow) + Expect(r.ReplayWorkflowHistoryFromJSONFile(nil, "testdata/spin_up_network_history.json")). + NotTo(HaveOccurred()) + }) + + It("SpinDownNetworkWorkflow is deterministic against recorded history", func() { + r := worker.NewWorkflowReplayer() + r.RegisterWorkflow(SpinDownNetworkWorkflow) + Expect(r.ReplayWorkflowHistoryFromJSONFile(nil, "testdata/spin_down_network_history.json")). + NotTo(HaveOccurred()) + }) + + It("SpinUpEKSWorkflow is deterministic against recorded history", func() { + r := worker.NewWorkflowReplayer() + r.RegisterWorkflow(SpinUpEKSWorkflow) + Expect(r.ReplayWorkflowHistoryFromJSONFile(nil, "testdata/spin_up_eks_history.json")). + NotTo(HaveOccurred()) + }) + + It("SpinDownEKSWorkflow is deterministic against recorded history", func() { + r := worker.NewWorkflowReplayer() + r.RegisterWorkflow(SpinDownEKSWorkflow) + Expect(r.ReplayWorkflowHistoryFromJSONFile(nil, "testdata/spin_down_eks_history.json")). + NotTo(HaveOccurred()) + }) +}) diff --git a/internal/workflows/testdata/README.md b/internal/workflows/testdata/README.md new file mode 100644 index 0000000..5c537c4 --- /dev/null +++ b/internal/workflows/testdata/README.md @@ -0,0 +1,29 @@ +# Workflow History Fixtures + +This directory holds recorded workflow history JSON files used by the replay tests in `replay_test.go`. + +## Generating History Files + +Run each workflow against a local Temporal dev server, then export its history: + +```bash +# Start the dev server and worker +temporal server start-dev +go run ./cmd/worker/main.go + +# After a workflow completes, export its history: +temporal workflow show --workflow-id --output json > spin_up_iam_history.json +temporal workflow show --workflow-id --output json > spin_down_iam_history.json +temporal workflow show --workflow-id --output json > spin_up_network_history.json +temporal workflow show --workflow-id --output json > spin_down_network_history.json +temporal workflow show --workflow-id --output json > spin_up_eks_history.json +temporal workflow show --workflow-id --output json > spin_down_eks_history.json +temporal workflow show --workflow-id --output json > spin_up_history.json +temporal workflow show --workflow-id --output json > spin_down_history.json +``` + +## Running Replay Tests + +```bash +RUN_REPLAY_TESTS=1 ginkgo -v --label-filter "replay" ./internal/workflows/ +```