wal: flush 20260717T1012

This commit is contained in:
pqhuy1987
2026-07-17 10:12:17 +07:00
parent 9f404d3f2d
commit 3ad4f2c53e
5 changed files with 116 additions and 15 deletions

View File

@ -3,18 +3,18 @@ updated: 2026-07-17 | session: S133 | branch: main
goal: PE budget FREEZE-at-DaDuyet (UAT anh Kiệt 07-16): phiếu DaDuyet bất biến NS, live = bản mới nhất; pipeline /fable-real invest → spec → hmw Opus MAX implement → review → deploy
chain:
[x] 1. wf: hmw fable-real investigator-codebase — wf_348f6155-167 · #53 return-fail → disk sub-file 22.5KB cứu · lead verify 7/7 claim PASS (finalize 4-site :861/:899/:931/:1053 · pair-live :911-912 · DTO 17-field :964 · guards :228-242/:419-422 · PeDetailTabs hash identical)
[x] 2. spec ghi: runs/2026-07-17-S133-pe-budget-freeze-daduyet/spec-pe-budget-freeze-daduyet-17-07-2026.md (Mig 67 AddPeApprovedBudgetSnapshot 11 cột · OD1 CÓ lũy-kế · OD2 CÓ block-admin · OD3 KHÔNG TuChoi)
[!] 3. wf: hmw INV-A 2-task (test-specialist T0-REPRODUCE RED ‖ implementer-frontend types+badge 2-app) — wf_d9639faa-97a · note: hook nén-17.1K bắn lần nữa trên inv-cb 19.7KB (carry hook-vs-budget-cap — KHÔNG tự nén, dưới cap anh 25.6K/30.7K)
[ ] 4. wf: hmw INV-B 1-task implementer-backend (Domain+Config+Mig67 3-file+accumulator+helper 4-site+display-gate+2 guard+build slnx+T0 GREEN)
[ ] 5. wf: hmw INV-C 1-task test-specialist (T1-T9 + full suite PASS)
[ ] 6. reviewer pre-commit (mig+financial) → em main commit push → cicd-monitor verify prod
[ ] 7. harvest + docs closeout
[x] 1. wf: hmw fable-real investigator-codebase — wf_348f6155-167 · #53 return-fail → disk 22.5KB cứu · lead verify 7/7 PASS
[x] 2. spec ghi + errata INV-A (query = GetPurchaseEvaluationQuery): runs/2026-07-17-S133-pe-budget-freeze-daduyet/spec-pe-budget-freeze-daduyet-17-07-2026.md
[x] 3. wf: hmw INV-A 2-task — wf_d9639faa-97a · T0 RED thật (Failed 1/1, found 999tr @:184, prod sạch) · FE 4 file, hash mirror 5534addb ×2, budgetFrozen 4 file · lead verify đĩa PASS
[!] 4. wf: hmw INV-B 1-task implementer-backend (Domain 11 col + Config + Mig 67 3-file backfill idempotent + PeBudgetAccumulator + helper 4-site :861/:899/:931/:1053 + display-gate GetPurchaseEvaluationQuery + 2 terminal-guard + build slnx + T0 GREEN không-sửa-test) — wf_4bef08b8-730
[ ] 5. wf: hmw INV-C 1-task test-specialist (T1-T9 + full suite PASS ≥510+8)
[ ] 6. reviewer pre-commit (mig+financial) → em main npm build ×2 re-verify + commit push → cicd-monitor verify prod
[ ] 7. harvest + docs closeout (STATUS Mig 67 · HANDOFF · session-log)
next: chờ INV-A return → verify (RED evidence + FE hash-equal + npm build ×2) → phóng INV-B
next: chờ INV-B return → verify (build slnx 0-err · T0 GREEN · 4 site snapshot-call · mig 3-file) → phóng INV-C
verify:
- ls .claude/workflows/runs/2026-07-17-S133-pe-budget-freeze-daduyet/
- git status --porcelain (sau INV-A: tests/ mới + fe-* đổi; src/Backend PHẢI sạch tới INV-B)
- dotnet test --filter PeBudgetFreeze (RED trước INV-B, GREEN sau)
- grep -rn "Phase = PurchaseEvaluationPhase.DaDuyet" src/Backend/ (4 hit, sau INV-B mỗi hit có snapshot-call kề)
- dotnet test SolutionErp.slnx --filter PeBudgetFreeze --nologo (GREEN sau INV-B)
- grep -rn "Phase = PurchaseEvaluationPhase.DaDuyet" src/Backend/ (4 hit, mỗi hit snapshot-call kề ≤6 dòng trước)
- grep -c AddColumn <Mig67>.cs = 11 · git status Migrations/ 3 file
- git hash-object 2 PeDetailTabs = 5534addb (FE không bị INV-B đụng)

View File

