From cc35994e68877a38621538acc6989bfdffda1310 Mon Sep 17 00:00:00 2001 From: Ashin Walpola Date: Tue, 25 Aug 2026 15:21:34 +0200 Subject: [PATCH] Fix four EventDetectorTest cases that never passed These have been red since the initial commit. All four failed for the same reason, and in every case the test was wrong rather than the detector. EventDetector requires two conditions to hold on the SAME frame, and one of them is a rolling-window statistic that takes time to respond. The tests ignored that lag, so they described situations the detector cannot see - and could not have seen at any point in its history. Braking (3 tests). Each filled the accel window with a CONSTANT value, then stepped the GPS speed down. The speed drop is therefore true on exactly one frame, and on that frame the accel std dev is still ~0.75 against a 1.2 threshold, because the window is full of the constant. By the time the window recovers (frame 4), prevSpeedMps has caught up and the drop is 0. The two conditions never coincide and no BRAKING is possible. Constant accelerometer output right up to the instant of a brake is not physical either: the IMU is sampled continuously while GPS speed lags, so the shaking precedes the reported drop. The tests now establish that variability during the cruise phase. Stopping. The second episode ran for stoppingFrames + 5. But stopping requires the accel std dev to be BELOW a threshold, and the window still held the five moving samples from the acceleration burst between episodes. Those take 8 frames to drain far enough for the std dev to fall under 0.15, leaving 17 of the 21 frames the event needs. The first episode is unaffected because the window starts empty. The episode is now long enough to cover the settling time. Verified by simulating the detector's exact arithmetic against both the old and new inputs before touching the file: the old profiles produce no events, the new ones produce BRAKING/HIGH, BRAKING/HIGH, BRAKING/MEDIUM and two STOPPING. No production code changed - the detector behaves consistently and defensibly. Assertions are unchanged; only the stimulus is now something a bicycle could actually produce. Suite is 41 tests, 0 failures. --- .../hawhamburg/micr0bu/EventDetectorTest.kt | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/app/src/test/java/com/hawhamburg/micr0bu/EventDetectorTest.kt b/app/src/test/java/com/hawhamburg/micr0bu/EventDetectorTest.kt index d6456f4..87b5f40 100644 --- a/app/src/test/java/com/hawhamburg/micr0bu/EventDetectorTest.kt +++ b/app/src/test/java/com/hawhamburg/micr0bu/EventDetectorTest.kt @@ -116,12 +116,17 @@ class EventDetectorTest { // ─── Hard brake ─────────────────────────────────────────────────────────── @Test fun `hard brake triggers BRAKING event`() = runCollecting { events -> - // Phase 1: fill window at 10 m/s with constant accel (no std dev → no braking) - repeat(config.windowSize) { i -> - detector.processSample(1.0, 0.05, 10.0, 0.0, 53.5, 10.0, i * 20L) - } - // Phase 2: GPS drops to 4 m/s (drop = 6 m/s > 0.5 threshold). - // Alternate hi/lo accel to exceed the std-dev threshold. + // Phase 1: cruising at 10 m/s with the accelerometer variability a moving bike actually + // has. This matters: [EventDetector] requires the speed drop and the accel std dev to be + // true on the SAME frame, and the std dev is a rolling window. Filling phase 1 with a + // constant accel drives that window to zero, so on the one frame where the speed drop + // exists the std dev is still ~0.75 and braking can never start - by the time the window + // has recovered, prevSpeedMps has caught up and the drop is gone. + // + // Constant accel right up to the instant of a brake is also not physical. The IMU is + // sampled continuously while GPS speed lags, so the shaking precedes the reported drop. + alternatingAccelFrames(n = config.windowSize, speedMps = 10.0, timeOffset = 0) + // Phase 2: GPS reports 4 m/s (drop = 6 m/s > 0.5 threshold). alternatingAccelFrames( n = config.brakingSustainedFrames + 5, speedMps = 4.0, @@ -132,9 +137,8 @@ class EventDetectorTest { } @Test fun `hard brake with large speed drop has HIGH confidence`() = runCollecting { events -> - repeat(config.windowSize) { i -> - detector.processSample(1.0, 0.05, 10.0, 0.0, 53.5, 10.0, i * 20L) - } + // Variability established before the drop - see the note in the test above. + alternatingAccelFrames(n = config.windowSize, speedMps = 10.0, timeOffset = 0) // Drop of 8 m/s > brakingHighConfidenceRate (1.5) alternatingAccelFrames( n = config.brakingSustainedFrames + 5, @@ -153,9 +157,8 @@ class EventDetectorTest { } @Test fun `moderate speed drop has MEDIUM confidence`() = runCollecting { events -> - repeat(config.windowSize) { i -> - detector.processSample(1.0, 0.05, 3.0, 0.0, 53.5, 10.0, i * 20L) - } + // Variability established before the drop - see `hard brake triggers BRAKING event`. + alternatingAccelFrames(n = config.windowSize, speedMps = 3.0, timeOffset = 0) // Drop of 0.8 m/s — above speed-drop threshold (0.5) but below high-conf rate (1.5) alternatingAccelFrames( n = config.brakingSustainedFrames + 5, @@ -294,8 +297,15 @@ class EventDetectorTest { repeat(5) { detector.processSample(0.5, 0.1, 5.0, 2.0, 53.5, 10.0, t++ * 20L) } - // Second stop episode - repeat(stopFrames) { + // Second stop episode. Deliberately longer than the first: stopping also requires the + // accel std dev to be BELOW a threshold, and the rolling window still holds the five + // moving samples above. It takes 8 further frames for those to drain out far enough for + // the std dev to fall under 0.15, and only then does the counter start. The first episode + // needs no such allowance because the window begins empty. + // + // stoppingFrames + 5 was not enough - the second episode reached 17 of the 21 frames it + // needs and silently emitted nothing, which is what made this test fail. + repeat(config.stoppingFrames + 10) { detector.processSample(0.02, 0.01, 0.1, 0.0, 53.5, 10.0, t++ * 20L) }