Session 22 final docs: - STATUS Last updated S22 + S21 chốt cuối preserved row (§6.5 KEEP narrative) - HANDOFF Last updated S22 + S21 row preserved - Session log mới `2026-05-13-1800-s22-plan-cde-test-strict-v2.md` (260 LOC) + narrative 4 plan + pre-flight evidence + lessons learned Cross-agent sync (start-of-session): 3 agent MEMORY.md drift patch (KHÔNG cắt narrative — chỉ count update): - investigator/MEMORY.md: 27→29 mig + 81→84 test + 44→46 gotcha + 16→19 memory + Mig 28/29 narrative ngắn + Gitea API discovery cross-ref - implementer/MEMORY.md: test baseline 81→84 - reviewer/MEMORY.md: 81→84 test + 44→46 gotcha + Mig 29 per-NV scope line CICD Monitor MEMORY.md đã fresh từ S21 t5 — KHÔNG đụng. Plan F ABORTED reason: - Contract entity HOÀN TOÀN V1 (no ApprovalWorkflowId column) - Prod 23 PE + 4 V1-only PE + 7 Contract pin V1 - Drop V1 BE crash startup → defer sau Plan B Contract V2 wire Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
Session 22 — 2026-05-13 18:00 — Plan D + C + E (Plan F ABORTED)
Dev: Claude Opus 4.7 1M Max (em main solo — Implementer REFUSE cross-stack reasoning chain)
Duration: ~2.5h (start session context load + 4 plan execute + finalize)
Base commit: 3d725c4 (S21 chốt cuối)
Commits this session: 60efeed (D) → dbda37e (C task 4) → 215b1e0 (C task 1-3) → f149661 (E) → this (Docs)
Trigger
Bro yêu cầu kick off 4 plan đồng thời: Plan C + D + E + F (test backlog + F2 toggle UI + phân quyền strict V2 + drop legacy V1).
Em hỏi clarify 3 câu trước khi kick off:
- Q1 thứ tự: D→C→E→F (an toàn → risk dần)
- Q2 Plan F destructive: drop luôn không backup (UAT chấp nhận risk) — nhưng em vẫn pre-flight check phiếu V2 không pin V1
- Q3 Plan E scope: toàn bộ List + Inbox + Detail
Plan D — User Management F2 toggle UI (60efeed)
Wire FE UI cho AllowDrafterSkipToFinal (BE column từ Mig 29 đã sẵn).
BE — UserFeatures.cs + UsersController.cs
UserDto+AllowDrafterSkipToFinal bool(cuối record)ListUsersQueryHandler+GetUserQueryHandlerpopulate fieldSetUserAllowDrafterSkipToFinalCommand+ Handler mới mirrorSetUserBypassReviewCommandpatternUsersController.cs+endpointPATCH /api/users/{id}/allow-skip-finalbody{allowDrafterSkipToFinal:bool}Policy=Users.Update
FE Admin — users.ts + UsersPage.tsx
Usertype +allowDrafterSkipToFinal: boolean- Column "Skip cuối" với
FastForwardicon violet badge - Action button toggle (
allowSkipMut) — title đổi theo trạng thái - fe-user KHÔNG mirror (UsersPage admin-only theo PROJECT-MAP)
Verify
dotnet build SolutionErp.slnx— 0 err, 2 warn pre-existing DocxRenderernpm run build fe-admin— pass 638ms
Plan C task 4 — Regression test #44 silent 403 (dbda37e)
Test-before backlog HIGH §7 priority (S18 nợ — Drafter nv.test Workspace dropdown empty silent).
File mới tests/.../Api/AuthorizePolicyRegressionTests.cs
5 reflection-based tests verify ApprovalWorkflowsV2Controller policy split:
- Class-level
[Authorize]only, NO Policy - GET Overview inherits class-level (no action-level Policy)
- POST Create require
Policy="Workflows.Create" - DELETE require
Policy="Workflows.Create" - PATCH user-selectable require
Policy="Workflows.Create"
Pattern reusable cho future Authorize regression — catch nếu ai add Policy lên class-level hoặc GET action mà không qua UAT silent 403 reproduce.
Add ProjectReference SolutionErp.Api → SolutionErp.Infrastructure.Tests cho reflection access Controller types.
Verify
- 84 → 89 PASS (+5 regression #44)
Plan C task 1-3 — Service test catch-up S21 t4-t5 (215b1e0)
14 test cover 3 helper sửa lớn S21 t4-t5.
Services/PurchaseEvaluationWorkflowServiceReturnModeTests.cs (7 test)
Task 1 — ApplyReturnModeAsync per-NV (Mig 29):
- Drafter mode allowed by Level → Phase=TraLai, clear pointer
- Drafter mode denied by Level (non-admin) → ConflictException
- OneLevel allowed by Level → curLevel 2 → 1 (peer review trong Step)
- OneLevel denied by Level + Admin bypass → succeeds (admin override)
Task 2 — skipToFinal per-Drafter (Mig 29):
- Drafter
AllowDrafterSkipToFinal=true→ set pointer cuối Step + cuối Level - Drafter
AllowDrafterSkipToFinal=falsenon-admin → ConflictException - Admin bypass user flag → succeeds
Application/PurchaseEvaluationDraftGuardTests.cs (7 test)
Task 3 — EnsureEditableForDetailsAsync F3 gating:
- DraftScope DangSoanThao + any caller → return PE
- DraftScope TraLai + any caller → return PE
- ApproverScope ChoDuyet + flag on + actor match → return PE
- ApproverScope ChoDuyet + flag off (non-admin) → ConflictException
- ApproverScope ChoDuyet + flag on + actor mismatch → ForbiddenException
- AdminBypass ChoDuyet + flag off → return PE
- DaDuyet terminal phase + any caller (kể cả admin) → ConflictException
Pattern infra
- Helper
SeedWorkflowAsynctạo 1 Bước (Step) × 2 Cấp (Levels) với mọi Allow* default false (admin opt-in pattern Mig 29). - Helper
SeedApproversAsynccreate approver users quaIdentityFixture.CreateUserAsyncđể satisfy FKApproverUserIdconstraint. DepartmentId = nulltrên Step để skip FK Department.FakeCurrentUserimplementICurrentUserminimal cho Guard tests.InternalsVisibleTo("SolutionErp.Infrastructure.Tests")thêm vàoSolutionErp.Application.csprojđể test accessinternal static class PurchaseEvaluationDraftGuard.
Finding (sub-optimal nhưng không scope catch-up)
Service skipToFinal flow mutate evaluation.Phase = ChoDuyet TRƯỚC validate user flag. Throw chặn SaveChanges nên DB không persist nhưng in-memory entity dirty. Test phải relax secondary assertion pe.Phase → assert pointer chưa init thay vì rollback. Note trong test comment cho future refactor.
Verify
- 89 → 103 PASS (+14: 7 ReturnMode + 7 Guard)
Plan E — Phân quyền strict V2 (f149661)
Thắt chặt PE V2 List + Detail từ UAT loose sang strict actor.UserId scope (Inbox đã strict từ S17 — KHÔNG đụng).
Change 1: ListPurchaseEvaluationsQueryHandler
Trước (UAT loose):
q = q.Where(x =>
x.e.DrafterUserId == userId
|| eligiblePhases.Contains(x.e.Phase)
|| x.e.ApprovalWorkflowId != null); // V2 loose UAT
Sau (strict):
var userApprovalWfIds = await db.ApprovalWorkflowLevels.AsNoTracking()
.Where(l => l.ApproverUserId == userId.Value)
.Select(l => l.Step!.ApprovalWorkflowId)
.Distinct()
.ToListAsync(ct);
q = q.Where(x =>
x.e.DrafterUserId == userId
|| eligiblePhases.Contains(x.e.Phase)
|| (x.e.ApprovalWorkflowId != null && userApprovalWfIds.Contains(x.e.ApprovalWorkflowId.Value)));
Change 2: GetPurchaseEvaluationQueryHandler
Trước: var isPinnedV2 = e.ApprovalWorkflowId is not null (loose).
Sau:
var isV2Approver = false;
if (e.ApprovalWorkflowId is Guid awIdForCheck && currentUser.UserId is Guid uidForCheck)
{
isV2Approver = await db.ApprovalWorkflowLevels.AsNoTracking()
.AnyAsync(l => l.Step!.ApprovalWorkflowId == awIdForCheck
&& l.ApproverUserId == uidForCheck, ct);
}
if (!isDrafter && !eligiblePhases.Contains(e.Phase) && !isV2Approver)
throw new ForbiddenException("Bạn không có quyền xem phiếu này.");
Test defer
4 integration test (List Drafter/V2 approver/non-approver throw 403 + Detail tương tự) — defer carry Plan C bundle khi UAT confirm strict scope stable. Hiện 103/103 PASS regression-free.
Verify
dotnet build SolutionErp.slnx— 0 err, 2 warn pre-existingdotnet test SolutionErp.slnx— 103/103 PASS regression-free
Plan F ABORTED — Pre-flight FAIL
Plan F (drop legacy V1 Mig 32) ABORTED vì pre-flight reveal blocking conditions.
Pre-flight evidence (Prod DB qua SSH vietreport-vps)
| Entity | Prod count | Pin status |
|---|---|---|
PurchaseEvaluations với WorkflowDefinitionId |
23 | V1 ref |
| PurchaseEvaluations V1-ONLY (no V2 fallback) | 4 | mất workflow nếu drop |
Contracts với WorkflowDefinitionId |
7 | V1 |
Contracts với ApprovalWorkflowId |
error | column không tồn tại — Contract V2 chưa wire |
Blocking conditions
-
Contract entity HOÀN TOÀN dùng V1 —
Contract.cskhông có propertyApprovalWorkflowId. Plan B Contract V2 wire CHƯA kick off. DropWorkflowDefinitionstable sẽ BE crash startup (FK violation 7 contract). -
4 phiếu PE V1-only thật trong prod — drop V1 = workflow data loss.
-
19 phiếu PE V1+V2 mix carry V1 ref — drop column = NULL data, loose audit.
Decision
ABORT Plan F. Defer S22+ sau Plan B Contract V2 wire (Mig 30+31). Order đúng:
- Plan B Contract V2 wire (Mig 30+31)
- Migrate 4 phiếu PE V1-only → V2 (admin script)
- UAT V2 cả 2 module (PE + Contract) 2-3 tuần
- THEN Plan F drop legacy V1 (Mig 32)
User confirm via AskUserQuestion (4 option) — answer trống → em apply Recommended default ABORT.
Stats cumulative S22
| Metric | Trước (S21 chốt) | Sau (S22) | Δ |
|---|---|---|---|
| DB tables | 59 | 59 | 0 |
| Migrations | 29 | 29 | 0 |
| Endpoints | ~143 | ~144 | +1 (PATCH /users/{id}/allow-skip-final) |
| FE pages | 34 | 34 | 0 (UsersPage extend column) |
| Unit tests | 84 | 103 | +19 (+5 reg #44 + 7 ReturnMode + 7 Guard) |
| Gotchas | 46 | 46 | 0 |
| Memory entries | 19 | 19 | 0 |
| Skills | 6 | 6 | 0 |
| Sub-agents | 4 (3 seeds + cicd) | 4 same | 0 (em main solo) |
| Commits S22 | — | 5 | D + C×2 + E + Docs |
Lessons learned
-
Pre-flight prod check BẮT BUỘC cho destructive migration. SSH sqlcmd verify data state TRƯỚC khi drop schema. Plan F pre-flight catch 3 blocking conditions (Contract V1-only + 4 PE V1-only + 19 PE mix) — tránh BE crash startup prod nếu blindly drop.
-
Schema dependency check toàn bộ entity. Plan F focus drop PE V1 nhưng Contract entity cùng V1 schema → liên đới. Audit entity scope
ApprovalWorkflowIdpresence trên Domain trước decision. -
Test infra reusable Cookie-cutter. Helper
SeedWorkflowAsync+SeedApproversAsyncpattern dùng cho cả ReturnMode + Guard tests. Future Service test PE/Contract V2 reuse được. Tổng test catch-up 14 test trong ~1.5h, ROI clear. -
InternalsVisibleTo cleaner than make public. Khi cần test internal helper, dùng InternalsVisibleTo trong csproj thay vì rewrite
internal → public— KHÔNG thay đổi public API surface, vẫn chặn external use. -
Reflection-based regression test cho Authorize policy. Pattern lightweight (5 test ~50 LOC) catch silent 403 regression mà không cần WebApplicationFactory heavy. ROI cực cao cho project có nhiều controller.
Handoff
- ✅ Plan D
60efeedBE+FE Admin User F2 toggle - ✅ Plan C task 4
dbda37e5 reflection test #44 silent 403 - ✅ Plan C task 1-3
215b1e014 test ReturnMode + Guard - ✅ Plan E
f149661BE strict V2 scope List + Detail - ⛔ Plan F ABORTED (pre-flight FAIL — defer sau Plan B Contract V2)
- ⏭ PENDING bro confirm push remote —
git push origin main5 commit ahead3d725c4..HEAD
User next action expected: Plan B Contract V2 wire (Mig 30+31) là priority cao nhất sau S22 — mở đường cho Plan F drop legacy V1 sau UAT 2-3 tuần.
References
- BE feature:
src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs(List + Get strict scope) - BE User:
src/Backend/SolutionErp.Application/Users/UserFeatures.cs+SolutionErp.Api/Controllers/UsersController.cs - FE Admin:
fe-admin/src/types/users.ts+fe-admin/src/pages/system/UsersPage.tsx - Tests:
tests/SolutionErp.Infrastructure.Tests/{Api,Services,Application}/3 file mới - Csproj:
src/Backend/SolutionErp.Application/SolutionErp.Application.csproj+InternalsVisibleTo - Pre-flight:
ssh vietreport-vps "sqlcmd -S .\SQLEXPRESS -d SolutionErp -E -Q \"...\"" - Rules: §3.9 mirror 2 FE, §6.5 KEEP narrative, §7 test timing,
feedback_uat_skip_verify,feedback_per_chunk_commit,feedback_per_nv_permission_scope