From c85acad632d18929c536e43b5ed6ea9bdefdd9d0 Mon Sep 17 00:00:00 2001 From: skulitom Date: Wed, 7 Oct 2026 23:19:40 +0100 Subject: [PATCH 1/2] Quick checks: read a command job until it finishes instead of judging one 10 s wait (B-024) The "execution jobs" check started Windows PowerShell workers with waitMs 10000 and required the job to be finished in that first reply. On a cold runner PowerShell can take longer than 10 s to start, which failed CI on main at 2dbf078 with "exit code or streams missing" and the summary "running, exit code pending". The three steps that wait for a worker to finish (the exit-code job, the UTF-8 job and the recovered job) now read it again, 10 s at a time, for up to 30 s, and judge the result only then. A job that never finishes still fails with its summary. Co-Authored-By: Claude Opus 5.5 --- src/Anode/Cli/DevelopmentChecks.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Anode/Cli/DevelopmentChecks.cs b/src/Anode/Cli/DevelopmentChecks.cs index 23448cf..bbf887f 100644 --- a/src/Anode/Cli/DevelopmentChecks.cs +++ b/src/Anode/Cli/DevelopmentChecks.cs @@ -148,9 +148,18 @@ ProcessStartInfo Worker(string code) Convert.ToBase64String(Encoding.Unicode.GetBytes("$null = [Console]::In.ReadLine(); " + code)) }) info.ArgumentList.Add(arg); return info; } + // Windows PowerShell can take longer than one wait (10 s at most) to start on a cold machine, so read a job + // that should finish until it does, for up to 30 s, before judging its result. + static async Task Finished(ExecutionJobs jobs, JsonObject reply) + { + for (var clock = Stopwatch.StartNew(); reply.Bool("finished") != true && clock.ElapsedMilliseconds < 30000; ) + reply = await jobs.ReadAsync(new JsonObject { ["jobId"] = reply.Str("jobId"), + ["waitMs"] = (int)Math.Clamp(30000 - clock.ElapsedMilliseconds, 1, 10000) }, reply.Str("agentId")!); + return reply; + } using (var jobs = new ExecutionJobs(() => Worker("[Console]::Out.Write('hello'); [Console]::Error.Write('problem'); exit 7"))) { - var done = await jobs.StartAsync(new JsonObject { ["path"] = "unused-test-command", ["waitMs"] = 10000 }, "test-agent"); + var done = await Finished(jobs, await jobs.StartAsync(new JsonObject { ["path"] = "unused-test-command", ["waitMs"] = 10000 }, "test-agent")); Require(done.Bool("finished") == true && done.Int("exitCode") == 7 && done.Str("stdout") == "hello" && done.Str("stderr") == "problem", "exit code or streams missing: " + done); var listed = await jobs.ReadAsync(new JsonObject { ["action"] = "list" }, "test-agent"); Require(listed["jobs"] is JsonArray { Count: 1 }, "interrupted-call job recovery unavailable"); @@ -163,7 +172,7 @@ ProcessStartInfo Worker(string code) return info; })) { - var done = await jobs.StartAsync(new JsonObject { ["path"] = "unused-test-command", ["waitMs"] = 10000 }, "test-agent"); + var done = await Finished(jobs, await jobs.StartAsync(new JsonObject { ["path"] = "unused-test-command", ["waitMs"] = 10000 }, "test-agent")); Require(done.Bool("finished") == true && done.Int("exitCode") == 0 && done.Str("stdout") == "caf\u00e9 \u6771\u4eac \U0001F680" && done.Str("stderr") == "caf\u00e9 \u6771\u4eac \U0001F680", "UTF-8 output depends on the host console encoding: " + done); @@ -186,7 +195,7 @@ ProcessStartInfo Worker(string code) Require(rejected, "the cancelled client call still waited for its command"); var listed = await jobs.ReadAsync(new JsonObject { ["action"] = "list" }, "test-agent"); Require(listed["jobs"] is JsonArray { Count: 1 }, "interrupted start was not recoverable or created more than one job"); - var recovered = await jobs.ReadAsync(new JsonObject { ["jobId"] = listed["jobs"]![0]!["jobId"]!.DeepClone(), ["waitMs"] = 10000 }, "test-agent"); + var recovered = await Finished(jobs, await jobs.ReadAsync(new JsonObject { ["jobId"] = listed["jobs"]![0]!["jobId"]!.DeepClone(), ["waitMs"] = 10000 }, "test-agent")); Require(recovered.Bool("finished") == true && recovered.Str("state") == "completed" && recovered.Str("stdout") == "survived", "client cancellation killed an already started command"); } From 04b2d64d414e4b82010511f3ac74dfbc609bc7e0 Mon Sep 17 00:00:00 2001 From: skulitom Date: Wed, 7 Oct 2026 23:33:31 +0100 Subject: [PATCH 2/2] Quick checks: name the job reader ReadUntilFinished, give the PID step 30 s too (B-024 review) The second review's P3s: the descendant-PID step allowed 15 s against the other steps' 30 s, and the helper's name read like a yes/no test. Co-Authored-By: Claude Opus 5.5 --- src/Anode/Cli/DevelopmentChecks.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Anode/Cli/DevelopmentChecks.cs b/src/Anode/Cli/DevelopmentChecks.cs index bbf887f..04350cf 100644 --- a/src/Anode/Cli/DevelopmentChecks.cs +++ b/src/Anode/Cli/DevelopmentChecks.cs @@ -150,7 +150,7 @@ ProcessStartInfo Worker(string code) } // Windows PowerShell can take longer than one wait (10 s at most) to start on a cold machine, so read a job // that should finish until it does, for up to 30 s, before judging its result. - static async Task Finished(ExecutionJobs jobs, JsonObject reply) + static async Task ReadUntilFinished(ExecutionJobs jobs, JsonObject reply) { for (var clock = Stopwatch.StartNew(); reply.Bool("finished") != true && clock.ElapsedMilliseconds < 30000; ) reply = await jobs.ReadAsync(new JsonObject { ["jobId"] = reply.Str("jobId"), @@ -159,7 +159,7 @@ static async Task Finished(ExecutionJobs jobs, JsonObject reply) } using (var jobs = new ExecutionJobs(() => Worker("[Console]::Out.Write('hello'); [Console]::Error.Write('problem'); exit 7"))) { - var done = await Finished(jobs, await jobs.StartAsync(new JsonObject { ["path"] = "unused-test-command", ["waitMs"] = 10000 }, "test-agent")); + var done = await ReadUntilFinished(jobs, await jobs.StartAsync(new JsonObject { ["path"] = "unused-test-command", ["waitMs"] = 10000 }, "test-agent")); Require(done.Bool("finished") == true && done.Int("exitCode") == 7 && done.Str("stdout") == "hello" && done.Str("stderr") == "problem", "exit code or streams missing: " + done); var listed = await jobs.ReadAsync(new JsonObject { ["action"] = "list" }, "test-agent"); Require(listed["jobs"] is JsonArray { Count: 1 }, "interrupted-call job recovery unavailable"); @@ -172,7 +172,7 @@ static async Task Finished(ExecutionJobs jobs, JsonObject reply) return info; })) { - var done = await Finished(jobs, await jobs.StartAsync(new JsonObject { ["path"] = "unused-test-command", ["waitMs"] = 10000 }, "test-agent")); + var done = await ReadUntilFinished(jobs, await jobs.StartAsync(new JsonObject { ["path"] = "unused-test-command", ["waitMs"] = 10000 }, "test-agent")); Require(done.Bool("finished") == true && done.Int("exitCode") == 0 && done.Str("stdout") == "caf\u00e9 \u6771\u4eac \U0001F680" && done.Str("stderr") == "caf\u00e9 \u6771\u4eac \U0001F680", "UTF-8 output depends on the host console encoding: " + done); @@ -195,7 +195,7 @@ static async Task Finished(ExecutionJobs jobs, JsonObject reply) Require(rejected, "the cancelled client call still waited for its command"); var listed = await jobs.ReadAsync(new JsonObject { ["action"] = "list" }, "test-agent"); Require(listed["jobs"] is JsonArray { Count: 1 }, "interrupted start was not recoverable or created more than one job"); - var recovered = await Finished(jobs, await jobs.ReadAsync(new JsonObject { ["jobId"] = listed["jobs"]![0]!["jobId"]!.DeepClone(), ["waitMs"] = 10000 }, "test-agent")); + var recovered = await ReadUntilFinished(jobs, await jobs.ReadAsync(new JsonObject { ["jobId"] = listed["jobs"]![0]!["jobId"]!.DeepClone(), ["waitMs"] = 10000 }, "test-agent")); Require(recovered.Bool("finished") == true && recovered.Str("state") == "completed" && recovered.Str("stdout") == "survived", "client cancellation killed an already started command"); } @@ -207,7 +207,7 @@ static async Task Finished(ExecutionJobs jobs, JsonObject reply) var output = started; for (var clock = Stopwatch.StartNew(); output.Str("stdout") is not { } line || !line.Contains('\n'); ) { - Require(clock.ElapsedMilliseconds < 15000 && output.Bool("finished") != true, "descendant did not start: " + output.Str("summary")); + Require(clock.ElapsedMilliseconds < 30000 && output.Bool("finished") != true, "descendant did not start: " + output.Str("summary")); output = await jobs.ReadAsync(new JsonObject { ["jobId"] = started.Str("jobId"), ["waitMs"] = 200 }, "test-agent"); } Require(int.TryParse(output.Str("stdout")!.Trim(), out int descendant), "descendant did not start: " + output.Str("summary"));