mirror of
https://github.com/ruvnet/RuView
synced 2026-08-03 19:21:42 +00:00
192b4c6b4a
- safety stop is now control flow: run_calibration terminates the sweep on any stop (partial calibration recorded) - cross-session terminate-and-lock: persisted latched ParticipantSafetyState; locked participants refuse sessions across governor instances until unlock_with_acknowledgment writes an audit record; documented class->lock map - SafetyEnvelope safe-by-construction: private fields, validated try_new/with_* constructors, serde routed through the validator, compiled-in absolute bounds (floor >=30Hz keeps the whole band above the 15-25Hz photosensitive zone; caps <=0.6; <=30min) — an 18-22Hz config fails to deserialize - dose governance: <=4 sittings/24h + >=60min cooldown from the persisted ledger - per-tick latched SafetyMonitor wired into run_session (simulator emits ticks; mid-session latch truncates delivered duration); clean-session witness byte-identical (pinned proof unchanged) - cohort warm-start k-floor (>=3 profiles) ADR-250 + crate README updated. 125+13 tests green. Co-Authored-By: claude-flow <ruv@ruv.net>
150 lines
4.7 KiB
Rust
150 lines
4.7 KiB
Rust
//! Unit tests for `stimulus` — split out to keep stimulus.rs under 500 lines.
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn prior_is_inside_conservative_envelope() {
|
||
let env = SafetyEnvelope::conservative();
|
||
assert!(env.contains(&StimulusParameters::prior()));
|
||
}
|
||
|
||
#[test]
|
||
fn frequency_outside_band_is_rejected() {
|
||
let env = SafetyEnvelope::conservative();
|
||
let mut s = StimulusParameters::prior();
|
||
s.frequency_hz = 50.0;
|
||
let err = env.validate(&s).unwrap_err();
|
||
assert!(matches!(err[0], EnvelopeViolation::Frequency { .. }));
|
||
}
|
||
|
||
#[test]
|
||
fn brightness_above_cap_is_rejected() {
|
||
let env = SafetyEnvelope::conservative();
|
||
let mut s = StimulusParameters::prior();
|
||
s.brightness_level = 0.9;
|
||
assert!(!env.contains(&s));
|
||
}
|
||
|
||
#[test]
|
||
fn clamp_output_is_always_contained() {
|
||
let env = SafetyEnvelope::conservative();
|
||
let hostile = StimulusParameters {
|
||
frequency_hz: 1000.0,
|
||
modality: Modality::Visual,
|
||
brightness_level: 5.0,
|
||
volume_level: -2.0,
|
||
duty_cycle: DutyCycle::Pulsed,
|
||
phase_offset_ms: 999.0,
|
||
duration_minutes: 1e6,
|
||
};
|
||
assert!(env.contains(&env.clamp(hostile)));
|
||
}
|
||
|
||
#[test]
|
||
fn clamp_neutralizes_nan() {
|
||
let env = SafetyEnvelope::conservative();
|
||
let mut s = StimulusParameters::prior();
|
||
s.frequency_hz = f64::NAN;
|
||
s.brightness_level = f64::INFINITY;
|
||
let c = env.clamp(s);
|
||
assert!(env.contains(&c));
|
||
assert_eq!(c.frequency_hz, env.min_hz());
|
||
assert_eq!(c.brightness_level, 0.0);
|
||
}
|
||
|
||
// ---- Finding 3: absolute bounds are a property of construction ----
|
||
|
||
#[test]
|
||
fn conservative_is_within_absolute_bounds() {
|
||
let e = SafetyEnvelope::conservative();
|
||
assert!(e.min_hz() >= absolute::MIN_HZ);
|
||
assert!(e.max_hz() <= absolute::MAX_HZ);
|
||
assert!(e.brightness_cap() <= absolute::BRIGHTNESS_CAP);
|
||
assert!(e.volume_cap() <= absolute::VOLUME_CAP);
|
||
assert!(e.max_duration_minutes() <= absolute::MAX_DURATION_MINUTES);
|
||
// The whole conservative band clears the 15–25 Hz photosensitive zone.
|
||
assert!(e.min_hz() > 25.0);
|
||
}
|
||
|
||
#[test]
|
||
fn try_new_rejects_photosensitive_band() {
|
||
// An 18–22 Hz envelope is squarely inside the provocative band.
|
||
let err = SafetyEnvelope::try_new(18.0, 22.0, 0.3, 0.3, 10.0, 0.0).unwrap_err();
|
||
assert!(matches!(err, EnvelopeError::FrequencyFloor { .. }));
|
||
}
|
||
|
||
#[test]
|
||
fn try_new_rejects_uncapped_brightness_and_volume() {
|
||
assert!(matches!(
|
||
SafetyEnvelope::try_new(36.0, 44.0, 1.0, 0.3, 10.0, 0.0).unwrap_err(),
|
||
EnvelopeError::BrightnessCap { .. }
|
||
));
|
||
assert!(matches!(
|
||
SafetyEnvelope::try_new(36.0, 44.0, 0.3, 1.0, 10.0, 0.0).unwrap_err(),
|
||
EnvelopeError::VolumeCap { .. }
|
||
));
|
||
}
|
||
|
||
#[test]
|
||
fn try_new_rejects_million_minute_session() {
|
||
assert!(matches!(
|
||
SafetyEnvelope::try_new(36.0, 44.0, 0.3, 0.3, 1e6, 0.0).unwrap_err(),
|
||
EnvelopeError::Duration { .. }
|
||
));
|
||
}
|
||
|
||
#[test]
|
||
fn try_new_rejects_ceiling_and_inverted_band() {
|
||
assert!(matches!(
|
||
SafetyEnvelope::try_new(36.0, 80.0, 0.3, 0.3, 10.0, 0.0).unwrap_err(),
|
||
EnvelopeError::FrequencyCeiling { .. }
|
||
));
|
||
assert!(matches!(
|
||
SafetyEnvelope::try_new(44.0, 36.0, 0.3, 0.3, 10.0, 0.0).unwrap_err(),
|
||
EnvelopeError::BandInverted { .. }
|
||
));
|
||
}
|
||
|
||
#[test]
|
||
fn deserialization_cannot_build_an_18_22hz_envelope() {
|
||
// Hostile config places the band in the photosensitive zone — serde must
|
||
// route through try_new and FAIL, not silently accept it.
|
||
let hostile = r#"{
|
||
"min_hz": 18.0, "max_hz": 22.0,
|
||
"brightness_cap": 1.0, "volume_cap": 1.0,
|
||
"max_duration_minutes": 1000000.0, "max_phase_offset_ms": 0.0
|
||
}"#;
|
||
let parsed: Result<SafetyEnvelope, _> = serde_json::from_str(hostile);
|
||
assert!(
|
||
parsed.is_err(),
|
||
"deserialization must reject a photosensitive-band envelope"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn valid_envelope_roundtrips_through_serde() {
|
||
let e = SafetyEnvelope::conservative();
|
||
let json = serde_json::to_string(&e).unwrap();
|
||
let back: SafetyEnvelope = serde_json::from_str(&json).unwrap();
|
||
assert_eq!(e, back);
|
||
}
|
||
|
||
#[test]
|
||
fn with_builders_revalidate_against_absolute_bounds() {
|
||
let e = SafetyEnvelope::conservative();
|
||
// Legal override succeeds.
|
||
assert!(e.with_caps(0.5, 0.5).is_ok());
|
||
// Illegal override (above the brightness ceiling) is refused.
|
||
assert!(e.with_caps(0.9, 0.3).is_err());
|
||
// Re-banding into the photosensitive zone is refused.
|
||
assert!(e.with_band(18.0, 22.0).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn calibration_grid_is_36_to_44() {
|
||
let env = SafetyEnvelope::conservative();
|
||
let grid = env.calibration_frequencies();
|
||
assert_eq!(grid.first(), Some(&36.0));
|
||
assert_eq!(grid.last(), Some(&44.0));
|
||
assert_eq!(grid.len(), 9);
|
||
}
|