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.
This commit is contained in:
Ashin Walpola
2026-08-25 15:21:34 +02:00
parent 04b0076b8b
commit cc35994e68
@@ -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)
}