feat(adr-115): flip Status → Accepted (MQTT track) + property-based fuzz tests + ADR index entry
## Status flip — ADR-115 §Status Per maintainer ACK (#776 issue body + 13 ACK'd open questions) and the shipped implementation in PR #778 (410 lib tests, witness bundle VERIFIED), the MQTT track is now Accepted. The Matter SDK wiring P8b remains Proposed pending the §9.10 deferral to v0.7.1. ADR header table updated: - Status: "**Accepted** (MQTT track P1-P7 + P8a + P9 + P10 shipped 2026-05-23 in PR #778, 410 lib tests, witness bundle VERIFIED) / **Proposed** (Matter SDK wiring P8b deferred to v0.7.1 per §9.10)" - Codename: HA-DISCO (MQTT) + HA-FABRIC (Matter) + **HA-MIND** (semantic primitives) — the third codename always belonged in the masthead. - Tracking issue: now points at #776 + PR #778 `docs/adr/README.md` ADR index gets an ADR-115 row in the "Platform and UI" section with the same Accepted/Proposed split. ## Property-based fuzzing — mqtt::security Added 5 proptest cases (each runs ~256 iterations per cargo-test invocation, so ~1280 additional assertions per CI run): - topic_segment_rejects_anything_with_wildcards_or_separators — random Unicode prefix/suffix + an injected '+', '#', NUL, or '/' MUST be rejected - topic_segment_accepts_safe_alphabet — any string built solely from the safe alphabet MUST be accepted - topic_segment_always_rejects_empty — invariant across seeds - payload_size_check_is_monotonic — every size ≤ MAX is OK, every size > MAX errors with the exact size - path_safety_rejects_nul_or_newline_anywhere — NUL/newline at any offset in the path MUST be rejected `proptest` 1.5 added as dev-dep with default features off (no proptest-derive needed). ~3 transitive crates added, dev-only. Total lib tests: 410 → 415 passed, 0 failed, 1 properly ignored. Refs #776, PR #778. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
c8b6cd7ace
commit
0f7a4bd36e
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Status** | **Accepted** (MQTT track P1–P7 + P8a + P9 + P10 shipped 2026-05-23 in PR #778, 410 lib tests, witness bundle VERIFIED) / **Proposed** (Matter SDK wiring P8b deferred to v0.7.1 per §9.10) |
|
||||
| **Date** | 2026-05-23 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **HA-DISCO** (MQTT) + **HA-FABRIC** (Matter) |
|
||||
| **Codename** | **HA-DISCO** (MQTT) + **HA-FABRIC** (Matter) + **HA-MIND** (semantic primitives) |
|
||||
| **Relates to** | ADR-018 (CSI binary frame format), ADR-021 (ESP32 vitals), ADR-031 (RuView sensing-first), ADR-039 (edge vitals packet 0xC511_0002), ADR-079 (camera ground-truth), ADR-103 (cog-person-count), ADR-110 (ESP32-C6 firmware), ADR-114 (cog-quantum-vitals) |
|
||||
| **Tracking issue** | TBD — file under RuView issue tracker, link in §10 |
|
||||
| **Tracking issue** | [#776](https://github.com/ruvnet/RuView/issues/776) — implementation in PR [#778](https://github.com/ruvnet/RuView/pull/778) |
|
||||
| **Related issues** | [#574](https://github.com/ruvnet/RuView/issues/574) (mDNS for seed_url), [#760](https://github.com/ruvnet/RuView/issues/760) (sensing UI), [#761](https://github.com/ruvnet/RuView/issues/761) (HA competitor scan) |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
|
|||
| [ADR-035](ADR-035-live-sensing-ui-accuracy.md) | Live Sensing UI Accuracy and Data Transparency | Accepted |
|
||||
| [ADR-036](ADR-036-rvf-training-pipeline-ui.md) | Training Pipeline UI Integration | Proposed |
|
||||
| [ADR-043](ADR-043-sensing-server-ui-api-completion.md) | Sensing Server UI API Completion (14 endpoints) | Accepted |
|
||||
| [ADR-115](ADR-115-home-assistant-integration.md) | Home Assistant integration via MQTT auto-discovery + Matter bridge (HA-DISCO + HA-FABRIC + HA-MIND) | Accepted (MQTT track) / Proposed (Matter SDK P8b) |
|
||||
|
||||
### Architecture and infrastructure
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@ tower = { workspace = true }
|
|||
# Heavy dep tree (~80 transitive crates) so it's dev-only; benches live
|
||||
# behind --features mqtt because they bench the mqtt module.
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
# ADR-115 P9 — property-based fuzzing for the wire-boundary security
|
||||
# audit. Catches edge cases the example-based unit tests would miss
|
||||
# (random Unicode, control chars, etc.). Pinned to a small version that
|
||||
# doesn't pull in proptest-derive (we don't need it).
|
||||
proptest = { version = "1.5", default-features = false, features = ["std"] }
|
||||
|
||||
[[bench]]
|
||||
name = "mqtt_throughput"
|
||||
|
|
|
|||
|
|
@ -250,4 +250,77 @@ mod tests {
|
|||
// --mqtt-password flag, this test fails on purpose.
|
||||
assert!(password_via_env_only(Some("secret")).is_err());
|
||||
}
|
||||
|
||||
// ─── Property-based fuzzing (proptest) ──────────────────────────
|
||||
//
|
||||
// The example-based tests above hit the obvious cases. These
|
||||
// property tests hit *every* case clap could pass us: random
|
||||
// Unicode, control chars, embedded NULs at arbitrary offsets,
|
||||
// multi-character wildcards, etc. They catch regressions where a
|
||||
// future refactor accidentally narrows the rejection envelope.
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
/// For ANY string that contains `+`, `#`, NUL, or `/`, the
|
||||
/// safety check must return false. No exceptions.
|
||||
#[test]
|
||||
fn topic_segment_rejects_anything_with_wildcards_or_separators(
|
||||
prefix in "[a-zA-Z0-9_-]{0,16}",
|
||||
suffix in "[a-zA-Z0-9_-]{0,16}",
|
||||
offender in proptest::char::any().prop_filter(
|
||||
"must be reserved char", |c| matches!(c, '+' | '#' | '\0' | '/')
|
||||
),
|
||||
) {
|
||||
let s = format!("{prefix}{offender}{suffix}");
|
||||
prop_assert!(!topic_segment_is_safe(&s), "must reject {:?}", s);
|
||||
}
|
||||
|
||||
/// For any non-empty string containing ONLY chars from the
|
||||
/// "safe" alphabet (alphanumeric + a few punctuation), the
|
||||
/// check must pass.
|
||||
#[test]
|
||||
fn topic_segment_accepts_safe_alphabet(s in "[a-zA-Z0-9_.\\-]{1,64}") {
|
||||
prop_assert!(topic_segment_is_safe(&s), "must accept {:?}", s);
|
||||
}
|
||||
|
||||
/// Empty strings always rejected, regardless of input source.
|
||||
#[test]
|
||||
fn topic_segment_always_rejects_empty(seed in any::<u64>()) {
|
||||
let _ = seed; // just to randomize the test runner
|
||||
prop_assert!(!topic_segment_is_safe(""));
|
||||
}
|
||||
|
||||
/// Payload-size check: every size ≤ MAX_PUBLISH_BYTES is OK;
|
||||
/// every size > MAX_PUBLISH_BYTES errors with the actual size.
|
||||
#[test]
|
||||
fn payload_size_check_is_monotonic(
|
||||
len in 0usize..=(MAX_PUBLISH_BYTES * 2)
|
||||
) {
|
||||
// Don't actually allocate MAX_PUBLISH_BYTES * 2 of memory
|
||||
// every test; use a small payload + lie about its length
|
||||
// via slicing semantics. The function only checks .len().
|
||||
let buf = vec![0u8; len];
|
||||
let r = check_payload_size(&buf);
|
||||
if len > MAX_PUBLISH_BYTES {
|
||||
prop_assert!(r.is_err());
|
||||
prop_assert_eq!(r.unwrap_err(), len);
|
||||
} else {
|
||||
prop_assert!(r.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
/// Path safety: a path containing NUL or newline must be
|
||||
/// rejected, regardless of the rest of the path.
|
||||
#[test]
|
||||
fn path_safety_rejects_nul_or_newline_anywhere(
|
||||
prefix in "[a-zA-Z0-9_/.\\-]{0,32}",
|
||||
suffix in "[a-zA-Z0-9_/.\\-]{0,32}",
|
||||
offender in prop_oneof!["\\u{0000}", "\\n"],
|
||||
) {
|
||||
let s = format!("{prefix}{offender}{suffix}");
|
||||
let p = std::path::Path::new(&s);
|
||||
prop_assert!(!path_is_safe(p), "must reject path with offender: {:?}", s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue