diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Termination/EnrollmentTerminationHandlerTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Termination/EnrollmentTerminationHandlerTests.cs index 6debe090..a4053311 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Termination/EnrollmentTerminationHandlerTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Termination/EnrollmentTerminationHandlerTests.cs @@ -424,6 +424,57 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Termination Assert.Contains("enrollment_summary_shown", rig.EmittedEventTypes); } + // ============================================================= PR-X1 (bdb3cf9d, 2026-05-04) + // WhiteGlove Part 1 must NOT launch the summary dialog or emit + // enrollment_summary_shown — the device is about to be reseal-rebooted, the user + // session does not exist yet, and apps have not installed. The other terminal + // stages (Completed / Failed / WhiteGloveCompletedPart2) still launch the dialog. + + [Fact] + public void Handle_skips_summary_dialog_on_whiteglove_part1_even_with_dialog_enabled() + { + using var rig = new Rig(); + rig.State = new DecisionStateBuilder(DecisionState.CreateInitial("S1", "T1")) { Stage = SessionStage.WhiteGloveSealed }.Build(); + // Self-destruct stays off so we don't tear down between assertions, dialog is + // explicitly enabled to prove the Part-1 stage gate beats the config flag. + var cfg = rig.BuildConfig(showDialog: true, selfDestruct: false); + + rig.Build(cfg).Handle(sender: null!, + Args(EnrollmentTerminationReason.DecisionTerminalStage, EnrollmentTerminationOutcome.Succeeded, SessionStage.WhiteGloveSealed)); + + Assert.DoesNotContain("enrollment_summary_shown", rig.EmittedEventTypes); + Assert.False(File.Exists(Path.Combine(rig.StateDir, SummaryDialogLauncher.FinalStatusFileName)), + "final-status.json must not be written on WG Part 1 — the dialog launch is skipped."); + } + + [Fact] + public void Handle_emits_enrollment_summary_shown_on_whiteglove_part2_completion() + { + using var rig = new Rig(); + rig.State = new DecisionStateBuilder(DecisionState.CreateInitial("S1", "T1")) { Stage = SessionStage.WhiteGloveCompletedPart2 }.Build(); + var cfg = rig.BuildConfig(showDialog: true, selfDestruct: false); + + rig.Build(cfg).Handle(sender: null!, + Args(EnrollmentTerminationReason.DecisionTerminalStage, EnrollmentTerminationOutcome.Succeeded, SessionStage.WhiteGloveCompletedPart2)); + + Assert.Contains("enrollment_summary_shown", rig.EmittedEventTypes); + Assert.True(File.Exists(Path.Combine(rig.StateDir, SummaryDialogLauncher.FinalStatusFileName))); + } + + [Fact] + public void Handle_emits_enrollment_summary_shown_on_failed_termination() + { + using var rig = new Rig(); + rig.State = new DecisionStateBuilder(DecisionState.CreateInitial("S1", "T1")) { Stage = SessionStage.Failed }.Build(); + var cfg = rig.BuildConfig(showDialog: true, selfDestruct: false); + + rig.Build(cfg).Handle(sender: null!, + Args(EnrollmentTerminationReason.DecisionTerminalStage, EnrollmentTerminationOutcome.Failed, SessionStage.Failed)); + + Assert.Contains("enrollment_summary_shown", rig.EmittedEventTypes); + Assert.True(File.Exists(Path.Combine(rig.StateDir, SummaryDialogLauncher.FinalStatusFileName))); + } + // ============================================================= Plan §6.2 agent_shutting_down [Fact] diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Termination/EnrollmentTerminationHandler.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Termination/EnrollmentTerminationHandler.cs index 812cba70..0b186871 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Termination/EnrollmentTerminationHandler.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Termination/EnrollmentTerminationHandler.cs @@ -191,13 +191,23 @@ namespace AutopilotMonitor.Agent.V2.Core.Termination // also ends the running agent process. EmitAgentShuttingDown(args); - if (state == null) + // PR-X1 (bdb3cf9d follow-up, 2026-05-04) — WhiteGlove Part 1 sealing has + // no end-user session and no completed app installs. Launching the summary + // dialog there either lands invisibly in the SYSTEM context (about to be + // reseal-rebooted) or fails outright; either way the enrollment_summary_shown + // event is semantically wrong and pollutes the timeline. Skip the dialog + // build + launch + final-status.json write entirely on Part 1; the regular + // Completed / Failed / WhiteGloveCompletedPart2 paths still run unchanged. + if (!isWhiteGlovePart1) { - _logger.Warning("EnrollmentTerminationHandler: current state unavailable — skipping FinalStatus + SummaryDialog."); - } - else - { - RunBuildAndLaunchDialog(state, args); + if (state == null) + { + _logger.Warning("EnrollmentTerminationHandler: current state unavailable — skipping FinalStatus + SummaryDialog."); + } + else + { + RunBuildAndLaunchDialog(state, args); + } } // Plan §5 Fix 4b — emit app_tracking_summary terminal event before the diff --git a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt index 30ce1158..3bfc51a6 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt @@ -1 +1 @@ -559 +562 diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index bd9b5070..cd2bf3dd 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -1242 +1243