CrowdVision · source-available, not open source · © 2026 Nicolò Ghignatti
Contracts, not implementation: each entry is a Given/When/Then pre/postcondition, with no test framework, runner, or service named. Numbers are asserted here, never invented — each one is the target owned by the matching Quality Attribute Scenario. Every criterion below traces to something the story actually says; a criterion that tests a different story’s concern (tenancy scoping) or a different environment (peak load) is kept separate rather than folded in.
Where each contract is checked in code — and how it is measured, including what the measurement does not cover — is kept out of the criteria themselves and collected in Verification at the end of the page, so a criterion never has to change when a test file moves.
Target owned by QA-P-03.
Behavior under a burst — redelivery, a dying worker, backlog drain — is QA-P-04’s concern, not this story’s: it describes a different environment than BT-1 was written for, and belongs entirely on the Quality Attributes page.
Target owned by QA-P-01.
Both stories’ performance criteria are implemented and asserted as numbers, not described. The functional criteria around them are covered too, with the two exceptions named below. Paths are repository-relative.
Suite: backend/digital-twin/tests/. Cucumber features in tests/features/, their steps in tests/steps/, endpoint-level tests in tests/api/. Runner just test twin-integration — real MongoDB on a shared Docker network, no mocks at the boundary. CI runs it on every digital-twin change (tpl-rust-ci.yml → scripts/test/rust-integration-tests.sh).
tests/api/registration.rs, an_accepted_upload_becomes_a_viewable_twin: POST /register, then GET /building/:id returns the declared name and room count.tests/features/redelivery.feature, “holds exactly one building”, asserted against GET /buildings/:domain.tests/steps/registration.rs, acknowledged: 202 plus a non-empty buildingId.tests/steps/registration.rs, handle_reports: polls GET /building/:id/status until it reports the expected state, bounded by READY_TIMEOUT.tests/features/registration.feature with tests/steps/registration.rs, refused: 400, no handle, and the organization holds no buildings. The rejection message naming the failing element comes from src/domain/building.rs via DomainError::Validation.tests/features/redelivery.feature: resolve the same handle twice, status stays ready, still exactly one building.Not yet asserted. Two clauses of the first criterion have no check behind them: that each room carries the position and dimensions the uploaded description declared (tests/api/rooms.rs covers geometry on the room-update endpoint, not on the provisioning path), and that the twin is editable straight after provisioning (tests/steps/load.rs, every_twin_available, only confirms that availability was tracked). “Nothing is left pending” after a refusal is likewise unchecked against the pending_uploads queue.
tests/features/load.feature with tests/steps/load.rs. 100 Domain Administrators upload concurrently, once for a 20-room building and once for a 200-room one — both sizes inside QA-P-03’s stated envelope.
POST /register call, per upload.202 until GET /building/:id/status first reports ready, polled every 50 ms, capped by AVAILABILITY_TIMEOUT (15 s).p99 refuses to run under MIN_SAMPLES_FOR_P99 (100) samples, because below that the 99th percentile is just the maximum.oneshot), so the figures exclude network transit, the edge proxy and TLS. They measure the service, not the deployed path.Suite: backend/acceptance/acceptance/ld4_dashboard_freshness/, pytest against a composed stack (telemetry, socket, dashboard and their infra), started by backend/acceptance/run-integration-tests.sh behind just test integration.
test_live_update.py: ingest returns 202, the subscribed socket client receives a telemetry event carrying the same building, room and value. Wire-level; the render step itself is Vue reactivity, not a backend concern.test_reconnect_catchup.py: a known pre-outage value, disconnect, ingest, reconnect, then assert the new value is what the dashboard reads back — so a stale leftover cannot pass. The client half of that recovery is frontend/src/stores/sensorData.ts, whose connect handler re-subscribes every building and refetches every bucket; BuildingTable.vue reaches it through useBuildingReadings.Not yet asserted. That the UI itself repaints on reconnect — proving it needs a real browser, out of this suite’s scope. Separately, this suite has no CI job: it runs only on demand via just test integration.
test_performance.py. 100 round trips across 20 threads, each with its own room, dashboard subscription and socket connection.
telemetry event; p99 over the 100 samples, same minimum-sample rule as BT-1.≤ 1 s for “inside the system” and ≤ 5 s end to end. That is a proxy in both directions: a superset of “leaves socket” (the test client is the very next hop) and of “rendered” (Vue’s re-render after a socket event is single-digit milliseconds against a 5-second budget).ingestedAt field telemetry already stamps on every tick. The inner ≤ 1 s boundary is therefore not independently measured; instrumenting socket is what would make it one.