mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-26 05:29:43 +00:00
57b0557558
Admin "Mark as Succeeded/Failed", rule-engine terminal failures, and the maintenance auto-fail path all called UpdateSessionStatusAsync without a completedAt, falling through to DateTime.UtcNow. DurationSeconds was then (click-time − earliestEvent), which on session fe5d7153-… inflated a real ~7min enrollment to 29min on the dashboard list and Avg. Duration card. The session detail page silently masked the bug because it recomputes duration client-side from events. Fix: extract ResolveCompletionTimestamp helper that prefers the caller- supplied value, then falls back to the session's LastEventAt, and only to UtcNow when the session never emitted events. Use the helper at both write sites in TableStorageService.Sessions, and stop passing the bogus DateTime.UtcNow from RuleEngine.TryMarkSessionFailedFromRuleAsync so the fallback can take effect there too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
57 lines
2.6 KiB
C#
57 lines
2.6 KiB
C#
using AutopilotMonitor.Functions.Services;
|
||
|
||
namespace AutopilotMonitor.Functions.Tests;
|
||
|
||
/// <summary>
|
||
/// Regression cover for the duration-inflation bug observed on session
|
||
/// fe5d7153-1e0c-4ff9-812b-2531ff77bed3 (DurationSeconds=1729 vs. real ~415s).
|
||
/// Root cause: admin-marked terminals + rule-engine + maintenance auto-fail used
|
||
/// <c>DateTime.UtcNow</c> as <c>CompletedAt</c> when the agent never sent an
|
||
/// enrollment_complete event. The dashboard list/avg consumed the inflated stored
|
||
/// duration; the session detail page silently masked the bug by recomputing from
|
||
/// events client-side.
|
||
/// </summary>
|
||
public class SessionCompletionTimestampTests
|
||
{
|
||
private static readonly DateTime LastEvent = new(2026, 4, 29, 10, 59, 30, DateTimeKind.Utc);
|
||
private static readonly DateTime Now = new(2026, 4, 29, 11, 21, 27, DateTimeKind.Utc);
|
||
private static readonly DateTime AgentEvt = new(2026, 4, 29, 10, 58, 16, DateTimeKind.Utc);
|
||
|
||
[Fact]
|
||
public void ResolveCompletionTimestamp_prefers_caller_supplied_completion_event()
|
||
{
|
||
// Agent ingest path: CompletionEvent.Timestamp is authoritative — never overridden.
|
||
var resolved = TableStorageService.ResolveCompletionTimestamp(AgentEvt, LastEvent, Now);
|
||
Assert.Equal(AgentEvt, resolved);
|
||
}
|
||
|
||
[Fact]
|
||
public void ResolveCompletionTimestamp_falls_back_to_LastEventAt_when_completedAt_null()
|
||
{
|
||
// Admin "Mark as Succeeded" / rule-engine / maintenance auto-fail paths pass
|
||
// completedAt: null. Without this fallback, DurationSeconds would inflate to
|
||
// (button-click − earliestEvent) instead of (lastEvent − earliestEvent).
|
||
var resolved = TableStorageService.ResolveCompletionTimestamp(null, LastEvent, Now);
|
||
Assert.Equal(LastEvent, resolved);
|
||
}
|
||
|
||
[Fact]
|
||
public void ResolveCompletionTimestamp_falls_back_to_now_when_session_has_no_events()
|
||
{
|
||
// Pathological case: terminal flip on a session that never emitted any event.
|
||
// No better anchor exists, so wall-clock is the sane last-resort.
|
||
var resolved = TableStorageService.ResolveCompletionTimestamp(null, null, Now);
|
||
Assert.Equal(Now, resolved);
|
||
}
|
||
|
||
[Fact]
|
||
public void ResolveCompletionTimestamp_does_not_override_explicit_completedAt_with_lastEvent()
|
||
{
|
||
// Even if LastEventAt > completedAt (out-of-order ingest), explicit completedAt
|
||
// wins. Reordering would silently shift DurationSeconds without a code change.
|
||
var late = LastEvent.AddMinutes(5);
|
||
var resolved = TableStorageService.ResolveCompletionTimestamp(AgentEvt, late, Now);
|
||
Assert.Equal(AgentEvt, resolved);
|
||
}
|
||
}
|