Files
solution-erp/.claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/sub-test-specialist-0.md
2026-07-17 14:46:51 +07:00

11 KiB
Raw Blame History

sub-test-specialist-0 — STAGE A T0 REPRODUCE (PE budget-freeze @DaDuyet)

Role: test-specialist · Task: viết ĐÚNG 1 test T0 REPRODUCE (test-before cho bug "phiếu DaDuyet đổi số ngân sách theo bảng LIVE"). Kỳ vọng RED trên code hiện tại.

Boundary: CHỈ ghi tests/SolutionErp.Infrastructure.Tests/Services/PeBudgetFreezeTests.cs + file này. KHÔNG chạm production (src/Backend, fe-*), KHÔNG sửa test cũ.


1. Investigation (code = single source of truth)

Bug cơ chế (verified qua đọc code):

  • Query GetPurchaseEvaluationQuery (KHÔNG phải GetPurchaseEvaluationDetailQuery — tên trong SPEC hơi lệch; query THẬT = GetPurchaseEvaluationQuery, trả PurchaseEvaluationDetailBundleDto). Handler PurchaseEvaluationFeatures.cs:853.
  • PurchaseEvaluationFeatures.cs:911-912pairRec load LIVE từ db.PeWorkItemBudgets.FirstOrDefaultAsync(b => b.ProjectId == e.ProjectId && b.WorkItemId == wiKey) — KHÔNG phân biệt Phase.
  • :964-972PeBudgetSummaryDto build từ pairRec (positional 17-field). Field cần assert:
    • ProInitialAmount = arg 16 ← pairRec?.ProInitialAmount (DTO def PurchaseEvaluationDtos.cs:348)
    • InitialAmount = arg 4 ← pairRec?.InitialAmount (CCM ban hành, DTO def :335)
  • Property bundle: PurchaseEvaluationDetailBundleDto.BudgetSummary (PurchaseEvaluationDtos.cs:260).
  • ⇒ mọi phiếu cùng cặp (ProjectId×WorkItemId) đọc CHUNG record live. Phiếu DaDuyet đổi số theo bản cập nhật mới nhất = BUG.

Finalize path (đưa phiếu tới DaDuyet ĐÚNG cách — KHÔNG seed thẳng):

  • PurchaseEvaluationWorkflowService.cs:855-873 nhánh level-finalize: if (matchingLevel.AllowApproverFinalize && applyLevelFinalize):861 evaluation.Phase = DaDuyet. Đây là 1 trong 4 site DaDuyet (SPEC: :861/:899/:931/:1053). Hiện KHÔNG có snapshot ngân sách.
  • Mirror PeApproverFinalizeTests test #5 (AtLastSlot): workflow V2 1 Bước 1 Cấp, AllowApproverFinalize=true, approver duyệt + giá chốt (approvedPriceAmount=400tr, source="Ncc"ValidApprovedPriceSources :954) → TỰ DaDuyet.
  • TransitionAsync sig :37-52 (14 param, applyLevelFinalize=true default :51).

Commands đổi pair (SAU khi duyệt — KHÔNG có phase-guard nên chạy được mọi phase):

  • UpdatePeBudgetProCommand(Guid PeId, decimal? ProInitialAmount, decimal? ProAdjustmentAmount, string? ProNote) handler (IApplicationDbContext, ICurrentUser)PeWorkItemBudgetFeatures.cs:63/80. Role-gate Admin|Procurement.
  • UpdatePeBudgetCcmCommand(Guid PeId, decimal? InitialAmount, decimal? AdjustmentAmount, string? CcmNote) handler :134/151. Role-gate Admin|CostControl. Absolute-set (null=clear). EnsureTrackedAsync reuse pair đã seed.

Helpers reuse: IdentityFixture.CreateUserAsync(email, fullName, deptId, roles[]) · TestCurrentUser(userId, fullName?, email?, params roles) (Common) · FixedDateTime (Common/SqliteDbFixture.cs:71) · NoOpNotificationService (Services namespace, PurchaseEvaluationWorkflowServiceGuardTests.cs:188).

2. Test design (1 [Fact] T0 REPRODUCE)

  • (a) seed Project + WorkItem + PeWorkItemBudget pair (ProInitial=100tr, CCM InitialAmount=200tr).
  • (b) build PE ChoDuyet gắn cặp + workflow finalize; đưa tới DaDuyet QUA svc.TransitionAsync (approver duyệt, applyLevelFinalize default true, giá chốt 400tr/"Ncc").
  • (c) đổi pair LIVE: PRO 100tr→999tr (UpdatePeBudgetProCommand), CCM 200tr→888tr (UpdatePeBudgetCcmCommand), Admin. + sanity assert record live ĐÃ đổi (chống vacuous).
  • (d) query GetPurchaseEvaluationQuery bằng drafter (user thường, isDrafter=true qua authz :874/:891) → assert BudgetSummary.ProInitialAmount==100tr && InitialAmount==200tr.
  • Kỳ vọng RED (code hiện tại đọc live → 999tr/888tr).

