fix(diagnostics): keep UI scripts on game-loop time
Preserve sub-millisecond frame deltas with a command-local compensated clock so uncapped diagnostic routes do not outrun materialization and settling. Keep sleep boundaries inclusive, wait timeouts strict, and handle long-duration and invalid-delta cases deterministically. Record the aligned seven-destination connected route with a full turn at every stop. Release build succeeds and all 6,308 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
3718e341be
commit
b95bb3e21c
4 changed files with 336 additions and 15 deletions
|
|
@ -204,4 +204,241 @@ public sealed class RetailUiAutomationProbeTests
|
|||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptRunner_sleep_preserves_fractional_uncapped_frame_time()
|
||||
{
|
||||
var (root, _, _, _, objects) = RootWithTwoItemLists();
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
var submitted = new List<string>();
|
||||
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
|
||||
File.WriteAllLines(path, ["sleep 10", "command /ls"]);
|
||||
|
||||
try
|
||||
{
|
||||
var runner = new RetailUiAutomationScriptRunner(
|
||||
probe,
|
||||
path,
|
||||
dumpOnStart: false,
|
||||
submitCommand: submitted.Add);
|
||||
|
||||
runner.Tick(0d); // Arm the sleep at script time zero.
|
||||
for (int i = 0; i < 1_000; i++)
|
||||
runner.Tick(0.000001); // 1 ms total, not the old artificial 1,000 ms.
|
||||
|
||||
Assert.Empty(submitted);
|
||||
Assert.False(runner.Completed);
|
||||
|
||||
runner.Tick(0.009); // Exact 10 ms deadline, including the 1 ms above.
|
||||
|
||||
Assert.Equal(["/ls"], submitted);
|
||||
Assert.True(runner.Completed);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptRunner_sleep_releases_on_exact_fractional_frame_deadline()
|
||||
{
|
||||
var (root, _, _, _, objects) = RootWithTwoItemLists();
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
var submitted = new List<string>();
|
||||
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
|
||||
File.WriteAllLines(path, ["sleep 1000", "command /ls"]);
|
||||
|
||||
try
|
||||
{
|
||||
var runner = new RetailUiAutomationScriptRunner(
|
||||
probe,
|
||||
path,
|
||||
dumpOnStart: false,
|
||||
submitCommand: submitted.Add);
|
||||
|
||||
runner.Tick(0d); // Arm the sleep at script time zero.
|
||||
for (int i = 0; i < 60; i++)
|
||||
runner.Tick(1d / 60d);
|
||||
|
||||
Assert.Equal(["/ls"], submitted);
|
||||
Assert.True(runner.Completed);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptRunner_sleep_rebases_fractional_clock_after_long_prior_command()
|
||||
{
|
||||
var (root, _, _, _, objects) = RootWithTwoItemLists();
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
var submitted = new List<string>();
|
||||
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
|
||||
File.WriteAllLines(path, ["sleep 1000000", "sleep 1000", "command /ls"]);
|
||||
|
||||
try
|
||||
{
|
||||
var runner = new RetailUiAutomationScriptRunner(
|
||||
probe,
|
||||
path,
|
||||
dumpOnStart: false,
|
||||
submitCommand: submitted.Add);
|
||||
|
||||
runner.Tick(0d);
|
||||
runner.Tick(1000d); // Complete the 1,000,000 ms precursor and arm the next sleep.
|
||||
for (int i = 0; i < 60; i++)
|
||||
runner.Tick(1d / 60d);
|
||||
|
||||
Assert.Equal(["/ls"], submitted);
|
||||
Assert.True(runner.Completed);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptRunner_sleep_ignores_invalid_or_overflowing_frame_deltas()
|
||||
{
|
||||
var (root, _, _, _, objects) = RootWithTwoItemLists();
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
var submitted = new List<string>();
|
||||
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
|
||||
File.WriteAllLines(path, ["sleep 1", "command /ls"]);
|
||||
|
||||
try
|
||||
{
|
||||
var runner = new RetailUiAutomationScriptRunner(
|
||||
probe,
|
||||
path,
|
||||
dumpOnStart: false,
|
||||
submitCommand: submitted.Add);
|
||||
|
||||
runner.Tick(0d);
|
||||
runner.Tick(double.NaN);
|
||||
runner.Tick(double.PositiveInfinity);
|
||||
runner.Tick(double.NegativeInfinity);
|
||||
runner.Tick(double.MaxValue); // Finite input whose millisecond conversion overflows.
|
||||
runner.Tick(-1d);
|
||||
|
||||
Assert.Empty(submitted);
|
||||
Assert.False(runner.Completed);
|
||||
|
||||
runner.Tick(0.001d);
|
||||
|
||||
Assert.Equal(["/ls"], submitted);
|
||||
Assert.True(runner.Completed);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(30)]
|
||||
[InlineData(120)]
|
||||
public void ScriptRunner_wait_timeout_does_not_fire_on_exact_fractional_deadline(int framesPerSecond)
|
||||
{
|
||||
var (root, _, _, _, objects) = RootWithTwoItemLists();
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
var logs = new List<string>();
|
||||
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
|
||||
File.WriteAllLines(path, ["sleep 1000000", "wait item 0xDEAD inventory 1000"]);
|
||||
|
||||
try
|
||||
{
|
||||
var runner = new RetailUiAutomationScriptRunner(
|
||||
probe,
|
||||
path,
|
||||
dumpOnStart: false,
|
||||
log: logs.Add);
|
||||
|
||||
runner.Tick(0d);
|
||||
runner.Tick(1000d); // Complete the precursor and arm the wait at a rebased zero.
|
||||
for (int i = 0; i < framesPerSecond; i++)
|
||||
runner.Tick(1d / framesPerSecond);
|
||||
|
||||
Assert.False(runner.Completed);
|
||||
Assert.DoesNotContain(logs, line => line.Contains("timed out", StringComparison.Ordinal));
|
||||
|
||||
runner.Tick(1d / framesPerSecond);
|
||||
|
||||
Assert.True(runner.Completed);
|
||||
Assert.Contains(logs, line => line.Contains("timed out", StringComparison.Ordinal));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptRunner_one_hour_sleep_releases_on_exact_60Hz_deadline()
|
||||
{
|
||||
var (root, _, _, _, objects) = RootWithTwoItemLists();
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
var submitted = new List<string>();
|
||||
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
|
||||
File.WriteAllLines(path, ["sleep 3600000", "command /ls"]);
|
||||
|
||||
try
|
||||
{
|
||||
var runner = new RetailUiAutomationScriptRunner(
|
||||
probe,
|
||||
path,
|
||||
dumpOnStart: false,
|
||||
submitCommand: submitted.Add);
|
||||
|
||||
runner.Tick(0d);
|
||||
for (int i = 0; i < 60 * 60 * 60; i++)
|
||||
runner.Tick(1d / 60d);
|
||||
|
||||
Assert.Equal(["/ls"], submitted);
|
||||
Assert.True(runner.Completed);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptRunner_one_hour_wait_does_not_timeout_on_exact_30Hz_deadline()
|
||||
{
|
||||
var (root, _, _, _, objects) = RootWithTwoItemLists();
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
var logs = new List<string>();
|
||||
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
|
||||
File.WriteAllText(path, "wait item 0xDEAD inventory 3600000");
|
||||
|
||||
try
|
||||
{
|
||||
var runner = new RetailUiAutomationScriptRunner(
|
||||
probe,
|
||||
path,
|
||||
dumpOnStart: false,
|
||||
log: logs.Add);
|
||||
|
||||
runner.Tick(0d);
|
||||
for (int i = 0; i < 30 * 60 * 60; i++)
|
||||
runner.Tick(1d / 30d);
|
||||
|
||||
Assert.False(runner.Completed);
|
||||
Assert.DoesNotContain(logs, line => line.Contains("timed out", StringComparison.Ordinal));
|
||||
|
||||
runner.Tick(1d / 30d);
|
||||
|
||||
Assert.True(runner.Completed);
|
||||
Assert.Contains(logs, line => line.Contains("timed out", StringComparison.Ordinal));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue