Flagship · Nov 2025 – present · SynergyBoat

ChargerDojo, conformance testing for EV roaming

A hosted conformance runner for the OCPI roaming protocol that I build and run alone, with paying B2B customers and a mutation harness where every check is either proven able to fail or listed as one no mutation may name.

Numbers this essay already carries
spec citations 1,500+
audit findings closed roughly 552
injectable disturbances 22

The problem

Two EV charging platforms both implement OCPI, both pass their own tests, and they still disagree in production. The expensive disagreements land on charge detail records, where one side rounded differently, read a tariff differently, or put calibration metadata somewhere the other side doesn’t look. Nobody finds out until a billing cycle closes and the numbers don’t reconcile.

The people living with this are protocol engineers at charge point operators, e-mobility providers and payment terminal vendors. They arrive mid-integration with a partner deadline already set, needing to know whether their implementation is wrong or their partner’s is, and needing to prove it to someone else.

What shipped

A hosted service that implements OCPI on both sides of the wire. Point your client at the simulator peer and it answers like a partner would. Give the runner your endpoint and it calls you, message by message, then hands back a report you can forward to the company you’re integrating with.

Four spec versions are vendored verbatim: 2.1.1, 2.2, 2.2.1 and 2.3.0, plus the EVRoaming Foundation extensions for direct payment and booking. Beyond static conformance there are timed charging journeys, 17 templates across 12 packs, graded by 16 oracles across four verdict lanes. Any run can take one of 22 injectable disturbances: a duplicated CDR, a session that never closes, a transport outage mid-charge.

The connector exists because a customer hit a wall. It opens an outbound WebSocket so you can test an endpoint sitting on a laptop or behind a corporate firewall, which beats re-issuing a tunnel URL every time it rotates. There is also a hosted MCP server that answers protocol questions directly, unauthenticated for the spec and key-authenticated for account data.

A test you have not tried to break is a belief, not a result.

How it works

The interesting part is not the checks. It’s proving the checks work.

Four mutant peers, one per spec version, are copies of the simulator with exactly one required rule deliberately broken (Figure 1). Between them they carry more than a hundred conformance mutations, and every mutation names in advance which check ids must turn red when it runs. A sweep where a named check stays green fails the build.

There’s a second condition that matters more. Where a check is proven, it has to be reddened by a mutant that did not redden everything. A check proven only by a sledgehammer counts as unproven, because a test that fires on any breakage tells you nothing about the specific one. Another 27 scenario mutations do the same job for journey verdicts.

Not every check gets that proof. Some grade only that a partner accepted something the spec also lets them refuse, so a mutation naming them would assert that we fail compliant partners. Those are listed instead, and the build fails if a check is neither proven nor listed.

Underneath that, spec citation is a build gate. There are more than 1,500 @see vendor/ocpi-spec/... citations across the backend, each carrying the governing clause and a verified anchor. A protocol rule that can’t cite the text it enforces gets removed or explicitly recorded as non-spec. This sounds bureaucratic until you meet the version inversions: identifiers are case-sensitive in 2.1.1 and case-insensitive from 2.2, so a fix that is correct in one version is a defect in the next. Without the citation you cannot tell which one you’re looking at.

The rest is unglamorous. A Cloudflare Worker renders pages at the edge while /api, /ws and /ocpi reach the origin directly. Protocol code splits by which side of the wire it acts on. An SSRF guard sits on the outbound client, because the entire paid product is making the server call a URL a stranger typed into a form.

What I’d do differently

I ran the spec conformance audit in three rounds and closed roughly 552 findings. Round two taught the lesson: three of its defects were in the conformance suites themselves, not the server. The suites had been agreeing with bugs. Only running them against already-corrected modules surfaced it. The mutant peers exist because of that round, and they should have existed first. A test you have not tried to break is a belief, not a result.

The second thing is instrumentation. Nothing counted a conformance run before 11 July 2026, so how much the thing actually got used in the first half of the year has no instrument at all. Not hard to query, absent. Adding an events table on day one costs an afternoon and I skipped it.

Third, the connector. I built it in response to a real customer problem, shipped it, and no customer has completed a pairing with it yet. It should have been a thing I offered to one person by hand before it was a thing I cross-compiled for five platforms.

The throughput here was agent-assisted: a root instruction file and a spec per feature give the agents something to execute against, and I direct, review and merge. That is the second reason the mutant peers exist, because work arriving at that rate needs a check that is not me reading the diff.

The ChargerDojo mutation harness A runner on the left calls the sim-peer on the right, which is the reference implementation. The OCPI wire between them is drawn as one lane each way with a tick per message, because the runner calls message by message rather than once. Four copies of the sim-peer hang off a bus, one per spec version. 2.1.1, where mod_cdrs says a CDR cannot change once sent, and this peer reads one back at a new total. 2.2, where status_codes says 2.2 has no hub code 4000, and this peer answers 4000 to a hub error. 2.2.1, where transport_and_format says limit=10 returns ten records, and this peer serves one object more. 2.3.0, where mod_tariffs says tax_included is required, and this peer serves a tariff without it. Each peer runs one mutation at a time. Under them a build gate stamps a tick for every mutant the checks catch, and one hold mark for a check no mutation may name, which is listed rather than proven. At the foot, dojo-connect, the Go connector, dials out through a dashed customer firewall on a WebSocket so an endpoint on a laptop can be reached. ocpi, message by message runner the runner walks a whole conversation, not one request sim-peer reference implementation copies, one per version 2.1.1 mod_cdrs: a CDR cannot change once sent this peer reads one back at a new total 2.2 status_codes: 2.2 has no hub code 4000 this peer answers 4000 to a hub error 2.2.1 transport_and_format: limit=10 returns ten records this peer serves one object more 2.3.0 mod_tariffs: tax_included is required this peer serves a tariff without it each peer runs one mutation at a time four copies of the simulator, each breaking one required rule build gate 2.1.1 2.2 2.2.1 2.3.0 listed every check is proven able to fail, or listed with a reason a tick per mutant caught, one hold mark for the unprovable check outbound websocket customer firewall dojo-connect go endpoint dojo-connect dials out through the customer firewall
version rule broken gate
wire The runner calls the peer over OCPI message by message, one lane each way, not a single request. n/a
2.1.1 mod_cdrs: a CDR cannot change once sent. This peer reads one back at a new total. caught
2.2 status_codes: 2.2 has no hub code 4000. This peer answers 4000 to a hub error. caught
2.2.1 transport_and_format: limit=10 returns ten records. This peer serves one object more. caught
2.3.0 mod_tariffs: tax_included is required. This peer serves a tariff without it. caught
listed A check no mutation may name, listed with its reason. not proven
connector dojo-connect, in Go, dials out through the customer firewall on a WebSocket so an endpoint on a laptop can be reached. n/a
Fig. 1. The mutation harness. The runner sweeps four mutant peers, each a copy of the simulator with exactly one required rule broken. A check no mutant reddens is listed rather than trusted, and the build fails if a check is neither proven nor listed. Drawn for ChargerDojo only. Nothing on this page reuses it.