3. Kết quả chạy — RED (đúng kỳ vọng T0)

Lệnh: dotnet test SolutionErp.slnx --filter "FullyQualifiedName~PeBudgetFreeze" --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1

Build: succeeded (test compile sạch → mọi API call/using đúng). Kết quả: Failed! - Failed: 1, Passed: 0, Skipped: 0, Total: 1, Duration: 7 s.

Assert-failure nguyên văn (EVIDENCE):

[xUnit.net 00:00:10.89]     SolutionErp.Infrastructure.Tests.Services.PeBudgetFreezeTests.DaDuyet_BudgetSummary_KeepsSnapshotValue_DoesNotFollowLivePairUpdate [FAIL]
  Failed SolutionErp.Infrastructure.Tests.Services.PeBudgetFreezeTests.DaDuyet_BudgetSummary_KeepsSnapshotValue_DoesNotFollowLivePairUpdate [7 s]
  Error Message:
   Expected s.ProInitialAmount to be 100000000M because phiếu DaDuyet phải GIỮ ngân sách PRO tại thời điểm duyệt (100tr), KHÔNG chạy theo record live (999tr), but found 999000000.0M (difference of 899000000.0).
  Stack Trace:
     ... at SolutionErp.Infrastructure.Tests.Services.PeBudgetFreezeTests.DaDuyet_BudgetSummary_KeepsSnapshotValue_DoesNotFollowLivePairUpdate() in ...\PeBudgetFreezeTests.cs:line 184
Failed!  - Failed:     1, Passed:     0, Skipped:     0, Total:     1, Duration: 7 s - SolutionErp.Infrastructure.Tests.dll (net10.0)

Đọc kết quả:

  • Fail tại PeBudgetFreezeTests.cs:184 = assert snapshot PRO s.ProInitialAmount == 100trfound 999tr (record live). Bug tái hiện chính xác: phiếu DaDuyet chạy theo bảng live.
  • 2 assert TRƯỚC ĐÃ PASS (nếu fail sẽ báo dòng khác): pe.Phase == DaDuyet (finalize path chạy đúng, KHÔNG seed thẳng) + liveRec.ProInitialAmount == 999tr/InitialAmount == 888tr (update ăn → assert (d) không vacuous). Chứng minh flow đầy đủ, CHỈ thiếu hành-vi snapshot.
  • Assert CCM (InitialAmount == 200tr, dòng sau) chưa chạy tới vì FluentAssertions throw ở assert PRO đầu tiên — bình thường cho RED.
  • Domain.Tests: "No test matches filter" = đúng (test nằm ở Infrastructure.Tests). KHÔNG phải lỗi.

Verdict: T0 RED PASS-as-designed. KHÔNG PREMISE-FAIL (không GREEN bất ngờ). KHÔNG chạm production. Sẽ GREEN sau khi implementer thêm snapshot ngân sách tại 4 site DaDuyet + display-gate đọc cột snapshot. Baseline: 509 → 510 test (test này RED tới khi fix land — test-before đúng quy trình bug-fix docs/rules.md §7).


STAGE C — INV-C T1-T9 (BE ĐÃ LAND, T0 ĐÃ GREEN → phủ nhánh/guard/edge còn lại)

Role: test-specialist. KHÔNG đụng src/ · KHÔNG sửa T0/test cũ. Thêm T1-T9 vào cùng file PeBudgetFreezeTests.cs.

Anchors verified (đọc code truth — BE post-fix)

  • Domain PurchaseEvaluation.cs:122-132 — 11 cột snapshot: ApprovedBudgetProInitialAmount/ProAdjustmentAmount/ProNote · CcmInitialAmount/CcmAdjustmentAmount/CcmNote · PrevSubmittedTotal/Count · PrevSelectedTotal/Count · SnapshotAt.
  • Service helper ApplyBudgetSnapshotOnFinalizeAsync def :994; gọi 4 site: :861 (level-finalize AllowApproverFinalize) · :900 (CCM-threshold delegation) · :933 (all-steps-done) · :1108 (V1 legacy — CHỈ snapshot, KHÔNG ApplyApprovedPrice). Helper: WorkItemId null → 10 cột null + SnapshotAt set; pair thiếu → 6 pair cols null; 4 lũy kế qua PeBudgetAccumulator; LUÔN overwrite + LUÔN SnapshotAt=dateTime.UtcNow.
  • Helper NAMING: pair.InitialAmount/AdjustmentAmount = CCM; pair.ProInitialAmount/ProAdjustmentAmount = PRO.
  • Display-gate PurchaseEvaluationFeatures.cs:928 frozen = Phase==DaDuyet && ApprovedBudgetSnapshotAt!=null. Frozen branch :929-954 → CanEditPro/Ccm HARDCODE false,false (KỂ CẢ Admin) + BudgetFrozen=true + lũy kế từ snapshot + CurrentProposalTotal VẪN live-own-quotes. Live branch :956-984 → accumulator + canEditPro/Ccm theo role + BudgetFrozen=false.
  • Guard 7a SetPeCcmBudgetPeriodCommandHandler PeWorkItemBudgetFeatures.cs:235 — Phase is DaDuyet or TuChoi → Conflict TRƯỚC role-gate (chặn CẢ Admin lẫn CCM).
  • Guard 7b AdjustPurchaseEvaluationBudgetCommandHandler PurchaseEvaluationFeatures.cs:365 — cùng message, TRƯỚC isAdmin block.
  • Accumulator: peers cùng (ProjectId,WorkItemId), Id!=this, CreatedAt<this. ⚠️ TestApplicationDbContext KHÔNG wire audit interceptor → set CreatedAt thủ công (peers earlier, current later).
  • V1 legacy: PurchaseEvaluationWorkflowDefinition + Steps + Approvers(Kind=User AssignmentValue=userId). Match qua userManager.FindByIdAsync → cần user THẬT (fix.CreateUserAsync).

