- deferment.md: node ACLs no longer authored-but-inert (#520 shipped through stage 7, opt-in Security:Acl:Enabled); failure-visibility trio mostly closed (01/S-1 + 03/S4 fixed, 06/S-1 partial); bulkhead + WriteIdempotentAttribute closed by R2-02; Sql Postgres+Oracle dialects shipped (d54f3c83); akka-hosting partials verified superseded - archreview/00-OVERALL.md: staleness banner on the carried-open block (04/C-1→R2-05, 03/S2+S3→R2-08, 03/P1→R2-07, driver fleet→R2-09/R2-11/#457, hygiene→R2-12, trio per above; what genuinely remains from theme 4) - archreview/plans/STATUS.md: carved out the true exceptions to "every Round-2 live gate GREEN" (R2-03 T11/T12 deferred-live, R2-10 T10 breaker-OPEN never forced); #459 closed by mesh Phase 7 drill D4 (c50ebcf7) - archreview/plans/INTEGRATION-SWEEP-STATUS.md: "follow-ups unstarted" → all 7 shipped as PRs #446–#452 - archreview/06-gateway-integrations.md: 06/S-1 STILL OPEN → PARTIAL (fail-closed advise + unconfirmed meter; residual cross-repo) - 2026-07-31-apikey-acl-enforcement-design.md: status → implemented through stage 7, #520 CLOSED 2026-08-01 - 2026-07-21-per-cluster-mesh-design.md: "Phases 1–7 not started" → PROGRAM COMPLETE 2026-07-24 (Phase 7c50ebcf7) - 2026-07-20-localdb-phase2-live-gate.md: merged as 06b5d81b; open design question shipped by mesh Phase 6 (checks 3/4 still never re-run on the three-mesh topology) - 2026-07-20-localdb-phase1-progress.md: Tasks 10–16 REMAIN → Phase 1 complete, live-gated 8/8, mergedc957db52- 2026-08-06-clusterclient-to-grpc-tracking.md: defect 4 OPEN → fixed byc0475f2d+e54883a8, merged @b360239e- 2026-08-06-clusterclient-to-grpc.md.tasks.json: Task 15 pending → completed (Phase 4 gate PASS 8/8 @0b194003, recordd0ac6fc5) - 2026-07-22-mesh-phase2-live-gate.md: DriverStatusPanel orphan → wired into /hosts by #546 (9420f8ef) - 2026-07-21-issue-485-live-gate.md: pointer from the "NOT fixed here" finding to the #486 fix (DriverHostActor.cs:1696-1717 + FetchAndCache mirror) - 2026-08-05-sql-postgres-oracle.md: chrome/AdminUI gate not deferred — PASSED 2026-08-05, found + fixed the Test-Connect provider gate (4e461951) - v2-ga-lab-gates-plan.md / v2-release-readiness.md / live-hardware-validation-runbooks.md: FOCAS live-CNC smoke #54 RESOLVED on a real 31i-B 2026-06-25 (cnc_rdparam caveat preserved) - Mqtt-Test-Fixture.md: STATE/DCMD "not implemented" → shipped + live-gated (#5082a7d1167, #511857a10b4) - 2026-08-01-continuous-historization-live-gate.md: poison-tag FIFO pin "filed, not fixed" → #538 fixed same day (e7fbe977, gate 4bdce910; Capacity default now 500k) - 2026-05-26-akka-hosting-alignment-plan.md.tasks.json: F8/F9/F10/F13/F14 partial → completed, superseded by the v3 address-space rework (Phase7Composer/ScriptedAlarmActor deleted; evaluators/SDK sinks/security policies production-wired) Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
7.6 KiB
MQTT test fixture
Coverage map + gap inventory for the MQTT driver's harness.
TL;DR: the unit suite (581 tests) carries the mapping, indexing, Sparkplug state-machine and failure-classification logic; the live suite (15 tests) proves the parts only a real broker can prove — TLS, real authentication, CA pinning in both directions, retained-message seeding, that a rejected CONNACK is actually surfaced, and the Sparkplug birth/alias/rebirth/death path against a real edge-node simulator.
What the fixture is
eclipse-mosquitto:2.0.22 plus a second container of the same image running publisher/publish.sh
(the image ships mosquitto_pub, so no bespoke image is needed), plus — behind the sparkplug
compose profile — otopcua-sparkplug-sim, a project-owned C# Sparkplug-B edge-node simulator
(tests/Drivers/….Driver.Mqtt.IntegrationTests/SparkplugSimulator/). Compose lives at
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/; the deployed stack is
/opt/otopcua-mqtt on the shared Docker host.
Two authenticated listeners, no anonymous fallback (allow_anonymous false) — deliberate: an
anonymous broker would let a broken auth config pass every test.
| Listener | Endpoint | Env var |
|---|---|---|
| TLS + auth | 10.100.0.35:8883 |
MQTT_FIXTURE_ENDPOINT |
| plaintext + auth | 10.100.0.35:1883 |
MQTT_FIXTURE_PLAIN_ENDPOINT |
MqttFixture gates the suite on MQTT_FIXTURE_ENDPOINT / MQTT_FIXTURE_USERNAME /
MQTT_FIXTURE_PASSWORD (+ MQTT_FIXTURE_CA_CERT, MQTT_FIXTURE_TOPIC_PREFIX) and skips cleanly
when they are absent, so the suite is safe to run offline.
Unlike every other fixture it needs generated material first — ./gen-fixture-material.sh writes
the gitignored secrets/ (password file, CA, server cert/key). The broker will not start without it.
Bring-up and the CA-fetch recipe are in infra/README.md §3.
Published topics
| Topic | Shape | Retain | Why it exists |
|---|---|---|---|
otopcua/fixture/retained/seed |
{"value":42.5,…} |
✅ published exactly once | The only way to prove retained seeding. On a republished topic a retained seed is indistinguishable from a fast live publish |
otopcua/fixture/line1/temperature |
{"value":…,"unit":"C","seq":n} |
✅ | JSON + JSONPath extraction |
otopcua/fixture/line1/counter |
{"value":n} |
✅ | Monotonic, for ordering |
otopcua/fixture/line1/state |
RUNNING (bare text) |
✅ | Non-JSON Raw/Scalar payloads |
otopcua/fixture/noise/chatter |
JSON | ❌ | Never authored as a tag — a broker carrying unauthored traffic is the normal case and the driver must stay silent about it |
What it actually covers
The 7 live tests: TLS+auth connect; connect pinned to the fixture CA; connect pinned to a foreign
CA is rejected (the falsifiability control — a pinning test that only ever passes proves nothing);
wrong password is rejected and surfaced (the MQTTnet-5 silent-Healthy defect); plain
subscribe→value under a RawPath; retained message seeds a value on subscribe; and an unauthored topic
stays silent while an authored tag keeps flowing.
The Sparkplug simulator
Group OtOpcUaSim; edge nodes EdgeA and EdgeB; EdgeA additionally publishes device
Filler1. Node metrics Temperature (Float), Pressure (Double), Count (Int32), Running
(Boolean), Serial (String) plus the mandatory bdSeq and Node Control/Rebirth — the last one
deliberately carries a /, so it exercises the "a metric name is not a topic segment" rule. Device
metrics: Temperature (Float), FillCount (Int64), Jammed (Boolean). Cadence ~2 s.
It subscribes to spBv1.0/OtOpcUaSim/NCMD/{node} and re-births on a rebirth request, so it
exercises the real NCMD round trip rather than simulating it. Births are not retained (per spec), so
docker restart otopcua-sparkplug-sim is the way to force a fresh birth on demand:
ssh dohertj2@10.100.0.35 'cd /opt/otopcua-mqtt && docker compose --profile sparkplug up -d'
ssh dohertj2@10.100.0.35 'docker restart otopcua-sparkplug-sim' # force a birth
ssh dohertj2@10.100.0.35 'docker logs -f otopcua-sparkplug-sim'
Live-gated end-to-end (2026-07-24, P1 milestone): deployed on the docker-dev rig against this fixture,
both tags resolved and served changing values through OPC UA, and the AdminUI #-observation picker
rendered the real topic tree. Re-gated for P2 (2026-07-25) against the Sparkplug simulator — see
Mqtt.md.
What it does NOT cover
1. Sparkplug primary-host STATE, and write-through
The simulator never observes a Host Application STATE, because the driver never publishes one
(ActAsPrimaryHost is unimplemented). Nor is there any DCMD/NCMD command coverage: rebirth is the
only NCMD the driver ever sends, and IWritable is deferred.
Sparkplug seq-gap injection is unit-proven only — the simulator always publishes a well-formed
monotonic seq, so no live test drives the gap → rebirth path end-to-end.
2. Broker-side failure injection
No test kills the broker mid-session, partitions the network, or forces a SUBACK failure against a real broker. Reconnect, backoff and per-topic degradation are unit-proven only.
3. Wildcard tags against a live broker
The by-filter indexing fix that keeps wildcard tags alive across a reconnect is covered by unit tests;
no live test authors a +/# tag and bounces the connection.
4. QoS 2 delivery semantics
The fixture publishes at QoS 0/1. QoS 2's exactly-once handshake is never exercised end-to-end.
5. Scale
One publisher, five topics, a ~2 s cadence. Nothing probes the 50 000-node browse cap, the 1 MiB payload cap, or thousands of authored tags.
6. Redundant-pair client-id collision
Found by hand during the P1 gate (a fixed clientId makes pair nodes evict each other), not by a
test. No automated coverage asserts that two concurrent drivers coexist.
When to trust the MQTT fixture, when to reach for a real broker
| Question | Fixture answers it? |
|---|---|
| Does TLS + auth work, and does bad auth fail loudly? | ✅ yes |
| Is a retained message seeded on subscribe? | ✅ yes |
| Does JSONPath extraction + coercion behave? | ✅ yes (unit + live) |
| Does the driver ignore unauthored traffic? | ✅ yes |
| Does it survive a broker restart / flapping link? | ❌ unit-only |
| Does it hold up at plant tag counts? | ❌ no |
| Does a Sparkplug birth → alias → DATA → death cycle work? | ✅ yes (live, vs. the simulator) |
| Does an NCMD rebirth request actually get answered? | ✅ yes (the simulator subscribes + re-births) |
| Sparkplug seq-gap → rebirth | ❌ unit-only (the sim never skips a seq) |
| Sparkplug primary-host STATE / DCMD writes | ✅ both shipped + live-gated (corrected 2026-08-07, truth sweep) — NCMD/DCMD write-through as Gitea #508 (merge 2a7d1167, 2026-07-31, 21/21 live) and primary-host STATE + Last Will as Gitea #511 (merge 857a10b4, 2026-07-31, 5/5 live) |
Follow-up candidates
- Broker-restart / SUBACK-failure live tests (closes gap 2).
- A live wildcard-across-reconnect test (closes gap 3).
- A concurrent-two-driver test pinning the client-id hazard (closes gap 6).
- A seq-gap injection switch on the simulator, closing the last unit-only Sparkplug leg.
Key fixture / config files
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.IntegrationTests/Docker/docker-compose.ymltests/Drivers/…/Docker/mosquitto.conf·Docker/gen-fixture-material.sh·Docker/publisher/publish.shtests/Drivers/…/MqttFixture.cs·PlainMqttLiveTests.csinfra/README.md§3 — bring-up, endpoints, CA fetch