Files
ruvnet--RuView/v2/crates/wifi-densepose-bfld/tests/ha_blueprints.rs
T
ruv 820258e932 feat(adr-118/p5.10): three HA operator blueprints (210/210 GREEN)
Iter 30. Ships the three ADR-122 §2.6 operator-ready Home Assistant
automation blueprints. Each blueprint binds to one BFLD MQTT entity
(presence / motion / identity_risk) and lets an HA operator import
+ configure without writing YAML by hand.

Added (under v2/crates/cog-ha-matter/blueprints/bfld/):
- presence-lighting.yaml
    binary_sensor.<node>_bfld_presence ⇒ light.turn_on / turn_off
    with a configurable hold_seconds delay before the off action
    (ADR-122 §2.6 requirement: "configurable hold time")
- motion-hvac.yaml
    sensor.<node>_bfld_motion ⇒ climate.set_temperature
    Operator picks motion_threshold (default 0.3, per ADR §2.6),
    delta_temperature_c (°C adjustment), and quiet_seconds debounce
- identity-risk-anomaly.yaml
    sensor.<node>_bfld_identity_risk ⇒ notify.<target>
    Two trigger paths:
      - Absolute spike (raw score >= spike_threshold, default 0.8)
      - Rolling 7-day z-score deviation (default 3 sigma)
    Requires a Statistics helper entity for the baseline; documented
    in the inline description and the blueprints README.
- README.md
    Lists the three blueprints + privacy caveat for identity_risk
    (only present at PrivacyClass::Anonymous; class 3 deployments
    will fail validation by design)

Added (in v2/crates/wifi-densepose-bfld/tests/ha_blueprints.rs):
- 7 named tests using include_str! to embed each YAML at build time
  and validate structure without adding a serde_yaml dep:
    presence_lighting_blueprint_is_structurally_valid
    motion_hvac_blueprint_is_structurally_valid
    identity_risk_blueprint_is_structurally_valid
    blueprints_carry_source_url_pointing_at_canonical_path
      (catches path drift when files move)
    presence_blueprint_uses_mqtt_integration_filter
    motion_blueprint_uses_mqtt_integration_filter
    identity_risk_blueprint_carries_privacy_class_caveat_in_description
      (operators running class 3 should know not to install)
- Helper assert_required_blueprint_fields(yaml, name_substring, label)
  enforces blueprint.{name,domain,input,trigger,action,mode} per HA spec