T1-T9 danh sách (tên test = scenario_expected)

  • T1 LevelFinalize_SnapshotsAll11Columns_FromPairAndAccumulator (:861) — 6 pair cột + notes + 2 peer (PrevSubmitted 150tr/2, PrevSelected 90tr/1) + SnapshotAt.
  • T2 CcmThresholdFinalize_SetsBudgetSnapshot (:900) — CCM delegation below-threshold → snapshot pair cols + SnapshotAt.
  • T3 AllStepsDoneFinalize_SetsBudgetSnapshot (:933) — 1 Bước 1 Cấp no-finalize-flag, approve last slot → snapshot + DaDuyet.
  • T4 V1LegacyFinalize_SetsBudgetSnapshot_WithoutApprovedPrice (:1108) — def+step+user-approver → snapshot set + ApprovedPriceAmount==null.
  • T5 SetCcmBudgetPeriodGuard_BlocksDaDuyet_ForCcmAndAdmin_AllowsChoDuyet.
  • T6 AdjustBudgetGuard_BlocksDaDuyetAndTuChoi_ForAdmin.
  • T7 BudgetSummary_LiveVsFrozen_FlagsAndEditability.
  • T8 Finalize_NullSafe_NoWorkItemAndNoPairRecord.
  • T9 ReFinalize_AlwaysOverwritesStaleSnapshot_RefreshesSnapshotAt.

Kết quả chạy — ALL GREEN (T1-T9 phủ đủ, 0 RED phase vì BE đã land)

Filtered: dotnet test SolutionErp.slnx --filter "FullyQualifiedName~PeBudgetFreeze" --nologo -p:BuildInParallel=false -maxcpucount:1Passed! - Failed: 0, Passed: 10, Skipped: 0, Total: 10 (T0 + T1-T9).

FULL: dotnet test SolutionErp.slnx --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1 → Domain Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45 → Infra Passed! - Failed: 0, Passed: 474, Skipped: 0, Total: 474Tổng 519 PASS / 0 fail / 0 skip = baseline 510 (509 + T0) + 9 (T1-T9).

Bảng T1-T9 (verdict)

Test Nhánh/guard/edge Verdict
T1 LevelFinalize_SnapshotsAll11Columns_FromPairAndAccumulator :861 level-finalize → 11 cột PASS
T2 CcmThresholdFinalize_SetsBudgetSnapshot :900 CCM-delegation PASS
T3 AllStepsDoneFinalize_SetsBudgetSnapshot :933 all-steps PASS
T4 V1LegacyFinalize_SetsBudgetSnapshot_WithoutApprovedPrice :1108 V1 + giá null PASS
T5 SetCcmBudgetPeriodGuard_BlocksDaDuyet_ForCcmAndAdmin_AllowsChoDuyet guard 7a PASS
T6 AdjustBudgetGuard_BlocksDaDuyetAndTuChoi_ForAdmin guard 7b PASS
T7 BudgetSummary_LiveVsFrozen_FlagsAndEditability display live/frozen PASS
T8 Finalize_NullSafe_NoWorkItemAndNoPairRecord null-safe (a)+(b) PASS
T9 ReFinalize_AlwaysOverwritesStaleSnapshot_RefreshesSnapshotAt overwrite semantics PASS

Surprise / bẫy đã xử

  • UNIQUE(ApprovalWorkflows.Code, Version): T8 gọi SeedAndFinalizeAsync 2 lần/1 db → helper SeedFinalizeWorkflowAsync Code cố định "QT-BF-V2" v1 → SQLite Error 19 (RED lần 1). Fix: thêm param optional code="QT-BF-V2" (T0 call-site 2-arg GIỮ NGUYÊN, hành-vi T0 bất biến) + SeedAndFinalizeAsync truyền $"QT-BF-V2-{suffix}". Không đụng T0-logic.
  • KHÔNG chạm src/ production. KHÔNG sửa T0. Test cũ CŨ 0 regression (509 baseline vẫn xanh). KHÔNG REGRESSION-FOUND.

Verdict: INV-C COMPLETE — 9 test mới GREEN, phủ 4 nhánh snapshot + 2 guard + 2 edge + display live/frozen. Tổng 519/519.