Skip to content

Commit 4365e39

Browse files
fix(audience-session): clamp livePause in ComputeEngagedSecondsLocked
Cursor Bugbot finding on c43044a. Resume() clamps pauseDuration ≥ 0 for wall-clock rewinds, but the mirror calculation in ComputeEngagedSecondsLocked did not. If the clock rewinds while the session is still paused and End fires (e.g. Shutdown while backgrounded with NTP pulling time back), livePause goes negative. Being subtracted, it inflated engagedSeconds past the wall-clock window. The final engagedSeconds ≥ 0 clamp caught negatives but not over-credit. Mirror the Resume-side clamp: livePause < 0 → 0. Added End_ClockRewindsWhilePaused_DoesNotInflateDuration as a regression guard. 10s before pause, clock rewinds 5s while paused, End fires — duration must stay within the 5s wall-clock window. Without the clamp the test reports 15s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4a03d88 commit 4365e39

2 files changed

Lines changed: 35 additions & 0 deletions

File tree

src/Packages/Audience/Runtime/Core/Session.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,12 @@ private long ComputeEngagedSecondsLocked()
342342
{
343343
var now = _getUtcNow();
344344
var livePause = _pausedAt.HasValue ? now - _pausedAt.Value : TimeSpan.Zero;
345+
// Clamp: mirrors the Resume() guard. If the clock rewinds while the
346+
// session is still paused and End / EmitEndAndSeal fires (e.g.
347+
// Shutdown while backgrounded), livePause would be negative and,
348+
// being subtracted, would inflate engagedSeconds past the wall-clock
349+
// window. The final ≥0 clamp catches negatives but not inflation.
350+
if (livePause < TimeSpan.Zero) livePause = TimeSpan.Zero;
345351
var engagedSeconds = ((now - _sessionStart) - _accumulatedPause - livePause).TotalSeconds;
346352
if (engagedSeconds < 0) return 0;
347353
return (long)Math.Round(engagedSeconds, MidpointRounding.AwayFromZero);

src/Packages/Audience/Tests/Runtime/Core/SessionTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,35 @@ public void End_ClockRewindsSinceStart_ClampsDurationToZero()
363363
"negative engaged time from a wall-clock rewind must clamp to zero");
364364
}
365365

366+
[Test]
367+
public void End_ClockRewindsWhilePaused_DoesNotInflateDuration()
368+
{
369+
// Wall-clock rewinds while the session is still paused (e.g. the
370+
// app is backgrounded and NTP corrects backwards before Shutdown
371+
// fires End). Without the livePause ≥ 0 clamp in
372+
// ComputeEngagedSecondsLocked, livePause goes negative and,
373+
// being subtracted, inflates duration past the wall-clock window
374+
// — the final engagedSeconds ≥ 0 clamp only catches negatives,
375+
// not over-credit. Sabotage: removing the livePause clamp lets
376+
// this test report 15s instead of the ≤ 5s wall-clock window.
377+
var now = new DateTime(2026, 4, 20, 12, 0, 0, DateTimeKind.Utc);
378+
DateTime Clock() => now;
379+
380+
using var session = new Session(MockTrack, performanceSnapshot: null, getUtcNow: Clock);
381+
session.Start();
382+
383+
now = now.AddSeconds(10);
384+
session.Pause();
385+
386+
now = now.AddSeconds(-5); // clock rewinds 5s while paused
387+
session.End();
388+
389+
var sessionEnd = _events.Last(e => e.name == "session_end");
390+
var duration = (long)sessionEnd.props["durationSec"];
391+
Assert.LessOrEqual(duration, 5L,
392+
"clock rewind while paused must not over-credit engagement past the wall-clock window");
393+
}
394+
366395
[Test]
367396
public void End_AfterShortPause_ReportsDurationMinusPause()
368397
{

0 commit comments

Comments
 (0)