fix: DEBT-025/026 - wire Draft->Proposed transition and GET /approvals/{id}
DEBT-026 (high impact): ProposeForReviewHandler + POST /approvals/{id}/propose
wires ApprovalWorkflowPolicy.CanProposeForReview, which previously had no
Handler/Endpoint calling it. Before this, a proposal created via POST
/approvals could never reach Approved/Active through the running application
- the maker-checker gate was not completable end-to-end via HTTP.
DEBT-025 (medium impact): GetApprovalByIdEndpoint (GET /approvals/{id}) +
ApprovalWorkflowSql.GetEvidenceForProposalAsync make evidence attached during
approval (PBO/DSR/OOS artifact links) readable via HTTP instead of only by
querying model_operations.approval_evidence directly.
Both discovered while resolving DEBT-017 earlier the same session. 4 new
tests added. dotnet build -c Release clean. Not verified against a live
database (no SSH tunnel open in this environment) - see
TECH_DEBT_REGISTER.md and WBS_PROGRESS_TRACKER.csv AEG-VS-26-01 for the
honest verification status; do not mark COMPLETED until a real Postgres
run passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,77 @@ public class ApprovalWorkflowTests : IAsyncLifetime
|
||||
handler.Handle("viewer@company.com", "Viewer", modelId, DateOnly.FromDateTime(DateTime.UtcNow), "no role", Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProposeForReview_ByCreatingMaker_TransitionsDraftToProposed()
|
||||
{
|
||||
// Arrange: this handler didn't exist until DEBT-026 was fixed — before that, a proposal
|
||||
// created via POST /approvals could never reach Proposed/Approved/Active through the
|
||||
// running application at all (only test code bypassing the handler via
|
||||
// _sql.UpdateProposalStatusAsync directly could move it, as the other tests in this file
|
||||
// still do to set up their own preconditions).
|
||||
var modelId = await SeedModelAsync();
|
||||
var createHandler = new CreateApprovalProposalHandler(_sql, _clock);
|
||||
var proposalId = await createHandler.Handle(
|
||||
"maker@company.com", "Maker", modelId, DateOnly.FromDateTime(DateTime.UtcNow.AddDays(7)),
|
||||
"Ready to send for review", Guid.NewGuid());
|
||||
|
||||
var proposeHandler = new ProposeForReviewHandler(_sql, _clock);
|
||||
|
||||
// Act
|
||||
await proposeHandler.Handle(proposalId, "maker@company.com", Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
var proposal = await _sql.GetProposalAsync(proposalId);
|
||||
Assert.NotNull(proposal);
|
||||
Assert.Equal(ApprovalStatus.Proposed, proposal!.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProposeForReview_ByDifferentUserThanCreator_ThrowsUnauthorized()
|
||||
{
|
||||
// Arrange
|
||||
var modelId = await SeedModelAsync();
|
||||
var createHandler = new CreateApprovalProposalHandler(_sql, _clock);
|
||||
var proposalId = await createHandler.Handle(
|
||||
"maker@company.com", "Maker", modelId, DateOnly.FromDateTime(DateTime.UtcNow.AddDays(7)),
|
||||
"Only the creator may propose it", Guid.NewGuid());
|
||||
|
||||
var proposeHandler = new ProposeForReviewHandler(_sql, _clock);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
|
||||
proposeHandler.Handle(proposalId, "someone-else@company.com", Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetEvidenceForProposalAsync_ReturnsEvidenceAttachedDuringApproval()
|
||||
{
|
||||
// Arrange: this query didn't exist until DEBT-025 was fixed — evidence attached during
|
||||
// approval (the whole point of this slice per CLAUDE.md's "Activation gating") was
|
||||
// otherwise only readable by querying model_operations.approval_evidence directly.
|
||||
var modelId = await SeedModelAsync();
|
||||
var createHandler = new CreateApprovalProposalHandler(_sql, _clock);
|
||||
var proposalId = await createHandler.Handle(
|
||||
"maker@company.com", "Maker", modelId, DateOnly.FromDateTime(DateTime.UtcNow.AddDays(7)),
|
||||
"Needs evidence readback", Guid.NewGuid());
|
||||
await _sql.UpdateProposalStatusAsync(proposalId, ApprovalStatus.Proposed);
|
||||
|
||||
var approveHandler = new ApproveApprovalHandler(_sql, _clock);
|
||||
var evidence = new List<(string Type, string Url, string? Comment)>
|
||||
{
|
||||
("PBO_SCORE", "s3://evidence/pbo-0.95.json", "Verified"),
|
||||
};
|
||||
await approveHandler.Handle(proposalId, "checker@company.com", "Checker", "Approved", evidence, Guid.NewGuid());
|
||||
|
||||
// Act
|
||||
var retrieved = await _sql.GetEvidenceForProposalAsync(proposalId);
|
||||
|
||||
// Assert
|
||||
Assert.Single(retrieved);
|
||||
Assert.Equal("PBO_SCORE", retrieved[0].EvidenceType);
|
||||
Assert.Equal("s3://evidence/pbo-0.95.json", retrieved[0].EvidenceUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Approve_WithDifferentChecker_TransitionsToApproved_AndAttachesEvidence()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user