fix(agent): stop Office detector false-failed; defer completion to TasksState capture

Field session 8353e03b declared a successful Office install as failed. Root cause
(diag zip + do-status.jsonl + registry):
- OfficeC2RClient.exe is a transient launcher (~2s in the field) that kicks off the
  operation; the OfficeClickToRun.exe service installs for minutes after it exits.
  Treating worker-exit as completion → false office_install_failed.
- The registry has NO StreamingFinished value (so the completion branch could never
  fire and the "abandoned" branch always did), and Scenario\INSTALL persists
  post-install (so presence is not an "active" signal).
- Office C2R content comes from f.c2r.ts.cdn.office.net AND officecdn.microsoft.com;
  the old DO matcher only checked "officecdn" and would never attribute Office jobs.

Conservative fix (completion/failure detection deferred until a live-install capture
of Scenario\INSTALL\TasksState transitions — the real signal: per-task
TASKSTATE_COMPLETED/_INPROGRESS):
- OnWorkerStopped no longer finalizes — emits no terminal. started + progress only.
- Removed the StreamingFinished-based completed/abandoned logic; only an explicit
  numeric error code finalizes as failed.
- phase label is "Installing" (cannot distinguish Streaming/Finalizing without a real
  signal).
- DO Office matcher now keys on the stable CDN hosts cdn.office.net +
  officecdn.microsoft.com (dropped the version-fragile _16_0_ FileId marker).