@ -21,7 +21,7 @@
- [x] 1. invest fable-real (wf: `wf_348f6155-167`) — #53 return-fail, disk sub-file 22.5KB cứu; lead verify 7/7 claim PASS
- [x] 2. lead verify + ghi `spec-pe-budget-freeze-daduyet-17-07-2026.md` (OD1 CÓ · OD2 CÓ · OD3 KHÔNG)
- [ ] 3. hmw implement Opus MAX theo spec — 3 invocation staged:
- [ ] INV-A (2-task ‖): test-specialist T0 REPRODUCE (RED evidence, KHÔNG đụng src/) ‖ implementer-frontend (types ×2 riêng + PeDetailTabs SHA-mirror badge + npm build ×2)
- [x] INV-A (2-task ‖, wf_d9639faa-97a, 0-garble): T0 RED thật (`Failed 1/1` found 999tr @PeBudgetFreezeTests.cs:184, prod sạch — evidence sub-test-specialist-0.md) · FE 4 file (badge amber @ "Tổng hợp ngân sách trình ký" :1352, hash mirror `5534addb` ×2, npm build ×2 PASS) · errata spec: query = GetPurchaseEvaluationQuery (bundle prop BudgetSummary)
- [ ] INV-B (1-task): implementer-backend (Domain +11 col · Config · Mig 67 3-file + backfill idempotent · PeBudgetAccumulator · ApplyBudgetSnapshotOnFinalizeAsync 4-site · display-gate BudgetFrozen · 2 terminal-guard · build slnx · T0 GREEN)
- [ ] INV-C (1-task): test-specialist T1-T9 + full suite
- [ ] 4. reviewer pre-commit → commit/push → cicd verify prod

View File

