fix(management): ParseDeploymentStatus rejects ordinals — Enum.TryParse accepts "2" as InProgress despite the row-88 names-only contract; same LooksNumeric guard as the audit strict parser (found by the Wave 6 rows-98/99 agent survey)

This commit is contained in:
Joseph Doherty
2026-08-17 06:21:12 -04:00
parent 8a9a8d2951
commit 16056f8eb6
2 changed files with 28 additions and 1 deletions
@@ -2745,7 +2745,13 @@ public class ManagementActor : ReceiveActor
private static DeploymentStatus? ParseDeploymentStatus(string? status) private static DeploymentStatus? ParseDeploymentStatus(string? status)
{ {
if (string.IsNullOrWhiteSpace(status)) return null; if (string.IsNullOrWhiteSpace(status)) return null;
if (Enum.TryParse<DeploymentStatus>(status, ignoreCase: true, out var parsed) var trimmed = status.Trim();
// Reject ordinals before TryParse — "2" parses to InProgress, which the
// documented names-only contract does not admit. No enum member name can
// start with a digit or a sign (same guard as AuditQueryParamParsers).
var looksNumeric = char.IsAsciiDigit(trimmed[0]) || trimmed[0] == '+' || trimmed[0] == '-';
if (!looksNumeric
&& Enum.TryParse<DeploymentStatus>(trimmed, ignoreCase: true, out var parsed)
&& Enum.IsDefined(parsed)) && Enum.IsDefined(parsed))
{ {
return parsed; return parsed;
@@ -1678,6 +1678,27 @@ public class ManagementActorTests : TestKit, IDisposable
deployRepo.DidNotReceiveWithAnyArgs().QueryDeploymentListPageAsync(default!, default, default); deployRepo.DidNotReceiveWithAnyArgs().QueryDeploymentListPageAsync(default!, default, default);
} }
[Theory]
[InlineData("2")]
[InlineData("+1")]
[InlineData("-1")]
public void QueryDeployments_OrdinalStatus_IsRejectedLikeAnyUnknownValue(string status)
{
// Register row 88 postscript (Wave 6): Enum.TryParse accepts ordinals, so
// `--status 2` silently resolved to InProgress despite the documented
// names-only contract. Same guard as AuditQueryParamParsers.LooksNumeric.
var deployRepo = Substitute.For<IDeploymentManagerRepository>();
StubDeploymentPageQuery(deployRepo, (1, 1));
_services.AddScoped(_ => deployRepo);
var actor = CreateActor();
actor.Tell(Envelope(new QueryDeploymentsCommand(Status: status), "Deployer"));
var response = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
Assert.Contains("Success", response.Error);
deployRepo.DidNotReceiveWithAnyArgs().QueryDeploymentListPageAsync(default!, default, default);
}
[Theory] [Theory]
[InlineData("success")] [InlineData("success")]
[InlineData("SUCCESS")] [InlineData("SUCCESS")]