feat(adr-118/p1.7): reserved-flag-bits forward-compat (243/243 GREEN)
Iter 36. Locks down the ADR-119 §2.1 forward-compat promise that
reserved flag bits round-trip unchanged through the parser. A future
protocol revision may light up bits 2 or 4..=15; today's parser
preserves them so a node running iter N can forward unknown bits to
a peer running iter N+M without losing information.
Added (in src/frame.rs::flags):
- pub const KNOWN_FLAGS_MASK = HAS_CSI_DELTA | PRIVACY_MODE | SELF_ONLY
(the three currently-named flags, occupying bits 0, 1, 3)
- pub const RESERVED_FLAGS_MASK = !KNOWN_FLAGS_MASK
(bit 2 + bits 4..=15 — every position not currently assigned)
- Docstrings reference ADR-119 §2.1 verbatim so a future reviewer
understands why the constants exist.
tests/reserved_flags.rs (8 named tests, all green, no_std-compatible
so they run in BOTH feature configs):
known_flags_mask_covers_exactly_three_named_flags
(count_ones() == 3 catches accidental flag additions that should
also update KNOWN_FLAGS_MASK)
reserved_and_known_masks_are_complementary
(mask | reserved == u16::MAX; mask & reserved == 0)
known_flags_do_not_overlap_with_each_other
(HAS_CSI_DELTA, PRIVACY_MODE, SELF_ONLY all on distinct bits)
header_preserves_reserved_flag_bits_through_round_trip
*** Headline test: set RESERVED_FLAGS_MASK on a header, serialize,
parse, verify the bits survived. ***
header_preserves_mixed_known_and_reserved_bits
(HAS_CSI_DELTA | PRIVACY_MODE | (1<<7) | (1<<14) — mixed case)
reserved_bits_do_not_collide_with_self_only_bit_3
(bit 2 is reserved but bit 3 is named — pins the asymmetry)
all_zero_flags_round_trip_cleanly
all_one_flags_round_trip_cleanly (stress: every bit set)
The new tests are no_std-compatible (no Vec / no serde) so they run
in both `cargo test --no-default-features` and default feature
configs. The no_default test count therefore jumps from 72 to 80.
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-119 §2.1 "Reserved flag bits 2-15 lock in future-extension
order; any new bit assignment is a version bump." — the test now
enforces the OTHER half of this contract: a peer running the
future version can set a reserved bit and our parser will preserve
it through the round-trip rather than masking it off.
Test config:
- cargo test --no-default-features → 80 passed (72 + 8 no_std-compat)
- cargo test → 243 passed (235 + 8)
Out of scope (next iter target):
- PR-readiness pivot: witness bundle regeneration, CHANGELOG batch
across iters 1-36, AC closeout table for the PR description.
All in-crate ACs are now covered; remaining work is either
external-resource-gated (KIT BFId, Pi5/Nexmon) or PR-prep.
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
9ee7c5df04
commit
a3d26a4fad
|
|
@ -47,6 +47,20 @@ pub mod flags {
|
|||
pub const PRIVACY_MODE: u16 = 1 << 1;
|
||||
/// ESP32-S3 self-only adapter (ADR-123 §2.5): no `identity_risk_score`.
|
||||
pub const SELF_ONLY: u16 = 1 << 3;
|
||||
|
||||
/// Bitmask covering every named flag this version of the crate knows
|
||||
/// about. Useful for "did the wire form set any flags I don't recognize?"
|
||||
/// forward-compat checks.
|
||||
pub const KNOWN_FLAGS_MASK: u16 = HAS_CSI_DELTA | PRIVACY_MODE | SELF_ONLY;
|
||||
|
||||
/// Complement of [`KNOWN_FLAGS_MASK`] — every bit position not currently
|
||||
/// assigned a meaning. Bits set in this mask MUST round-trip unchanged
|
||||
/// per ADR-119 §2.1 ("Reserved flag bits 2-15 lock in future-extension
|
||||
/// order; any new bit assignment is a version bump"). A future protocol
|
||||
/// revision may light these up; today's parser preserves them so a node
|
||||
/// running iter N can forward unknown bits to a peer running iter N+M
|
||||
/// without losing information.
|
||||
pub const RESERVED_FLAGS_MASK: u16 = !KNOWN_FLAGS_MASK;
|
||||
}
|
||||
|
||||
/// On-the-wire BFLD frame header. 86 bytes, little-endian, packed.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
//! ADR-119 §2.1 reserved-flag-bits forward-compat. The 16-bit `flags` field
|
||||
//! currently uses bits 0 (HAS_CSI_DELTA), 1 (PRIVACY_MODE), and 3 (SELF_ONLY).
|
||||
//! Bits 2 and 4..=15 are reserved. The parser must preserve any reserved bit
|
||||
//! set by a future peer — otherwise round-tripping a frame through a node
|
||||
//! running an older crate version silently drops information that a newer
|
||||
//! peer might depend on.
|
||||
|
||||
use wifi_densepose_bfld::frame::flags;
|
||||
use wifi_densepose_bfld::{BfldFrameHeader, BFLD_HEADER_SIZE};
|
||||
|
||||
fn header_with_flags(flags_value: u16) -> BfldFrameHeader {
|
||||
let mut h = BfldFrameHeader::empty();
|
||||
h.flags = flags_value;
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_flags_mask_covers_exactly_three_named_flags() {
|
||||
assert_eq!(
|
||||
flags::KNOWN_FLAGS_MASK,
|
||||
flags::HAS_CSI_DELTA | flags::PRIVACY_MODE | flags::SELF_ONLY,
|
||||
);
|
||||
// The three currently-named flags occupy bits 0, 1, 3 — three bits set.
|
||||
assert_eq!(flags::KNOWN_FLAGS_MASK.count_ones(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_and_known_masks_are_complementary() {
|
||||
assert_eq!(flags::KNOWN_FLAGS_MASK | flags::RESERVED_FLAGS_MASK, u16::MAX);
|
||||
assert_eq!(flags::KNOWN_FLAGS_MASK & flags::RESERVED_FLAGS_MASK, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_flags_do_not_overlap_with_each_other() {
|
||||
// Each named flag uses exactly one bit and no two of them share a bit.
|
||||
let pairs = [
|
||||
(flags::HAS_CSI_DELTA, flags::PRIVACY_MODE),
|
||||
(flags::HAS_CSI_DELTA, flags::SELF_ONLY),
|
||||
(flags::PRIVACY_MODE, flags::SELF_ONLY),
|
||||
];
|
||||
for (a, b) in pairs {
|
||||
assert_eq!(a & b, 0, "named flag overlap: 0x{a:04X} & 0x{b:04X}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_preserves_reserved_flag_bits_through_round_trip() {
|
||||
// Light bit 2 + bits 4..=15 — the full reserved space.
|
||||
let reserved_set = flags::RESERVED_FLAGS_MASK;
|
||||
let h = header_with_flags(reserved_set);
|
||||
let bytes = h.to_le_bytes();
|
||||
let parsed = BfldFrameHeader::from_le_bytes(&bytes).expect("parse");
|
||||
assert_eq!(
|
||||
{ parsed.flags },
|
||||
reserved_set,
|
||||
"reserved bits must round-trip unchanged for forward-compat",
|
||||
);
|
||||
assert_eq!(bytes.len(), BFLD_HEADER_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_preserves_mixed_known_and_reserved_bits() {
|
||||
let mixed = flags::HAS_CSI_DELTA | flags::PRIVACY_MODE | (1 << 7) | (1 << 14);
|
||||
let h = header_with_flags(mixed);
|
||||
let parsed = BfldFrameHeader::from_le_bytes(&h.to_le_bytes()).expect("parse");
|
||||
assert_eq!({ parsed.flags }, mixed);
|
||||
// Known flags still readable via the named constants.
|
||||
assert_ne!(({ parsed.flags }) & flags::HAS_CSI_DELTA, 0);
|
||||
assert_ne!(({ parsed.flags }) & flags::PRIVACY_MODE, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_bits_do_not_collide_with_self_only_bit_3() {
|
||||
// SELF_ONLY uses bit 3 — bit 2 is the only unused bit in the 0..=3 range
|
||||
// and IS part of the reserved mask.
|
||||
assert_ne!(flags::SELF_ONLY & flags::RESERVED_FLAGS_MASK, flags::SELF_ONLY);
|
||||
assert_eq!(flags::RESERVED_FLAGS_MASK & (1 << 2), 1 << 2);
|
||||
assert_eq!(flags::RESERVED_FLAGS_MASK & (1 << 3), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_zero_flags_round_trip_cleanly() {
|
||||
let h = header_with_flags(0);
|
||||
let parsed = BfldFrameHeader::from_le_bytes(&h.to_le_bytes()).expect("parse");
|
||||
assert_eq!({ parsed.flags }, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_one_flags_round_trip_cleanly() {
|
||||
// Stress: every bit set. The parser has no business interpreting this
|
||||
// configuration but must preserve it.
|
||||
let h = header_with_flags(u16::MAX);
|
||||
let parsed = BfldFrameHeader::from_le_bytes(&h.to_le_bytes()).expect("parse");
|
||||
assert_eq!({ parsed.flags }, u16::MAX);
|
||||
}
|
||||
Loading…
Reference in New Issue