cog-ha-matter (ADR-116 P4): MdnsService -> mdns-sd ServiceInfo bridge

Pure conversion from our wire-format `MdnsService` to the
`mdns_sd::ServiceInfo` shape the responder daemon consumes. No
socket binding, no daemon registration yet — that lands next iter
as a `runtime::spawn_mdns_responder(info)` JoinHandle returning
helper, same shape as `runtime::spawn_publisher`.

  * `MdnsService::to_service_info(hostname, ipv4) ->
        Result<ServiceInfo, mdns_sd::Error>`
  * `mdns-sd = "0.11"` added — aligned with the workspace pin from
    wifi-densepose-desktop so the lockfile doesn't fork dalek-like
    surfaces.

3 new tests:

  * to_service_info_carries_service_type_and_port — locks that
    `_ruview-ha._tcp` (with or without mdns-sd's trailing-dot
    normalisation) and the control port round-trip through the
    conversion
  * to_service_info_propagates_txt_records — every locked TXT
    key from iter 4 (cog_id, mqtt_port, privacy, proto, node_id,
    cog_version) reachable via `get_property_val_str` on the
    converted ServiceInfo
  * to_service_info_does_not_silently_drop_caller_hostname —
    locks the caller-side responsibility for the .local. suffix.
    mdns-sd 0.11 accepts bare hostnames (verified empirically by
    initial test expecting it to reject — it didn't), so the
    wrapper layer must do the trailing-dot dance. Documenting
    that via a named test catches future bumps where the lib
    starts mutating the value.

63/63 cog tests green (60 → 63).

ADR-116 P4 now ⁶⁄₇:  mDNS record-builder,  chain,  JSONL, 
file persistence,  Ed25519 signing,  ServiceInfo conversion;
 daemon register + embedded broker.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-05-23 18:28:10 -04:00
parent bb154d4e78
commit 34eced880f
4 changed files with 83 additions and 1 deletions
Generated
+1
View File
@@ -935,6 +935,7 @@ version = "0.3.0"
dependencies = [
"clap",
"ed25519-dalek",
"mdns-sd",
"serde",
"serde_json",
"sha2",
+5
View File
@@ -41,5 +41,10 @@ wifi-densepose-hardware = { version = "0.3.0", path = "../wifi-densepose-hardwar
sha2 = { workspace = true }
ed25519-dalek = "2.1"
# mDNS responder (ADR-116 P4 §2.2): pure-Rust zero-conf daemon.
# Same version pinned in wifi-densepose-desktop to keep the
# workspace lockfile narrow.
mdns-sd = "0.11"
[dev-dependencies]
tempfile = "3.10"
+76
View File
@@ -38,6 +38,10 @@
//! are broadcast in cleartext and harvested by passive scanners, so
//! treating them as PII-clean is part of the privacy posture.
use std::collections::HashMap;
use mdns_sd::ServiceInfo;
use crate::COG_ID;
/// Default mDNS instance name template. `{node_id}` is substituted
@@ -74,6 +78,33 @@ impl MdnsService {
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
/// Convert into the `mdns_sd::ServiceInfo` the responder daemon
/// consumes. Pure transform — no socket binding, no daemon
/// registration. The caller wires the resulting `ServiceInfo`
/// into `ServiceDaemon::register` (next iter).
///
/// `hostname` should end in `.local.` per RFC 6762 — e.g.
/// `"cognitum-seed-1.local."`. `ipv4` is the LAN-routable
/// address HA's discovery will reach back on.
pub fn to_service_info(
&self,
hostname: &str,
ipv4: &str,
) -> Result<ServiceInfo, mdns_sd::Error> {
let mut props: HashMap<String, String> = HashMap::with_capacity(self.txt_records.len());
for (k, v) in &self.txt_records {
props.insert(k.clone(), v.clone());
}
ServiceInfo::new(
&self.service_type,
&self.instance_name,
hostname,
ipv4,
self.control_port,
Some(props),
)
}
}
/// Build the cog's mDNS advertisement record from the cog's typed
@@ -203,6 +234,51 @@ mod tests {
}
}
#[test]
fn to_service_info_carries_service_type_and_port() {
let svc = build_mdns_service(&id(), 9180, 1883, false);
let info = svc
.to_service_info("cognitum-seed-1.local.", "192.168.1.50")
.expect("valid service info");
// mdns-sd may rewrite the type with a trailing dot; allow
// both forms.
let ty = info.get_type();
assert!(
ty == "_ruview-ha._tcp" || ty == "_ruview-ha._tcp.",
"unexpected service type: {ty}"
);
assert_eq!(info.get_port(), 9180);
}
#[test]
fn to_service_info_propagates_txt_records() {
let svc = build_mdns_service(&id(), 9180, 1883, true);
let info = svc
.to_service_info("cognitum-seed-1.local.", "192.168.1.50")
.expect("valid service info");
// Every locked TXT key must reach the wire-format payload.
assert_eq!(info.get_property_val_str("cog_id"), Some(crate::COG_ID));
assert_eq!(info.get_property_val_str("mqtt_port"), Some("1883"));
assert_eq!(info.get_property_val_str("privacy"), Some("1"));
assert_eq!(info.get_property_val_str("proto"), Some("ruview-ha/1"));
assert!(info.get_property_val_str("node_id").is_some());
assert!(info.get_property_val_str("cog_version").is_some());
}
#[test]
fn to_service_info_does_not_silently_drop_caller_hostname() {
// mdns-sd 0.11 accepts bare hostnames (no `.local.`); the
// responsibility for the trailing dot lives in our wrapper.
// Lock that the caller's hostname survives the conversion
// verbatim — a future bump that starts mutating the value
// surfaces a named test instead of a silent change.
let svc = build_mdns_service(&id(), 9180, 1883, false);
let info = svc
.to_service_info("cognitum-seed-1.local.", "192.168.1.50")
.unwrap();
assert!(info.get_hostname().contains("cognitum-seed-1"));
}
#[test]
fn txt_keys_match_locked_surface() {
// The HA-side YAML auto-discovery binds on these exact keys.