mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
feat(agent): compiled-in fallback rule catalogs for the no-config default
The built-in default config shipped empty ImeLogPatterns/GatherRules lists, so a session that got neither a live fetch nor a cached config ran with the entire IME tracking pipeline dead — no app installs, no script results, no health-script compliance, no IME ESP-phase signals — although the agent binary carries all 37 action handlers. The scalar knobs never drift (shared AgentConfigResponse property initializers are also the backend's fallbacks), but the catalogs cannot come from initializers. BuiltInConfigCatalog embeds the SAME CI-combined rules/dist artifacts the backend embeds and serves (ime-log-patterns.json, gather-rules.json) and feeds the enabled entries into CreateDefaultConfig. Single source by construction: each agent release snapshots the catalog state of its own build, so the offline fallback can never drift further than the running agent version itself. A live fetch (startup, post-registration recovery, rotate_config) always wins. Fail-soft: a load error degrades to the previous empty-list behaviour. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+62
@@ -0,0 +1,62 @@
|
||||
using System.Linq;
|
||||
using AutopilotMonitor.Agent.V2.Core.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Tests.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Pins the compiled-in fallback rule catalogs (<see cref="BuiltInConfigCatalog"/>):
|
||||
/// the agent embeds the SAME CI-combined rules/dist artifacts the backend serves, so a
|
||||
/// no-config/no-cache session still runs full IME tracking. These tests guard the
|
||||
/// embedding + deserialization end-to-end — a broken LogicalName, a moved resource or a
|
||||
/// shape change in the combined JSON must fail HERE, not silently strip the fallback
|
||||
/// back down to zero patterns in the field.
|
||||
/// </summary>
|
||||
public class BuiltInConfigCatalogTests
|
||||
{
|
||||
[Fact]
|
||||
public void ImeLogPatterns_LoadFromEmbeddedCatalog_EnabledOnly()
|
||||
{
|
||||
var patterns = BuiltInConfigCatalog.GetEnabledImeLogPatterns(logger: null);
|
||||
|
||||
Assert.NotEmpty(patterns);
|
||||
Assert.All(patterns, p =>
|
||||
{
|
||||
Assert.True(p.Enabled);
|
||||
Assert.False(string.IsNullOrWhiteSpace(p.PatternId));
|
||||
Assert.False(string.IsNullOrWhiteSpace(p.Pattern));
|
||||
Assert.False(string.IsNullOrWhiteSpace(p.Action));
|
||||
Assert.False(string.IsNullOrWhiteSpace(p.Category));
|
||||
});
|
||||
Assert.Equal(patterns.Count, patterns.Select(p => p.PatternId).Distinct().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImeLogPatterns_CarryCoreTrackingActions()
|
||||
{
|
||||
// The app-tracking backbone: without these two actions the ImeLogTracker cannot
|
||||
// attribute installs to apps or follow ESP phases. Their presence proves the
|
||||
// deserialization actually populated the action field (a shape drift in the
|
||||
// combined JSON would otherwise still pass the structural checks above).
|
||||
var actions = BuiltInConfigCatalog.GetEnabledImeLogPatterns(logger: null)
|
||||
.Select(p => p.Action).ToHashSet();
|
||||
|
||||
Assert.Contains("setCurrentApp", actions);
|
||||
Assert.Contains("espPhaseDetected", actions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GatherRules_LoadFromEmbeddedCatalog_EnabledOnly()
|
||||
{
|
||||
// The built-in gather-rule catalog ships mostly disabled rules — only the
|
||||
// default-enabled ones may reach the fallback config.
|
||||
var rules = BuiltInConfigCatalog.GetEnabledGatherRules(logger: null);
|
||||
|
||||
Assert.All(rules, r =>
|
||||
{
|
||||
Assert.True(r.Enabled);
|
||||
Assert.False(string.IsNullOrWhiteSpace(r.RuleId));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -114,6 +114,22 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Configuration
|
||||
Assert.Equal(1, svc.LastFetchAttempts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchConfig_DefaultsFallback_CarriesBuiltInCatalogs()
|
||||
{
|
||||
// A no-config/no-cache session must still get the compiled-in rule catalogs —
|
||||
// an empty ImeLogPatterns list kills the entire IME tracking pipeline (the
|
||||
// pre-catalog behaviour this pins against regressing to).
|
||||
var svc = NewService(BackendBehaviour.AlwaysTransientFailure);
|
||||
|
||||
var result = await svc.FetchConfigAsync(retryOnTransientErrors: true);
|
||||
|
||||
Assert.Equal(0, result.ConfigVersion);
|
||||
Assert.NotEmpty(result.ImeLogPatterns);
|
||||
Assert.All(result.ImeLogPatterns, p => Assert.True(p.Enabled));
|
||||
Assert.All(result.GatherRules, r => Assert.True(r.Enabled));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchConfig_EmptyResponse_TreatedAsTransient()
|
||||
{
|
||||
|
||||
@@ -52,6 +52,11 @@
|
||||
carries the V2 assembly namespace so GatherRuleGuards locates it under
|
||||
"AutopilotMonitor.Agent.V2.Core.Resources.guardrails.json". -->
|
||||
<EmbeddedResource Include="..\..\..\rules\guardrails.json" LogicalName="AutopilotMonitor.Agent.V2.Core.Resources.guardrails.json" />
|
||||
<!-- Built-in fallback rule catalogs (BuiltInConfigCatalog): the SAME CI-combined
|
||||
rules/dist artifacts the backend embeds and serves. Snapshotted per agent build so
|
||||
the no-config/no-cache fallback still runs full IME tracking instead of nothing. -->
|
||||
<EmbeddedResource Include="..\..\..\rules\dist\ime-log-patterns.json" LogicalName="AutopilotMonitor.Agent.V2.Core.Resources.ime-log-patterns.json" />
|
||||
<EmbeddedResource Include="..\..\..\rules\dist\gather-rules.json" LogicalName="AutopilotMonitor.Agent.V2.Core.Resources.gather-rules.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Compiled-in fallback copies of the global rule catalogs, embedded from the same
|
||||
/// CI-generated <c>rules/dist</c> artifacts the backend seeds and serves — single
|
||||
/// source by construction: each agent release snapshots the catalog state of its own
|
||||
/// build, so the offline fallback can never drift further than the running agent
|
||||
/// version itself (no manually maintained default to forget).
|
||||
/// <para>
|
||||
/// Consumed only by <see cref="RemoteConfigService"/>'s built-in default config,
|
||||
/// i.e. when a session has NO live config and NO cache. Before this existed, that
|
||||
/// path shipped an empty <c>ImeLogPatterns</c> list, which left the entire IME
|
||||
/// tracking pipeline (app installs, platform/remediation scripts, health-script
|
||||
/// compliance, IME ESP-phase signals) dead for the whole session — the core of
|
||||
/// enrollment monitoring. A live fetch (startup, post-registration recovery or
|
||||
/// rotate_config) always wins over this catalog.
|
||||
/// </para>
|
||||
/// Fail-soft: any load/parse error returns an empty list (the pre-catalog behaviour).
|
||||
/// </summary>
|
||||
internal static class BuiltInConfigCatalog
|
||||
{
|
||||
/// <summary>Enabled built-in IME log patterns from the embedded catalog.</summary>
|
||||
public static List<ImeLogPattern> GetEnabledImeLogPatterns(AgentLogger logger)
|
||||
=> LoadEnabled<ImeLogPattern>("ime-log-patterns.json", p => p.Enabled, logger);
|
||||
|
||||
/// <summary>Enabled built-in gather rules from the embedded catalog.</summary>
|
||||
public static List<GatherRule> GetEnabledGatherRules(AgentLogger logger)
|
||||
=> LoadEnabled<GatherRule>("gather-rules.json", r => r.Enabled, logger);
|
||||
|
||||
private static List<T> LoadEnabled<T>(string name, Func<T, bool> isEnabled, AgentLogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = typeof(BuiltInConfigCatalog).Assembly;
|
||||
using (var stream = assembly.GetManifestResourceStream(
|
||||
"AutopilotMonitor.Agent.V2.Core.Resources." + name))
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
logger?.Warning($"Built-in config catalog resource '{name}' not found — fallback runs without it.");
|
||||
return new List<T>();
|
||||
}
|
||||
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var wrapper = JsonConvert.DeserializeObject<RulesFile<T>>(reader.ReadToEnd());
|
||||
var all = wrapper?.Rules ?? new List<T>();
|
||||
return all.Where(isEnabled).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.Warning($"Built-in config catalog '{name}' failed to load ({ex.Message}) — fallback runs without it.");
|
||||
return new List<T>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shape of the CI-combined catalog files: <c>{ "rules": [...] }</c>.</summary>
|
||||
private sealed class RulesFile<T>
|
||||
{
|
||||
[JsonProperty("rules")]
|
||||
public List<T> Rules { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,9 +231,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Configuration
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Fall back to defaults
|
||||
_logger.Info("No cached config available, using defaults (all optional collectors disabled)");
|
||||
// Fall back to built-in defaults (collector/analyzer defaults from the shared
|
||||
// model initializers + the compiled-in rule catalogs).
|
||||
var defaults = CreateDefaultConfig();
|
||||
_logger.Info($"No cached config available, using built-in defaults (ConfigVersion=0, " +
|
||||
$"{defaults.ImeLogPatterns?.Count ?? 0} built-in IME patterns, " +
|
||||
$"{defaults.GatherRules?.Count ?? 0} built-in gather rules)");
|
||||
SetConfig(defaults);
|
||||
LastFetchOutcome = RemoteConfigFetchOutcome.UsedDefaults;
|
||||
return defaults;
|
||||
@@ -393,6 +396,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Configuration
|
||||
|
||||
private AgentConfigResponse CreateDefaultConfig()
|
||||
{
|
||||
// Everything not set here falls to the shared AgentConfigResponse property
|
||||
// initializers — the same values the backend uses as its `??` fallbacks, so the
|
||||
// scalar knobs cannot drift. The rule catalogs are the one thing initializers
|
||||
// cannot supply: without them the whole IME tracking pipeline is dead, so they
|
||||
// come from the compiled-in snapshot of the served catalogs (BuiltInConfigCatalog).
|
||||
return new AgentConfigResponse
|
||||
{
|
||||
ConfigVersion = 0,
|
||||
@@ -404,8 +412,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Configuration
|
||||
EnableGatherRuleDebugLog = false,
|
||||
EnableEspContinueAnywayObservation = false,
|
||||
Collectors = CollectorConfiguration.CreateDefault(),
|
||||
GatherRules = new System.Collections.Generic.List<GatherRule>(),
|
||||
ImeLogPatterns = new System.Collections.Generic.List<ImeLogPattern>()
|
||||
GatherRules = BuiltInConfigCatalog.GetEnabledGatherRules(_logger),
|
||||
ImeLogPatterns = BuiltInConfigCatalog.GetEnabledImeLogPatterns(_logger)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user