[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:
pqhuy1987
2026-05-15 11:15:47 +07:00
parent 508b17a43c
commit 4dd6f9c013
2 changed files with 116 additions and 2 deletions

View File

@ -204,6 +204,7 @@ KHÔNG `*` / `latest`. Critical pins:
## 📅 Recent activity (last 10 FIFO) ## 📅 Recent activity (last 10 FIFO)
- **2026-05-15 (S24, Plan M Chunk M2 PASS):** F1 edge case Bước 1 reset ChoDuyet tests (em main M1 service edit `PurchaseEvaluationWorkflowService.cs` line 287-333 đã DONE — fallback Drafter TraLai → reset (0, 1) giữ ChoDuyet + audit log "không lùi được"). Cookie-cutter 1 file test `PurchaseEvaluationWorkflowServiceReturnModeTests.cs` — 2 sub-tasks: (1) extend `SeedWorkflowAsync` helper +2 params optional `allowReturnOneLevelL1` + `allowReturnOneStepL2` (default false, không phá compat 4 test ReturnMode existing), set vào `l1.AllowReturnOneLevel` + `l2.AllowReturnOneStep` tương ứng — Pattern 3 audit-reuse EXTEND không clone helper; (2) add 2 `[Fact]` test ngay sau test admin bypass OneLevel (line 241) — `ApplyReturnMode_OneLevel_AtStep1Level1_ResetsToBuoc1Cap1_KeepsChoDuyet` (PE init Step 0 Cấp 1 + actor=a1 + slot Cấp 1 tick AllowReturnOneLevel, build PE inline vì helper `BuildPeAtLevel2` không phù hợp cho Cấp 1) + `ApplyReturnMode_OneStep_AtStep1_ResetsToBuoc1Cap1_KeepsChoDuyet` (PE Step 0 Cấp 2 + actor=a2 + slot Cấp 2 tick AllowReturnOneStep, reuse `BuildPeAtLevel2`, OneStep service check `curStepIdx > 0` → fallback ngay không quan tâm Cấp). Assert: Phase=ChoDuyet (KHÔNG TraLai như Drafter mode) + pointer (0, 1) + SLA NotNull + Changelog **ContextNote** chứa "không lùi được" (Summary field cố định `"Chuyển phase {from} → {to}"`, summary từ ApplyReturnModeAsync chèn vào comment qua line 96-99 service → LogTransition `ContextNote = comment`). K7 cascade verify NO regression: 3 ApproveV2_SkipToFinal_* tests still green (M1 edit chỉ F1 OneLevel/OneStep edge case, KHÔNG đụng F2 path `ApproveV2Async`). Verify: `dotnet test SolutionErp.slnx` clean 0 err 2 warn pre-existing DocxRenderer, **106/106 PASS** (58 Domain + 48 Infra: +2 từ 46 baseline post Plan L). 10 ReturnMode-class tests verified individually PASS (4 ReturnMode + 3 ApproveV2_SkipToFinal + 1 Reject_NonApprover + 2 edge case mới). Diff +94 LOC trên 1 test file (test add + helper signature 2 params). Token ~10k. Spec deterministic + 1 file independent + < 1h verified.
- **2026-05-15 (S24, Plan M Chunk M3 PASS):** FE rename Phase=TraLai (98) display label "Trả lại" "Cần chỉnh sửa lại" cho UAT disconnect fix. Spec scope HẸP (display badge label + status reference) tuân thủ strict: 4 file FE × 2 app = 8 edit total. **2 file types/purchaseEvaluation.ts** × 2 app: `PurchaseEvaluationPhaseLabel[98]` (raw phase badge) + `PeDisplayStatusLabel.TraLai` (display status badge main user-facing). **2 file components/pe/PeWorkflowPanel.tsx** × 2 app: rename 2 inline literal hardcode "Trả lại" trong F1 dialog tooltip (`Phase → "..."`) + confirm message (`Phiếu sẽ về "..."`) đây status display reference, KHÔNG phải action verb. Pattern 5 mirror 2 app strict applied. KHÔNG đụng: (1) action button label `← Trả lại` (verb), (2) F1 mode picker label `Trả về Người soạn thảo` (4 mode action), (3) comments narrative line 62/71/etc giữ rationale dev (rule §6.5), (4) `types/contracts.ts` + `types/budget.ts` Phase 98 'Trả lại' module Contract + Budget khác PE, ngoài scope M3. Verify: `npm run build` fe-admin PASS clean (0 TS err, 9.40s, 1395 KB bundle unchanged trivial) + `npm run build` fe-user PASS clean (0 TS err, 6.92s, 1275 KB). Diff +4/-4 LOC × 2 = 8 LOC tổng trên 4 file. Token ~9k. Decision tactical: 2 chỗ inline literal PeWorkflowPanel rename để giữ UX consistency với badge label sau rename KHÔNG drift outside spec cùng "status display reference" semantics (badge + tooltip + confirm message phải mirror). Anti-fiddle threshold <20% LOC respected. - **2026-05-15 (S24, Plan M Chunk M3 PASS):** FE rename Phase=TraLai (98) display label "Trả lại" "Cần chỉnh sửa lại" cho UAT disconnect fix. Spec scope HẸP (display badge label + status reference) tuân thủ strict: 4 file FE × 2 app = 8 edit total. **2 file types/purchaseEvaluation.ts** × 2 app: `PurchaseEvaluationPhaseLabel[98]` (raw phase badge) + `PeDisplayStatusLabel.TraLai` (display status badge main user-facing). **2 file components/pe/PeWorkflowPanel.tsx** × 2 app: rename 2 inline literal hardcode "Trả lại" trong F1 dialog tooltip (`Phase → "..."`) + confirm message (`Phiếu sẽ về "..."`) đây status display reference, KHÔNG phải action verb. Pattern 5 mirror 2 app strict applied. KHÔNG đụng: (1) action button label `← Trả lại` (verb), (2) F1 mode picker label `Trả về Người soạn thảo` (4 mode action), (3) comments narrative line 62/71/etc giữ rationale dev (rule §6.5), (4) `types/contracts.ts` + `types/budget.ts` Phase 98 'Trả lại' module Contract + Budget khác PE, ngoài scope M3. Verify: `npm run build` fe-admin PASS clean (0 TS err, 9.40s, 1395 KB bundle unchanged trivial) + `npm run build` fe-user PASS clean (0 TS err, 6.92s, 1275 KB). Diff +4/-4 LOC × 2 = 8 LOC tổng trên 4 file. Token ~9k. Decision tactical: 2 chỗ inline literal PeWorkflowPanel rename để giữ UX consistency với badge label sau rename KHÔNG drift outside spec cùng "status display reference" semantics (badge + tooltip + confirm message phải mirror). Anti-fiddle threshold <20% LOC respected.
- **2026-05-14 (S23 t1, K7 Chunk F PASS):** Mig 31 Approver F2 service regression tests. Sub-task 1 fix broken Drafter F2 test reference K1 flagged: `PurchaseEvaluationWorkflowServiceReturnModeTests.cs:253` `drafter.AllowDrafterSkipToFinal = true` (DELETE 3 deprecated Drafter F2 tests entire `SkipToFinal_DrafterAllowed_SetsPointerToFinalLevel` + `SkipToFinal_DrafterDenied_NonAdmin_Throws` + `SkipToFinal_AdminBypass_Succeeds`, semantic deprecated no value). Sub-task 2 add 3 new Approver F2 tests: `ApproveV2_SkipToFinal_AdminTickFlag_SetsPhaseDaDuyet` (happy path slot Cấp 1 Bước 1 admin tick Phase=DaDuyet, pointer cleared, opinion + PEA + Changelog logged), `ApproveV2_SkipToFinal_FlagOff_NonAdmin_ThrowsConflictException` (denied flag off non-admin slot user throw "chưa được phép duyệt thẳng Cấp cuối"), `ApproveV2_SkipToFinal_FlagOff_Admin_BypassesFlagCheck` (admin bypass flag off admin role DaDuyet allowed). Pattern 11 SeedWorkflowAsync cookie-cutter REUSE created `SeedApproverF2WorkflowAsync` helper (2 Steps × 2 Levels multi-step verify skip thẳng terminal KHÔNG fallthrough advance pointer next Step), AllowApproverSkipToFinal per-slot param. PE init Phase=ChoDuyet + CurrentWorkflowStepIndex=0 + CurrentApprovalLevelOrder=1 (vs S22 happy path từ DangSoanThao). Add `using Microsoft.EntityFrameworkCore` cho `.ToListAsync()` PEL/PEA/Changelog query. File header narrative line 17-25 REWRITE để track Mig 31 refactor semantic Approver scope ChoDuyet vs Drafter-from-Nháp . Verify: `dotnet build` clean 0 err 2 warn pre-existing DocxRenderer. `dotnet test SolutionErp.slnx` 104 PASS (58 Domain + 46 Infra, 3 deleted + 3 added cancel out, baseline preserved). 3 Approver F2 tests verified individually PASS. Diff +175/-92 LOC trên 1 file. Token ~14k. - **2026-05-14 (S23 t1, K7 Chunk F PASS):** Mig 31 Approver F2 service regression tests. Sub-task 1 fix broken Drafter F2 test reference K1 flagged: `PurchaseEvaluationWorkflowServiceReturnModeTests.cs:253` `drafter.AllowDrafterSkipToFinal = true` (DELETE 3 deprecated Drafter F2 tests entire `SkipToFinal_DrafterAllowed_SetsPointerToFinalLevel` + `SkipToFinal_DrafterDenied_NonAdmin_Throws` + `SkipToFinal_AdminBypass_Succeeds`, semantic deprecated no value). Sub-task 2 add 3 new Approver F2 tests: `ApproveV2_SkipToFinal_AdminTickFlag_SetsPhaseDaDuyet` (happy path slot Cấp 1 Bước 1 admin tick Phase=DaDuyet, pointer cleared, opinion + PEA + Changelog logged), `ApproveV2_SkipToFinal_FlagOff_NonAdmin_ThrowsConflictException` (denied flag off non-admin slot user throw "chưa được phép duyệt thẳng Cấp cuối"), `ApproveV2_SkipToFinal_FlagOff_Admin_BypassesFlagCheck` (admin bypass flag off admin role DaDuyet allowed). Pattern 11 SeedWorkflowAsync cookie-cutter REUSE created `SeedApproverF2WorkflowAsync` helper (2 Steps × 2 Levels multi-step verify skip thẳng terminal KHÔNG fallthrough advance pointer next Step), AllowApproverSkipToFinal per-slot param. PE init Phase=ChoDuyet + CurrentWorkflowStepIndex=0 + CurrentApprovalLevelOrder=1 (vs S22 happy path từ DangSoanThao). Add `using Microsoft.EntityFrameworkCore` cho `.ToListAsync()` PEL/PEA/Changelog query. File header narrative line 17-25 REWRITE để track Mig 31 refactor semantic Approver scope ChoDuyet vs Drafter-from-Nháp . Verify: `dotnet build` clean 0 err 2 warn pre-existing DocxRenderer. `dotnet test SolutionErp.slnx` 104 PASS (58 Domain + 46 Infra, 3 deleted + 3 added cancel out, baseline preserved). 3 Approver F2 tests verified individually PASS. Diff +175/-92 LOC trên 1 file. Token ~14k.

View File

@ -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. // 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). // 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). // 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)> private static async Task<(ApprovalWorkflow wf, ApprovalWorkflowStep step, ApprovalWorkflowLevel l1, ApprovalWorkflowLevel l2)>
SeedWorkflowAsync( SeedWorkflowAsync(
TestApplicationDbContext db, TestApplicationDbContext db,
@ -49,7 +51,9 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
Guid approver2UserId, Guid approver2UserId,
bool allowReturnOneLevelL2 = false, bool allowReturnOneLevelL2 = false,
bool allowReturnToDrafterL2 = false, bool allowReturnToDrafterL2 = false,
bool allowApproverEditL2 = false) bool allowApproverEditL2 = false,
bool allowReturnOneLevelL1 = false,
bool allowReturnOneStepL2 = false)
{ {
var wf = new ApprovalWorkflow var wf = new ApprovalWorkflow
{ {
@ -75,7 +79,8 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
ApprovalWorkflowStepId = step.Id, ApprovalWorkflowStepId = step.Id,
Order = 1, Order = 1,
ApproverUserId = approver1UserId, 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 var l2 = new ApprovalWorkflowLevel
{ {
@ -84,6 +89,7 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
Order = 2, Order = 2,
ApproverUserId = approver2UserId, ApproverUserId = approver2UserId,
AllowReturnOneLevel = allowReturnOneLevelL2, AllowReturnOneLevel = allowReturnOneLevelL2,
AllowReturnOneStep = allowReturnOneStepL2,
AllowReturnToDrafter = allowReturnToDrafterL2, AllowReturnToDrafter = allowReturnToDrafterL2,
AllowApproverEditDetails = allowApproverEditL2, 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) ============ // ============ 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. // 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). // Storage: matchingLevel.AllowApproverSkipToFinal (per-Approver-slot, admin opt-in).