Skip to main content
BlockX splits its tests by which layer of truth they protect: core unit tests protect the state machines, adapter tests protect protocol mapping, process E2E tests protect the external contract of a single process, contract / system E2E tests protect the boundaries between components, and perf tests separately protect throughput and latency. This page only covers how to run them, where they live, and what they need; for environment setup see Local development environment.

Test layers at a glance

Except for e2e/perf, every layer is covered by go test ./...; layers that need the Python executor call t.Skip instead of failing when the venv is missing (RequirePythonModules in internal/testutil/python.go). A few additional notes:
  • make test adds -short to every package, but no layer other than perf is skipped because of it, so make test is the full E2E run.
  • cmd/syncinvoker is removed from FAST_TEST_PACKAGES by the Makefile and run file by file: every test starts its own process + Python executor, and the perf cases aren’t guarded by -short, so running the whole package at once would exceed the budget; running per file makes SYNCINVOKER_TEST_TIMEOUT the budget for each file and produces coverage per file that is merged afterwards.
  • Example of -run filtering for a single module: go test ./internal/worker/core/ -run TestWorkerCore_RequestTaskSlot.
  • A single Python module: PYTHONPATH=python uv run --project python python -m unittest python.tests.test_executor_protocol_contract.

Test organization conventions

Three files define where tests go and how they’re named:
  • docs/specs/worker-test-organization.md in the blockx repository: applies to internal/worker/**, cmd/worker/, and cmd/bundle_worker/. Three tiers: core unit (tests only the Sans-IO core; HTTP, processes, and Python are forbidden), adapter integration (handler / subscriber / orchestrator / executor pool; fakes and stubs allowed), and process e2e (a real cmd/worker process, only under cmd/<entrypoint>/). File names express the implementation boundary: worker_test.go, dispatcher_retry_test.go, handler_protocol_test.go, worker_process_<slice>_test.go; weakly named files like misc_test.go and e2e_test.go are forbidden. Top-level functions are Test<Boundary>_<Theme>, and t.Run names must state “scenario + expected outcome”. Split thresholds: a file with more than 3 logical blocks, a top-level test with more than 8 t.Runs, or a process E2E file with more than 6 independent contracts.
  • docs/specs/python-test-organization.md in the blockx repository: one implementation boundary per file in python/tests/, named test_<boundary>.py; TestCase names are <Boundary><Theme>Test; method names are “scenario + expected outcome”, for example test_deadline_after_resume_prevents_followup_waiting.
  • docs/specs/system-e2e-testing.md in the blockx repository: contract files are named contract_<behavior>_test.go with functions TestContractE2E_<Boundary>_<Behavior>; system files are named system_<behavior>_test.go with functions TestSystemE2E_<Behavior>; a system file holds only one scenario family. When unsure which layer a test belongs in, default to the contract layer.
The testing guidelines in AGENTS.md add four rules: Go tests live in *_test.go in the same directory as the implementation; prefer table-driven tests for state machines, protocol mapping, and core/adapter boundaries; new behavior must cover both the happy path and the failure / timeout paths from the spec; and when you change protocol or lifecycle semantics, update the corresponding spec in the same change.
Different layers assert different truths: core unit tests assert state transitions, idempotency, command output, TTLs, and result convergence; adapter tests assert protocol mapping and error translation; process E2E tests assert only externally observable behavior (gRPC responses, terminal states, log fields). Don’t enumerate the DispatcherCore retry matrix in E2E tests, and don’t use process tests as a substitute for handler error-mapping assertions.

Test utilities and doubles

testutil has no fake clock. Core methods take now explicitly (for example WorkerCore.HandleRequestTaskSlot(taskID string, ttlMs int64, now int64)), so core unit tests simply pass a time value.

CI

.github/workflows/test.yml runs on PRs to dev, pushes to dev, and manual triggers; every job except Deploy Script Tests configures access to the private github.com/Chaintable/* modules through a GitHub App token: .github/workflows/build.yml only builds and pushes the image when a tag is pushed; it doesn’t run tests.

Performance testing

Each e2e/perf case starts its own etcd + coordinator + worker and uses PERF_* environment variables to control scale; checkSLO asserts latency against thresholds scaled up by PERF_SLO_MULTIPLIER, and the thresholds come from docs/specs/perf-methodology.md. The whole package is gated by TestMain in e2e/perf/short_test.go: when BLOCKX_RUN_PERF is not 1, it returns immediately and runs no cases at all.
  • Main variables: PERF_N (tasks per scenario, default 20), PERF_C (concurrency, default 4), PERF_EXECUTOR_COUNT (default 4), PERF_IO_READS (default 5), PERF_SLO_MULTIPLIER (default 1.0, CI uses 5). test.sh itself reads PERF_CPU, PERF_MEM, and PERF_TIMEOUT, and passes through WORKER_CPU_PROFILE, WORKER_TRACE, COORDINATOR_CPU_PROFILE, and COORDINATOR_TRACE.
  • Files are named after the Stages in docs/specs/perf-methodology.md §3: s0_framework_*, s1_builder_*, s2_call_count_*, s3_cpu_*, s4_io_*, s5_plugin_*; phase_*, m3_stream_build_test.go, and calibration_test.go are legacy files that predate the spec. helpers_test.go holds scaffolding such as runScenario / startPerfInfra* / checkSLO.
  • Artifacts land in e2e/perf/_artifacts/ (gitignored): <TestName>_<subtest>/spans.jsonl (OTel spans from the with_trace subtests, enabled by BLOCKX_OTEL_SPAN_FILE + BLOCKX_OTEL_UDS_EVENTS=1), <TestName>/*.pb.gz (pprof), pyspy/.
  • Offline span analysis: python3 analyze_spans.py e2e/perf/_artifacts/<TestName>_with_trace/spans.jsonl. The script lives at the repository root, accepts a single positional argument, and defaults to /tmp/blockx_spans.jsonl.
  • Reports: docs/specs/perf-test-report.md (v1 main report), perf-test-report-appendix.md (v1 appendix), perf-test-report-v2.md (the only current baseline, cgroup 12 vCPU / 32G), perf-capability-backend-report.md (capability backend framework overhead). Reproduction steps are in docs/specs/perf-test-how-to-v2.md and the methodology in docs/specs/perf-methodology.md.
  • cmd/perf is a load CLI that targets a deployed environment (-coordinator, -n, -c, -mode) and isn’t part of go test. cmd/bundle-loadgen is the load generator for Bundle clusters and comes with its own pure unit tests.
  • Local development environment: venv, CGO, minimal worker startup.
  • Contributing: PRs must include the verification commands.
  • Worker, Coordinator, Sync Invoker, Bundle clusters: the “Testing” section on each component page points to the corresponding files.
  • docs/specs/worker-test-organization.md, docs/specs/python-test-organization.md, docs/specs/system-e2e-testing.md in the blockx repository: organization conventions.
  • docs/specs/perf-methodology.md, docs/specs/perf-test-how-to-v2.md, e2e/perf/README.md in the blockx repository: performance testing methodology, reproduction, and case index.
  • AGENTS.md in the blockx repository: the authoritative source for commands and testing guidelines.