feat(agent-v2): wire --replay-log-dir CLI flag to ImeLogTracker SimulationMode

V2 had the complete replay infrastructure in place but never wired up:
ImeLogTracker.SimulationMode / SpeedFactor properties (ImeLogTracker.cs:66-67)
and the ApplySimulationDelay compression loop (ImeLogTracker.Handlers.cs:529-546)
were fully implemented, and AgentConfiguration had ReplayLogDir + ReplaySpeedFactor
fields (L.128, L.134), but nothing set them from the CLI and nothing read them in
the factory.

This commit adds:
- CLI args --replay-log-dir <path> and --replay-speed-factor <n> in
  Program.BuildAgentConfiguration (invariant-culture parse, zero/malformed rejected).
- DefaultComponentFactory.ImeLogHost ctor accepts a simulation mode + speed factor
  and applies them to the inner tracker when ReplayLogDir is non-empty.
- PrintUsage documents the flags under a new "Dev / Test" section.

For the deterministic signal-level replay (V2 ReplayHarness + signal-log.jsonl
fixtures) no wiring is needed - that is already the primary V2 test path. The IME-
log replay activated here is the V1 compatibility dev-mode for support cases where
only raw IME logs are available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-04-22 16:06:58 +02:00
parent 5b7f424d19
commit f149eef3fe
5 changed files with 117 additions and 6 deletions
@@ -0,0 +1,66 @@
using System;
using AutopilotMonitor.Agent.V2;
using AutopilotMonitor.Agent.V2.Core.Configuration;
using Xunit;
namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
{
/// <summary>
/// Verifies that <see cref="AutopilotMonitor.Agent.V2.Program.BuildAgentConfiguration"/>
/// propagates the dev-only <c>--replay-log-dir</c> + <c>--replay-speed-factor</c> CLI flags
/// onto <see cref="AgentConfiguration"/> so the IME log replay SimulationMode actually
/// activates downstream. V2 parity PR-A.
/// </summary>
public sealed class BuildAgentConfigurationReplayTests
{
private static AgentConfiguration Build(params string[] args) =>
AutopilotMonitor.Agent.V2.Program.BuildAgentConfiguration(
args: args,
tenantId: "tenant-t",
sessionId: "session-s",
bootstrapConfig: null,
awaitConfig: null);
[Fact]
public void Without_replay_flags_ReplayLogDir_stays_null_and_speed_factor_default()
{
var cfg = Build();
Assert.Null(cfg.ReplayLogDir);
Assert.Equal(50.0, cfg.ReplaySpeedFactor);
}
[Fact]
public void Replay_log_dir_populates_configuration()
{
var cfg = Build("--replay-log-dir", @"C:\tmp\ime-logs");
Assert.Equal(@"C:\tmp\ime-logs", cfg.ReplayLogDir);
}
[Fact]
public void Replay_speed_factor_is_parsed_as_invariant_double()
{
// Comma locale must NOT flip the meaning — parser uses InvariantCulture.
var cfg = Build("--replay-log-dir", @"C:\tmp\x", "--replay-speed-factor", "125.5");
Assert.Equal(125.5, cfg.ReplaySpeedFactor);
}
[Fact]
public void Replay_speed_factor_zero_is_rejected_and_default_kept()
{
var cfg = Build("--replay-log-dir", @"C:\tmp\x", "--replay-speed-factor", "0");
Assert.Equal(50.0, cfg.ReplaySpeedFactor);
}
[Fact]
public void Replay_speed_factor_malformed_is_rejected_and_default_kept()
{
var cfg = Build("--replay-log-dir", @"C:\tmp\x", "--replay-speed-factor", "fast");
Assert.Equal(50.0, cfg.ReplaySpeedFactor);
}
}
}
@@ -126,6 +126,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
hosts.Add(new AadJoinHost(logger, ingress, clock));
// Dev / test — if --replay-log-dir is set, the tracker reads from the replay folder
// with SimulationMode ON + the configured SpeedFactor instead of tailing the live
// IME log folder. Production agents leave ReplayLogDir empty.
var simulationMode = !string.IsNullOrEmpty(_agentConfig.ReplayLogDir);
var imeLogFolder = simulationMode
? _agentConfig.ReplayLogDir
: _agentConfig.ImeLogPathOverride;
_imeLogHost = new ImeLogHost(
sessionId: sessionId,
tenantId: tenantId,
@@ -133,11 +141,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
logger: logger,
ingress: ingress,
clock: clock,
imeLogPathOverride: _agentConfig.ImeLogPathOverride,
imeLogPathOverride: imeLogFolder,
imeMatchLogPath: _agentConfig.ImeMatchLogPath,
imePatterns: _remoteConfig.ImeLogPatterns,
stateDirectory: _stateDirectory,
whiteGloveSealingPatternIds: whiteGloveSealingPatternIds);
whiteGloveSealingPatternIds: whiteGloveSealingPatternIds,
simulationMode: simulationMode,
simulationSpeedFactor: _agentConfig.ReplaySpeedFactor);
hosts.Add(_imeLogHost);
if (collectors.StallProbeEnabled)
@@ -401,6 +411,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
/// </summary>
internal Monitoring.Enrollment.Ime.ImeLogTracker Tracker => _tracker;
/// <summary>
/// Exposes the tracker's simulation flag so the Dev / Test CLI flag
/// <c>--replay-log-dir</c> is testable without poking through reflection.
/// </summary>
internal bool IsSimulationMode => _tracker.SimulationMode;
public ImeLogHost(
string sessionId,
string tenantId,
@@ -412,7 +428,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
string? imeMatchLogPath,
List<ImeLogPattern>? imePatterns,
string stateDirectory,
IReadOnlyCollection<string>? whiteGloveSealingPatternIds)
IReadOnlyCollection<string>? whiteGloveSealingPatternIds,
bool simulationMode = false,
double simulationSpeedFactor = 50)
{
_logger = logger;
var logFolder = string.IsNullOrEmpty(imeLogPathOverride) ? DefaultImeLogFolder : imeLogPathOverride!;
@@ -428,6 +446,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
matchLogPath: expandedMatchLogPath,
stateDirectory: stateDirectory);
if (simulationMode)
{
_tracker.SimulationMode = true;
_tracker.SpeedFactor = simulationSpeedFactor;
logger.Info($"ImeLogHost: SimulationMode ENABLED (speedFactor={simulationSpeedFactor}, path={logFolder})");
}
_adapter = new ImeLogTrackerAdapter(_tracker, ingress, clock, whiteGloveSealingPatternIds);
_processWatcher = new ImeProcessWatcher(sessionId, tenantId, onEnrollmentEvent, logger);
+21 -1
View File
@@ -1113,7 +1113,7 @@ namespace AutopilotMonitor.Agent.V2
// ---------------------------------------------------------------- Configuration
private static AgentConfiguration BuildAgentConfiguration(
internal static AgentConfiguration BuildAgentConfiguration(
string[] args,
string tenantId,
string sessionId,
@@ -1124,6 +1124,20 @@ namespace AutopilotMonitor.Agent.V2
var imeLogPathOverride = GetArgValue(args, "--ime-log-path");
var imeMatchLogPath = GetArgValue(args, "--ime-match-log");
// Dev / test — IME log replay (V1 compat mode). Feeds ImeLogTracker.SimulationMode
// + SpeedFactor so recorded raw IME logs are replayed at an accelerated rate; signal
// timestamps + ingress ordinals are regenerated as the replay runs.
var replayLogDir = GetArgValue(args, "--replay-log-dir");
var replaySpeedFactorRaw = GetArgValue(args, "--replay-speed-factor");
var replaySpeedFactor = 50.0;
if (!string.IsNullOrEmpty(replaySpeedFactorRaw)
&& double.TryParse(replaySpeedFactorRaw, System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var parsedSpeed)
&& parsedSpeed > 0)
{
replaySpeedFactor = parsedSpeed;
}
var bootstrapToken = GetArgValue(args, "--bootstrap-token")
?? bootstrapConfig?.BootstrapToken;
@@ -1169,6 +1183,8 @@ namespace AutopilotMonitor.Agent.V2
KeepLogFile = keepLogFile,
SelfDestructOnComplete = !noCleanup,
CommandLineArgs = FormatArgsForLog(args),
ReplayLogDir = replayLogDir,
ReplaySpeedFactor = replaySpeedFactor,
};
}
@@ -1269,6 +1285,10 @@ namespace AutopilotMonitor.Agent.V2
Console.Out.WriteLine(" --api-url <URL> Override backend API base URL (alias: --backend-api)");
Console.Out.WriteLine(" --ime-log-path <PATH> Override IME logs directory");
Console.Out.WriteLine(" --ime-match-log <PATH> Write matched IME log lines to file (debug)");
Console.Out.WriteLine();
Console.Out.WriteLine("Dev / Test:");
Console.Out.WriteLine(" --replay-log-dir <PATH> Replay IME logs from this directory (simulation mode)");
Console.Out.WriteLine(" --replay-speed-factor <N> Time-compression factor for log replay (default: 50)");
}
private static void PrintVersion()
@@ -1 +1 @@
104
106
@@ -1 +1 @@
1073
1074