@ -2,6 +2,7 @@
> **Provenance:** UAT anh Kiệt FDC chat 2026-07-16 17:24-17:27 (screenshot anh gửi) → `/fable-real investigator-codebase` deep-pass `wf_348f6155-167` (#53 return-fail → disk-truth `sub-investigator-codebase-0.md` 22.5KB) → **lead verify từ đĩa 2026-07-17** (finalize-map 4/4 hit khớp grep · pair-load :911-912 khớp · guard-vectors :228-242/:419-422 khớp · PeDetailTabs 2-app hash IDENTICAL `931025f2` · types 2-app hash KHÁC) → lead ghi spec này.
> **Owner-resolve 3 open-decision (theo khuyến nghị invest, veto được):** OD1 **CÓ** snapshot 4 cột lũy kế · OD2 **CÓ** terminal-guard block cả Admin · OD3 **KHÔNG** freeze TuChoi (DaDuyet-only).
> **Errata @INV-A (2026-07-17):** tên query detail-bundle ĐÚNG theo code = `GetPurchaseEvaluationQuery` (trả `PurchaseEvaluationDetailBundleDto.BudgetSummary`) — bản đầu spec ghi nhầm `GetPurchaseEvaluationDetailQuery` (test-specialist bắt khi viết T0; đã sửa toàn spec). Con số dòng `:905-973/:964` không đổi.
## ① Tính-năng / đề-bài
@ -21,7 +22,7 @@ Phiếu PE **Phase=DaDuyet bất biến số ngân sách** (R1 — anh Kiệt: "
*(predicate PHẢI khớp `PurchaseEvaluationFeatures.cs:917-943` — reviewer đối chiếu 3 bản: SQL ↔ accumulator ↔ Features cũ; enum: ChoDuyet=10, DaDuyet=7)*
- Down: DropColumn ×11 (data-restore no-op mirror Mig 61/65).
- Apply local: `dotnet ef database update` cả `SolutionErp_Dev` (--connection override) + `SolutionErp_Design` (per skill ef-core-migration + feedback_designtime_runtime_db).
4. **Shared accumulator (chống drift 3-bản):** extract khối lũy kế + pair-load semantics từ `GetPurchaseEvaluationDetailQuery` (`PurchaseEvaluationFeatures.cs:917-951`) ra helper static mới `Application/PurchaseEvaluations/PeBudgetAccumulator.cs` (input: db, projectId, workItemId, peId, peCreatedAt → output record {PrevSubmittedTotal/Count, PrevSelectedTotal/Count}) — dùng chung: GetDetail nhánh live + service snapshot. GetDetail refactor gọi accumulator (0 đổi hành vi — so output trước/sau bằng test hiện có).
4. **Shared accumulator (chống drift 3-bản):** extract khối lũy kế + pair-load semantics từ `GetPurchaseEvaluationQuery` (`PurchaseEvaluationFeatures.cs:917-951`) ra helper static mới `Application/PurchaseEvaluations/PeBudgetAccumulator.cs` (input: db, projectId, workItemId, peId, peCreatedAt → output record {PrevSubmittedTotal/Count, PrevSelectedTotal/Count}) — dùng chung: GetDetail nhánh live + service snapshot. GetDetail refactor gọi accumulator (0 đổi hành vi — so output trước/sau bằng test hiện có).
5. **Service snapshot helper** `Infrastructure/Services/PurchaseEvaluationWorkflowService.cs` — +`private async Task ApplyBudgetSnapshotOnFinalizeAsync(PurchaseEvaluation evaluation, CancellationToken ct)`:
- Load pair `AsNoTracking` theo (ProjectId, WorkItemId) — null-safe (WorkItemId null / pair thiếu → các cột số = null, VẪN set `ApprovedBudgetSnapshotAt` = UtcNow làm marker).
- Gọi accumulator cho 4 số lũy kế. **LUÔN overwrite** (re-finalize sau un-terminal/TraLai = re-snapshot lần DaDuyet cuối — khớp "bản cập nhật gần nhất" áp cho thời điểm duyệt).
@ -29,7 +30,7 @@ Phiếu PE **Phase=DaDuyet bất biến số ngân sách** (R1 — anh Kiệt: "
- **Gọi tại ĐÚNG 4 site, ngay trước `evaluation.Phase = PurchaseEvaluationPhase.DaDuyet`:** `:861` (level-finalize) · `:899` (CCM-threshold) · `:931` (all-steps-done) · `:1053` (V1 legacy — nhánh này hiện KHÔNG gọi ApplyApprovedPriceOnFinalize, đừng copy nhầm behavior giá-chốt vào V1; CHỈ thêm budget-snapshot).
6. **DTO + display-gate** `Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs` + `PurchaseEvaluationFeatures.cs`:
- `PeBudgetSummaryDto` (positional record, hiện 17 field, 1 construction site `:964`) — **append cuối** `bool BudgetFrozen = false`.
- `GetPurchaseEvaluationDetailQuery` (`:905-973`): `var frozen = e.Phase == PurchaseEvaluationPhase.DaDuyet && e.ApprovedBudgetSnapshotAt != null;` → nhánh frozen: build DTO từ cột `ApprovedBudget*` (fullAmount/FullIsEstimate dùng CÙNG công thức `:957-962` nhưng input = snapshot cols; `CanEditPro=CanEditCcm=false`; `CurrentProposalTotal` vẫn từ own quotes — tự-frozen bởi edit-guard; `BudgetFrozen=true`); nhánh else: live y nguyên + `BudgetFrozen=false`. Fallback an toàn: DaDuyet mà SnapshotAt null (không xảy ra sau backfill) → live như cũ.
- `GetPurchaseEvaluationQuery` (`:905-973`): `var frozen = e.Phase == PurchaseEvaluationPhase.DaDuyet && e.ApprovedBudgetSnapshotAt != null;` → nhánh frozen: build DTO từ cột `ApprovedBudget*` (fullAmount/FullIsEstimate dùng CÙNG công thức `:957-962` nhưng input = snapshot cols; `CanEditPro=CanEditCcm=false`; `CurrentProposalTotal` vẫn từ own quotes — tự-frozen bởi edit-guard; `BudgetFrozen=true`); nhánh else: live y nguyên + `BudgetFrozen=false`. Fallback an toàn: DaDuyet mà SnapshotAt null (không xảy ra sau backfill) → live như cũ.
7. **Write-guards (OD2 — block CẢ Admin trên terminal):**
- `PeWorkItemBudgetFeatures.cs` `SetPeCcmBudgetPeriodCommandHandler` (`:228+`): +`ConflictException` khi `pe.Phase ∈ {DaDuyet, TuChoi}` (đặt TRƯỚC role-check side-effects, sau NotFound).
- `PurchaseEvaluationFeatures.cs` `AdjustBudgetCommand` (`:419-422`): +Conflict terminal-phase TRƯỚC nhánh `else { actorTag = "[Admin]" }`.

View File

@ -0,0 +1,38 @@
# sub-implementer-frontend-1 — S133 STAGE A (FE types + badge frozen)
Role: implementer-frontend. Task: FE `budgetFrozen` field + badge (disjoint BE). 4 files only.
## Refuse self-check → PASS (proceed)
- UX decision? NO — badge placement + style fully specified (pill cạnh header "Tổng hợp ngân sách trình ký", amber/slate rounded-full text-xs).
- Schema design? NO — field `budgetFrozen: boolean` chốt sẵn spec ②.
- BE touch? NO — chỉ FE 4 file.
- First-time pattern? NO — pill badge proven (line 588/591/759 emerald "Đã ký"/"Đã duyệt").
- Ambiguity? < 20% spec chi tiết + MEMORY-PACK định vị chính xác.
## Recon (evidence file:line)
- Baseline SHA both PeDetailTabs = `931025f240e04404a7d1821dbfd948f56da5eae5` (git hash-object) khớp spec `931025f2`. 2 file byte-identical.
- `PeBudgetSummary` type: fe-user `types/purchaseEvaluation.ts:314-333` · fe-admin `:312-331`. Kết thúc bằng `currentProposalTotal: number` + `}`. 2 file block NỘI-DUNG identical (chỉ lệch line-number edit riêng, same old_string OK).
- `bs = ev.budgetSummary` (`PeBudgetSummary|null`) @ PeDetailTabs.tsx:1226-1228. Null-guard `if(!bs) return` @:1298 `bs` narrowed non-null tại render `bs.budgetFrozen` typecheck OK.
- Header render @ PeDetailTabs.tsx:1350-1352: `<div className="bg-[#1F7DC1] px-3 py-2 text-[12px] font-bold uppercase tracking-wide text-white">Tổng hợp ngân sách trình ký</div>`. className `bg-[#1F7DC1]...` LẶP @:1931 (winners section) anchor phải gồm text "Tổng hợp ngân sách trình " (unique).
- Pill pattern sync: `:588-594` header `flex items-center justify-between` + badge `inline-flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-medium text-emerald-700`. Dùng amber variant cho frozen (blue header bar amber pill đọc ).
## Plan 4 edit
1. types fe-user + fe-admin: +`budgetFrozen: boolean` cuối type PeBudgetSummary (comment [S133]). Edit RIÊNG 2 file (same old_string).
2. PeDetailTabs fe-user + fe-admin: header div flex justify-between, wrap title `<span>`, +badge `{bs.budgetFrozen && ...}` amber pill "🔒 Ngân sách chốt tại thời điểm duyệt" (normal-case tracking-normal override header uppercase). Edit GIỐNG HỆT byte 2 file.
## Progress — ALL DONE ✅
- [x] Edit types ×2: `budgetFrozen: boolean` @ fe-user types:333 · fe-admin types:331 (edit riêng, same old_string; comment [S133]).
- [x] Edit PeDetailTabs ×2 (byte-identical): header div `flex items-center justify-between gap-2`, title wrap `<span>`, +badge `{bs.budgetFrozen && (<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-medium normal-case tracking-normal text-amber-800">🔒 Ngân sách chốt tại thời điểm duyệt</span>)}` @ both :1352. `normal-case tracking-normal` override header `uppercase tracking-wide` để badge text đọc thường. Layout non-frozen giữ nguyên (1 span trong flex = left-align).
- [x] git hash-object 2 PeDetailTabs EQUAL: `5534addb9a7887c00562a99ab1f52ed4ee77c0ca` (cả 2) mirror giữ (baseline `931025f2` post `5534addb`).
- [x] grep budgetFrozen = 4 hit: fe-user types:333 + fe-user PeDetailTabs:1352 + fe-admin types:331 + fe-admin PeDetailTabs:1352.
- [x] npm build fe-user EXIT 0 1939 modules, 19.84s, `index-DxCKQyJZ.js` 1534.07kB. Warn pre-existing (@import order + >500KB chunk + INEFFECTIVE_DYNAMIC_IMPORT realtime.ts).
- [x] npm build fe-admin EXIT 0 — 1950 modules, 8.75s, `index-DTUy4uPK.js` 1627.72kB. Warn pre-existing (same 3).
## Isolation audit (git status --short)
- Mine (4 target FE): fe-admin/fe-user × {PeDetailTabs.tsx, types/purchaseEvaluation.ts} — M ✓
- Run-folder: sub-implementer-frontend-1.md — ?? ✓
- NO src/Backend touched (grep Backend → none) ✓
- ⚠️ `.claude/governance/.session-counter.json` M = harness/session-tooling artifact, KHÔNG do tôi ghi (mọi Bash tôi chạy read-only: build→dist/ gitignored, hash-object, grep, status). Flag để em main không quy cho STAGE A.
## Note cho STAGE B (BE — implementer-backend)
- FE khai `budgetFrozen: boolean` (non-null) theo DTO chốt spec ②. BE PeBudgetSummaryDto phải append `bool BudgetFrozen = false` → serialize camelCase `budgetFrozen`. Field CHƯA ship BE → runtime `bs.budgetFrozen` undefined = falsy → badge ẩn an-toàn (KHÔNG vỡ UI, KHÔNG runtime error). Khi BE ship: DaDuyet+snapshot → true → badge hiện.

View File

@ -0,0 +1,62 @@
# 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-912``pairRec` load **LIVE** từ `db.PeWorkItemBudgets.FirstOrDefaultAsync(b => b.ProjectId == e.ProjectId && b.WorkItemId == wiKey)` — KHÔNG phân biệt Phase.
- `:964-972``PeBudgetSummaryDto` 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 == 100tr`**found 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).