mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
tests: cross-language parity manifest + agent safety nets (audit P3/P4)
P3 — shared manifest infrastructure: SharedManifestParityTests reflects the wire-relevant catalogs out of AutopilotMonitor.Shared into a committed JSON manifest (regenerate: AM_WRITE_SHARED_MANIFESTS=1 dotnet test); a web codegen lifts it into literal types; sharedManifestChecks.ts enforces AdminConfiguration field parity at COMPILE time and 12 vitest tests pin the value catalogs (sources/operators, lanes/verdicts, severities incl. the missing Trace=-1, phases both directions, terminal statuses, event-type typo guard). KnownSources/KnownOperators made internal for the export. P4 — agent safety nets: DecisionState builder/serializer completeness (4-site sync tripwire), signal-kind fixture coverage with a shrink-only debt set (found and fixed realmjoin-timeout-v1.json, which never deserialized), RemoteConfigMerger completeness tripwire, DefaultComponentFactory ordering invariants as real construction tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+125
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AutopilotMonitor.Agent.V2.Core.Configuration;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Tests.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Completeness tripwire for <see cref="RemoteConfigMerger"/>: the merger hand-maps
|
||||
/// <see cref="AgentConfigResponse"/> fields onto <c>AgentConfiguration</c>, and
|
||||
/// historically new tenant knobs were forgotten there and silently ignored. Every public
|
||||
/// property of <see cref="AgentConfigResponse"/> must be classified in exactly one of the
|
||||
/// two lists below. A new property fails this test until a human either wires it through
|
||||
/// <see cref="RemoteConfigMerger.Merge"/> or documents where else it is consumed.
|
||||
/// </summary>
|
||||
public sealed class RemoteConfigMergerCompletenessTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Fields <see cref="RemoteConfigMerger.Merge"/> maps onto <c>AgentConfiguration</c>.
|
||||
/// Keep in sync with the Merge body — the test only proves classification coverage,
|
||||
/// the value-level mapping itself is covered by <see cref="RemoteConfigMergerTests"/>.
|
||||
/// </summary>
|
||||
private static readonly string[] MergedByMerger =
|
||||
{
|
||||
nameof(AgentConfigResponse.SelfDestructOnComplete),
|
||||
nameof(AgentConfigResponse.KeepLogFile),
|
||||
nameof(AgentConfigResponse.RebootOnComplete),
|
||||
nameof(AgentConfigResponse.EnableGeoLocation),
|
||||
nameof(AgentConfigResponse.LogLevel),
|
||||
nameof(AgentConfigResponse.RebootDelaySeconds),
|
||||
nameof(AgentConfigResponse.ShowEnrollmentSummary),
|
||||
nameof(AgentConfigResponse.EnrollmentSummaryTimeoutSeconds),
|
||||
nameof(AgentConfigResponse.EnrollmentSummaryBrandingImageUrl),
|
||||
nameof(AgentConfigResponse.EnrollmentSummaryLaunchRetrySeconds),
|
||||
nameof(AgentConfigResponse.NtpServer),
|
||||
nameof(AgentConfigResponse.EnableTimezoneAutoSet),
|
||||
nameof(AgentConfigResponse.DiagnosticsUploadEnabled),
|
||||
nameof(AgentConfigResponse.DiagnosticsUploadMode),
|
||||
nameof(AgentConfigResponse.DiagnosticsLogPaths),
|
||||
nameof(AgentConfigResponse.SendTraceEvents),
|
||||
nameof(AgentConfigResponse.EnableEspContinueAnywayObservation),
|
||||
nameof(AgentConfigResponse.UnrestrictedMode),
|
||||
nameof(AgentConfigResponse.EnableImeMatchLog),
|
||||
nameof(AgentConfigResponse.EnableGatherRuleDebugLog),
|
||||
nameof(AgentConfigResponse.UploadIntervalSeconds),
|
||||
nameof(AgentConfigResponse.MaxBatchSize),
|
||||
nameof(AgentConfigResponse.MaxAuthFailures),
|
||||
nameof(AgentConfigResponse.AuthFailureTimeoutMinutes),
|
||||
// Merge reads two Collectors knobs (AgentMaxLifetimeMinutes, HelloWaitTimeoutSeconds);
|
||||
// the remaining CollectorConfiguration flags flow untouched to DefaultComponentFactory.
|
||||
nameof(AgentConfigResponse.Collectors),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Fields deliberately NOT mapped by the merger because another component consumes them
|
||||
/// directly from the remote config. One-line justification per entry — verify the
|
||||
/// consumer before classifying a new field here.
|
||||
/// </summary>
|
||||
private static readonly string[] IntentionallyNotMerged =
|
||||
{
|
||||
// Schema/version metadata — logged and cached by RemoteConfigService, not a runtime knob.
|
||||
nameof(AgentConfigResponse.ConfigVersion),
|
||||
// Consumed by DefaultComponentFactory → GatherRuleExecutorHost (rules run against the raw remote list).
|
||||
nameof(AgentConfigResponse.GatherRules),
|
||||
// Consumed by DefaultComponentFactory → ImeLogHost (pattern list handed to ImeLogTracker).
|
||||
nameof(AgentConfigResponse.ImeLogPatterns),
|
||||
// Passed by AgentRuntimeHost into IComponentFactory.CreateCollectorHosts → ImeLogHost sealing re-emit.
|
||||
nameof(AgentConfigResponse.WhiteGloveSealingPatternIds),
|
||||
// Consumed by DefaultComponentFactory (RealmJoin/KeepAwake/ConsoleBypass host gates) + AgentAnalyzerManager.
|
||||
nameof(AgentConfigResponse.Analyzers),
|
||||
// Self-update ZIP integrity second trust channel — BinaryIntegrityVerifier / Program update path.
|
||||
nameof(AgentConfigResponse.LatestAgentSha256),
|
||||
// Running-EXE integrity verification — BinaryIntegrityVerifier post-config check.
|
||||
nameof(AgentConfigResponse.LatestAgentExeSha256),
|
||||
// Self-updater force-update downgrade gate — BinaryIntegrityVerifier / Program update path.
|
||||
nameof(AgentConfigResponse.AllowAgentDowngrade),
|
||||
// Control-channel block signal — RemoteConfigService/ServerControlPlane; stripped from the disk cache.
|
||||
nameof(AgentConfigResponse.DeviceBlocked),
|
||||
// Control-channel kill signal — ServerControlPlane / Program.Guards; live fetch only.
|
||||
nameof(AgentConfigResponse.DeviceKillSignal),
|
||||
// Temporary-block expiry accompanying DeviceBlocked — ServerControlPlane.
|
||||
nameof(AgentConfigResponse.UnblockAt),
|
||||
// Endpoint migration — EndpointMigration/RemoteConfigService re-run the fetch, live fetch only.
|
||||
nameof(AgentConfigResponse.MigrateToApiBaseUrl),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Every_AgentConfigResponse_property_is_classified_exactly_once()
|
||||
{
|
||||
var properties = typeof(AgentConfigResponse)
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Select(p => p.Name)
|
||||
.OrderBy(n => n, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
var merged = new HashSet<string>(MergedByMerger, StringComparer.Ordinal);
|
||||
var notMerged = new HashSet<string>(IntentionallyNotMerged, StringComparer.Ordinal);
|
||||
|
||||
var inBoth = merged.Intersect(notMerged, StringComparer.Ordinal).ToArray();
|
||||
Assert.True(
|
||||
inBoth.Length == 0,
|
||||
$"Fields listed in BOTH classification lists: [{string.Join(", ", inBoth)}].");
|
||||
|
||||
var unclassified = properties
|
||||
.Where(p => !merged.Contains(p) && !notMerged.Contains(p))
|
||||
.ToArray();
|
||||
Assert.True(
|
||||
unclassified.Length == 0,
|
||||
"New AgentConfigResponse field(s) without a classification: " +
|
||||
$"[{string.Join(", ", unclassified)}]. Either map them in RemoteConfigMerger.Merge " +
|
||||
"(+ MergedByMerger here) or verify the consuming component and add them to " +
|
||||
"IntentionallyNotMerged with a justification comment.");
|
||||
|
||||
// Stale entries (renamed/removed properties) must be cleaned up too.
|
||||
var known = new HashSet<string>(properties, StringComparer.Ordinal);
|
||||
var stale = merged.Concat(notMerged).Where(n => !known.Contains(n)).ToArray();
|
||||
Assert.True(
|
||||
stale.Length == 0,
|
||||
$"Classification lists reference non-existent AgentConfigResponse properties: [{string.Join(", ", stale)}].");
|
||||
}
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AutopilotMonitor.Agent.V2.Core.Configuration;
|
||||
using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using AutopilotMonitor.Agent.V2.Core.Orchestration;
|
||||
using AutopilotMonitor.Agent.V2.Core.Tests.Harness;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Tests.Orchestration
|
||||
{
|
||||
/// <summary>
|
||||
/// Enforces the comment-only ordering invariants of <see cref="DefaultComponentFactory"/> by
|
||||
/// building the REAL host list (construction only — no host is started) and asserting the
|
||||
/// relative positions of the order-sensitive host types. The orchestrator starts hosts in
|
||||
/// list order, so list order IS start order:
|
||||
/// <list type="bullet">
|
||||
/// <item>DeviceInfoHost before EspAndHelloHost — L9 (delta review 2026-07-02): DeviceInfoHost
|
||||
/// must subscribe to SignalPosted before EspAndHelloHost.Start can post the registry-backfill
|
||||
/// EspPhaseChanged(DeviceSetup), or the enrollment-start re-collect never runs.</item>
|
||||
/// <item>OsBuildChangeHost before WindowsUpdateWatcherHost — the WU channel census reads
|
||||
/// OsBuildChangeHost.BuildChanged at its own start.</item>
|
||||
/// <item>DeliveryOptimizationHost before OfficeInstallDetectorHost — the DO host must have
|
||||
/// subscribed to the Office process-start signal before the watcher starts, or an install
|
||||
/// already in flight misses its wake.</item>
|
||||
/// <item>DesktopArrivalHost before AadJoinHost — the AadJoinHost ctor takes the desktop
|
||||
/// host's RequestResetForRealUserSwitch callback, so the target must exist first.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class DefaultComponentFactoryOrderingTests : IDisposable
|
||||
{
|
||||
private readonly TempDirectory _tmp = new TempDirectory();
|
||||
|
||||
public void Dispose() => _tmp.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Build the production host list with default remote config (all order-sensitive hosts
|
||||
/// enabled by default) and system-boundary fakes only: temp state dir, fake ingress,
|
||||
/// virtual clock, no network metrics, no telemetry spool.
|
||||
/// </summary>
|
||||
private CollectorSurfaces CreateSurfaces()
|
||||
{
|
||||
var factory = new DefaultComponentFactory(
|
||||
agentConfig: new AgentConfiguration
|
||||
{
|
||||
ApiBaseUrl = "https://example",
|
||||
TenantId = "t",
|
||||
SessionId = "s",
|
||||
},
|
||||
remoteConfig: new AgentConfigResponse
|
||||
{
|
||||
Collectors = CollectorConfiguration.CreateDefault(),
|
||||
},
|
||||
networkMetrics: null,
|
||||
agentVersion: "test",
|
||||
stateDirectory: _tmp.Path,
|
||||
startupEventGate: null);
|
||||
|
||||
return factory.CreateCollectorHosts(
|
||||
sessionId: "s",
|
||||
tenantId: "t",
|
||||
logger: new AgentLogger(_tmp.Path, AgentLogLevel.Info),
|
||||
whiteGloveSealingPatternIds: Array.Empty<string>(),
|
||||
ingress: new FakeSignalIngressSink(),
|
||||
clock: new VirtualClock(new DateTime(2026, 4, 20, 10, 0, 0, DateTimeKind.Utc)),
|
||||
telemetrySpool: null,
|
||||
timelineEvents: null);
|
||||
}
|
||||
|
||||
private static int IndexOf<THost>(IReadOnlyList<ICollectorHost> hosts) where THost : ICollectorHost
|
||||
{
|
||||
var indices = hosts
|
||||
.Select((host, index) => (host, index))
|
||||
.Where(t => t.host is THost)
|
||||
.Select(t => t.index)
|
||||
.ToArray();
|
||||
Assert.True(
|
||||
indices.Length == 1,
|
||||
$"Expected exactly one {typeof(THost).Name} in the host list, found {indices.Length}. " +
|
||||
$"Hosts: [{string.Join(", ", hosts.Select(h => h.GetType().Name))}]");
|
||||
return indices[0];
|
||||
}
|
||||
|
||||
private static void AssertBefore<TFirst, TSecond>(IReadOnlyList<ICollectorHost> hosts, string reason)
|
||||
where TFirst : ICollectorHost
|
||||
where TSecond : ICollectorHost
|
||||
{
|
||||
var first = IndexOf<TFirst>(hosts);
|
||||
var second = IndexOf<TSecond>(hosts);
|
||||
Assert.True(
|
||||
first < second,
|
||||
$"{typeof(TFirst).Name} (index {first}) must come before {typeof(TSecond).Name} (index {second}): {reason}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Order_sensitive_hosts_keep_their_documented_relative_order()
|
||||
{
|
||||
var surfaces = CreateSurfaces();
|
||||
var hosts = surfaces.Hosts;
|
||||
try
|
||||
{
|
||||
AssertBefore<DeviceInfoHost, EspAndHelloHost>(
|
||||
hosts,
|
||||
"L9 — DeviceInfoHost must subscribe to SignalPosted before EspAndHelloHost's " +
|
||||
"registry backfill can post EspPhaseChanged(DeviceSetup).");
|
||||
|
||||
AssertBefore<OsBuildChangeHost, WindowsUpdateWatcherHost>(
|
||||
hosts,
|
||||
"the Windows Update channel census reads OsBuildChangeHost.BuildChanged at its own start.");
|
||||
|
||||
AssertBefore<DeliveryOptimizationHost, OfficeInstallDetectorHost>(
|
||||
hosts,
|
||||
"the DO host must subscribe to the Office process-start signal before the watcher starts.");
|
||||
|
||||
AssertBefore<DesktopArrivalHost, AadJoinHost>(
|
||||
hosts,
|
||||
"AadJoinHost wires its onRealUserJoined callback to the already-constructed DesktopArrivalHost.");
|
||||
|
||||
// Closure-latch sanity (factory lines ~139/297): the EspAndHelloHost probes close
|
||||
// over the ImeLogHost reference, which is assigned during the same factory call —
|
||||
// the host must therefore exist in the returned list.
|
||||
Assert.Equal(1, hosts.Count(h => h is ImeLogHost));
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var host in hosts)
|
||||
{
|
||||
try { host.Dispose(); } catch { /* construction-only test — best-effort cleanup */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using AutopilotMonitor.DecisionCore.Serialization;
|
||||
using AutopilotMonitor.DecisionCore.Signals;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.DecisionCore.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Enforces the fixture convention documented on <see cref="DecisionSignalKind"/>:
|
||||
/// "New kinds or version bumps require a replay fixture in
|
||||
/// <c>tests/fixtures/signal-kinds/{kind}-v{n}.json</c> — missing fixture = merge block."
|
||||
/// The comment alone enforced nothing; this test does. Kinds without a fixture today are
|
||||
/// carried in an explicit debt set that is only allowed to shrink.
|
||||
/// </summary>
|
||||
public sealed class SignalKindFixtureCoverageTests
|
||||
{
|
||||
// DEBT: backfill — new kinds must NOT be added here. Every entry predates this test;
|
||||
// when a fixture is added for one of these kinds, the test forces its removal.
|
||||
private static readonly HashSet<DecisionSignalKind> KnownMissingFixtures = new HashSet<DecisionSignalKind>
|
||||
{
|
||||
DecisionSignalKind.EspPhaseChanged,
|
||||
DecisionSignalKind.EspExiting,
|
||||
DecisionSignalKind.EspResumed,
|
||||
DecisionSignalKind.EspTerminalFailure,
|
||||
DecisionSignalKind.DesktopArrived,
|
||||
DecisionSignalKind.HelloResolved,
|
||||
DecisionSignalKind.ImeUserSessionCompleted,
|
||||
DecisionSignalKind.DeviceSetupProvisioningComplete,
|
||||
DecisionSignalKind.WhiteGloveShellCoreSuccess,
|
||||
DecisionSignalKind.WhiteGloveSealingPatternDetected,
|
||||
DecisionSignalKind.AadUserJoinedLate,
|
||||
DecisionSignalKind.DeviceInfoCollected,
|
||||
DecisionSignalKind.AutopilotProfileRead,
|
||||
DecisionSignalKind.EspConfigDetected,
|
||||
DecisionSignalKind.HelloPolicyDetected,
|
||||
DecisionSignalKind.ClassifierVerdictIssued,
|
||||
DecisionSignalKind.SessionStarted,
|
||||
DecisionSignalKind.SessionAborted,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Kinds that exist in the enum but are never posted as their own signal: they are
|
||||
/// dispatched over the <see cref="DecisionSignalKind.DeadlineFired"/> rail (the reducer's
|
||||
/// DeadlineFiredV1 switch routes on the <c>deadline</c> payload). Their fixture is named
|
||||
/// after the kind, but its declared <c>Kind</c> is <c>DeadlineFired</c> — e.g.
|
||||
/// <c>realmjoin-timeout-v1.json</c> carries <c>Payload.deadline = "realmjoin_timeout"</c>.
|
||||
/// </summary>
|
||||
private static readonly HashSet<DecisionSignalKind> DeadlineDispatchedKinds = new HashSet<DecisionSignalKind>
|
||||
{
|
||||
DecisionSignalKind.RealmJoinTimeout,
|
||||
};
|
||||
|
||||
/// <summary>Locate <c>tests/fixtures/signal-kinds</c> by walking up to the repo root (AutopilotMonitor.sln).</summary>
|
||||
private static string FixtureRoot()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
if (File.Exists(Path.Combine(dir, "AutopilotMonitor.sln")))
|
||||
{
|
||||
return Path.Combine(dir, "tests", "fixtures", "signal-kinds");
|
||||
}
|
||||
var parent = Directory.GetParent(dir)?.FullName;
|
||||
if (parent == null || parent == dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
throw new DirectoryNotFoundException(
|
||||
"Could not locate repo root (AutopilotMonitor.sln) walking up from " + AppContext.BaseDirectory);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> FixtureFiles()
|
||||
{
|
||||
var root = FixtureRoot();
|
||||
Assert.True(Directory.Exists(root), $"Fixture directory not found: {root}");
|
||||
var files = Directory.GetFiles(root, "*.json").OrderBy(f => f, StringComparer.Ordinal).ToArray();
|
||||
Assert.NotEmpty(files);
|
||||
return files;
|
||||
}
|
||||
|
||||
/// <summary>Signal JSONL lines of one fixture ('#' comments and blank lines stripped).</summary>
|
||||
private static IReadOnlyList<string> SignalLines(string path)
|
||||
{
|
||||
var lines = File.ReadAllLines(path)
|
||||
.Select(l => l.Trim())
|
||||
.Where(l => l.Length > 0 && l[0] != '#')
|
||||
.ToArray();
|
||||
Assert.True(lines.Length > 0, $"Fixture contains no signal lines: {path}");
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strictly parse the raw <c>Kind</c> of a fixture line. Deliberately NOT via
|
||||
/// <see cref="SignalSerializer"/> alone: its unknown-kind fallback maps typos to
|
||||
/// <see cref="DecisionSignalKind.SessionStarted"/>, which would silently mis-count coverage.
|
||||
/// </summary>
|
||||
private static DecisionSignalKind StrictKind(string line, string path)
|
||||
{
|
||||
var raw = JObject.Parse(line).Value<string>("Kind");
|
||||
Assert.False(string.IsNullOrEmpty(raw), $"Fixture line has no Kind: {path}");
|
||||
Assert.True(
|
||||
Enum.TryParse<DecisionSignalKind>(raw, ignoreCase: false, out var kind)
|
||||
&& Enum.IsDefined(typeof(DecisionSignalKind), kind),
|
||||
$"Fixture {Path.GetFileName(path)} declares unknown Kind '{raw}'.");
|
||||
return kind;
|
||||
}
|
||||
|
||||
/// <summary>Dash-insensitive lowercase — "realmjoin-detected-v1" and "RealmJoinDetected" both normalize consistently.</summary>
|
||||
private static string Normalize(string value) => value.Replace("-", string.Empty).ToLowerInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Map a fixture file name to the kind it is named after: the longest enum member whose
|
||||
/// normalized name prefixes the normalized file name, followed by <c>v{n}[-variant]</c>
|
||||
/// (dash placement per the existing corpus, e.g. <c>realmjoin-detected-v1.json</c> /
|
||||
/// <c>admin-preemption-detected-v1-failed.json</c>).
|
||||
/// </summary>
|
||||
private static DecisionSignalKind KindFromFileName(string fileNameWithoutExtension)
|
||||
{
|
||||
var normalized = Normalize(fileNameWithoutExtension);
|
||||
var match = ((DecisionSignalKind[])Enum.GetValues(typeof(DecisionSignalKind)))
|
||||
.Where(k =>
|
||||
{
|
||||
var prefix = Normalize(k.ToString());
|
||||
return normalized.StartsWith(prefix, StringComparison.Ordinal)
|
||||
&& Regex.IsMatch(normalized.Substring(prefix.Length), "^v[0-9]+");
|
||||
})
|
||||
.OrderByDescending(k => k.ToString().Length)
|
||||
.Cast<DecisionSignalKind?>()
|
||||
.FirstOrDefault();
|
||||
Assert.True(
|
||||
match.HasValue,
|
||||
$"Fixture file name '{fileNameWithoutExtension}.json' does not follow " +
|
||||
"'{kind}-v{n}[-variant].json' for any DecisionSignalKind.");
|
||||
return match!.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load every fixture through the REAL deserialization path and return the set of kinds
|
||||
/// the corpus covers. Also enforces per-file consistency: the declared Kind must match
|
||||
/// the file name (or be the DeadlineFired dispatch shape for deadline-dispatched kinds).
|
||||
/// </summary>
|
||||
private static ISet<DecisionSignalKind> LoadAndVerifyFixtures()
|
||||
{
|
||||
var covered = new HashSet<DecisionSignalKind>();
|
||||
foreach (var file in FixtureFiles())
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(file);
|
||||
var namedKind = KindFromFileName(fileName);
|
||||
|
||||
foreach (var line in SignalLines(file))
|
||||
{
|
||||
var declaredKind = StrictKind(line, file);
|
||||
|
||||
// Real deserialization path — the same call FixtureLoader/ReplayHarness use.
|
||||
var signal = SignalSerializer.Deserialize(line);
|
||||
Assert.Equal(declaredKind, signal.Kind);
|
||||
Assert.True(signal.KindSchemaVersion >= 1, $"KindSchemaVersion must be >= 1 in {fileName}.");
|
||||
|
||||
if (declaredKind == namedKind)
|
||||
{
|
||||
covered.Add(declaredKind);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only the documented dispatch exception is allowed: a fixture named after
|
||||
// a deadline-dispatched kind carries a DeadlineFired signal with the
|
||||
// deadline name in its payload.
|
||||
string? deadlineName = null;
|
||||
signal.Payload?.TryGetValue("deadline", out deadlineName);
|
||||
Assert.True(
|
||||
DeadlineDispatchedKinds.Contains(namedKind)
|
||||
&& declaredKind == DecisionSignalKind.DeadlineFired
|
||||
&& !string.IsNullOrEmpty(deadlineName),
|
||||
$"Fixture '{fileName}.json' is named after {namedKind} but declares Kind={declaredKind}. " +
|
||||
"Either the file is misnamed, or a new deadline-dispatched kind must be " +
|
||||
"registered in DeadlineDispatchedKinds (with a payload.deadline entry).");
|
||||
covered.Add(namedKind);
|
||||
covered.Add(declaredKind); // it IS also a genuine DeadlineFired fixture
|
||||
}
|
||||
}
|
||||
}
|
||||
return covered;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_fixture_deserializes_through_the_real_signal_path_and_matches_its_filename()
|
||||
{
|
||||
LoadAndVerifyFixtures();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_kind_is_either_fixtured_or_registered_as_known_debt()
|
||||
{
|
||||
var fixtured = LoadAndVerifyFixtures();
|
||||
var allKinds = (DecisionSignalKind[])Enum.GetValues(typeof(DecisionSignalKind));
|
||||
|
||||
var unfixturedAndUntracked = allKinds
|
||||
.Where(k => !fixtured.Contains(k) && !KnownMissingFixtures.Contains(k))
|
||||
.ToArray();
|
||||
Assert.True(
|
||||
unfixturedAndUntracked.Length == 0,
|
||||
"DecisionSignalKind values without a replay fixture in tests/fixtures/signal-kinds/: " +
|
||||
$"[{string.Join(", ", unfixturedAndUntracked)}]. The convention on DecisionSignalKind is " +
|
||||
"'new kind => fixture' — add a {kind}-v{n}.json fixture (do NOT extend KnownMissingFixtures).");
|
||||
|
||||
// Debt only shrinks: once a fixture exists, the kind must leave the debt set.
|
||||
var fixturedButStillListedAsDebt = KnownMissingFixtures.Where(fixtured.Contains).ToArray();
|
||||
Assert.True(
|
||||
fixturedButStillListedAsDebt.Length == 0,
|
||||
"Kinds now have fixtures but are still listed in KnownMissingFixtures — remove them: " +
|
||||
$"[{string.Join(", ", fixturedButStillListedAsDebt)}].");
|
||||
}
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AutopilotMonitor.DecisionCore.Serialization;
|
||||
using AutopilotMonitor.DecisionCore.Signals;
|
||||
using AutopilotMonitor.DecisionCore.State;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.DecisionCore.Tests.State
|
||||
{
|
||||
/// <summary>
|
||||
/// Drift tripwire for the 4-site sync required when adding a field to
|
||||
/// <see cref="DecisionState"/>: ctor parameter + <see cref="DecisionStateBuilder"/>
|
||||
/// copy-ctor line + builder property + <c>Build()</c> argument. A forgotten copy-ctor
|
||||
/// line silently resets the fact on every reducer step; a forgotten <c>Build()</c>
|
||||
/// argument resets it on the first copy-with-changes. These tests are fully
|
||||
/// reflection-driven so a new field fails them until all four sites are wired
|
||||
/// (and until a value factory for any brand-new property type is added below).
|
||||
/// </summary>
|
||||
public sealed class DecisionStateBuilderCompletenessTests
|
||||
{
|
||||
private static readonly DateTime BaseUtc = new DateTime(2026, 4, 20, 10, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
private static PropertyInfo[] StateProperties() =>
|
||||
typeof(DecisionState)
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.OrderBy(p => p.Name, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
private static PropertyInfo[] BuilderProperties() =>
|
||||
typeof(DecisionStateBuilder)
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.SetMethod != null && p.SetMethod.IsPublic)
|
||||
.OrderBy(p => p.Name, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Produce a distinct, non-default value for a builder property. <paramref name="seed"/>
|
||||
/// is folded into every value so no two properties of the same type ever share an
|
||||
/// instance — a swapped <c>Build()</c> argument (e.g. espFinalExitUtc passed into
|
||||
/// desktopArrivedUtc) is therefore detected, not masked. A property type without a
|
||||
/// factory entry fails loudly: extend the factory when a new field type appears.
|
||||
/// </summary>
|
||||
private static object CreateDistinctValue(Type type, int seed)
|
||||
{
|
||||
long ordinal = 100 + seed;
|
||||
if (type == typeof(string)) return "test-value-" + seed;
|
||||
if (type == typeof(int)) return 7 + seed;
|
||||
if (type == typeof(long)) return 7L + seed;
|
||||
if (type == typeof(DateTime?)) return BaseUtc.AddMinutes(seed);
|
||||
if (type == typeof(SessionStage)) return SessionStage.AwaitingHello;
|
||||
if (type == typeof(SessionOutcome?)) return SessionOutcome.EnrollmentComplete;
|
||||
if (type == typeof(SignalFact<DateTime>)) return new SignalFact<DateTime>(BaseUtc.AddMinutes(seed), ordinal);
|
||||
if (type == typeof(SignalFact<string>)) return new SignalFact<string>("test-value-" + seed, ordinal);
|
||||
if (type == typeof(SignalFact<bool>)) return new SignalFact<bool>(true, ordinal);
|
||||
if (type == typeof(SignalFact<EnrollmentPhase>)) return new SignalFact<EnrollmentPhase>(EnrollmentPhase.AccountSetup, ordinal);
|
||||
if (type == typeof(List<ActiveDeadline>))
|
||||
{
|
||||
return new List<ActiveDeadline>
|
||||
{
|
||||
new ActiveDeadline(
|
||||
name: "deadline-" + seed,
|
||||
dueAtUtc: BaseUtc.AddMinutes(seed),
|
||||
firesSignalKind: DecisionSignalKind.DeadlineFired,
|
||||
firesPayload: new Dictionary<string, string> { ["k"] = "v" + seed }),
|
||||
};
|
||||
}
|
||||
// Immutable fact aggregates — built through their public APIs so the instances are
|
||||
// reference-distinct from their Empty defaults AND fully serializable (a dropped
|
||||
// copy-ctor line or Build() argument falls back to Empty → reference inequality).
|
||||
if (type == typeof(AppInstallFacts)) return AppInstallFacts.Empty.WithCompleted("Installed").WithFailed("app-" + seed);
|
||||
if (type == typeof(EnrollmentScenarioProfile))
|
||||
{
|
||||
return EnrollmentScenarioProfile.Empty.With(
|
||||
mode: EnrollmentMode.WhiteGlove,
|
||||
confidence: ProfileConfidence.High,
|
||||
evidenceOrdinal: ordinal,
|
||||
reason: "test-value-" + seed);
|
||||
}
|
||||
if (type == typeof(EnrollmentScenarioObservations)) return EnrollmentScenarioObservations.Empty.WithSkipUserEsp(true, ordinal);
|
||||
if (type == typeof(ClassifierOutcomes))
|
||||
{
|
||||
return ClassifierOutcomes.Empty.WithWhiteGloveSealing(new Hypothesis(
|
||||
level: HypothesisLevel.Strong,
|
||||
reason: "test-value-" + seed,
|
||||
score: 50,
|
||||
lastUpdatedUtc: BaseUtc.AddMinutes(seed),
|
||||
lastClassifierVerdictId: "verdict-" + seed));
|
||||
}
|
||||
if (type == typeof(RealmJoinFacts)) return RealmJoinFacts.Empty.WithDetected(BaseUtc.AddMinutes(seed), ordinal);
|
||||
|
||||
throw new Xunit.Sdk.XunitException(
|
||||
$"No value factory for builder property type {type}. A new DecisionState field " +
|
||||
"introduced a new type — add a distinct-value factory entry so the completeness " +
|
||||
"tests can populate it.");
|
||||
}
|
||||
|
||||
/// <summary>Populate every settable builder property with a distinct non-default value.</summary>
|
||||
private static Dictionary<string, object> PopulateBuilder(DecisionStateBuilder builder)
|
||||
{
|
||||
var assigned = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
int seed = 0;
|
||||
foreach (var prop in BuilderProperties())
|
||||
{
|
||||
var value = CreateDistinctValue(prop.PropertyType, seed++);
|
||||
prop.SetValue(builder, value);
|
||||
assigned[prop.Name] = value;
|
||||
}
|
||||
return assigned;
|
||||
}
|
||||
|
||||
private static void AssertMatches(string propertyName, object expected, object? actual)
|
||||
{
|
||||
// Deadlines: Build() copies the List into an array and the copy-ctor re-wraps it —
|
||||
// the container is a new instance by design, but the elements must be the SAME refs.
|
||||
if (expected is List<ActiveDeadline> expectedDeadlines)
|
||||
{
|
||||
var actualDeadlines = Assert.IsAssignableFrom<IEnumerable<ActiveDeadline>>(actual).ToList();
|
||||
Assert.Equal(expectedDeadlines.Count, actualDeadlines.Count);
|
||||
for (int i = 0; i < expectedDeadlines.Count; i++)
|
||||
{
|
||||
Assert.Same(expectedDeadlines[i], actualDeadlines[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Value types (enums, int, long, DateTime?) and strings compare by value; every
|
||||
// reference-typed fact must be the identical instance (a re-created equal-looking
|
||||
// value would still hide a dropped pass-through).
|
||||
if (expected is string || expected.GetType().IsValueType)
|
||||
{
|
||||
Assert.True(
|
||||
expected.Equals(actual),
|
||||
$"{propertyName}: expected {expected}, got {actual ?? "(null)"}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Same(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================= (a) name-set parity
|
||||
|
||||
[Fact]
|
||||
public void Builder_property_set_matches_DecisionState_property_set()
|
||||
{
|
||||
var stateNames = StateProperties().Select(p => p.Name).ToArray();
|
||||
var builderNames = BuilderProperties().Select(p => p.Name).ToArray();
|
||||
|
||||
var missingOnBuilder = stateNames.Except(builderNames, StringComparer.Ordinal).ToArray();
|
||||
var missingOnState = builderNames.Except(stateNames, StringComparer.Ordinal).ToArray();
|
||||
|
||||
Assert.True(
|
||||
missingOnBuilder.Length == 0 && missingOnState.Length == 0,
|
||||
"DecisionState and DecisionStateBuilder property sets drifted apart. " +
|
||||
$"State-only: [{string.Join(", ", missingOnBuilder)}]; " +
|
||||
$"Builder-only: [{string.Join(", ", missingOnState)}]. " +
|
||||
"Adding a DecisionState field requires: ctor parameter, builder copy-ctor line, " +
|
||||
"builder property, and Build() argument.");
|
||||
}
|
||||
|
||||
// ============================================================= (b) Build() completeness
|
||||
|
||||
[Fact]
|
||||
public void Build_carries_every_builder_property_onto_the_state()
|
||||
{
|
||||
var builder = DecisionState.CreateInitial("seed-session", "seed-tenant").ToBuilder();
|
||||
var assigned = PopulateBuilder(builder);
|
||||
|
||||
var state = builder.Build();
|
||||
|
||||
foreach (var stateProp in StateProperties())
|
||||
{
|
||||
Assert.True(
|
||||
assigned.TryGetValue(stateProp.Name, out var expected),
|
||||
$"DecisionState.{stateProp.Name} has no builder counterpart — see the name-set parity test.");
|
||||
AssertMatches($"DecisionState.{stateProp.Name}", expected!, stateProp.GetValue(state));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================= (c) copy-ctor completeness
|
||||
|
||||
[Fact]
|
||||
public void ToBuilder_copies_every_property_back_from_the_state()
|
||||
{
|
||||
var builder = DecisionState.CreateInitial("seed-session", "seed-tenant").ToBuilder();
|
||||
var assigned = PopulateBuilder(builder);
|
||||
var state = builder.Build();
|
||||
|
||||
var roundtrippedBuilder = state.ToBuilder();
|
||||
|
||||
foreach (var builderProp in BuilderProperties())
|
||||
{
|
||||
var expected = assigned[builderProp.Name];
|
||||
AssertMatches($"DecisionStateBuilder.{builderProp.Name}", expected, builderProp.GetValue(roundtrippedBuilder));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================= (d) serializer roundtrip
|
||||
|
||||
[Fact]
|
||||
public void StateSerializer_roundtrip_of_fully_populated_state_is_lossless()
|
||||
{
|
||||
// Catches Newtonsoft ctor-binding mismatches: deserialization binds JSON properties
|
||||
// onto the single public ctor by (case-insensitive) parameter name, so a renamed
|
||||
// ctor parameter silently drops the field back to its default. Serializing the
|
||||
// deserialized state must therefore reproduce the exact same JSON.
|
||||
var builder = DecisionState.CreateInitial("seed-session", "seed-tenant").ToBuilder();
|
||||
PopulateBuilder(builder);
|
||||
var state = builder.Build();
|
||||
|
||||
var json = StateSerializer.Serialize(state);
|
||||
var roundtrippedJson = StateSerializer.Serialize(StateSerializer.Deserialize(json));
|
||||
|
||||
Assert.Equal(json, roundtrippedJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using AutopilotMonitor.Functions.Functions.Rules;
|
||||
using AutopilotMonitor.Shared;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using AutopilotMonitor.Shared.Models.Notifications;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Functions.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Cross-language parity anchor: reflects the wire-relevant catalogs out of
|
||||
/// AutopilotMonitor.Shared (models, enums, string vocabularies) into a canonical JSON
|
||||
/// manifest committed at <c>src/Web/autopilot-monitor-web/utils/shared-manifests.json</c>.
|
||||
/// The web's vitest suite and compile-time checks compare their hand-written TS mirrors
|
||||
/// against that manifest — so a C# change that would silently drift a TS mirror first
|
||||
/// fails HERE, and the TS side fails until its mirror follows.
|
||||
///
|
||||
/// Regenerate after intentional changes:
|
||||
/// AM_WRITE_SHARED_MANIFESTS=1 dotnet test --filter SharedManifestParityTests
|
||||
/// then run the web codegen: node scripts/generate-shared-manifest-types.js
|
||||
/// </summary>
|
||||
public sealed class SharedManifestParityTests
|
||||
{
|
||||
private const string ManifestRepoPath = "src/Web/autopilot-monitor-web/utils/shared-manifests.json";
|
||||
private const string WriteEnvVar = "AM_WRITE_SHARED_MANIFESTS";
|
||||
|
||||
[Fact]
|
||||
public void Committed_manifest_matches_shared_assembly()
|
||||
{
|
||||
var expected = BuildManifestJson();
|
||||
var path = Path.Combine(FindRepoRoot(), ManifestRepoPath.Replace('/', Path.DirectorySeparatorChar));
|
||||
|
||||
if (Environment.GetEnvironmentVariable(WriteEnvVar) == "1")
|
||||
{
|
||||
File.WriteAllText(path, expected);
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.True(File.Exists(path),
|
||||
$"Manifest missing at {ManifestRepoPath}. Regenerate: {WriteEnvVar}=1 dotnet test --filter SharedManifestParityTests");
|
||||
|
||||
var committed = File.ReadAllText(path).Replace("\r\n", "\n");
|
||||
Assert.True(string.Equals(expected, committed, StringComparison.Ordinal),
|
||||
$"shared-manifests.json is out of date with AutopilotMonitor.Shared. " +
|
||||
$"Regenerate: {WriteEnvVar}=1 dotnet test --filter SharedManifestParityTests " +
|
||||
$"(then: node scripts/generate-shared-manifest-types.js in the web folder).");
|
||||
}
|
||||
|
||||
// ── manifest construction ───────────────────────────────────────────────
|
||||
|
||||
private static string BuildManifestJson()
|
||||
{
|
||||
var manifest = new Dictionary<string, object?>
|
||||
{
|
||||
["$comment"] = "GENERATED from AutopilotMonitor.Shared by SharedManifestParityTests — do not edit by hand. " +
|
||||
"Regenerate: AM_WRITE_SHARED_MANIFESTS=1 dotnet test --filter SharedManifestParityTests, " +
|
||||
"then node scripts/generate-shared-manifest-types.js.",
|
||||
["adminConfiguration"] = new Dictionary<string, object?>
|
||||
{
|
||||
["fields"] = WireFieldNames(typeof(AdminConfiguration)),
|
||||
},
|
||||
["tenantConfiguration"] = new Dictionary<string, object?>
|
||||
{
|
||||
["fields"] = WireFieldNames(typeof(TenantConfiguration)),
|
||||
},
|
||||
["sessionSummary"] = new Dictionary<string, object?>
|
||||
{
|
||||
["fields"] = WireFieldsWithOptionality(typeof(SessionSummary)),
|
||||
},
|
||||
// Enum member order is load-bearing (append-only ordinals) — declaration order kept.
|
||||
["sessionStatuses"] = Enum.GetNames(typeof(SessionStatus)),
|
||||
["enrollmentPhases"] = EnumMap<EnrollmentPhase>(),
|
||||
["eventSeverities"] = EnumMap<EventSeverity>(),
|
||||
["webhookProviderTypes"] = EnumMap<WebhookProviderType>(),
|
||||
["annotationLanes"] = AnnotationLanes.All,
|
||||
["annotationVerdicts"] = AnnotationVerdicts.All,
|
||||
["tenantRoles"] = ConstStrings(typeof(Constants.TenantRoles)),
|
||||
["globalRoles"] = ConstStrings(typeof(Constants.GlobalRoles)),
|
||||
["delegatedRoles"] = ConstStrings(typeof(Constants.DelegatedRoles)),
|
||||
["analyzeRuleSources"] = DryRunAnalyzeRuleFunction.KnownSources.OrderBy(s => s, StringComparer.Ordinal).ToArray(),
|
||||
["analyzeRuleOperators"] = DryRunAnalyzeRuleFunction.KnownOperators.OrderBy(s => s, StringComparer.Ordinal).ToArray(),
|
||||
["eventTypes"] = ConstStrings(typeof(Constants.EventTypes)).OrderBy(s => s, StringComparer.Ordinal).ToArray(),
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(manifest, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
});
|
||||
// Normalized to LF + trailing newline so the file is byte-stable across OSes.
|
||||
return json.Replace("\r\n", "\n") + "\n";
|
||||
}
|
||||
|
||||
/// <summary>Wire (camelCase) names of the public instance properties, declaration order.</summary>
|
||||
private static string[] WireFieldNames(Type type)
|
||||
=> WireProperties(type).Select(p => JsonNamingPolicy.CamelCase.ConvertName(p.Name)).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Wire names plus whether the field can be ABSENT from the JSON: nullable fields are
|
||||
/// omitted under WhenWritingNull, so nullable ⇒ the TS mirror must type it optional.
|
||||
/// </summary>
|
||||
private static object[] WireFieldsWithOptionality(Type type)
|
||||
{
|
||||
var nullability = new NullabilityInfoContext();
|
||||
return WireProperties(type)
|
||||
.Select(p => (object)new Dictionary<string, object?>
|
||||
{
|
||||
["name"] = JsonNamingPolicy.CamelCase.ConvertName(p.Name),
|
||||
["optional"] = IsNullable(p, nullability),
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<PropertyInfo> WireProperties(Type type)
|
||||
=> type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.GetMethod != null && p.GetIndexParameters().Length == 0);
|
||||
|
||||
private static bool IsNullable(PropertyInfo p, NullabilityInfoContext ctx)
|
||||
{
|
||||
if (Nullable.GetUnderlyingType(p.PropertyType) != null) return true;
|
||||
if (p.PropertyType.IsValueType) return false;
|
||||
return ctx.Create(p).ReadState != NullabilityState.NotNull;
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> EnumMap<TEnum>() where TEnum : struct, Enum
|
||||
=> Enum.GetValues<TEnum>()
|
||||
.ToDictionary(v => v.ToString(), v => Convert.ToInt32(v));
|
||||
|
||||
private static string[] ConstStrings(Type constClass)
|
||||
=> constClass.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(f => f.IsLiteral && f.FieldType == typeof(string))
|
||||
.Select(f => (string)f.GetRawConstantValue()!)
|
||||
.ToArray();
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AutopilotMonitor.sln")))
|
||||
{
|
||||
dir = dir.Parent;
|
||||
}
|
||||
Assert.NotNull(dir);
|
||||
return dir!.FullName;
|
||||
}
|
||||
}
|
||||
@@ -131,13 +131,14 @@ namespace AutopilotMonitor.Functions.Functions.Rules
|
||||
// switch is case-sensitive ("event_type", not "Event_Type"), and an unknown source/operator
|
||||
// silently evaluates to false in production — for a dry-run that silence is exactly the
|
||||
// confusion we want to catch up front.
|
||||
private static readonly HashSet<string> KnownSources = new(StringComparer.Ordinal)
|
||||
// internal: SharedManifestParityTests exports these into the cross-language manifest.
|
||||
internal static readonly HashSet<string> KnownSources = new(StringComparer.Ordinal)
|
||||
{
|
||||
"event_type", "event_data", "event_data_array", "event_count",
|
||||
"phase_duration", "app_install_duration", "event_correlation",
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> KnownOperators = new(StringComparer.OrdinalIgnoreCase)
|
||||
internal static readonly HashSet<string> KnownOperators = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"equals", "not_equals", "contains", "not_contains", "regex", "not_regex",
|
||||
"gt", "lt", "gte", "lte", "exists", "not_exists",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Generate utils/shared-manifests.generated.ts from utils/shared-manifests.json.
|
||||
*
|
||||
* The JSON is produced by the backend reflection guard (SharedManifestParityTests,
|
||||
* regenerated via AM_WRITE_SHARED_MANIFESTS=1) — this script only lifts it into an
|
||||
* `as const` TS module so the manifest's entries exist as LITERAL types, enabling the
|
||||
* compile-time drift checks in utils/sharedManifestChecks.ts.
|
||||
*
|
||||
* Run: node scripts/generate-shared-manifest-types.js
|
||||
* Freshness is pinned by utils/__tests__/sharedManifestParity.test.ts.
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const WEB_ROOT = path.resolve(__dirname, "..");
|
||||
const SOURCE = path.join(WEB_ROOT, "utils", "shared-manifests.json");
|
||||
const DEST = path.join(WEB_ROOT, "utils", "shared-manifests.generated.ts");
|
||||
|
||||
/** Pure builder so the vitest freshness check can reuse it. */
|
||||
function buildGeneratedSource(manifestJsonText) {
|
||||
const manifest = JSON.parse(manifestJsonText);
|
||||
return (
|
||||
"// GENERATED from shared-manifests.json — do not edit by hand.\n" +
|
||||
"// Regenerate: node scripts/generate-shared-manifest-types.js\n" +
|
||||
"// (after AM_WRITE_SHARED_MANIFESTS=1 dotnet test --filter SharedManifestParityTests)\n" +
|
||||
"\n" +
|
||||
"export const SHARED_MANIFEST = " +
|
||||
JSON.stringify(manifest, null, 2) +
|
||||
" as const;\n"
|
||||
);
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(SOURCE)) {
|
||||
console.error(`[shared-manifests] Source not found: ${SOURCE}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const out = buildGeneratedSource(fs.readFileSync(SOURCE, "utf8"));
|
||||
fs.writeFileSync(DEST, out, "utf8");
|
||||
console.log(`[shared-manifests] Wrote ${path.relative(WEB_ROOT, DEST)}`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { buildGeneratedSource };
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Runtime parity between hand-written TS mirrors and the C#-generated shared manifest.
|
||||
* The manifest is the single cross-language anchor: the backend reflection guard
|
||||
* (SharedManifestParityTests) pins JSON ↔ C#, these tests pin TS ↔ JSON.
|
||||
* Compile-time (key-level) checks live in utils/sharedManifestChecks.ts.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createRequire } from "node:module";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { SHARED_MANIFEST } from "../shared-manifests.generated";
|
||||
import { SEVERITY_INT } from "../sessionExportUtils";
|
||||
import { isTerminalStatus } from "../sessionStatus";
|
||||
import { OPERATORS, PRECONDITION_OPERATORS, SOURCES } from "@/app/analyze-rules/types";
|
||||
import {
|
||||
ANNOTATION_LANES,
|
||||
ANNOTATION_VERDICTS,
|
||||
} from "@/app/sessions/components/sessionAnnotationLogic";
|
||||
import { V1_PHASE_NAMES, V2_PHASE_NAMES } from "@/app/sessions/utils/phaseConstants";
|
||||
import { KNOWN_EVENT_TYPES } from "@/app/gather-rules/eventTypes";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { buildGeneratedSource } = require("../../scripts/generate-shared-manifest-types.js");
|
||||
|
||||
const utilsDir = path.resolve(__dirname, "..");
|
||||
|
||||
describe("shared-manifests.generated.ts freshness", () => {
|
||||
it("matches a fresh run of the codegen over shared-manifests.json", () => {
|
||||
const json = fs.readFileSync(path.join(utilsDir, "shared-manifests.json"), "utf8");
|
||||
const committed = fs.readFileSync(
|
||||
path.join(utilsDir, "shared-manifests.generated.ts"),
|
||||
"utf8"
|
||||
);
|
||||
expect(committed.replace(/\r\n/g, "\n")).toBe(buildGeneratedSource(json));
|
||||
});
|
||||
});
|
||||
|
||||
describe("analyze-rule builder catalogs", () => {
|
||||
it("SOURCES equals the backend's KnownSources", () => {
|
||||
expect([...SOURCES].sort()).toEqual([...SHARED_MANIFEST.analyzeRuleSources].sort());
|
||||
});
|
||||
|
||||
it("OPERATORS equals the backend's KnownOperators", () => {
|
||||
expect([...OPERATORS].sort()).toEqual([...SHARED_MANIFEST.analyzeRuleOperators].sort());
|
||||
});
|
||||
|
||||
it("PRECONDITION_OPERATORS is a subset of the backend's KnownOperators", () => {
|
||||
const known = new Set<string>(SHARED_MANIFEST.analyzeRuleOperators);
|
||||
for (const op of PRECONDITION_OPERATORS) expect(known).toContain(op);
|
||||
});
|
||||
});
|
||||
|
||||
describe("annotation lanes and verdicts", () => {
|
||||
it("lanes match", () => {
|
||||
expect([...ANNOTATION_LANES]).toEqual([...SHARED_MANIFEST.annotationLanes]);
|
||||
});
|
||||
|
||||
it("verdicts match", () => {
|
||||
expect([...ANNOTATION_VERDICTS]).toEqual([...SHARED_MANIFEST.annotationVerdicts]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("event severities", () => {
|
||||
it("SEVERITY_INT mirrors the C# EventSeverity enum exactly (names and ordinals)", () => {
|
||||
expect(SEVERITY_INT).toEqual(SHARED_MANIFEST.eventSeverities);
|
||||
});
|
||||
});
|
||||
|
||||
describe("enrollment phases", () => {
|
||||
it("every numeric phase key used by the UI exists on the C# enum", () => {
|
||||
// Display STRINGS are deliberately surface-specific (V1 vs V2 wording); the
|
||||
// numeric ordinals are the wire contract.
|
||||
const enumValues = new Set<number>(Object.values(SHARED_MANIFEST.enrollmentPhases));
|
||||
for (const key of [...Object.keys(V1_PHASE_NAMES), ...Object.keys(V2_PHASE_NAMES)]) {
|
||||
expect(enumValues).toContain(Number(key));
|
||||
}
|
||||
});
|
||||
|
||||
it("every C# enum ordinal has a UI name in both version maps", () => {
|
||||
for (const value of Object.values(SHARED_MANIFEST.enrollmentPhases)) {
|
||||
expect(V1_PHASE_NAMES[value], `V1 name for phase ${value}`).toBeTruthy();
|
||||
expect(V2_PHASE_NAMES[value], `V2 name for phase ${value}`).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("session statuses", () => {
|
||||
it("the terminal trio exists on the C# enum and is classified terminal", () => {
|
||||
for (const status of ["Succeeded", "Failed", "Incomplete"]) {
|
||||
expect(SHARED_MANIFEST.sessionStatuses).toContain(status);
|
||||
expect(isTerminalStatus(status)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("non-terminal enum members are not classified terminal", () => {
|
||||
for (const status of SHARED_MANIFEST.sessionStatuses) {
|
||||
if (["Succeeded", "Failed", "Incomplete"].includes(status)) continue;
|
||||
expect(isTerminalStatus(status), `${status} must not be terminal`).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("gather-rule event-type autocomplete", () => {
|
||||
it("contains no event type the agent/backend does not know", () => {
|
||||
// Typo guard: every autocomplete entry must be a real Constants.EventTypes value.
|
||||
// (Completeness in the other direction is a deliberate curation question — the
|
||||
// catalog carries ~3x more types than the rule builder surfaces today.)
|
||||
const known = new Set<string>(SHARED_MANIFEST.eventTypes);
|
||||
for (const entry of KNOWN_EVENT_TYPES) {
|
||||
expect(known, `unknown event type '${entry.value}' in KNOWN_EVENT_TYPES`).toContain(
|
||||
entry.value
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,643 @@
|
||||
// GENERATED from shared-manifests.json — do not edit by hand.
|
||||
// Regenerate: node scripts/generate-shared-manifest-types.js
|
||||
// (after AM_WRITE_SHARED_MANIFESTS=1 dotnet test --filter SharedManifestParityTests)
|
||||
|
||||
export const SHARED_MANIFEST = {
|
||||
"$comment": "GENERATED from AutopilotMonitor.Shared by SharedManifestParityTests — do not edit by hand. Regenerate: AM_WRITE_SHARED_MANIFESTS=1 dotnet test --filter SharedManifestParityTests, then node scripts/generate-shared-manifest-types.js.",
|
||||
"adminConfiguration": {
|
||||
"fields": [
|
||||
"partitionKey",
|
||||
"rowKey",
|
||||
"lastUpdated",
|
||||
"updatedBy",
|
||||
"globalRateLimitRequestsPerMinute",
|
||||
"userRateLimitRequestsPerMinute",
|
||||
"globalAdminRateLimitRequestsPerMinute",
|
||||
"planTierDefinitionsJson",
|
||||
"platformStatsBlobSasUrl",
|
||||
"collectorIdleTimeoutMinutes",
|
||||
"desktopDetectorNoCandidateTimeoutMinutes",
|
||||
"excessiveEventCountThreshold",
|
||||
"excessiveEventAutoActionMode",
|
||||
"excessiveEventAutoActionThreshold",
|
||||
"excessiveEventAutoActionDurationHours",
|
||||
"opsEventRetentionDays",
|
||||
"slaNotificationCooldownHours",
|
||||
"feedbackEnabled",
|
||||
"feedbackMinTenantAgeDays",
|
||||
"feedbackCooldownDays",
|
||||
"diagnosticsGlobalLogPathsJson",
|
||||
"maxDiagnosticsDownloadSizeMB",
|
||||
"diagnosticsDownloadTimeoutSeconds",
|
||||
"mcpAccessPolicy",
|
||||
"nvdApiKey",
|
||||
"opsAlertRulesJson",
|
||||
"opsAlertTelegramEnabled",
|
||||
"opsAlertTelegramChatId",
|
||||
"opsAlertTeamsEnabled",
|
||||
"opsAlertTeamsWebhookUrl",
|
||||
"opsAlertSlackEnabled",
|
||||
"opsAlertSlackWebhookUrl",
|
||||
"latestAgentV2Version",
|
||||
"latestAgentV2Sha256",
|
||||
"latestAgentV2ExeSha256",
|
||||
"latestBootstrapV2ScriptVersion",
|
||||
"allowAgentDowngrade",
|
||||
"agentMigrateApiBaseUrl",
|
||||
"agentMigrateTenantOverridesJson",
|
||||
"modernDeploymentHarmlessEventIdsJson",
|
||||
"whiteGloveSealingPatternIdsJson",
|
||||
"vulnerabilityCorrelationEnabled",
|
||||
"enableIndexDualWrite",
|
||||
"autoApproveNewTenants",
|
||||
"selfServiceAppHomingEnabled",
|
||||
"sessionDeletionKillSwitch",
|
||||
"vulnerabilityDataLastSyncUtc",
|
||||
"msrcLastSyncUtc"
|
||||
]
|
||||
},
|
||||
"tenantConfiguration": {
|
||||
"fields": [
|
||||
"tenantId",
|
||||
"domainName",
|
||||
"lastUpdated",
|
||||
"updatedBy",
|
||||
"onboardedBy",
|
||||
"contactEmail",
|
||||
"onboardedAt",
|
||||
"homedAppClientId",
|
||||
"lastAuthClientId",
|
||||
"lastAuthClientIdSince",
|
||||
"disabled",
|
||||
"disabledReason",
|
||||
"disabledUntil",
|
||||
"customRateLimitRequestsPerMinute",
|
||||
"customUserRateLimitRequestsPerMinute",
|
||||
"planTier",
|
||||
"trialExpiresUtc",
|
||||
"trialStartedUtc",
|
||||
"trialConsumed",
|
||||
"trialGrantedBy",
|
||||
"manufacturerWhitelist",
|
||||
"modelWhitelist",
|
||||
"validateAutopilotDevice",
|
||||
"validateCorporateIdentifier",
|
||||
"validateDeviceAssociation",
|
||||
"validateCloudPcDevice",
|
||||
"allowInsecureAgentRequests",
|
||||
"dataRetentionDays",
|
||||
"sessionTimeoutHours",
|
||||
"sessionGraceHours",
|
||||
"maxNdjsonPayloadSizeMB",
|
||||
"enablePerformanceCollector",
|
||||
"performanceCollectorIntervalSeconds",
|
||||
"helloWaitTimeoutSeconds",
|
||||
"maxAuthFailures",
|
||||
"authFailureTimeoutMinutes",
|
||||
"agentMaxLifetimeMinutes",
|
||||
"absoluteMaxSessionHours",
|
||||
"selfDestructOnComplete",
|
||||
"keepLogFile",
|
||||
"rebootOnComplete",
|
||||
"rebootDelaySeconds",
|
||||
"enableGeoLocation",
|
||||
"ntpServer",
|
||||
"enableTimezoneAutoSet",
|
||||
"enableImeMatchLog",
|
||||
"enableGatherRuleDebugLog",
|
||||
"enableEspContinueAnywayObservation",
|
||||
"logLevel",
|
||||
"maxBatchSize",
|
||||
"showEnrollmentSummary",
|
||||
"enrollmentSummaryTimeoutSeconds",
|
||||
"enrollmentSummaryBrandingImageUrl",
|
||||
"enrollmentSummaryLaunchRetrySeconds",
|
||||
"showScriptOutput",
|
||||
"enableLocalAdminAnalyzer",
|
||||
"enableSoftwareInventoryAnalyzer",
|
||||
"enableIntegrityBypassAnalyzer",
|
||||
"enableRealmJoinWatcher",
|
||||
"keepAwakeDuringUserEsp",
|
||||
"enableConsoleBypassDetection",
|
||||
"localAdminAllowedAccountsJson",
|
||||
"bootstrapTokenEnabled",
|
||||
"unrestrictedModeEnabled",
|
||||
"unrestrictedMode",
|
||||
"entraAppRolesEnabled",
|
||||
"diagnosticsLogPathsJson",
|
||||
"diagnosticsBlobSasUrl",
|
||||
"diagnosticsUploadMode",
|
||||
"diagnosticsUploadDestination",
|
||||
"sendTraceEvents",
|
||||
"teamsWebhookUrl",
|
||||
"teamsNotifyOnSuccess",
|
||||
"teamsNotifyOnFailure",
|
||||
"teamsNotifyOnStart",
|
||||
"webhookProviderType",
|
||||
"webhookUrl",
|
||||
"webhookNotifyOnSuccess",
|
||||
"webhookNotifyOnFailure",
|
||||
"webhookNotifyOnHardwareRejection",
|
||||
"webhookNotifyOnStart",
|
||||
"webhookCustomHeadersJson",
|
||||
"notificationChannelsJson",
|
||||
"slaTargetSuccessRate",
|
||||
"slaTargetMaxDurationMinutes",
|
||||
"slaTargetAppInstallSuccessRate",
|
||||
"slaNotifyOnSuccessRateBreach",
|
||||
"slaSuccessRateNotifyThreshold",
|
||||
"slaNotifyOnDurationBreach",
|
||||
"slaNotifyOnAppInstallBreach",
|
||||
"slaNotifyOnConsecutiveFailures",
|
||||
"slaConsecutiveFailureThreshold"
|
||||
]
|
||||
},
|
||||
"sessionSummary": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "sessionId",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "tenantId",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "serialNumber",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "deviceName",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "manufacturer",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "startedAt",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "completedAt",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "currentPhase",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "currentPhaseDetail",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "failureReason",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "failureSource",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "reconcileReason",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "espSoftFailure",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "completionSource",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "adminMarkedAction",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "validatedBy",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "eventCount",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "durationSeconds",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "avgApiLatencyMs",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "apiRequestCount",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "connectionType",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "enrollmentType",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "diagnosticsBlobName",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "diagnosticsBlobDestination",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "lastEventAt",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "isPreProvisioned",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "resumedAt",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "stalledAt",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "isHybridJoin",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "isSelfDeployingProfile",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "isCloudPc",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "osName",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "osBuild",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "osDisplayVersion",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "osEdition",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "osLanguage",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "isUserDriven",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "agentVersion",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "imeAgentVersion",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "geoCountry",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "geoRegion",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "geoCity",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "geoLoc",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "platformScriptCount",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "remediationScriptCount",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "rebootCount",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "excessiveEventsAlerted",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "excessiveEventsAutoActioned",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "pendingActionsJson",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "pendingActionsQueuedAt",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "failureSnapshotJson",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "deletionState",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "pendingDeletionManifestId",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionStatuses": [
|
||||
"InProgress",
|
||||
"Pending",
|
||||
"Stalled",
|
||||
"Succeeded",
|
||||
"Failed",
|
||||
"Unknown",
|
||||
"AwaitingUser",
|
||||
"Incomplete"
|
||||
],
|
||||
"enrollmentPhases": {
|
||||
"Start": 0,
|
||||
"DevicePreparation": 1,
|
||||
"DeviceSetup": 2,
|
||||
"AppsDevice": 3,
|
||||
"AccountSetup": 4,
|
||||
"AppsUser": 5,
|
||||
"FinalizingSetup": 6,
|
||||
"Complete": 7,
|
||||
"Failed": 99,
|
||||
"Unknown": -1
|
||||
},
|
||||
"eventSeverities": {
|
||||
"Debug": 0,
|
||||
"Info": 1,
|
||||
"Warning": 2,
|
||||
"Error": 3,
|
||||
"Critical": 4,
|
||||
"Trace": -1
|
||||
},
|
||||
"webhookProviderTypes": {
|
||||
"None": 0,
|
||||
"TeamsLegacyConnector": 1,
|
||||
"TeamsWorkflowWebhook": 2,
|
||||
"Slack": 10,
|
||||
"GenericJson": 20
|
||||
},
|
||||
"annotationLanes": [
|
||||
"operator",
|
||||
"tenantadmin",
|
||||
"globaladmin"
|
||||
],
|
||||
"annotationVerdicts": [
|
||||
"root_cause_confirmed",
|
||||
"analysis_wrong",
|
||||
"different_problem",
|
||||
"inconclusive"
|
||||
],
|
||||
"tenantRoles": [
|
||||
"Admin",
|
||||
"Operator",
|
||||
"Viewer"
|
||||
],
|
||||
"globalRoles": [
|
||||
"GlobalAdmin",
|
||||
"GlobalReader"
|
||||
],
|
||||
"delegatedRoles": [
|
||||
"DelegatedReader",
|
||||
"DelegatedAdmin"
|
||||
],
|
||||
"analyzeRuleSources": [
|
||||
"app_install_duration",
|
||||
"event_correlation",
|
||||
"event_count",
|
||||
"event_data",
|
||||
"event_data_array",
|
||||
"event_type",
|
||||
"phase_duration"
|
||||
],
|
||||
"analyzeRuleOperators": [
|
||||
"contains",
|
||||
"count_gte",
|
||||
"count_per_group_gte",
|
||||
"equals",
|
||||
"exists",
|
||||
"gt",
|
||||
"gte",
|
||||
"in",
|
||||
"lt",
|
||||
"lte",
|
||||
"not_contains",
|
||||
"not_equals",
|
||||
"not_exists",
|
||||
"not_in",
|
||||
"not_regex",
|
||||
"regex"
|
||||
],
|
||||
"eventTypes": [
|
||||
"aad_join_status",
|
||||
"aad_placeholder_user_detected",
|
||||
"aad_user_joined_observed",
|
||||
"admin_marked_session",
|
||||
"agent_emergency_break",
|
||||
"agent_late_start",
|
||||
"agent_metrics_collector_stopped",
|
||||
"agent_metrics_snapshot",
|
||||
"agent_shutdown",
|
||||
"agent_shutting_down",
|
||||
"agent_started",
|
||||
"agent_trace",
|
||||
"agent_unrestricted_mode_changed",
|
||||
"agent_version_check",
|
||||
"all_apps_completed",
|
||||
"app_download_started",
|
||||
"app_install_completed",
|
||||
"app_install_failed",
|
||||
"app_install_skipped",
|
||||
"app_install_started",
|
||||
"app_install_starved",
|
||||
"app_tracking_summary",
|
||||
"autologon_analysis",
|
||||
"autopilot_profile",
|
||||
"autopilot_profile_missing",
|
||||
"bitlocker_status",
|
||||
"boot_time",
|
||||
"cert_validation",
|
||||
"collector_degraded",
|
||||
"completion_check",
|
||||
"completion_waiting",
|
||||
"configmgr_client_detected",
|
||||
"console_prefetch_detected",
|
||||
"decision_process_completion",
|
||||
"desktop_arrived",
|
||||
"desktop_detector_first_poll",
|
||||
"desktop_detector_no_candidate",
|
||||
"desktop_detector_started",
|
||||
"device_location",
|
||||
"diagnostics_collecting",
|
||||
"diagnostics_upload_failed",
|
||||
"diagnostics_uploaded",
|
||||
"disk_space_low",
|
||||
"dns_configuration",
|
||||
"do_telemetry",
|
||||
"download_progress",
|
||||
"enrollment_complete",
|
||||
"enrollment_failed",
|
||||
"enrollment_summary_shown",
|
||||
"enrollment_type_detected",
|
||||
"enrollment_type_mismatch",
|
||||
"error_detected",
|
||||
"esp_apps_failure_correlation",
|
||||
"esp_appx_failure_analysis",
|
||||
"esp_config_detected",
|
||||
"esp_exiting",
|
||||
"esp_failure",
|
||||
"esp_failure_advisory",
|
||||
"esp_failure_advisory_resolved",
|
||||
"esp_failure_recovered",
|
||||
"esp_failure_retry_detected",
|
||||
"esp_failure_settle_recovered",
|
||||
"esp_failure_settle_started",
|
||||
"esp_phase_changed",
|
||||
"esp_policy_provider_stalled",
|
||||
"esp_provisioning_raw",
|
||||
"esp_provisioning_settle_started",
|
||||
"esp_provisioning_status",
|
||||
"esp_resumed",
|
||||
"esp_state_change",
|
||||
"esp_ui_state",
|
||||
"gather_result",
|
||||
"gather_rules_collection_completed",
|
||||
"gather_rules_collection_started",
|
||||
"hardware_spec",
|
||||
"hello_completion_timeout",
|
||||
"hello_pin_status",
|
||||
"hello_policy_detected",
|
||||
"hello_policy_detection_mismatch",
|
||||
"hello_processing_started",
|
||||
"hello_processing_stopped",
|
||||
"hello_provisioning_blocked",
|
||||
"hello_provisioning_completed",
|
||||
"hello_provisioning_failed",
|
||||
"hello_skipped",
|
||||
"hello_wait_timeout",
|
||||
"hello_wizard_started",
|
||||
"historic_ime_replay_detected",
|
||||
"hybrid_login_pending",
|
||||
"ime_agent_version",
|
||||
"ime_process_exited",
|
||||
"ime_session_change",
|
||||
"ime_user_session_completed",
|
||||
"ingress_backpressure",
|
||||
"integrity_bypass_analysis",
|
||||
"keep_awake_engaged",
|
||||
"keep_awake_released",
|
||||
"local_admin_analysis",
|
||||
"log_entry",
|
||||
"mdm_policy_reboot_required",
|
||||
"modern_deployment_error",
|
||||
"modern_deployment_log",
|
||||
"modern_deployment_warning",
|
||||
"network_adapters",
|
||||
"network_bandwidth_estimate",
|
||||
"network_connectivity_check",
|
||||
"network_interface_info",
|
||||
"network_state_change",
|
||||
"ntp_time_check",
|
||||
"office_install_completed",
|
||||
"office_install_failed",
|
||||
"office_install_progress",
|
||||
"office_install_started",
|
||||
"office_preinstalled_detected",
|
||||
"oobe_console_spawned",
|
||||
"oobe_state_completed",
|
||||
"os_build_changed",
|
||||
"os_info",
|
||||
"outbound_ip",
|
||||
"performance_collector_stopped",
|
||||
"performance_snapshot",
|
||||
"phase_transition",
|
||||
"power_state_check",
|
||||
"previous_crash_detected",
|
||||
"prior_run_died_with_state",
|
||||
"provisioning_package_scan",
|
||||
"proxy_configuration",
|
||||
"realmjoin_detected",
|
||||
"realmjoin_first_deployment_incomplete",
|
||||
"realmjoin_package_completed",
|
||||
"realmjoin_package_started",
|
||||
"realmjoin_phase_changed",
|
||||
"realmjoin_resolved",
|
||||
"realmjoin_timeout",
|
||||
"reboot_triggered",
|
||||
"remote_config_fetch_failed",
|
||||
"script_completed",
|
||||
"script_failed",
|
||||
"script_started",
|
||||
"script_timeout_suspected",
|
||||
"secureboot_status",
|
||||
"security_audit",
|
||||
"security_warning",
|
||||
"server_action_executed",
|
||||
"server_action_failed",
|
||||
"server_action_received",
|
||||
"session_parked_without_deadline",
|
||||
"session_stalled",
|
||||
"session_timeout",
|
||||
"shadow_discrepancy",
|
||||
"software_inventory_analysis",
|
||||
"spool_pressure_detected",
|
||||
"stall_probe_check",
|
||||
"stall_probe_result",
|
||||
"state_quarantine_recovered",
|
||||
"system_reboot_detected",
|
||||
"telemetry_upload_blocked",
|
||||
"telemetry_upload_poisoned",
|
||||
"timezone_auto_set",
|
||||
"tpm_status",
|
||||
"vulnerability_report",
|
||||
"waiting_for_hello",
|
||||
"whiteglove_classification",
|
||||
"whiteglove_complete",
|
||||
"whiteglove_part1_complete",
|
||||
"whiteglove_resumed",
|
||||
"whiteglove_started",
|
||||
"wifi_signal_info",
|
||||
"windows_update_channel_census",
|
||||
"windows_update_failed",
|
||||
"windows_update_history",
|
||||
"windows_update_reboot_pending",
|
||||
"windows_update_started",
|
||||
"windows_update_succeeded"
|
||||
]
|
||||
} as const;
|
||||
@@ -0,0 +1,639 @@
|
||||
{
|
||||
"$comment": "GENERATED from AutopilotMonitor.Shared by SharedManifestParityTests — do not edit by hand. Regenerate: AM_WRITE_SHARED_MANIFESTS=1 dotnet test --filter SharedManifestParityTests, then node scripts/generate-shared-manifest-types.js.",
|
||||
"adminConfiguration": {
|
||||
"fields": [
|
||||
"partitionKey",
|
||||
"rowKey",
|
||||
"lastUpdated",
|
||||
"updatedBy",
|
||||
"globalRateLimitRequestsPerMinute",
|
||||
"userRateLimitRequestsPerMinute",
|
||||
"globalAdminRateLimitRequestsPerMinute",
|
||||
"planTierDefinitionsJson",
|
||||
"platformStatsBlobSasUrl",
|
||||
"collectorIdleTimeoutMinutes",
|
||||
"desktopDetectorNoCandidateTimeoutMinutes",
|
||||
"excessiveEventCountThreshold",
|
||||
"excessiveEventAutoActionMode",
|
||||
"excessiveEventAutoActionThreshold",
|
||||
"excessiveEventAutoActionDurationHours",
|
||||
"opsEventRetentionDays",
|
||||
"slaNotificationCooldownHours",
|
||||
"feedbackEnabled",
|
||||
"feedbackMinTenantAgeDays",
|
||||
"feedbackCooldownDays",
|
||||
"diagnosticsGlobalLogPathsJson",
|
||||
"maxDiagnosticsDownloadSizeMB",
|
||||
"diagnosticsDownloadTimeoutSeconds",
|
||||
"mcpAccessPolicy",
|
||||
"nvdApiKey",
|
||||
"opsAlertRulesJson",
|
||||
"opsAlertTelegramEnabled",
|
||||
"opsAlertTelegramChatId",
|
||||
"opsAlertTeamsEnabled",
|
||||
"opsAlertTeamsWebhookUrl",
|
||||
"opsAlertSlackEnabled",
|
||||
"opsAlertSlackWebhookUrl",
|
||||
"latestAgentV2Version",
|
||||
"latestAgentV2Sha256",
|
||||
"latestAgentV2ExeSha256",
|
||||
"latestBootstrapV2ScriptVersion",
|
||||
"allowAgentDowngrade",
|
||||
"agentMigrateApiBaseUrl",
|
||||
"agentMigrateTenantOverridesJson",
|
||||
"modernDeploymentHarmlessEventIdsJson",
|
||||
"whiteGloveSealingPatternIdsJson",
|
||||
"vulnerabilityCorrelationEnabled",
|
||||
"enableIndexDualWrite",
|
||||
"autoApproveNewTenants",
|
||||
"selfServiceAppHomingEnabled",
|
||||
"sessionDeletionKillSwitch",
|
||||
"vulnerabilityDataLastSyncUtc",
|
||||
"msrcLastSyncUtc"
|
||||
]
|
||||
},
|
||||
"tenantConfiguration": {
|
||||
"fields": [
|
||||
"tenantId",
|
||||
"domainName",
|
||||
"lastUpdated",
|
||||
"updatedBy",
|
||||
"onboardedBy",
|
||||
"contactEmail",
|
||||
"onboardedAt",
|
||||
"homedAppClientId",
|
||||
"lastAuthClientId",
|
||||
"lastAuthClientIdSince",
|
||||
"disabled",
|
||||
"disabledReason",
|
||||
"disabledUntil",
|
||||
"customRateLimitRequestsPerMinute",
|
||||
"customUserRateLimitRequestsPerMinute",
|
||||
"planTier",
|
||||
"trialExpiresUtc",
|
||||
"trialStartedUtc",
|
||||
"trialConsumed",
|
||||
"trialGrantedBy",
|
||||
"manufacturerWhitelist",
|
||||
"modelWhitelist",
|
||||
"validateAutopilotDevice",
|
||||
"validateCorporateIdentifier",
|
||||
"validateDeviceAssociation",
|
||||
"validateCloudPcDevice",
|
||||
"allowInsecureAgentRequests",
|
||||
"dataRetentionDays",
|
||||
"sessionTimeoutHours",
|
||||
"sessionGraceHours",
|
||||
"maxNdjsonPayloadSizeMB",
|
||||
"enablePerformanceCollector",
|
||||
"performanceCollectorIntervalSeconds",
|
||||
"helloWaitTimeoutSeconds",
|
||||
"maxAuthFailures",
|
||||
"authFailureTimeoutMinutes",
|
||||
"agentMaxLifetimeMinutes",
|
||||
"absoluteMaxSessionHours",
|
||||
"selfDestructOnComplete",
|
||||
"keepLogFile",
|
||||
"rebootOnComplete",
|
||||
"rebootDelaySeconds",
|
||||
"enableGeoLocation",
|
||||
"ntpServer",
|
||||
"enableTimezoneAutoSet",
|
||||
"enableImeMatchLog",
|
||||
"enableGatherRuleDebugLog",
|
||||
"enableEspContinueAnywayObservation",
|
||||
"logLevel",
|
||||
"maxBatchSize",
|
||||
"showEnrollmentSummary",
|
||||
"enrollmentSummaryTimeoutSeconds",
|
||||
"enrollmentSummaryBrandingImageUrl",
|
||||
"enrollmentSummaryLaunchRetrySeconds",
|
||||
"showScriptOutput",
|
||||
"enableLocalAdminAnalyzer",
|
||||
"enableSoftwareInventoryAnalyzer",
|
||||
"enableIntegrityBypassAnalyzer",
|
||||
"enableRealmJoinWatcher",
|
||||
"keepAwakeDuringUserEsp",
|
||||
"enableConsoleBypassDetection",
|
||||
"localAdminAllowedAccountsJson",
|
||||
"bootstrapTokenEnabled",
|
||||
"unrestrictedModeEnabled",
|
||||
"unrestrictedMode",
|
||||
"entraAppRolesEnabled",
|
||||
"diagnosticsLogPathsJson",
|
||||
"diagnosticsBlobSasUrl",
|
||||
"diagnosticsUploadMode",
|
||||
"diagnosticsUploadDestination",
|
||||
"sendTraceEvents",
|
||||
"teamsWebhookUrl",
|
||||
"teamsNotifyOnSuccess",
|
||||
"teamsNotifyOnFailure",
|
||||
"teamsNotifyOnStart",
|
||||
"webhookProviderType",
|
||||
"webhookUrl",
|
||||
"webhookNotifyOnSuccess",
|
||||
"webhookNotifyOnFailure",
|
||||
"webhookNotifyOnHardwareRejection",
|
||||
"webhookNotifyOnStart",
|
||||
"webhookCustomHeadersJson",
|
||||
"notificationChannelsJson",
|
||||
"slaTargetSuccessRate",
|
||||
"slaTargetMaxDurationMinutes",
|
||||
"slaTargetAppInstallSuccessRate",
|
||||
"slaNotifyOnSuccessRateBreach",
|
||||
"slaSuccessRateNotifyThreshold",
|
||||
"slaNotifyOnDurationBreach",
|
||||
"slaNotifyOnAppInstallBreach",
|
||||
"slaNotifyOnConsecutiveFailures",
|
||||
"slaConsecutiveFailureThreshold"
|
||||
]
|
||||
},
|
||||
"sessionSummary": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "sessionId",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "tenantId",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "serialNumber",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "deviceName",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "manufacturer",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "startedAt",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "completedAt",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "currentPhase",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "currentPhaseDetail",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "failureReason",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "failureSource",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "reconcileReason",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "espSoftFailure",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "completionSource",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "adminMarkedAction",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "validatedBy",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "eventCount",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "durationSeconds",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "avgApiLatencyMs",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "apiRequestCount",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "connectionType",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "enrollmentType",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "diagnosticsBlobName",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "diagnosticsBlobDestination",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "lastEventAt",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "isPreProvisioned",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "resumedAt",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "stalledAt",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "isHybridJoin",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "isSelfDeployingProfile",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "isCloudPc",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "osName",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "osBuild",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "osDisplayVersion",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "osEdition",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "osLanguage",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "isUserDriven",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "agentVersion",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "imeAgentVersion",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "geoCountry",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "geoRegion",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "geoCity",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "geoLoc",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "platformScriptCount",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "remediationScriptCount",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "rebootCount",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "excessiveEventsAlerted",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "excessiveEventsAutoActioned",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "pendingActionsJson",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "pendingActionsQueuedAt",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "failureSnapshotJson",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "deletionState",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "pendingDeletionManifestId",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionStatuses": [
|
||||
"InProgress",
|
||||
"Pending",
|
||||
"Stalled",
|
||||
"Succeeded",
|
||||
"Failed",
|
||||
"Unknown",
|
||||
"AwaitingUser",
|
||||
"Incomplete"
|
||||
],
|
||||
"enrollmentPhases": {
|
||||
"Start": 0,
|
||||
"DevicePreparation": 1,
|
||||
"DeviceSetup": 2,
|
||||
"AppsDevice": 3,
|
||||
"AccountSetup": 4,
|
||||
"AppsUser": 5,
|
||||
"FinalizingSetup": 6,
|
||||
"Complete": 7,
|
||||
"Failed": 99,
|
||||
"Unknown": -1
|
||||
},
|
||||
"eventSeverities": {
|
||||
"Debug": 0,
|
||||
"Info": 1,
|
||||
"Warning": 2,
|
||||
"Error": 3,
|
||||
"Critical": 4,
|
||||
"Trace": -1
|
||||
},
|
||||
"webhookProviderTypes": {
|
||||
"None": 0,
|
||||
"TeamsLegacyConnector": 1,
|
||||
"TeamsWorkflowWebhook": 2,
|
||||
"Slack": 10,
|
||||
"GenericJson": 20
|
||||
},
|
||||
"annotationLanes": [
|
||||
"operator",
|
||||
"tenantadmin",
|
||||
"globaladmin"
|
||||
],
|
||||
"annotationVerdicts": [
|
||||
"root_cause_confirmed",
|
||||
"analysis_wrong",
|
||||
"different_problem",
|
||||
"inconclusive"
|
||||
],
|
||||
"tenantRoles": [
|
||||
"Admin",
|
||||
"Operator",
|
||||
"Viewer"
|
||||
],
|
||||
"globalRoles": [
|
||||
"GlobalAdmin",
|
||||
"GlobalReader"
|
||||
],
|
||||
"delegatedRoles": [
|
||||
"DelegatedReader",
|
||||
"DelegatedAdmin"
|
||||
],
|
||||
"analyzeRuleSources": [
|
||||
"app_install_duration",
|
||||
"event_correlation",
|
||||
"event_count",
|
||||
"event_data",
|
||||
"event_data_array",
|
||||
"event_type",
|
||||
"phase_duration"
|
||||
],
|
||||
"analyzeRuleOperators": [
|
||||
"contains",
|
||||
"count_gte",
|
||||
"count_per_group_gte",
|
||||
"equals",
|
||||
"exists",
|
||||
"gt",
|
||||
"gte",
|
||||
"in",
|
||||
"lt",
|
||||
"lte",
|
||||
"not_contains",
|
||||
"not_equals",
|
||||
"not_exists",
|
||||
"not_in",
|
||||
"not_regex",
|
||||
"regex"
|
||||
],
|
||||
"eventTypes": [
|
||||
"aad_join_status",
|
||||
"aad_placeholder_user_detected",
|
||||
"aad_user_joined_observed",
|
||||
"admin_marked_session",
|
||||
"agent_emergency_break",
|
||||
"agent_late_start",
|
||||
"agent_metrics_collector_stopped",
|
||||
"agent_metrics_snapshot",
|
||||
"agent_shutdown",
|
||||
"agent_shutting_down",
|
||||
"agent_started",
|
||||
"agent_trace",
|
||||
"agent_unrestricted_mode_changed",
|
||||
"agent_version_check",
|
||||
"all_apps_completed",
|
||||
"app_download_started",
|
||||
"app_install_completed",
|
||||
"app_install_failed",
|
||||
"app_install_skipped",
|
||||
"app_install_started",
|
||||
"app_install_starved",
|
||||
"app_tracking_summary",
|
||||
"autologon_analysis",
|
||||
"autopilot_profile",
|
||||
"autopilot_profile_missing",
|
||||
"bitlocker_status",
|
||||
"boot_time",
|
||||
"cert_validation",
|
||||
"collector_degraded",
|
||||
"completion_check",
|
||||
"completion_waiting",
|
||||
"configmgr_client_detected",
|
||||
"console_prefetch_detected",
|
||||
"decision_process_completion",
|
||||
"desktop_arrived",
|
||||
"desktop_detector_first_poll",
|
||||
"desktop_detector_no_candidate",
|
||||
"desktop_detector_started",
|
||||
"device_location",
|
||||
"diagnostics_collecting",
|
||||
"diagnostics_upload_failed",
|
||||
"diagnostics_uploaded",
|
||||
"disk_space_low",
|
||||
"dns_configuration",
|
||||
"do_telemetry",
|
||||
"download_progress",
|
||||
"enrollment_complete",
|
||||
"enrollment_failed",
|
||||
"enrollment_summary_shown",
|
||||
"enrollment_type_detected",
|
||||
"enrollment_type_mismatch",
|
||||
"error_detected",
|
||||
"esp_apps_failure_correlation",
|
||||
"esp_appx_failure_analysis",
|
||||
"esp_config_detected",
|
||||
"esp_exiting",
|
||||
"esp_failure",
|
||||
"esp_failure_advisory",
|
||||
"esp_failure_advisory_resolved",
|
||||
"esp_failure_recovered",
|
||||
"esp_failure_retry_detected",
|
||||
"esp_failure_settle_recovered",
|
||||
"esp_failure_settle_started",
|
||||
"esp_phase_changed",
|
||||
"esp_policy_provider_stalled",
|
||||
"esp_provisioning_raw",
|
||||
"esp_provisioning_settle_started",
|
||||
"esp_provisioning_status",
|
||||
"esp_resumed",
|
||||
"esp_state_change",
|
||||
"esp_ui_state",
|
||||
"gather_result",
|
||||
"gather_rules_collection_completed",
|
||||
"gather_rules_collection_started",
|
||||
"hardware_spec",
|
||||
"hello_completion_timeout",
|
||||
"hello_pin_status",
|
||||
"hello_policy_detected",
|
||||
"hello_policy_detection_mismatch",
|
||||
"hello_processing_started",
|
||||
"hello_processing_stopped",
|
||||
"hello_provisioning_blocked",
|
||||
"hello_provisioning_completed",
|
||||
"hello_provisioning_failed",
|
||||
"hello_skipped",
|
||||
"hello_wait_timeout",
|
||||
"hello_wizard_started",
|
||||
"historic_ime_replay_detected",
|
||||
"hybrid_login_pending",
|
||||
"ime_agent_version",
|
||||
"ime_process_exited",
|
||||
"ime_session_change",
|
||||
"ime_user_session_completed",
|
||||
"ingress_backpressure",
|
||||
"integrity_bypass_analysis",
|
||||
"keep_awake_engaged",
|
||||
"keep_awake_released",
|
||||
"local_admin_analysis",
|
||||
"log_entry",
|
||||
"mdm_policy_reboot_required",
|
||||
"modern_deployment_error",
|
||||
"modern_deployment_log",
|
||||
"modern_deployment_warning",
|
||||
"network_adapters",
|
||||
"network_bandwidth_estimate",
|
||||
"network_connectivity_check",
|
||||
"network_interface_info",
|
||||
"network_state_change",
|
||||
"ntp_time_check",
|
||||
"office_install_completed",
|
||||
"office_install_failed",
|
||||
"office_install_progress",
|
||||
"office_install_started",
|
||||
"office_preinstalled_detected",
|
||||
"oobe_console_spawned",
|
||||
"oobe_state_completed",
|
||||
"os_build_changed",
|
||||
"os_info",
|
||||
"outbound_ip",
|
||||
"performance_collector_stopped",
|
||||
"performance_snapshot",
|
||||
"phase_transition",
|
||||
"power_state_check",
|
||||
"previous_crash_detected",
|
||||
"prior_run_died_with_state",
|
||||
"provisioning_package_scan",
|
||||
"proxy_configuration",
|
||||
"realmjoin_detected",
|
||||
"realmjoin_first_deployment_incomplete",
|
||||
"realmjoin_package_completed",
|
||||
"realmjoin_package_started",
|
||||
"realmjoin_phase_changed",
|
||||
"realmjoin_resolved",
|
||||
"realmjoin_timeout",
|
||||
"reboot_triggered",
|
||||
"remote_config_fetch_failed",
|
||||
"script_completed",
|
||||
"script_failed",
|
||||
"script_started",
|
||||
"script_timeout_suspected",
|
||||
"secureboot_status",
|
||||
"security_audit",
|
||||
"security_warning",
|
||||
"server_action_executed",
|
||||
"server_action_failed",
|
||||
"server_action_received",
|
||||
"session_parked_without_deadline",
|
||||
"session_stalled",
|
||||
"session_timeout",
|
||||
"shadow_discrepancy",
|
||||
"software_inventory_analysis",
|
||||
"spool_pressure_detected",
|
||||
"stall_probe_check",
|
||||
"stall_probe_result",
|
||||
"state_quarantine_recovered",
|
||||
"system_reboot_detected",
|
||||
"telemetry_upload_blocked",
|
||||
"telemetry_upload_poisoned",
|
||||
"timezone_auto_set",
|
||||
"tpm_status",
|
||||
"vulnerability_report",
|
||||
"waiting_for_hello",
|
||||
"whiteglove_classification",
|
||||
"whiteglove_complete",
|
||||
"whiteglove_part1_complete",
|
||||
"whiteglove_resumed",
|
||||
"whiteglove_started",
|
||||
"wifi_signal_info",
|
||||
"windows_update_channel_census",
|
||||
"windows_update_failed",
|
||||
"windows_update_history",
|
||||
"windows_update_reboot_pending",
|
||||
"windows_update_started",
|
||||
"windows_update_succeeded"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* COMPILE-TIME drift checks between hand-written TS mirrors and the C#-generated
|
||||
* shared manifest (utils/shared-manifests.json → shared-manifests.generated.ts).
|
||||
*
|
||||
* A red squiggle / failing `tsc --noEmit` here means one side changed without the
|
||||
* other: either add the field to the TS mirror, or regenerate the manifest
|
||||
* (AM_WRITE_SHARED_MANIFESTS=1 dotnet test --filter SharedManifestParityTests,
|
||||
* then node scripts/generate-shared-manifest-types.js).
|
||||
*
|
||||
* Runtime (value-level) parity lives in utils/__tests__/sharedManifestParity.test.ts.
|
||||
*/
|
||||
import type { AdminConfiguration } from "@/types/adminConfig";
|
||||
import { SHARED_MANIFEST } from "./shared-manifests.generated";
|
||||
|
||||
/** Resolves only when T is empty — a non-empty union here IS the drift report. */
|
||||
type AssertNever<T extends never> = T;
|
||||
|
||||
type AdminWireKey = (typeof SHARED_MANIFEST)["adminConfiguration"]["fields"][number];
|
||||
|
||||
/** C# fields the TS interface is missing. Must stay `never`. */
|
||||
export type AdminConfigFieldsMissingInTs = AssertNever<
|
||||
Exclude<AdminWireKey, keyof AdminConfiguration>
|
||||
>;
|
||||
|
||||
/** TS fields the C# model no longer has (ghost fields). Must stay `never`. */
|
||||
export type AdminConfigGhostFieldsInTs = AssertNever<
|
||||
Exclude<keyof AdminConfiguration, AdminWireKey>
|
||||
>;
|
||||
+5
-1
@@ -5,4 +5,8 @@
|
||||
# Reducer: HandleRealmJoinTimeoutDeadlineFired → sets RealmJoinFacts.Outcome="Timeout",
|
||||
# emits a realmjoin_timeout timeline event, and (if Hello+Desktop already resolved OR
|
||||
# SelfDeployingDeferredCompletion is set) routes through CompleteIfDeferredOrBookkeep.
|
||||
{"SessionSignalOrdinal":99,"SessionTraceOrdinal":99,"Kind":"DeadlineFired","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-21T11:00:00.0000000Z","SourceOrigin":"DeadlineScheduler","Evidence":{"Kind":"Derived","Identifier":"deadline-realmjoin-timeout","Summary":"RealmJoin hard timeout fired"},"Payload":{"deadline":"realmjoin_timeout"}}
|
||||
# Evidence is Synthetic — production DeadlineFired posts come from
|
||||
# EnrollmentOrchestrator.OnDeadlineFired with EvidenceKind.Synthetic (Derived evidence would
|
||||
# require DerivationInputs and fail SignalSerializer.Deserialize; caught by
|
||||
# SignalKindFixtureCoverageTests 2026-08-13 — this fixture previously could not replay at all).
|
||||
{"SessionSignalOrdinal":99,"SessionTraceOrdinal":99,"Kind":"DeadlineFired","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-21T11:00:00.0000000Z","SourceOrigin":"DeadlineScheduler","Evidence":{"Kind":"Synthetic","Identifier":"realmjoin_timeout","Summary":"RealmJoin hard timeout fired"},"Payload":{"deadline":"realmjoin_timeout"}}
|
||||
|
||||
Reference in New Issue
Block a user