feat(adr-118/p6.9): BfldError Display format pinning (290/290 GREEN)

Iter 42. Pins the thiserror-derived Display output for every BfldError
variant. Operators grep log lines for these strings; format drift
between minor versions breaks monitoring queries and alerting rules.
This iter locks the contract.

Added (in tests/bfld_error_display.rs, 11 named tests):
- One test per BfldError variant asserting the documented substrings
  appear in to_string():
    invalid_magic_displays_both_expected_and_actual_in_hex
    unsupported_version_displays_the_offending_version
    crc_mismatch_displays_both_values_in_hex
    privacy_violation_displays_the_sink_reason
    invalid_privacy_class_displays_the_offending_byte
    truncated_frame_displays_got_and_need_byte_counts
    malformed_section_displays_offset_and_reason
    invalid_demote_displays_both_from_and_to_class_bytes
- Meta tests:
    bfld_error_implements_std_error_trait
      (compile-time witness via fn assert_error_trait<E: std::error::Error>())
    bfld_error_is_debug_so_panic_unwrap_messages_carry_diagnostics
    every_variant_has_a_non_empty_display_string
      (catch-all: 8 variants × non-empty Display assertion;
       guards against a future PR that adds a new variant without
       the #[error(...)] attribute)

ADR-124 status (iter step 0 sibling check):
- docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md unchanged
  at 431 lines. SENSE-BRIDGE scope remains orthogonal.

ACs progressed:
- ADR-118 §2.1 operator observability — error-message contract now
  pinned. A monitoring rule that greps for "payload CRC mismatch"
  or "privacy violation" continues to fire correctly across BFLD
  versions.

Test config:
- cargo test --no-default-features → 90 passed (bfld_error_display cfg-out)
- cargo test                       → 290 passed (279 + 11)

Out of scope (next iter target):
- PR-readiness pivot remains the genuine next move: CHANGELOG batch,
  witness bundle regeneration, AC closeout table. All in-crate ACs
  empirically covered; remaining work is external-resource-gated
  (KIT BFId, Pi5/Nexmon hardware) or PR-prep.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-05-24 19:22:32 -04:00
parent a7ccac7869
commit d1bc3cfcf1
1 changed files with 132 additions and 0 deletions

View File

@ -0,0 +1,132 @@
//! `BfldError` Display format pinning. Operators grep log lines for these
//! strings; format drift between minor versions breaks monitoring queries.
//! Each variant gets a test that asserts the documented substrings appear.
#![cfg(feature = "std")]
use wifi_densepose_bfld::BfldError;
#[test]
fn invalid_magic_displays_both_expected_and_actual_in_hex() {
let err = BfldError::InvalidMagic(0xDEAD_BEEF);
let s = err.to_string();
assert!(s.contains("invalid BFLD magic"), "got: {s}");
assert!(s.contains("0xBF1D0001"), "expected magic missing: {s}");
assert!(s.contains("0xDEADBEEF"), "actual magic missing: {s}");
}
#[test]
fn unsupported_version_displays_the_offending_version() {
let err = BfldError::UnsupportedVersion(99);
let s = err.to_string();
assert!(s.contains("unsupported BFLD version"), "got: {s}");
assert!(s.contains("99"), "version number missing: {s}");
}
#[test]
fn crc_mismatch_displays_both_values_in_hex() {
let err = BfldError::Crc {
expected: 0xCAFEBABE,
actual: 0xDEADBEEF,
};
let s = err.to_string();
assert!(s.contains("payload CRC mismatch"), "got: {s}");
assert!(s.contains("0xCAFEBABE"), "expected missing: {s}");
assert!(s.contains("0xDEADBEEF"), "actual missing: {s}");
}
#[test]
fn privacy_violation_displays_the_sink_reason() {
let err = BfldError::PrivacyViolation {
reason: "NetworkKind",
};
let s = err.to_string();
assert!(s.contains("privacy violation"), "got: {s}");
assert!(s.contains("NetworkKind"), "reason missing: {s}");
}
#[test]
fn invalid_privacy_class_displays_the_offending_byte() {
let err = BfldError::InvalidPrivacyClass(7);
let s = err.to_string();
assert!(s.contains("invalid PrivacyClass byte"), "got: {s}");
assert!(s.contains("7"), "byte value missing: {s}");
}
#[test]
fn truncated_frame_displays_got_and_need_byte_counts() {
let err = BfldError::TruncatedFrame { got: 50, need: 86 };
let s = err.to_string();
assert!(s.contains("truncated frame"), "got: {s}");
assert!(s.contains("50"), "got count missing: {s}");
assert!(s.contains("86"), "need count missing: {s}");
}
#[test]
fn malformed_section_displays_offset_and_reason() {
let err = BfldError::MalformedSection {
offset: 1234,
reason: "section body runs past buffer end",
};
let s = err.to_string();
assert!(s.contains("malformed payload section"), "got: {s}");
assert!(s.contains("1234"), "offset missing: {s}");
assert!(s.contains("buffer end"), "reason missing: {s}");
}
#[test]
fn invalid_demote_displays_both_from_and_to_class_bytes() {
let err = BfldError::InvalidDemote { from: 2, to: 1 };
let s = err.to_string();
assert!(s.contains("invalid demote"), "got: {s}");
assert!(s.contains("from class 2"), "from missing: {s}");
assert!(s.contains("to class 1"), "to missing: {s}");
}
// --- meta: error implements std::error::Error (for ? + dyn use) -------
#[test]
fn bfld_error_implements_std_error_trait() {
fn assert_error_trait<E: std::error::Error>() {}
assert_error_trait::<BfldError>();
}
#[test]
fn bfld_error_is_debug_so_panic_unwrap_messages_carry_diagnostics() {
let err = BfldError::Crc {
expected: 0xAA,
actual: 0xBB,
};
let debug = format!("{err:?}");
assert!(debug.contains("Crc"), "Debug must show variant name: {debug}");
}
// --- catch-all: every variant has a non-empty Display -----------------
#[test]
fn every_variant_has_a_non_empty_display_string() {
let cases: Vec<BfldError> = vec![
BfldError::InvalidMagic(0),
BfldError::UnsupportedVersion(0),
BfldError::Crc {
expected: 0,
actual: 0,
},
BfldError::PrivacyViolation { reason: "X" },
BfldError::InvalidPrivacyClass(0),
BfldError::TruncatedFrame { got: 0, need: 0 },
BfldError::MalformedSection {
offset: 0,
reason: "X",
},
BfldError::InvalidDemote { from: 0, to: 0 },
];
for err in cases {
let s = err.to_string();
assert!(!s.is_empty(), "Display for {err:?} returned empty string");
assert!(
s.len() >= 5,
"Display for {err:?} suspiciously short: {s:?}",
);
}
}