test(gates): sequence the two-client remote-shadow gate with harness signals

Imported from the codex worktree's uncommitted work
(C:/Users/erikn/.codex/worktrees/bd98/acdream on
codex/atmospheric-rendering-campaign), on top of its 8b7b601b.

The remote-player shadow gate ran two clients on fixed sleeps, so the
observer could move before the primary had taken its 'before' screenshot,
or the primary could take its 'after' shot before the observer had moved.
Now the harness publishes named signal files into each client's artifact
directory and the routes block on them:

- IRetailUiAutomationRuntime.TryIsAutomationSignalPublished, implemented
  by WorldLifecycleAutomationController over <artifactDir>/signals/
  <name>.signal (names validated by AutomationArtifactName, so no path
  escape).
- 'wait signal <name> [timeoutMs]' in RetailUiAutomationScriptRunner.
- run-connected-render-pack-remote-player-gate.ps1 publishes
  'primary-before' to the observer after the primary's before-shot
  completes, then 'observer-moved' to the primary after the observer's
  remote-observer-moved checkpoint.
- Both routes teleport with an explicit heading and wait 12 s to settle.

Build green; the three touched App test classes pass 58/58 including the
two new signal tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-22 17:44:13 +02:00
parent 8b7b601b32
commit c51b07ef95
8 changed files with 189 additions and 10 deletions

View file

@ -561,4 +561,21 @@ internal sealed class WorldLifecycleAutomationController :
_screenshots.TryRequest(name, out error);
public bool IsScreenshotComplete(string name) => _screenshots.IsComplete(name);
public bool TryIsAutomationSignalPublished(
string name,
out bool published,
out string error)
{
published = false;
if (!AutomationArtifactName.TryValidate(name, out error))
return false;
published = File.Exists(Path.Combine(
_artifactDirectory,
"signals",
name + ".signal"));
error = string.Empty;
return true;
}
}

View file

@ -99,6 +99,15 @@ public interface IRetailUiAutomationRuntime
void CancelCheckpoint(IRetailUiAutomationCheckpoint checkpoint);
bool TryRequestScreenshot(string name, out string error);
bool IsScreenshotComplete(string name);
bool TryIsAutomationSignalPublished(
string name,
out bool published,
out string error)
{
published = false;
error = "automation signals require ACDREAM_AUTOMATION_ARTIFACT_DIR";
return false;
}
}
/// <summary>
@ -395,7 +404,7 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
private bool DoWait(ScriptCommand command)
{
var p = command.Parts;
if (p.Length < 2) return Stop(command, "usage: wait item|element|ms|world-ready|world-visible|materialized|render-pack|render-pack-samples|framebuffer ...");
if (p.Length < 2) return Stop(command, "usage: wait item|element|ms|world-ready|world-visible|materialized|render-pack|render-pack-samples|framebuffer|signal ...");
string target = p[1].ToLowerInvariant();
if (target == "item")
@ -537,7 +546,28 @@ public sealed class RetailUiAutomationScriptRunner : IDisposable
$"framebuffer {width}x{height}");
}
return Stop(command, "usage: wait item|element|ms|world-ready|world-visible|materialized|render-pack|render-pack-samples|framebuffer ...");
if (target == "signal")
{
if (_runtime is null)
return Stop(command, "automation signal runtime is unavailable");
if (p.Length < 3)
return Stop(command, "usage: wait signal <name> [timeoutMs]");
if (!_runtime.TryIsAutomationSignalPublished(
p[2],
out bool published,
out string error))
{
return Stop(command, error);
}
if (published)
return true;
return WaitOrTimeout(
command,
TimeoutMs(p, 3, 120000),
$"automation signal '{p[2]}'");
}
return Stop(command, "usage: wait item|element|ms|world-ready|world-visible|materialized|render-pack|render-pack-samples|framebuffer|signal ...");
}
private bool DoRenderPack(ScriptCommand command)