Resolve Server-044..050: KillWorker accounting + admin service hardening
Server-044 KillWorkerAsync catch path now calls _metrics.SessionRemoved
so the open-session gauge does not leak when KillWorker throws.
Server-045 KillWorkerAsync routes through a new
GatewaySession.KillWorkerWithCloseGateAsync that takes the
per-session close lock, so concurrent kills count SessionsClosed
exactly once.
Server-046 CloseSessionCoreAsync's SessionCloseStartedException branch and
ShutdownAsync's kill fallback both increment SessionsClosed (not
just the gauge), so the counter and gauge stay consistent.
Server-047 ApiKeysPage.ConfirmPendingAsync holds PendingAction across the
awaited action and clears it in finally, matching the sessions
pages.
Server-048 Closed: the 044/045 regression tests cover the previously-
untested kill paths.
Server-049 IDashboardSessionAdminService + DashboardSessionAdminService
now carry XML docs that pin the Admin gate, missing-session
return-Fail semantics, and the dashboard-admin-kill reason.
Server-050 CloseSessionAsync and KillWorkerAsync catch unexpected
exceptions after the SessionManagerException catches and return
a friendly Fail; OperationCanceledException tied to the caller
token still propagates.
All resolved at 2026-05-24; 503/503 gateway tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,52 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
Assert.True(service.CanManage(CreateUser(DashboardRoles.Admin)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression for Server-050: an unexpected (non-<see cref="SessionManagerException"/>)
|
||||
/// exception from <c>CloseSessionAsync</c> — e.g. an <see cref="InvalidOperationException"/>
|
||||
/// or <see cref="IOException"/> surfaced from <c>RemoveSessionAsync</c>/<c>DisposeAsync</c> —
|
||||
/// must be converted to a friendly <see cref="DashboardSessionAdminResult.Fail(string)"/>
|
||||
/// rather than propagating raw into Blazor's error boundary.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CloseSessionAsync_WhenManagerThrowsUnexpected_ReturnsFriendlyFail()
|
||||
{
|
||||
FakeSessionManager sessionManager = new()
|
||||
{
|
||||
CloseThrowsUnexpected = new InvalidOperationException("unexpected"),
|
||||
};
|
||||
DashboardSessionAdminService service = CreateService(sessionManager);
|
||||
|
||||
DashboardSessionAdminResult result = await service.CloseSessionAsync(
|
||||
CreateUser(DashboardRoles.Admin),
|
||||
"session-1",
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.Message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression for Server-050: same friendly-fail contract for the Kill path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task KillWorkerAsync_WhenManagerThrowsUnexpected_ReturnsFriendlyFail()
|
||||
{
|
||||
FakeSessionManager sessionManager = new()
|
||||
{
|
||||
KillThrowsUnexpected = new IOException("pipe broken"),
|
||||
};
|
||||
DashboardSessionAdminService service = CreateService(sessionManager);
|
||||
|
||||
DashboardSessionAdminResult result = await service.KillWorkerAsync(
|
||||
CreateUser(DashboardRoles.Admin),
|
||||
"session-1",
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.Message));
|
||||
}
|
||||
|
||||
private static DashboardSessionAdminService CreateService(ISessionManager sessionManager)
|
||||
{
|
||||
DefaultHttpContext httpContext = new();
|
||||
@@ -150,6 +196,10 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
|
||||
public bool CloseThrowsNotFound { get; init; }
|
||||
|
||||
public Exception? CloseThrowsUnexpected { get; init; }
|
||||
|
||||
public Exception? KillThrowsUnexpected { get; init; }
|
||||
|
||||
public Task<GatewaySession> OpenSessionAsync(
|
||||
SessionOpenRequest request,
|
||||
string? clientIdentity,
|
||||
@@ -194,6 +244,11 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
$"Session {sessionId} was not found.");
|
||||
}
|
||||
|
||||
if (CloseThrowsUnexpected is not null)
|
||||
{
|
||||
throw CloseThrowsUnexpected;
|
||||
}
|
||||
|
||||
return Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
|
||||
}
|
||||
|
||||
@@ -205,6 +260,11 @@ public sealed class DashboardSessionAdminServiceTests
|
||||
KillCount++;
|
||||
LastKilledSessionId = sessionId;
|
||||
LastKillReason = reason;
|
||||
if (KillThrowsUnexpected is not null)
|
||||
{
|
||||
throw KillThrowsUnexpected;
|
||||
}
|
||||
|
||||
return Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user