fix(utc/locale): close Theme 2 — 8 UTC / time / locale findings
UTC invariant + culture-safety fixes across UI form binding, audit entity hydrate, and locale-dependent parses. Highlights: - CentralUI-026/027: AuditFilterBar / SiteCallsReport / NotificationReport / EventLogs now apply SpecifyKind(Local) + ToUniversalTime() at form submit so browser-local datetime-local inputs aren't silently treated as UTC. - Commons-019: AuditEvent.OccurredAtUtc / IngestedAtUtc init-setters re-tag any incoming DateTime as Kind=Utc, documenting the invariant. - CD-018: AuditLogEntityTypeConfiguration adds UTC ValueConverters on the *Utc DateTime columns so EF hydrate yields Kind=Utc (SQL Server's datetime2 has no Kind metadata, so reads were returning Unspecified). - CD-020: GetPartitionBoundariesOlderThanAsync now SpecifyKind(Utc) on the raw-ADO read, matching the existing defence in AuditLogPartitionMaintenance. - SEL-021: EventLogQueryService.DateTimeOffset.Parse now uses InvariantCulture + AssumeUniversal | AdjustToUniversal. - SR-023: Convert.ToDouble in ScriptActor + AlarmActor (4 sites) now passes InvariantCulture so non-US locales don't mis-parse string values. - HM-020: CentralHealthAggregator.MarkHeartbeat anchors LastHeartbeatAt to max(receivedAt, now) on offline→online so a stale receivedAt can't leave a recovered site one tick from re-going-offline. 3 new tests added (AuditLog UTC converter, AuditFilterBar/EventLogs/ NotificationReport-touching CentralUI tests already cover Apply paths, heartbeat offline→online). Build clean; ConfigurationDatabase 236, Commons 330, HealthMonitoring 71, SiteRuntime 301, SiteEventLogging 50, CentralUI 50 — all green. README regenerated: 104 open (was 112).
This commit is contained in:
@@ -306,6 +306,39 @@ public class CentralHealthAggregatorTests
|
||||
Assert.True(_aggregator.GetSiteState("site-1")!.IsOnline);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-020 regression: an offline-to-online transition must
|
||||
/// be backed by a fresh LastHeartbeatAt. Previously MarkHeartbeat used
|
||||
/// <c>max(receivedAt, existing.LastHeartbeatAt)</c>, so an out-of-order
|
||||
/// heartbeat carrying an older timestamp would bring the site online with
|
||||
/// a stale heartbeat and CheckForOfflineSites would flap it straight back
|
||||
/// to offline on the next tick.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MarkHeartbeat_OfflineToOnline_StampsFreshLastHeartbeatAt()
|
||||
{
|
||||
_aggregator.ProcessReport(MakeReport("site-1", 1));
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(61));
|
||||
_aggregator.CheckForOfflineSites();
|
||||
Assert.False(_aggregator.GetSiteState("site-1")!.IsOnline);
|
||||
|
||||
// An out-of-order heartbeat arrives with a timestamp older than the
|
||||
// existing LastHeartbeatAt (e.g. clock skew on the originating node).
|
||||
var nowAfter = _timeProvider.GetUtcNow();
|
||||
var stale = nowAfter - TimeSpan.FromSeconds(120);
|
||||
_aggregator.MarkHeartbeat("site-1", stale);
|
||||
|
||||
var state = _aggregator.GetSiteState("site-1")!;
|
||||
Assert.True(state.IsOnline);
|
||||
// The recorded LastHeartbeatAt must be ~"now", not the stale receivedAt.
|
||||
Assert.InRange((nowAfter - state.LastHeartbeatAt).TotalSeconds, 0, 5);
|
||||
|
||||
// And it must survive the very next offline check — proves no flap.
|
||||
_aggregator.CheckForOfflineSites();
|
||||
Assert.True(_aggregator.GetSiteState("site-1")!.IsOnline);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HealthMonitoring-005 regression: the synthetic "central" site has no
|
||||
/// heartbeat source — its LastHeartbeatAt is only bumped by the 30s
|
||||
|
||||
Reference in New Issue
Block a user