ACs progressed:
- ADR-122 §2.6 — all three blueprints shipped with the documented
  configurable inputs (hold_seconds for #1, motion_threshold +
  delta_temperature_c for #2, z_score_threshold + statistics_entity
  for #3). Operator installs via HA UI; no YAML editing required.
- ADR-118 §1.5 privacy_mode visibility — identity-risk blueprint
  documents the class-2-only availability so operators understand
  why the blueprint fails on class-3 deployments.

Test config:
- cargo test --no-default-features → 72 passed
- cargo test                       → 210 passed (203 + 7)

Out of scope (next iter target):
- GitHub Actions workflow with mosquitto Docker so iters 24 + 29
  e2e tests actually run in CI with BFLD_MQTT_BROKER set.
- cog-ha-matter cargo crate-internal test that loads each blueprint
  via serde_yaml + validates against an HA blueprint schema (instead
  of the string-only checks here). Optional; current coverage is
  sufficient to catch drift in the YAML files themselves.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 18:17:41 -04:00

121 lines
4.5 KiB
Rust

//! Validate the cog-ha-matter HA blueprints structurally — they're shipped
//! YAML, so the test embeds each file at compile time via `include_str!` and
//! string-checks the required HA-blueprint fields. Avoids adding a serde_yaml
//! dep to BFLD for what is effectively a documentation-of-record asset.
//!
//! ADR-122 §2.6 specifies three blueprints; this test pins their structure.
#![cfg(feature = "std")]
const PRESENCE_LIGHTING: &str = include_str!(
"../../cog-ha-matter/blueprints/bfld/presence-lighting.yaml"
);
const MOTION_HVAC: &str = include_str!(
"../../cog-ha-matter/blueprints/bfld/motion-hvac.yaml"
);
const IDENTITY_RISK: &str = include_str!(
"../../cog-ha-matter/blueprints/bfld/identity-risk-anomaly.yaml"
);
fn assert_required_blueprint_fields(yaml: &str, name_substring: &str, label: &str) {
assert!(
yaml.contains("blueprint:"),
"{label}: missing top-level `blueprint:` key",
);
assert!(yaml.contains("name:"), "{label}: missing `name`");
assert!(
yaml.contains(name_substring),
"{label}: name does not mention {name_substring}",
);
assert!(
yaml.contains("domain: automation"),
"{label}: missing `domain: automation`",
);
assert!(yaml.contains("input:"), "{label}: missing `input:` block");
assert!(yaml.contains("trigger:"), "{label}: missing `trigger:`");
assert!(yaml.contains("action:"), "{label}: missing `action:`");
assert!(yaml.contains("mode:"), "{label}: missing `mode:`");
}
#[test]
fn presence_lighting_blueprint_is_structurally_valid() {
assert_required_blueprint_fields(PRESENCE_LIGHTING, "Presence", "presence-lighting");
assert!(PRESENCE_LIGHTING.contains("bfld_presence"));
assert!(PRESENCE_LIGHTING.contains("light.turn_on"));
assert!(PRESENCE_LIGHTING.contains("light.turn_off"));
assert!(
PRESENCE_LIGHTING.contains("hold_seconds"),
"must expose configurable hold time per ADR-122 §2.6",
);
}
#[test]
fn motion_hvac_blueprint_is_structurally_valid() {
assert_required_blueprint_fields(MOTION_HVAC, "HVAC", "motion-hvac");
assert!(MOTION_HVAC.contains("bfld_motion"));
assert!(MOTION_HVAC.contains("climate.set_temperature"));
assert!(
MOTION_HVAC.contains("motion_threshold"),
"must expose configurable threshold per ADR-122 §2.6",
);
assert!(
MOTION_HVAC.contains("delta_temperature_c"),
"must expose configurable ΔT per ADR-122 §2.6",
);
}
#[test]
fn identity_risk_blueprint_is_structurally_valid() {
assert_required_blueprint_fields(IDENTITY_RISK, "Identity-Risk", "identity-risk-anomaly");
assert!(IDENTITY_RISK.contains("bfld_identity_risk"));
assert!(
IDENTITY_RISK.contains("z_score_threshold"),
"must expose rolling z-score threshold per ADR-122 §2.6",
);
assert!(
IDENTITY_RISK.contains("statistics_entity"),
"must require an HA Statistics helper entity for the 7-day baseline",
);
}
#[test]
fn blueprints_carry_source_url_pointing_at_canonical_path() {
for (label, yaml, fname) in [
("presence-lighting", PRESENCE_LIGHTING, "presence-lighting.yaml"),
("motion-hvac", MOTION_HVAC, "motion-hvac.yaml"),
("identity-risk-anomaly", IDENTITY_RISK, "identity-risk-anomaly.yaml"),
] {
let needle = format!(
"source_url: https://github.com/ruvnet/RuView/blob/main/v2/crates/cog-ha-matter/blueprints/bfld/{fname}"
);
assert!(
yaml.contains(&needle),
"{label}: source_url drift — expected {needle}",
);
}
}
#[test]
fn presence_blueprint_uses_mqtt_integration_filter() {
// The presence blueprint targets BFLD entities published via MQTT auto-
// discovery; the entity selector must filter to integration: mqtt so
// operators don't accidentally bind a non-BFLD presence sensor.
assert!(PRESENCE_LIGHTING.contains("integration: mqtt"));
}
#[test]
fn motion_blueprint_uses_mqtt_integration_filter() {
assert!(MOTION_HVAC.contains("integration: mqtt"));
}
#[test]
fn identity_risk_blueprint_carries_privacy_class_caveat_in_description() {
// The description should hint at the class 2-only availability so operators
// running Restricted (class 3) deployments don't waste time installing the
// blueprint.
assert!(
IDENTITY_RISK.contains("privacy_class") || IDENTITY_RISK.contains("Anonymous"),
"identity-risk blueprint description should reference privacy_class gating",
);
}