[CLAUDE] Tests: Chunk M2 — Add F1 OneLevel/OneStep edge case tests Bước 1 reset ChoDuyet
Hai test mới cover edge case Plan M S23 t3 (em main M1 edit
PurchaseEvaluationWorkflowService.cs line 287-333):
- ApplyReturnMode_OneLevel_AtStep1Level1_ResetsToBuoc1Cap1_KeepsChoDuyet
PE init Step 0 Cấp 1 + actor=a1 + slot Cấp 1 tick AllowReturnOneLevel.
Trước fallback Drafter Phase=TraLai → sau reset (0, 1) giữ Phase=ChoDuyet,
SLA reset 7d, audit log ContextNote chứa "không lùi được".
- ApplyReturnMode_OneStep_AtStep1_ResetsToBuoc1Cap1_KeepsChoDuyet
PE init Step 0 Cấp 2 + actor=a2 + slot Cấp 2 tick AllowReturnOneStep.
Service check curStepIdx > 0 → fallback ngay. Assert Phase=ChoDuyet,
pointer (0, 1), SLA reset, audit log "không lùi được".
Extend helper SeedWorkflowAsync +2 params optional (allowReturnOneLevelL1
+ allowReturnOneStepL2) — default false, không phá compat 4 test ReturnMode
existing. Pattern 3 audit-reuse (extend helper KHÔNG clone).
Audit log assertion dùng ContextNote thay vì Summary: LogTransitionAsync set
Summary cố định "Chuyển phase {from} → {to}", summary từ ApplyReturnModeAsync
chèn vào comment qua line 96-99 service → ContextNote = comment.
K7 cascade NO regression: 3 ApproveV2_SkipToFinal_* tests still green (M1
edit chỉ F1 path, KHÔNG đụng F2 ApproveV2Async).
Verify:
- Build pass (0 error, 2 pre-existing DocxRenderer warning)
- 106/106 test pass (58 Domain + 48 Infra: +2 từ 46 baseline Plan L)
- 10 ReturnMode-class tests pass (4 ReturnMode + 3 ApproveV2_SkipToFinal
+ 1 Reject_NonApprover + 2 edge case mới)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -42,6 +42,8 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
|
||||
// Workflow setup: 1 Bước (1 Step) — 2 Cấp (2 Levels) — mỗi Cấp 1 Approver.
|
||||
// Mặc định mọi Allow* = false trên Level slot (admin opt-in pattern Mig 29).
|
||||
// ApproverUserId mặc định = approverId truyền vào (caller có thể override).
|
||||
// Plan M S23 t3 — thêm 2 param L1 (`allowReturnOneLevelL1` + `allowReturnOneStepL2`)
|
||||
// phục vụ test edge case Bước 1 Cấp 1 reset ChoDuyet (không lùi được).
|
||||
private static async Task<(ApprovalWorkflow wf, ApprovalWorkflowStep step, ApprovalWorkflowLevel l1, ApprovalWorkflowLevel l2)>
|
||||
SeedWorkflowAsync(
|
||||
TestApplicationDbContext db,
|
||||
@ -49,7 +51,9 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
|
||||
Guid approver2UserId,
|
||||
bool allowReturnOneLevelL2 = false,
|
||||
bool allowReturnToDrafterL2 = false,
|
||||
bool allowApproverEditL2 = false)
|
||||
bool allowApproverEditL2 = false,
|
||||
bool allowReturnOneLevelL1 = false,
|
||||
bool allowReturnOneStepL2 = false)
|
||||
{
|
||||
var wf = new ApprovalWorkflow
|
||||
{
|
||||
@ -75,7 +79,8 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
|
||||
ApprovalWorkflowStepId = step.Id,
|
||||
Order = 1,
|
||||
ApproverUserId = approver1UserId,
|
||||
// L1 defaults: Allow* = false (test sad path easier)
|
||||
AllowReturnOneLevel = allowReturnOneLevelL1,
|
||||
// Các Allow* khác mặc định false (sad path)
|
||||
};
|
||||
var l2 = new ApprovalWorkflowLevel
|
||||
{
|
||||
@ -84,6 +89,7 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
|
||||
Order = 2,
|
||||
ApproverUserId = approver2UserId,
|
||||
AllowReturnOneLevel = allowReturnOneLevelL2,
|
||||
AllowReturnOneStep = allowReturnOneStepL2,
|
||||
AllowReturnToDrafter = allowReturnToDrafterL2,
|
||||
AllowApproverEditDetails = allowApproverEditL2,
|
||||
};
|
||||
@ -240,6 +246,113 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Plan M S23 t3 — F1 edge case Bước 1 reset ChoDuyet ============
|
||||
// Cũ: Approver Bước 1 Cấp 1 click "Trả lại 1 Cấp" / "Trả lại 1 Bước" → fallback
|
||||
// Drafter mode (Phase=TraLai, clear pointer). UX confusing: Approver A đột nhiên
|
||||
// bị "đẩy phiếu về Drafter" mặc dù mình chính là người đang giữ phiếu.
|
||||
// Mới: reset (Step=0, Level=1) GIỮ Phase=ChoDuyet — no-op effective (Approver A
|
||||
// vẫn giữ phiếu), SLA reset 7d, audit log ContextNote "không lùi được". 2 test
|
||||
// dưới cover OneLevel + OneStep edge case riêng — Drafter mode (line 268-275)
|
||||
// GIỮ semantic Phase=TraLai unchanged.
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyReturnMode_OneLevel_AtStep1Level1_ResetsToBuoc1Cap1_KeepsChoDuyet()
|
||||
{
|
||||
// Workflow 1 Step × 2 Levels, PE đang Bước 1 Cấp 1 (Approver A duyệt).
|
||||
// Slot Cấp 1 tick AllowReturnOneLevel=true (admin opt-in pattern Mig 29).
|
||||
var (svc, fix, db, _) = CreateService();
|
||||
using (fix)
|
||||
{
|
||||
var (a1, a2) = await SeedApproversAsync(fix, "rm-edge-onelevel");
|
||||
var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id, allowReturnOneLevelL1: true);
|
||||
|
||||
var pe = new PurchaseEvaluation
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = PurchaseEvaluationType.DuyetNcc,
|
||||
Phase = PurchaseEvaluationPhase.ChoDuyet,
|
||||
MaPhieu = "PE-RM-EDGE-OL",
|
||||
TenGoiThau = "OneLevel edge Bước 1 Cấp 1",
|
||||
ProjectId = Guid.NewGuid(),
|
||||
DrafterUserId = Guid.NewGuid(),
|
||||
ApprovalWorkflowId = wf.Id,
|
||||
CurrentWorkflowStepIndex = 0, // Bước 1
|
||||
CurrentApprovalLevelOrder = 1, // Cấp 1 — edge case "không lùi được"
|
||||
};
|
||||
db.PurchaseEvaluations.Add(pe);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await svc.TransitionAsync(
|
||||
evaluation: pe,
|
||||
targetPhase: PurchaseEvaluationPhase.TraLai,
|
||||
actorUserId: a1.Id,
|
||||
actorRoles: new[] { AppRoles.CostControl },
|
||||
decision: ApprovalDecision.Reject,
|
||||
comment: "thử trả 1 cấp tại Bước 1 Cấp 1",
|
||||
returnMode: WorkflowReturnMode.OneLevel,
|
||||
ct: CancellationToken.None);
|
||||
|
||||
pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
|
||||
"Plan M edge case: GIỮ ChoDuyet (no-op effective), KHÔNG fallback Drafter TraLai");
|
||||
pe.CurrentWorkflowStepIndex.Should().Be(0, "Pointer reset Bước 1");
|
||||
pe.CurrentApprovalLevelOrder.Should().Be(1, "Pointer reset Cấp 1");
|
||||
pe.SlaDeadline.Should().NotBeNull("SLA reset 7d cho Approver A giữ phiếu");
|
||||
|
||||
// Audit log "không lùi được" phải xuất hiện ở ContextNote (LogTransitionAsync
|
||||
// append summary từ ApplyReturnModeAsync vào field này — line 96-99 service).
|
||||
// Summary field cố định "Chuyển phase {from} → {to}".
|
||||
var changelog = await db.PurchaseEvaluationChangelogs
|
||||
.Where(c => c.PurchaseEvaluationId == pe.Id)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.FirstAsync();
|
||||
changelog.ContextNote.Should().NotBeNull();
|
||||
changelog.ContextNote!.Should().Contain("không lùi được",
|
||||
"Audit trail rõ ràng cho UAT review — phân biệt no-op vs lùi thật");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyReturnMode_OneStep_AtStep1_ResetsToBuoc1Cap1_KeepsChoDuyet()
|
||||
{
|
||||
// Workflow 1 Step × 2 Levels, PE đang Bước 1 Cấp 2 (Approver B duyệt).
|
||||
// Slot Cấp 2 tick AllowReturnOneStep=true. Action OneStep từ Bước 1 → edge
|
||||
// case "không lùi được Bước" (vẫn là first Step) → reset (0, 1) giữ ChoDuyet.
|
||||
// Approver B sau reset bị bàn giao về Approver A (Cấp 1) — vẫn trong chuỗi duyệt.
|
||||
var (svc, fix, db, _) = CreateService();
|
||||
using (fix)
|
||||
{
|
||||
var (a1, a2) = await SeedApproversAsync(fix, "rm-edge-onestep");
|
||||
var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id, allowReturnOneStepL2: true);
|
||||
var pe = BuildPeAtLevel2(wf.Id, drafterId: Guid.NewGuid(), code: "PE-RM-EDGE-OS");
|
||||
db.PurchaseEvaluations.Add(pe);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
await svc.TransitionAsync(
|
||||
evaluation: pe,
|
||||
targetPhase: PurchaseEvaluationPhase.TraLai,
|
||||
actorUserId: a2.Id,
|
||||
actorRoles: new[] { AppRoles.CostControl },
|
||||
decision: ApprovalDecision.Reject,
|
||||
comment: "thử trả 1 bước tại Bước 1",
|
||||
returnMode: WorkflowReturnMode.OneStep,
|
||||
ct: CancellationToken.None);
|
||||
|
||||
pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet,
|
||||
"Plan M edge case: GIỮ ChoDuyet (no-op effective), KHÔNG fallback Drafter TraLai");
|
||||
pe.CurrentWorkflowStepIndex.Should().Be(0, "Pointer reset Bước 1");
|
||||
pe.CurrentApprovalLevelOrder.Should().Be(1, "Pointer reset Cấp 1 (bàn giao về Approver A)");
|
||||
pe.SlaDeadline.Should().NotBeNull("SLA reset 7d cho approver mới");
|
||||
|
||||
var changelog = await db.PurchaseEvaluationChangelogs
|
||||
.Where(c => c.PurchaseEvaluationId == pe.Id)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.FirstAsync();
|
||||
changelog.ContextNote.Should().NotBeNull();
|
||||
changelog.ContextNote!.Should().Contain("không lùi được",
|
||||
"Audit trail rõ ràng cho UAT review — phân biệt no-op vs lùi thật");
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Task 2: skipToFinal — Mig 31 (S23 t1 Plan K Chunk F) ============
|
||||
// Semantic mới: Approver during ChoDuyet duyệt thẳng Cấp cuối → Phase=DaDuyet terminal.
|
||||
// Storage: matchingLevel.AllowApproverSkipToFinal (per-Approver-slot, admin opt-in).
|
||||
|
||||
Reference in New Issue
Block a user