fix(inbound-api): resolve InboundAPI-002,004,006,008 — disconnect vs timeout, body size limit, active-node gate; surface InboundAPI-007

This commit is contained in:
Joseph Doherty
2026-05-16 21:22:01 -04:00
parent 6563511b5f
commit da955042aa
10 changed files with 462 additions and 20 deletions

View File

@@ -239,4 +239,88 @@ public class InboundScriptExecutorTests
Assert.True(_executor.CompileAndRegister(method));
}
// --- InboundAPI-002: lazy compile-and-fetch must be atomic, never KeyNotFoundException ---
[Fact]
public async Task LazyCompile_RacingRemoveHandler_NeverThrowsKeyNotFound()
{
// The lazy-compile path must compile-and-fetch atomically: a concurrent
// RemoveHandler must not be able to turn a first-call into an "Internal
// script error" (the old check-then-act re-read could throw KeyNotFoundException).
var method = new ApiMethod("atomic", "return 5;") { Id = 1, TimeoutSeconds = 10 };
var removers = Enumerable.Range(0, 16).Select(_ => Task.Run(() =>
{
for (var n = 0; n < 200; n++)
_executor.RemoveHandler("atomic");
}));
var callers = Enumerable.Range(0, 16).Select(_ => Task.Run(async () =>
{
for (var n = 0; n < 50; n++)
{
var r = await _executor.ExecuteAsync(
method, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));
// Result must always be a clean success or a clean compilation
// failure — never the catch-all "Internal script error".
Assert.NotEqual("Internal script error", r.ErrorMessage);
}
}));
await Task.WhenAll(removers.Concat(callers));
}
// --- InboundAPI-004: a client disconnect must NOT be reported as a script timeout ---
[Fact]
public async Task ClientDisconnect_IsNotReportedAsTimeout()
{
// When the caller's request token is cancelled (client aborted the request),
// ExecuteAsync must report a client-cancelled failure, not "Script execution
// timed out" — that log line is reserved for genuine timeouts.
var method = new ApiMethod("aborted", "return 1;") { Id = 1, TimeoutSeconds = 30 };
_executor.RegisterHandler("aborted", async ctx =>
{
await Task.Delay(TimeSpan.FromSeconds(60), ctx.CancellationToken);
return "never";
});
using var clientAborted = new CancellationTokenSource();
clientAborted.CancelAfter(TimeSpan.FromMilliseconds(100));
var result = await _executor.ExecuteAsync(
method,
new Dictionary<string, object?>(),
_route,
// Generous method timeout so the timeout CTS is NOT the cause.
TimeSpan.FromSeconds(30),
clientAborted.Token);
Assert.False(result.Success);
Assert.DoesNotContain("timed out", result.ErrorMessage ?? string.Empty);
}
[Fact]
public async Task GenuineTimeout_StillReportedAsTimeout()
{
// A method that exceeds its timeout with no client abort must still be
// reported as "timed out" (regression guard for the InboundAPI-004 fix).
var method = new ApiMethod("genuine", "return 1;") { Id = 1, TimeoutSeconds = 1 };
_executor.RegisterHandler("genuine", async ctx =>
{
await Task.Delay(TimeSpan.FromSeconds(60), ctx.CancellationToken);
return "never";
});
var result = await _executor.ExecuteAsync(
method,
new Dictionary<string, object?>(),
_route,
TimeSpan.FromMilliseconds(100),
CancellationToken.None);
Assert.False(result.Success);
Assert.Contains("timed out", result.ErrorMessage);
}
}