Conformance testing for every ABsmartly SDK. Each SDK is wrapped in a small HTTP service exposing a uniform API, and a single Python orchestrator drives all of them through the same set of scenarios, asserting that they return identical results and emit identical events. This is how we know the SDKs behave the same across languages.
-
21 SDK wrappers — one HTTP service per SDK, each in a
<sdk>-wrapper/directory:javascript,typescript,react,angular,vue2,vue3,python,ruby,liquid,php,go,rust,java,kotlin,scala,swift,dart,flutter,dotnet,cpp,elixir. -
202 scenarios in
test_scenarios_complete.json. Each scenario is acontextDatapayload plus a list ofsteps(an action and its expected result/events). Scenarios with no executable steps are skipped by the orchestrator. -
An orchestrator (
orchestrator/test_runner.py) that talks to each wrapper over HTTP and validates responses against the expected results baked into the scenarios (the JavaScript SDK is the canonical reference). -
A live-backend e2e runner (
orchestrator/e2e_runner.py) that drives the wrappers against a real ABsmartly environment and verifies data arrives.
The wrapper API is specified in WRAPPER_API_SPEC.md.
- Docker and Docker Compose
- Python 3 with the
requestspackage (the scripts install it if missing) - The SDK source repositories checked out as siblings of this directory. The
wrapper Dockerfiles use build context
..andCOPYsibling<name>-sdk/directories, so e.g.../javascript-sdk,../python3-sdk,../go-sdkmust exist next tocross-sdk-tests/. Seesetup-sdks.shand WRAPPER_REPOS.md.
All commands run from this directory. The scripts build the needed Docker images, start the containers, drive the orchestrator, and tear down.
# Build and test every SDK
./run-tests.sh
# Test one or more SDKs (comma-separated)
./run-tests.sh --sdk react,vue2
# Skip the (slow) image build and just run
./run-tests.sh --skip-build
# Only build images, don't run
./run-tests.sh --build-onlyrun-tests.sh is the everyday entry point (filtered/local runs). Two more
scripts cover wider matrices:
./run-all-tests.sh— the full matrix: unit tests, cross-SDK tests, and optionally e2e, with--sdk,--exclude,--unit-only,--cross-only, and--e2efilters../verify_sdk.sh <sdk> [<sdk2> ...]— verify a single SDK by bringing its container up and driving the orchestrator directly. Use this to confirm a result: it avoids a port-resolution race that can make filteredrun-tests.shruns report stale numbers.
Results are written to test-results/report.json.
test_runner.py requires the SDK_SERVICES environment variable — a
comma-separated list of SDK names. It resolves each to http://<name>-sdk:3000
on the compose network, so it is meant to run inside the orchestrator container:
docker compose up -d javascript-sdk
docker compose run --rm --no-deps \
-e SDK_SERVICES=javascript \
orchestrator python3 test_runner.pytest_runner.py matches error strings strictly by default (CI runs it this way).
Pass --loose-error-match to relax error-message comparison when iterating
locally on a wrapper whose error text does not yet match.
The e2e suite creates a real experiment on a test ABsmartly environment, has
each SDK send real exposures and goals, polls the metrics API, and verifies the
counts. It is separate from the cross-SDK suite and mutates real experiments, so
it needs credentials and the abs CLI.
-
Copy the example config and fill in real values:
cp e2e-config.env.example e2e-config.env
-
Set
ABSMARTLY_E2E_ENDPOINTto the collector endpoint. This is a*.absmartly.iohost (e.g.https://test-1.absmartly.io/v1), not*.absmartly.com— the.comhost is the management API and will not accept collector traffic. Fill inABSMARTLY_E2E_API_KEYand adjust the application/ environment/unit count as needed. -
Run:
./run-e2e-tests.sh # all SDKs ./run-e2e-tests.sh --sdk javascript,python # specific SDKs ./run-e2e-tests.sh --cleanup # archive stale e2e experiments
Wrappers enter this path when a POST /context request sets mode: "e2e"; they
read credentials from their environment, not the request body. See the E2E Mode
section of WRAPPER_API_SPEC.md.
.github/workflows/cross-sdk.yml runs the full 202-scenario suite for all 21
SDKs on every pull request (and on demand), one independent matrix job per SDK.
Each job checks out this repo alongside the SDK source repo(s) its wrapper builds
from, builds just that wrapper plus the orchestrator, and drives the suite over
the compose network. The live-backend e2e suite is intentionally excluded from
CI — it needs secrets and the abs CLI and mutates real experiments.
A separate cross-sdk-consistency job brings up javascript, python, and go
together and runs the orchestrator once over all three, so the cross-SDK
assignment-consistency check (which needs at least two SDKs to compare
assignments) actually executes on every PR — the per-SDK matrix jobs run one SDK
each and never trigger it.
cross-sdk-tests/
├── README.md # this file
├── WRAPPER_API_SPEC.md # the HTTP API every wrapper implements
├── WRAPPER_REPOS.md # notes on standalone wrapper/SDK repos
├── test_scenarios_complete.json # 202 scenarios
├── generate_scenarios.py # regenerates scenarios; output
│ # test_scenarios_generated.json must be
│ # diffed against the canonical file above
│ # (the generated file is gitignored)
├── run-tests.sh # everyday entry point (filtered/local)
├── run-all-tests.sh # full matrix (unit + cross-SDK + e2e)
├── run-e2e-tests.sh # live-collector e2e (needs e2e-config.env)
├── verify_sdk.sh # single-SDK verification via orchestrator
├── setup-sdks.sh # clone/link sibling SDK source repos
├── e2e-config.env.example # template for e2e credentials
├── docker-compose.yml # wrapper + orchestrator service definitions
├── orchestrator/
│ ├── test_runner.py # cross-SDK orchestrator
│ ├── e2e_runner.py # live-backend e2e orchestrator
│ └── results_aggregator.py # merges per-SDK reports into report.json
├── <sdk>-wrapper/ # one HTTP wrapper per SDK (21 total)
├── docs/ # historical design docs (see banners)
└── test-results/ # report.json output (generated)
{
"name": "04 - Treatment - Queue Exposure",
"description": "treatment() should queue an exposure event",
"contextData": {
"experiments": [
{ "id": 1, "name": "exp_test", "iteration": 1, "unitType": "session_id", "seedHi": 0, "seedLo": 0, "split": [0.5, 0.5], "trafficSeedHi": 0, "trafficSeedLo": 0, "trafficSplit": [0, 1], "fullOnVariant": 0, "applications": [], "variants": [{ "config": null }, { "config": null }], "audienceStrict": false, "audience": null }
]
},
"steps": [
{
"action": "createContext",
"params": { "units": { "session_id": "test123" }, "options": { "publishDelay": -1 } },
"expect": { "result": { "ready": true, "failed": false }, "events": [{ "type": "ready" }] }
},
{
"action": "treatment",
"params": { "experimentName": "exp_test" },
"expect": { "result": 1, "events": [{ "type": "exposure" }] }
}
]
}Each SDK must return the exact result values and emit the expected event types and data. Cache-invalidation scenarios are a particular focus: the cache must be cleared (producing a new exposure) when an experiment changes, and retained (no new exposure) when nothing changes.