fix(inbound-api): defer DI-scope disposal to handler completion and count abandoned executions — closes the timeout use-after-dispose

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 04:18:00 -04:00
parent 39e1ca3eae
commit 0b916ff0c5
3 changed files with 75 additions and 4 deletions
@@ -728,6 +728,26 @@ public class InboundScriptExecutorTests
return new SqliteGateway("DataSource=file:movein-exec?mode=memory&cache=shared");
}
[Fact]
public async Task TimedOutScript_ScopeDisposalDeferred_NoOrphanLeak()
{
// Handler ignores the token and keeps running past the method timeout.
var release = new TaskCompletionSource();
_executor.RegisterHandler("slow", async _ => { await release.Task; return 1; });
var method = new ApiMethod("slow", "unused") { Id = 9, TimeoutSeconds = 1 };
var result = await _executor.ExecuteAsync(
method, new Dictionary<string, object?>(), _route, TimeSpan.FromMilliseconds(50));
Assert.False(result.Success);
Assert.Contains("timed out", result.ErrorMessage);
Assert.Equal(1, _executor.AbandonedExecutionCount); // orphan is COUNTED
release.SetResult(); // orphan finishes; continuation cleans up
await Task.Delay(200);
Assert.Equal(0, _executor.AbandonedExecutionCount); // ...and DECREMENTED
}
private sealed class CompileLogCounter
{
public int CompilationFailures;