V2 suite 1257/1257 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-06-08 19:21:02 +02:00
parent da15079f34
commit 01de1790e0
3 changed files with 61 additions and 85 deletions
@@ -122,7 +122,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Office
Assert.Equal("Info", Severity(started));
var data = Data(started);
Assert.Equal("Streaming", data["phase"]);
Assert.Equal("Installing", data["phase"]);
Assert.Equal("INSTALL", data["scenario"]);
Assert.Contains("O365ProPlusRetail", (List<string>)data["products"]);
Assert.Equal("Current", data["channel"]);
@@ -220,71 +220,45 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Office
Assert.Equal(2, events.Count); // started + one progress only
}
// ---------------------------------------------------------------- completed
// ---------------------------------------------------------------- terminal (conservative v1)
[Fact]
public void Worker_stopped_with_streaming_finished_emits_completed_with_version_and_duration()
public void Worker_stopped_emits_no_terminal_event()
{
// Conservative v1: the transient worker's exit is NOT install completion — no completed/
// failed is emitted (field session 8353e03b false-failed). started/progress only.
using var rig = new Rig(ActiveStreaming());
rig.Sut.OnWorkerStarted(); // started
rig.Clock.Advance(TimeSpan.FromSeconds(412));
rig.Current = CompletedIdle("16.0.17628.20144");
rig.Sut.OnWorkerStopped(); // completed
var events = rig.OfficeEvents();
Assert.Equal(2, events.Count);
var completed = events[1];
Assert.Equal(Constants.EventTypes.OfficeInstallCompleted, EventType(completed));
Assert.Equal("Info", Severity(completed));
var data = Data(completed);
Assert.Equal("Completed", data["phase"]);
Assert.Equal("16.0.17628.20144", data["versionReached"]);
Assert.Equal(412, data["durationSeconds"]);
}
[Fact]
public void Terminal_state_latches_no_further_events()
{
using var rig = new Rig(ActiveStreaming());
rig.Sut.OnWorkerStarted();
rig.Sut.OnWorkerStarted(); // started
rig.Current = CompletedIdle();
rig.Sut.OnWorkerStopped(); // completed
rig.Current = ActiveStreaming();
rig.Sut.OnWorkerStarted(); // must NOT re-trigger (one lifecycle)
rig.Sut.OnRegistryChanged();
rig.Sut.OnWorkerStopped(); // must NOT emit a terminal
var events = rig.OfficeEvents();
Assert.Equal(2, events.Count);
Assert.Equal(Constants.EventTypes.OfficeInstallCompleted, EventType(events[1]));
Assert.Single(events);
Assert.Equal(Constants.EventTypes.OfficeInstallStarted, EventType(events[0]));
}
// ---------------------------------------------------------------- failed
[Fact]
public void Worker_stopped_without_streaming_finished_emits_failed_warning()
public void After_worker_stopped_further_events_are_ignored()
{
using var rig = new Rig(ActiveStreaming());
rig.Sut.OnWorkerStarted();
rig.Sut.OnWorkerStarted(); // started
rig.Sut.OnWorkerStopped(); // stop tracking (no terminal)
rig.Current = new OfficeC2RSnapshot
{
ConfigurationKeyPresent = true,
StreamingFinished = false,
ActiveScenarioPresent = false,
OfficeC2RClientRunning = false,
};
rig.Sut.OnWorkerStopped(); // failed (abandoned)
// A brand-new worker / registry change must not re-trigger (one lifecycle).
rig.Current = ActiveStreaming();
rig.Sut.OnWorkerStarted();
rig.Sut.OnRegistryChanged();
rig.Sut.OnOfficeDoSample(new OfficeDoSample { JobCount = 1, FileSize = 100, TotalBytesDownloaded = 50 });
var events = rig.OfficeEvents();
Assert.Equal(2, events.Count);
Assert.Equal(Constants.EventTypes.OfficeInstallFailed, EventType(events[1]));
Assert.Equal("Warning", Severity(events[1]));
Assert.Single(events);
Assert.Equal(Constants.EventTypes.OfficeInstallStarted, EventType(events[0]));
}
// ---------------------------------------------------------------- failed (error code only)
[Fact]
public void Observed_error_code_during_progress_emits_failed_error_with_code()
{
@@ -30,9 +30,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
/// <item><see cref="OnOfficeDoSample"/> — aggregated Delivery-Optimization stats for Office's CDN
/// download (from the extended DeliveryOptimizationCollector). DO has no push API, so it is
/// sampled while the worker is alive; a byte advance folds a real download-% into progress.</item>
/// <item><see cref="OnWorkerStopped"/> — last worker exited (<see cref="Process.Exited"/>) →
/// emit <see cref="Constants.EventTypes.OfficeInstallCompleted"/> /
/// <see cref="Constants.EventTypes.OfficeInstallFailed"/> with version + duration + DO summary.</item>
/// <item><see cref="OnWorkerStopped"/> — last worker exited. NOT treated as completion (the
/// worker is a transient launcher; the C2R service installs on for minutes). Conservative v1:
/// stop tracking, emit no terminal. Completion/failure (via Scenario\INSTALL\TasksState) is
/// deferred to the live-install capture iteration. Only an explicit numeric error code finalizes
/// as <see cref="Constants.EventTypes.OfficeInstallFailed"/>.</item>
/// </list>
/// </para>
/// <para>
@@ -134,15 +136,25 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
}
}
/// <summary>Last Office C2R worker exited — resolve terminal completed / failed.</summary>
/// <summary>
/// Last Office C2R worker exited. The worker (OfficeC2RClient.exe) is a transient launcher —
/// in the field (session 8353e03b) it lived only ~2s and kicked off the operation, while the
/// OfficeClickToRun.exe service ran the actual install for minutes afterwards. Its exit is
/// therefore NOT install completion. Emitting failed on worker-exit was a false-positive, so
/// we do NOT finalize here — we simply stop tracking and emit nothing.
/// <para>
/// Completion/failure detection is deferred (conservative v1): the registry has NO
/// <c>StreamingFinished</c> value and <c>Scenario\INSTALL</c> persists post-install, so neither
/// is a usable signal. The real completion signal is <c>Scenario\INSTALL\TasksState</c>
/// (per-task <c>TASKSTATE_COMPLETED</c> / <c>_INPROGRESS</c> / failure); the in-progress/failed
/// wording is being captured from a live install before it is wired up — see office-followup.
/// </para>
/// </summary>
public void OnWorkerStopped()
{
lock (_lock)
{
if (_state != DetectorState.Active) return;
var snap = ReadSnapshotSafe();
if (snap == null) snap = new OfficeC2RSnapshot();
Finalize(snap);
if (_state == DetectorState.Active) _state = DetectorState.Terminal; // stop; no terminal event
}
}
@@ -152,9 +164,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
private void EvaluateActive(OfficeC2RSnapshot snap)
{
// Conservative v1: the only terminal we trust is an explicit numeric error code. Completion
// is NOT claimed (TasksState-based detection lands with the live-install capture).
if (!string.IsNullOrEmpty(snap.ErrorCode))
{
Finalize(snap);
FinalizeFailed(snap);
return;
}
@@ -166,33 +180,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
}
}
private void Finalize(OfficeC2RSnapshot snap)
private void FinalizeFailed(OfficeC2RSnapshot snap)
{
if (_state == DetectorState.Terminal) return;
_state = DetectorState.Terminal;
string eventType;
EventSeverity severity;
string phase;
if (!string.IsNullOrEmpty(snap.ErrorCode))
{
eventType = Constants.EventTypes.OfficeInstallFailed; severity = EventSeverity.Error; phase = "Failed";
}
else if (snap.StreamingFinished)
{
eventType = Constants.EventTypes.OfficeInstallCompleted; severity = EventSeverity.Info; phase = "Completed";
}
else
{
// Worker gone without StreamingFinished → abandoned install.
eventType = Constants.EventTypes.OfficeInstallFailed; severity = EventSeverity.Warning; phase = "Failed";
}
EmitLifecycle(eventType, snap, severity, phase, isTerminal: true);
EmitLifecycle(Constants.EventTypes.OfficeInstallFailed, snap, EventSeverity.Error, "Failed", isTerminal: true);
}
private static string PhaseOf(OfficeC2RSnapshot snap)
=> snap.StreamingFinished ? "Finalizing" : "Streaming";
// Conservative v1: StreamingFinished does not exist in the C2R registry, so we cannot label
// Streaming vs Finalizing — report a single "Installing" phase until TasksState-based phase
// detection is added.
private static string PhaseOf(OfficeC2RSnapshot snap) => "Installing";
/// <summary>
/// Stable signature of everything we treat as "progress". A change in any of these (registry
@@ -203,8 +201,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
{
var parts = new List<string>
{
"phase=" + PhaseOf(snap),
"streaming=" + snap.StreamingFinished,
"version=" + (snap.VersionToReport ?? string.Empty),
"scenario=" + (snap.ActiveScenarioName ?? string.Empty),
};
@@ -469,16 +469,22 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic
$"{progressCount} progress updates, {matchCount} new matches ({_enrichedAppIds.Count} total enriched)");
}
// Office C2R content CDN hosts — version-independent (a FileId build marker like "_16_0_" would
// break on older/newer Office versions). Field-validated against session 8353e03b: C2R uses BOTH
// the primary CDN officecdn.microsoft.com (registry OriginalCDNDomain) and the content CDN
// f.c2r.ts.cdn.office.net (registry FailoverDomain / PreferredCDNPrefix).
private static readonly string[] OfficeCdnHosts = { "cdn.office.net", "officecdn.microsoft.com" };
/// <summary>
/// True when a DO job's SourceURL points at the Office CDN (Office C2R content), i.e. a job
/// that is not an IME Win32 app. TODO(office-followup): validate the exact SourceURL/FileId
/// shape against a real enrollment's do-status.jsonl and tighten if needed.
/// True when a DO job's SourceURL points at an Office C2R content CDN (not an IME Win32 app).
/// Matched purely by the stable CDN host so it survives Office version changes.
/// </summary>
private static bool IsOfficeCdnJob(string sourceUrl)
{
if (string.IsNullOrEmpty(sourceUrl)) return false;
return sourceUrl.IndexOf("officecdn", StringComparison.OrdinalIgnoreCase) >= 0
|| sourceUrl.IndexOf("officeclient.microsoft.com", StringComparison.OrdinalIgnoreCase) >= 0;
foreach (var host in OfficeCdnHosts)
if (sourceUrl.IndexOf(host, StringComparison.OrdinalIgnoreCase) >= 0) return true;
return false;
}
// -----------------------------------------------------------------------