mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
eventdata projection to support also older event schema for new rule ANALYZE-ESP-002
This commit is contained in:
@@ -1 +1 @@
|
||||
971
|
||||
972
|
||||
|
||||
@@ -65,6 +65,75 @@ public class AnalyzeRuleSessionFailureTests
|
||||
Assert.False(effective);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BackfillDerivedEventFields_SynthesizesFailedSubcategoriesFromTransitions()
|
||||
{
|
||||
// Mirrors an esp_provisioning_status event emitted by an older agent build: no flat
|
||||
// `failedSubcategories` field, but the `transitions` list is present. The RuleEngine's
|
||||
// read-time projection must derive the flat field so ANALYZE-ESP-002 matches.
|
||||
var legacyEvent = new EnrollmentEvent
|
||||
{
|
||||
EventType = "esp_provisioning_status",
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
["category"] = "DeviceSetup",
|
||||
["changeType"] = "subcategory_state_change",
|
||||
["transitions"] = new List<object>
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["subcategory"] = "Certificates",
|
||||
["previousState"] = "inProgress",
|
||||
["newState"] = "failed"
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["subcategory"] = "Apps",
|
||||
["previousState"] = "inProgress",
|
||||
["newState"] = "succeeded"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var method = typeof(RuleEngine).GetMethod(
|
||||
"BackfillDerivedEventFields",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
||||
Assert.NotNull(method);
|
||||
method!.Invoke(null, new object[] { new List<EnrollmentEvent> { legacyEvent } });
|
||||
|
||||
Assert.True(legacyEvent.Data.ContainsKey("failedSubcategories"));
|
||||
Assert.Equal("Certificates", legacyEvent.Data["failedSubcategories"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BackfillDerivedEventFields_DoesNotOverwriteAgentProvidedValue()
|
||||
{
|
||||
var modernEvent = new EnrollmentEvent
|
||||
{
|
||||
EventType = "esp_provisioning_status",
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
["failedSubcategories"] = "AgentProvidedValue",
|
||||
["transitions"] = new List<object>
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["subcategory"] = "Certificates",
|
||||
["newState"] = "failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var method = typeof(RuleEngine).GetMethod(
|
||||
"BackfillDerivedEventFields",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
||||
method!.Invoke(null, new object[] { new List<EnrollmentEvent> { modernEvent } });
|
||||
|
||||
Assert.Equal("AgentProvidedValue", modernEvent.Data["failedSubcategories"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuleState_ExplicitOverride_WinsOverDefault()
|
||||
{
|
||||
|
||||
@@ -46,6 +46,10 @@ namespace AutopilotMonitor.Functions.Services
|
||||
return outcome;
|
||||
}
|
||||
|
||||
// Backfill derived fields for backward compatibility with events produced by older agents.
|
||||
// This is a pure read-time projection — we never persist the synthesized values.
|
||||
BackfillDerivedEventFields(allEvents);
|
||||
|
||||
// On reanalyze: skip deduplication so all rules are re-evaluated from scratch
|
||||
// On normal run: skip rules already evaluated to avoid duplicate storage
|
||||
HashSet<string> existingRuleIds;
|
||||
@@ -104,6 +108,60 @@ namespace AutopilotMonitor.Functions.Services
|
||||
return outcome;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read-time backfill of derived Event.Data fields that older agent builds didn't emit.
|
||||
/// Keeps rules forward-compatible with historical sessions. Pure in-memory mutation — the
|
||||
/// original event records in Table Storage are not modified.
|
||||
///
|
||||
/// Current projections:
|
||||
/// - esp_provisioning_status: synthesize `failedSubcategories` (comma-joined registry names)
|
||||
/// from `transitions[]` entries where newState == "failed". Matches what ProvisioningStatusTracker
|
||||
/// now emits natively, so ANALYZE-ESP-002 fires against pre-upgrade data too.
|
||||
/// </summary>
|
||||
private static void BackfillDerivedEventFields(List<EnrollmentEvent> events)
|
||||
{
|
||||
foreach (var evt in events)
|
||||
{
|
||||
if (evt.Data == null) continue;
|
||||
if (!string.Equals(evt.EventType, "esp_provisioning_status", System.StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
// Don't overwrite what the agent already provided.
|
||||
if (evt.Data.ContainsKey("failedSubcategories"))
|
||||
continue;
|
||||
|
||||
if (!evt.Data.TryGetValue("transitions", out var transitionsObj) || transitionsObj == null)
|
||||
continue;
|
||||
|
||||
// TableStorageService.DeserializeEventData normalizes JArray → List<object>, so every
|
||||
// transition shows up as a Dictionary<string, object> after the JToken conversion.
|
||||
if (transitionsObj is not System.Collections.IEnumerable enumerable)
|
||||
continue;
|
||||
|
||||
var failed = new List<string>();
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
if (item is not System.Collections.Generic.IDictionary<string, object> dict)
|
||||
continue;
|
||||
|
||||
if (!dict.TryGetValue("newState", out var newStateObj) || newStateObj == null)
|
||||
continue;
|
||||
if (!string.Equals(newStateObj.ToString(), "failed", System.StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
if (dict.TryGetValue("subcategory", out var nameObj) && nameObj != null)
|
||||
{
|
||||
var name = nameObj.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
failed.Add(name!);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed.Count > 0)
|
||||
evt.Data["failedSubcategories"] = string.Join(",", failed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Promotes a fired rule to a terminal Failed status on the session, but only when the session
|
||||
/// is still in a non-terminal state (InProgress/Pending/Stalled). Terminal states
|
||||
|
||||
Reference in New Issue
Block a user