Merge nucleic/lucid-river-toad-6efj into dev
This commit is contained in:
@@ -129,6 +129,67 @@ public sealed class BrokerServiceTests
|
||||
Assert.Equal("session_exists", error.GetProperty("data").GetProperty("kind").GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression for the bug the first live run hit (docs/WINDOWS_PORT.md §13.3): a command that
|
||||
/// finishes during start — `echo` — used to put `proc.stdout`/`proc.exit` on the wire BEFORE
|
||||
/// the response carrying the `procId` that names them. A client that registers interest when
|
||||
/// it learns the procId then never sees the exit and waits forever, which is exactly how the
|
||||
/// spike hung.
|
||||
///
|
||||
/// The fix is that BrokerService responds before calling StartAsync, so this asserts the
|
||||
/// ORDER and not merely that the messages all arrived.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ProcExec_RespondsBeforeAnyOutputFromAnInstantProcess()
|
||||
{
|
||||
wslc.ContainerStates["box"] = "running";
|
||||
// The fake emits during StartAsync, standing in for a process that has already exited by
|
||||
// the time Start() returns.
|
||||
wslc.NextProcessOnStart = () =>
|
||||
{
|
||||
wslc.Events!.ProcOutput(wslc.LastProcId, stderr: false, "done\n"u8);
|
||||
wslc.Events!.ProcExited(wslc.LastProcId, 0);
|
||||
};
|
||||
|
||||
var lines = await RoundTrip(
|
||||
"""{"jsonrpc":"2.0","id":1,"method":"proc.exec","params":{"container":"box","argv":["echo","done"],"tty":false}}""");
|
||||
|
||||
Assert.True(wslc.LastProcess!.Started, "BrokerService must start the process");
|
||||
// Line 0 must be the response: everything else is meaningless to a client that cannot yet
|
||||
// attribute the notifications.
|
||||
var procId = Result(lines[0]).GetProperty("procId").GetInt64();
|
||||
Assert.Equal(wslc.LastProcId, procId);
|
||||
var methods = lines.Skip(1)
|
||||
.Select(l => l.TryGetProperty("method", out var m) ? m.GetString() : null).ToList();
|
||||
Assert.Equal(["proc.stdout", "proc.exit"], methods);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The response is enqueued before the process is started, so a start failure can no longer be
|
||||
/// a JSON-RPC error — that would put TWO responses under one id. Observed on hardware
|
||||
/// (docs/WINDOWS_PORT.md §13.3), where the client then waited for an exit that never came.
|
||||
/// It must arrive as the process's own stderr plus an exit instead.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ProcExec_StartFailure_ReportsAsExitNotSecondResponse()
|
||||
{
|
||||
wslc.ContainerStates["box"] = "running";
|
||||
wslc.NextProcessOnStart = () => throw new WslcError(WslcError.StartFailed, "container is not running");
|
||||
|
||||
var lines = await RoundTrip(
|
||||
"""{"jsonrpc":"2.0","id":1,"method":"proc.exec","params":{"container":"box","argv":["sh"],"tty":false}}""");
|
||||
|
||||
// Exactly one message carries id 1 — anything else is a protocol violation.
|
||||
var responses = lines.Where(l => l.TryGetProperty("id", out var i) && i.GetInt32() == 1).ToList();
|
||||
Assert.Single(responses);
|
||||
Assert.False(responses[0].TryGetProperty("error", out _), "must be the success response");
|
||||
|
||||
var methods = lines.Skip(1)
|
||||
.Select(l => l.TryGetProperty("method", out var m) ? m.GetString() : null).ToList();
|
||||
Assert.Equal(["proc.stderr", "proc.exit"], methods);
|
||||
Assert.Equal(126, lines.Last().GetProperty("params").GetProperty("code").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImagePull_EmitsProgressNotificationsBeforeResult()
|
||||
{
|
||||
|
||||
@@ -22,6 +22,10 @@ public sealed class FakeWslc : IWslc
|
||||
public long LastProcId;
|
||||
public Exception? NextError;
|
||||
|
||||
/// <summary>Set before a proc.exec to make that process emit during StartAsync — i.e. behave
|
||||
/// like a command that finishes instantly.</summary>
|
||||
public Action? NextProcessOnStart;
|
||||
|
||||
public string? WslcVersion => "9.9-test";
|
||||
|
||||
/// The fake is deliberately fully-capable: it stands in for a facade with BOTH wslc surfaces
|
||||
@@ -157,7 +161,10 @@ public sealed class FakeWslc : IWslc
|
||||
if (ContainerStates.GetValueOrDefault(spec.Container) != "running")
|
||||
throw new WslcError(WslcError.NotRunning, $"container {spec.Container} isn't running");
|
||||
LastProcId = procId;
|
||||
LastProcess = new FakeProcess();
|
||||
// Mirror the real facade: create and attach, but do NOT run — BrokerService runs it after
|
||||
// the procId response is on the wire.
|
||||
LastProcess = new FakeProcess { OnStart = NextProcessOnStart };
|
||||
NextProcessOnStart = null;
|
||||
return Task.FromResult<IWslcProcess>(LastProcess);
|
||||
}
|
||||
|
||||
@@ -167,6 +174,18 @@ public sealed class FakeWslc : IWslc
|
||||
public bool StdinClosed;
|
||||
public List<int> Signals = [];
|
||||
public (int Cols, int Rows)? LastResize;
|
||||
public bool Started;
|
||||
|
||||
/// <summary>Lets a test stand in for a process that finishes DURING start — an `echo`,
|
||||
/// which is what exposed the response-ordering bug on hardware.</summary>
|
||||
public Action? OnStart;
|
||||
|
||||
public Task StartAsync(CancellationToken ct)
|
||||
{
|
||||
Started = true;
|
||||
OnStart?.Invoke();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task WriteStdinAsync(ReadOnlyMemory<byte> data, CancellationToken ct)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user