diff --git a/.claude/skills/ef-core-migration/SKILL.md b/.claude/skills/ef-core-migration/SKILL.md index 6c1218c..76730fe 100644 --- a/.claude/skills/ef-core-migration/SKILL.md +++ b/.claude/skills/ef-core-migration/SKILL.md @@ -48,8 +48,12 @@ Total: **52 bảng** dbo + `__EFMigrationsHistory`. Xem `docs/database/schema-di **Phase 8 update (2026-04-29 Session 5):** - Migration 15 `AddPurchaseEvaluationDepartmentOpinions` — 1 bảng riêng (UNIQUE PEId+Kind), max 4 row mỗi phiếu cho 4 phòng ban (Phê duyệt/Ccm/MuaHang/SmPm). UPDATE in-place khi user đổi ý, audit qua Changelog. -- Tests Phase 1-2 + CI gate live: `tests/SolutionErp.Domain.Tests/` (54 test policy state machine) + `tests/SolutionErp.Infrastructure.Tests/` (17 test code generator). Total 71 test pass / ~2s. CI fail → no deploy. -- Migration verify pattern thêm: chạy `dotnet test` local trước mỗi migration commit để chống regression policy/codegen. +- Tests Phase 1-2-3mini live: `tests/SolutionErp.Domain.Tests/` (54 test policy state machine) + `tests/SolutionErp.Infrastructure.Tests/` (17 test code generator + 6 test PE WF versioning). **Total 77 test pass / ~3s**. CI fail → no deploy. +- CI optimize 3 fix (29/04): + - Manual checkout bypass github.com (gotcha #39) — fix TCP timeout 21s + - Path filter docs-only skip (gotcha #41) — commit MD-only KHÔNG trigger CI + - npm junction cache (gotcha #40) — rolled back, debug session sau +- Migration verify pattern thêm: chạy `dotnet test SolutionErp.slnx` local trước mỗi migration commit để chống regression policy/codegen. **Phase 7 update (2026-04-28):** - Migration 14 `AddBudgets` — 4 bảng (Budgets/Details/Approvals/Changelogs) + 2 cột `BudgetId?` thêm vào Contracts & PurchaseEvaluations. Workflow hardcoded `BudgetPolicy.Default` (chưa versioned). diff --git a/CLAUDE.md b/CLAUDE.md index 1c4b50d..f1dc46c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,7 +63,7 @@ Kiến trúc: **.NET 10 Clean Architecture + 2 React FE (admin + user) + SQL Ser | Identity (User/Role/Permission/MenuItem) | `Domain/Identity/` | 1, 3, 11 | Feature-complete (30 demo user — 16 sample + 14 Solutions thật) | | Forms (Template + Clause) | `Domain/Forms/` | 4 | Feature-complete | | Notifications | `Domain/Notifications/` | 6 | In-app + SignalR OK, email SMTP TODO | -| **Tests** | `tests/SolutionErp.{Domain,Infrastructure}.Tests/` | — | **71 test pass** (Phase 1: 54 Domain policy + Phase 2: 17 Infra code generator) — CI gate live | +| **Tests** | `tests/SolutionErp.{Domain,Infrastructure}.Tests/` | — | **77 test pass** (54 Domain + 17 Infra + 6 PE WF Application Phase 3 mini) — CI gate + path filter docs-only skip | ### Commit convention @@ -77,17 +77,27 @@ Kiến trúc: **.NET 10 Clean Architecture + 2 React FE (admin + user) + SQL Ser ``` tests/ -├── SolutionErp.Domain.Tests/ (54 test policy state machine - Phase 1) -└── SolutionErp.Infrastructure.Tests/ (17 test code generator format/sequence - Phase 2) +├── SolutionErp.Domain.Tests/ (54 test - Phase 1: WorkflowPolicy / PEPolicy / BudgetPolicy) +└── SolutionErp.Infrastructure.Tests/ (17 + 6 = 23 test) + ├── Services/ (17 test - Phase 2: Contract + PE Code Generator) + └── Application/ (6 test - Phase 3 mini: PeWorkflowDefinition versioning) ``` -**71 unit test pass** / ~2s. CI gate `.gitea/workflows/deploy.yml` — `dotnet test` fail → no deploy. +**77 unit test pass** / ~3s. CI gate + path filter live. ```bash dotnet test SolutionErp.slnx # chạy cả 2 test project ``` -**Quy tắc:** mỗi feature mới có guard logic / business rule → thêm test trước khi commit. Bug found in production → 1 regression test added before merge. Detail xem `docs/architecture.md §11`. +**Quy tắc:** mỗi feature mới có guard logic / business rule → thêm test trước khi commit. Bug found in production → 1 regression test added before merge. Detail xem `docs/architecture.md §11` + `docs/rules.md §7`. + +### CI/CD pipeline (3 fix lớn 29/04) + +- ✅ Manual checkout bypass github.com (fix gotcha #39 TCP timeout) +- ✅ Path filter docs-only skip — `paths-ignore` (gotcha #41) +- ⏸️ npm cache (gotcha #40 — failed, rolled back) + +**Tốc độ:** code commit ~3 phút / docs-only commit **0s** (skip). ## 🛠️ Skills (.claude/skills/) — 6 skill PHẢI dùng khi task khớp diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 6b6aff9..170a326 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,6 +1,6 @@ # HANDOFF — Brief 5 phút cho session tiếp theo -**Last updated:** 2026-04-29 (Phase 8 — **Budget FE complete + PE/HD-Budget integration + PE Workflow Designer + Ý kiến 4 PB + Tests Phase 1-2 + CI gate**) +**Last updated:** 2026-04-29 tối (Phase 8 — **Budget complete + PE feature gap đóng + Tests Phase 1-2-3mini (77 test) + CI gate + Path filter + 3 gotcha CI mới (#39 #40 #41)**) ## TL;DR @@ -34,8 +34,15 @@ - Pwd `User@123456`. Reconcile pattern (gotcha #38 4-field rename). - Tổng 30 user (16 sample cũ giữ + 14 Solutions thật mới). -**Tổng cumulative:** 52 DB tables, ~128 endpoints, 15 migrations, 38 gotchas, -**71 unit test**, 6 commit session 5 push lên Gitea. +**Tổng cumulative:** 52 DB tables, ~128 endpoints, 15 migrations, **41 gotchas (+3 CI fixes)**, +**77 unit test (+6 PE WF Application)**, 11+ commit session 5 push lên Gitea. + +### 🆕 CI/CD optimize (29/04 tối) + +- ✅ **Manual checkout bypass github.com** — fix #108/#109 transient TCP timeout 21s. Run #110 pass. +- ✅ **Path filter docs-only skip** — `paths-ignore` trong on:push, commit MD-only KHÔNG trigger CI (verify `512880c` → no run #113). +- ⏸️ **npm junction cache** — thử ở #111 fail `tsc not found`, rollback. Cần debug session sau (gotcha #40 doc rồi). +- ✅ **Tests Phase 3 mini (PE Workflow Designer)** — 6 test versioning logic. Total 77. ## ⚠️ CẢNH BÁO session tiếp (Session 6) @@ -98,8 +105,12 @@ | **PE Workflow designer admin UI** `/system/pe-workflows/:typeCode` | ✅ Done (S5) | | **Ý kiến 4 phòng ban PE** — migration 15 + section sign-off 2x2 grid | ✅ Done (S5) | | **Tests Phase 1-2 + CI gate** — 71 test (Domain policy + Infra code generator) | ✅ Done (S5) | +| **CI manual checkout bypass github.com + Path filter docs-only skip** | ✅ Done (S5) | +| **Tests Phase 3 mini** — 6 test PE Workflow Designer versioning (total 77) | ✅ Done (S5) | | **Export phiếu PDF/Excel PE** — `IDocumentConverter` + template | 📝 Pending (không quan trọng) | -| **Tests Phase 3-5** — Application handlers + API smoke + FE Vitest | 📝 Pending (làm khi cần) | +| **Tests Phase 3 full** — Application handlers (UpsertOpinion + Budget link validation, cần UserManager setup) | 📝 Pending | +| **Tests Phase 4-5** — API smoke + FE Vitest | 📝 Pending (làm khi cần) | +| **npm cache CI optimize** (debug junction Move-Item issue #40) | 📝 Pending | | 9+ Post-launch (E-signature, Bravo/SAP, Mobile, AI) | 📝 Future | ## Run nhanh @@ -233,7 +244,7 @@ Trigger: user nói "audit skill" hoặc tự chạy đầu Phase mới. ## Lưu ý kỹ thuật quan trọng -**Đọc [`gotchas.md`](gotchas.md) (38 bẫy) trước khi:** +**Đọc [`gotchas.md`](gotchas.md) (41 bẫy) trước khi:** - Thêm package mới → .NET 10 compat (MediatR 14 fail → dùng 12.4.1) - Debug TS enum error → dùng const-object pattern (`erasableSyntaxOnly`) diff --git a/docs/STATUS.md b/docs/STATUS.md index bd03e2a..3542085 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -2,9 +2,9 @@ > **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`. -**Last updated:** 2026-04-29 chiều (Phase 8 — **Budget FE complete + PE feature gap đóng + Tests Phase 1-2 + CI gate + Path filter docs-only skip**) +**Last updated:** 2026-04-29 tối (Phase 8 — **Budget complete + PE feature gap đóng + Tests Phase 1-2-3mini + CI gate + Path filter + 3 gotcha CI mới**) -## 📍 Phase hiện tại: **Budget feature-complete + PE feature gap đóng + Test gate live** — 52 DB tables (+1 PEDepartmentOpinions), 15 migrations (+`AddPurchaseEvaluationDepartmentOpinions`), ~128 API endpoints (+4 PE WF admin + Opinion). 31 FE pages (+5 Budget + 1 PeWorkflowsPage). **71 unit test pass** (Phase 1: 54 Domain policy + Phase 2: 17 Infra code generator) — CI fail-fast nếu test fail. 30 demo user (16 sample + 14 Solutions thật). +## 📍 Phase hiện tại: **Feature freeze + CI optimize live** — 52 DB tables, 15 migrations, ~128 API endpoints, 31 FE pages. **77 unit test pass** (54 Domain policy + 17 Infra code generator + **6 PE Workflow versioning**) — CI fail-fast. Path filter docs-only skip = 0s cho commit MD. Manual checkout từ Gitea bypass github.com timeout (gotcha #39). 41 gotcha tổng. 30 demo user. ### 🌐 Production URLs @@ -48,6 +48,9 @@ | Ngày | Ai | Task | Commit | |---|---|---|---| +| 2026-04-29 | Claude | **Tests Phase 3 mini + 3 gotcha CI mới (#39 #40 #41)** — `tests/.../Application/PeWorkflowAdminTests.cs` 6 test versioning logic (CreatePeWorkflowDefinition: first version IsActive=true, second deactivates first, different EvaluationType independent, persists steps ordered + approvers per step, third version increments to v3). Total **77 test** (54 Domain + 17 Infra + 6 PE WF Application). Gotcha #39 act_runner github.com TCP timeout 21s + manual checkout fix. #40 npm junction cache fail `tsc not found` rolled back. #41 paths-ignore behavior + workflow file exclusion. | (cur commit) | +| 2026-04-29 | Claude | **CI Path filter docs-only skip live** — `paths-ignore` trong on:push lookup `docs/**`/`**/*.md`/`.claude/skills/**`/`.gitignore`. Commit chỉ touch docs SKIP CI hoàn toàn (saving ~196s/commit, ~30% commit thuộc loại này). Verify `512880c` (docs-only) → Gitea NO trigger run #113. | `29eb5d9` · `a21790d` · `512880c` | +| 2026-04-29 | Claude | **CI manual checkout bypass github.com (fix #108/#109)** — Run #108/#109 fail TCP timeout 21s khi act_runner fetch `actions/checkout@v4` từ github.com. Replace `uses: actions/checkout@v4` + `actions/upload-artifact@v4` bằng manual `git init` + `git fetch` từ Gitea internal. Token `${{ github.token }}` auth tự sẵn per-job. Fetch by ref + depth=30. Run #110 pass 3m16s. | `14b7d18` · `26075c4` | | 2026-04-29 | Claude | **Tests Phase 2 — Code generator format + sequence (SQLite in-memory)** — `tests/SolutionErp.Infrastructure.Tests/` xUnit + EF SQLite 10. `SqliteDbFixture` + `TestApplicationDbContext` subclass override `nvarchar(max) → TEXT` (SQLite không support `max`). 17 test: ContractCodeGenerator (format RG-001 5 type + Framework year scope vs Project scope + sequence per prefix + year boundary reset + persistence verify) + PurchaseEvaluationCodeGenerator (format A/B + 3-digit pad + independent A/B sequences + year boundary). CI gate +1 step. Total 71 test pass / 2.1s. | `df5988b` | | 2026-04-29 | Claude | **Tests Phase 1 — Domain unit tests + CI gate** — `tests/SolutionErp.Domain.Tests/` xUnit 2.9 + FluentAssertions 7.2 (pin trước v8 commercial). 54 test pure function (no DB/IO): WorkflowPolicy (Standard 9-phase + SkipCcm 7-phase + Registry per ContractType + FromDefinition versioned + UserKindApprover) / PEPolicy (NccOnly 3-step + NccWithPlan 5-step + reject paths) / BudgetPolicy (Default 3-step + terminals + SLA spec). `.gitea/workflows/deploy.yml` thêm step "Run unit tests" trước build, fail → `exit $LASTEXITCODE` → no deploy. SolutionErp.slnx + folder `/tests/`. | `d3f9346` | | 2026-04-29 | Claude | **PE Workflow designer admin UI + Ý kiến 4 phòng ban** — Migration 15 `AddPurchaseEvaluationDepartmentOpinions` (UNIQUE PEId+Kind, 1 row/phòng/phiếu). Domain `PurchaseEvaluationDepartmentOpinion` + enum `PeDepartmentKind` (PheDuyet/Ccm/MuaHang/SmPm). BE: `PeWorkflowAdminFeatures.cs` ~250 LOC mirror Contract pattern (GetOverview + Create version, deactivate cũ atomic) + `PeWorkflowsController` 2 endpoint reuse policy `Workflows.*`. `PeDepartmentOpinionFeatures.cs` Upsert (sign=true→set SignedAt+UserId, sign=false giữ chữ ký cũ) + Delete + 2 endpoint. FE: `PeWorkflowsPage.tsx` ~500 LOC + designer dialog (clone version + add/remove steps + +Role/+User approvers). Section "5. Ý kiến 4 phòng ban (sign-off)" 2x2 grid OpinionBox (read mode chữ ký vs edit textarea + 2 button Lưu/Lưu&Ký). | `5d94bb4` | @@ -163,8 +166,8 @@ Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1 | FE pages | 0 | 2 | 6 | 7 | 14 | 16 | 16 | ~20 | ~22 | ~23 | ~26 | ~26 | ~26 | **~31** (+5 Budget × 2 app + PeWorkflowsPage) | | FE components | — | — | — | — | — | — | — | many | many+ | +EditRowDialog | +PE 5-tab | +Compare section | — | **+Budget tabs/panel + PE OpinionBox + PE 4-section restructure** | | Scripts PS | 0 | 0 | 0 | 1 | 1 | 1 | 3 | 4 | 4 | 5 | 5 | 6 | 6 | 6 | -| CI/CD workflow | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | **1+test gate (2 step)** | -| **Tests** | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | **71** (54 Domain + 17 Infra) | +| CI/CD workflow | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | **1+test gate+path filter+manual checkout** | +| **Tests** | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | **77** (54 Domain + 17 Infra + 6 PE WF Application) | | Docs | 10 | 13 | 14 | 24 | 26 | 30 | 35 | ~40 | ~42 | ~44 | ~46 | ~48 | ~50 | **~52** (+session log + Test plan) | | Demo data | 0 | 0 | empty | 0 | 0 | 0 | 0 | 0 | 5+3 | 15+8+7+13+4 | +PE 4 phiếu | +rebrand email | 30 user | 30 user | | Commits | 1 | 2 | 3 | 5 | 6 | 7 | 8 | ~25 | ~47 | ~52 | ~63 | ~70 | ~75 | **~82** (+6 session 5) | diff --git a/docs/architecture.md b/docs/architecture.md index 6809888..b66dbdb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -318,13 +318,15 @@ Test pyramid bottom-heavy, **không E2E** (brittle, maintenance cao cho solo dev │ E2E / UI (Playwright) │ Brittle, slow, tốn time maintain ├──────────────────────────────────┤ ⚠️ TODO Phase 4 │ API smoke (WebApplicationFactory)│ 1-2 happy path roundtrip per critical endpoint -├──────────────────────────────────┤ ⚠️ TODO Phase 3 -│ Application Handler (CQRS) │ EF InMemory cho business rules +├──────────────────────────────────┤ 🟡 Mini done — 6 test (29/04) +│ Application Handler (CQRS) │ PE Workflow Definition versioning. Phase 3 full +│ │ (Opinion + Budget link) cần UserManager DI helper ├──────────────────────────────────┤ ✅ Done Phase 2 — 17 test │ Infrastructure (SQLite in-mem) │ Code generator format + sequence + year boundary ├──────────────────────────────────┤ ✅ Done Phase 1 — 54 test │ Domain Policy (pure functions) │ State machine + role transition + Registry mapping └──────────────────────────────────┘ + Total: 77 test pass / ~3s ``` **Stack:** @@ -348,18 +350,46 @@ Test pyramid bottom-heavy, **không E2E** (brittle, maintenance cao cho solo dev **Phase priority — anti-overkill:** -1. **Phase 1 (Done)** — Domain policy: 3 test file (Contract WF / PE WF / Budget) cover transition rules + Registry + FromDefinition. ~54 test, < 7s. -2. **Phase 2 (Done)** — Infra code generator: format RG-001/PE + sequence increment + year boundary (framework HĐ). ~17 test, ~2s. -3. **Phase 3 (Pending)** — Application CQRS handler: critical state-change handlers (Transition + Create) với EF InMemory. -4. **Phase 4 (Pending)** — API smoke: WebApplicationFactory + SQLite, 5-7 endpoint critical roundtrip. -5. **Phase 5 (Pending)** — FE Vitest cho lib utility (queryMatches, fmtMoney) — chỉ pure functions, KHÔNG component snapshot. +1. **Phase 1 (Done)** — Domain policy: 3 test file (Contract WF / PE WF / Budget) cover transition rules + Registry + FromDefinition. **54 test**, < 1s. +2. **Phase 2 (Done)** — Infra code generator: format RG-001/PE + sequence increment + year boundary (framework HĐ). **17 test**, ~2s. +3. **Phase 3 mini (Done)** — Application versioning: `CreatePeWorkflowDefinitionCommand` auto-increment + deactivate cũ + EvaluationType independence. **6 test**. +4. **Phase 3 full (Pending)** — Application handlers cần Identity (UpsertOpinion, Budget link validation in CreatePE/CreateContract). Cần build UserManager DI helper trong test fixture. +5. **Phase 4 (Pending)** — API smoke: WebApplicationFactory + SQLite, 5-7 endpoint critical roundtrip. +6. **Phase 5 (Pending)** — FE Vitest cho lib utility (queryMatches, fmtMoney) — chỉ pure functions, KHÔNG component snapshot. **Quy tắc bổ sung mỗi feature mới:** - Domain policy method → 2-3 test (positive + negative + edge) - CQRS handler có guard → 1 test mỗi guard branch - Bug found in production → 1 regression test added before merge -Detail run: `dotnet test SolutionErp.slnx` chạy cả 71 test ~2s. +Detail run: `dotnet test SolutionErp.slnx` chạy cả **77 test ~3s**. + +### CI/CD optimization (29/04) + +3 fix lớn cho Gitea Actions sau khi gặp incident #108/#109: + +1. **Manual checkout bypass github.com (gotcha #39)**: + - Replace `uses: actions/checkout@v4` bằng manual `git init` + `git fetch` từ Gitea internal + - Lý do: act_runner mỗi run đều fetch action source từ github.com → TCP timeout 21s liên tục → toàn job fail trước test gate + - Token `${{ github.token }}` Gitea tự cấp per-job, fetch by ref + depth=30 + +2. **Path filter docs-only skip (gotcha #41)**: + - `paths-ignore: ['docs/**', '**/*.md', '.claude/skills/**']` trong `on:push` + - Commit chỉ touch docs/MD/skill → **Gitea NO trigger workflow** (saving 100% time, ~196s/commit) + - Workflow file `.gitea/workflows/**` KHÔNG ignore → vẫn trigger khi sửa CI config + +3. **npm junction cache (gotcha #40)** — thử ở commit `29eb5d9`, fail `tsc not found`: + - Hypothesis: Move-Item của `node_modules` chứa nested junctions → .bin/ relative paths broken + - Rolled back ở `a21790d`, giữ fresh `npm install` mỗi run (49s + 33s = 82s) + - TODO future session: thử `robocopy /MIR` hoặc act_runner built-in cache server + +**Tốc độ deploy hiện tại** (run #110/#112): + +| Trigger | CI behavior | Total time | +|---|---|---:| +| Code commit | Build + test gate + deploy | ~3 phút | +| Docs-only commit | **Skip CI** (path filter) | **0s** | +| Test fail | Skip build/deploy | ~30-40s | ## 12. Liên quan diff --git a/docs/changelog/migration-todos.md b/docs/changelog/migration-todos.md index 1f41702..ef7da9b 100644 --- a/docs/changelog/migration-todos.md +++ b/docs/changelog/migration-todos.md @@ -143,13 +143,17 @@ Session log: `2026-04-28-chot-session-4-budget.md`. - [x] BE Upsert (sign=true → set SignedAt+UserId, sign=false giữ chữ ký cũ) + Delete + 2 endpoint - [x] FE Section "5. Ý kiến 4 phòng ban (sign-off)" 2x2 grid OpinionBox -### E. Tests Phase 1-2 + CI gate — done ✅ +### E. Tests Phase 1-2-3mini + CI optimize — done ✅ - [x] **Phase 1** — `tests/SolutionErp.Domain.Tests/` (xUnit + FluentAssertions 7.2): 54 test policy state machine (Contract WF + PE WF + Budget) + Registry + FromDefinition versioned + UserKindApprover - [x] **Phase 2** — `tests/SolutionErp.Infrastructure.Tests/` (EF SQLite + TestApplicationDbContext override `nvarchar(max) → TEXT`): 17 test code generator format + sequence + year boundary + persistence verify +- [x] **Phase 3 mini** — `tests/.../Application/PeWorkflowAdminTests.cs`: 6 test CreatePeWorkflowDefinitionCommand versioning (auto-increment + deactivate cũ + EvaluationType independence + steps/approvers persistence) - [x] CI gate `.gitea/workflows/deploy.yml` — 2 step `dotnet test` trước build, fail → no deploy -- [x] Total 71 test pass / ~2s -- [x] Session log + commit + push +- [x] **Total 77 test pass / ~3s** +- [x] **CI manual checkout bypass github.com** — fix gotcha #39 (act_runner TCP timeout 21s) +- [x] **CI path filter docs-only skip** — gotcha #41 (paths-ignore behavior) +- [ ] **Tests Phase 3 full** — Opinion Upsert + Budget link validation (cần Identity UserManager setup helper) +- [ ] **npm junction cache CI optimize** (rollback ở `a21790d` — gotcha #40 chưa debug) ## 📝 Phase 9 — UAT + Ops + carry over (Session 6+ active) diff --git a/docs/changelog/sessions/2026-04-29-2300-chot-final-ci-tests-gotchas.md b/docs/changelog/sessions/2026-04-29-2300-chot-final-ci-tests-gotchas.md new file mode 100644 index 0000000..da2c7d0 --- /dev/null +++ b/docs/changelog/sessions/2026-04-29-2300-chot-final-ci-tests-gotchas.md @@ -0,0 +1,220 @@ +# Session log — 2026-04-29 chiều/tối (Chốt final session 5 — CI optimize + Tests Phase 3 mini + 3 gotcha CI) + +**Topic:** CI/CD incident #108/#109 → fix manual checkout + path filter + thử npm cache (fail rollback) + Tests Phase 3 mini PE Workflow Designer + 3 gotcha CI mới + chốt MD lần 2. + +**Commits session 5 chiều/tối:** +- `52999f3` — Docs chốt session 5 đầu (Budget FE + PE feature gap) +- `26075c4` — Empty re-trigger sau #108 fail +- `14b7d18` — CI: Manual checkout bypass github.com (fix #108/#109 TCP timeout) +- `29eb5d9` — CI: Path filter docs-only + npm junction cache (Option C) +- `a21790d` — CI: Rollback npm cache (fail #111 tsc not found), giữ path filter +- `512880c` — Docs test: STATUS update — verify path filter docs-only skip → ✅ no run #113 +- (commit chốt MD này) + +## A. CI/CD incident & fix + +### Run #108/#109 fail TCP timeout 21s + +``` +Get "https://github.com/actions/checkout/info/refs?service=git-upload-pack": + dial tcp 20.205.243.166:443: connectex: A connection attempt failed + because the connected party did not properly respond +``` + +Test gate (Domain + Infra) chưa kịp chạy. act_runner mỗi run đều cố `git fetch` +`actions/checkout@v4` từ github.com để kiểm tra update. + +**Fix (commit `14b7d18`):** Replace `uses: actions/checkout@v4` + `actions/upload-artifact@v4` bằng manual git checkout từ Gitea internal — bypass github.com hoàn toàn. + +```yaml +- name: Checkout (manual git, bypass github.com) + shell: powershell + run: | + git config --global --add safe.directory '*' + git init -q + git remote add origin "https://gitea-actions:${{ github.token }}@git.baocaogiaoduc.vn/${{ github.repository }}.git" + $ref = "${{ github.ref }}" + if ($ref -like "refs/heads/*") { $ref = $ref.Substring(11) } + git fetch --depth=30 origin $ref + git checkout --quiet "${{ github.sha }}" + git log -1 --oneline +``` + +**Verify:** Run #110 commit `14b7d18` PASS 3m16s, smoke `/health/live` → 200. + +### Path filter docs-only skip (commit `29eb5d9` + verify `512880c`) + +User propose **Option C** (path filter + npm cache). Implement: + +```yaml +on: + push: + branches: [main] + paths-ignore: + - 'docs/**' + - '**/*.md' + - '.claude/skills/**' + - '.gitignore' + - 'scripts/**.md' +``` + +**Verify:** Push commit `512880c` chỉ touch `docs/STATUS.md` → +`curl https://git.baocaogiaoduc.vn/.../actions/runs/113/jobs/0/logs` trả `Not found` → +**Gitea KHÔNG trigger run mới**. ✅ + +**Saving:** ~196s/commit cho ~30% commit thuộc loại docs-only (chốt MD, session log, etc). + +### npm junction cache fail (commit `29eb5d9` rollback ở `a21790d`) + +Implement junction-based cache: +- Cache key = SHA256(package.json) 16-char prefix +- Cache stored: `C:\npm-cache-erp\\\node_modules` +- Junction `fe-admin\node_modules → cache` (instant, không file copy) + +Run #111 FAIL `'tsc' is not recognized` ở step Build fe-admin. Symptoms: +- VPS check: cache dir `C:\npm-cache-erp\` chưa có (cold) +- Log: KHÔNG có Write-Host "cache MISS" hay "added 239 packages" +- Timing: 1.6s từ end-of-BE-build → start-of-fe-admin npm run build (impossible cho npm install 49s) +- Test gate (Domain 54 + Infra 17) PASS nên không phải code regression + +**Hypothesis:** +- (A) Move-Item của `node_modules` chứa nested junctions/symlinks → .bin/ relative paths broken +- (B) act_runner PowerShell stream capture có quirk với cache MISS branch +- (C) Test-Path stale TRUE từ state khác + +**Decision:** Rollback về fresh `npm install` mỗi run (49s + 33s = 82s). Path filter +docs-only skip là alternative win lớn hơn (saving 100% time cho 30% commit). + +**Verify rollback:** Run #112 commit `a21790d` PASS 3m9s, smoke 200. + +**TODO debug session sau:** Thử `robocopy /MIR` thay `Move-Item` (handle symlinks +tốt hơn), hoặc dùng act_runner built-in `cache.host` server. + +## B. Tests Phase 3 mini — PE Workflow Designer + +``` +tests/SolutionErp.Infrastructure.Tests/Application/PeWorkflowAdminTests.cs +└── CreatePeWorkflowDefinitionCommandHandlerTests (6 test) + ├── Create_FirstVersion_PersistsWith_Version1_And_IsActiveTrue + ├── Create_SecondVersionSameCode_AutoIncrements_AndDeactivatesPrevious + ├── Create_DifferentEvaluationType_DoesNotAffect_Other + ├── Create_PersistsAllSteps_OrderedByOrderField + ├── Create_PersistsApprovers_PerStep + └── Create_ThirdVersion_DeactivatesV2_Increments_To_V3 +``` + +**Coverage:** +- Auto-increment Version per Code (1 → 2 → 3) +- Invariant "AT MOST ONE IsActive=true per EvaluationType" — deactivate cũ atomic +- Type independence — Type A active không ảnh hưởng Type B active +- Persistence: steps ordered + approvers per step + +**Skip Phase 3 full** (Opinion Upsert + Budget link validation) — cần UserManager +DI helper, time-cost cao. Document làm session sau khi cần. + +**Total tests sau session 5:** **77 pass / ~3s** (54 Domain + 17 Infra + 6 PE WF Application). + +## C. 3 gotcha CI mới + +### `#39` act_runner github.com TCP timeout 21s (manual checkout fix) + +Documented cause + workaround. Reference run #108 commit `52999f3` fail, run #110 +commit `14b7d18` fix pass. + +### `#40` npm junction cache `tsc not found` sau Move-Item — chưa xác định root cause + +Documented hypothesis + workaround tạm + TODO debug. Reference run #111 commit +`29eb5d9` fail, rollback ở `a21790d`. + +### `#41` Gitea Actions `paths-ignore` — workflow file change vẫn trigger + +Documented behavior + verify pattern. Reference commit `29eb5d9` add filter, +`512880c` (docs-only) verify no run #113. + +**Total gotchas: 38 → 41.** Checklist debug bug mới cũng update items 18-20. + +## D. Doc updates (chốt lần 2) + +| File | Change | +|---|---| +| `docs/STATUS.md` | Header Phase 8 update + 3 row Recently Done CI fixes + cumulative test count 71 → 77 | +| `docs/HANDOFF.md` | TL;DR thêm CI optimize section + Phase status table thêm 3 task done + gotcha count 38 → 41 | +| `docs/changelog/migration-todos.md` | Phase 8 §E thêm Phase 3 mini done + CI fixes | +| `docs/rules.md` §7 Testing | Rewrite full section: stack + test pyramid + quy tắc bổ sung mỗi feature + workflow user end-of-task + CI gate behavior | +| `docs/architecture.md` §11 Testing strategy | Update test pyramid + phased priority + CI optimization sub-section (3 fix) + tốc độ deploy table | +| `docs/gotchas.md` | + #39 #40 #41 + checklist debug items 18-20 | +| `.claude/skills/ef-core-migration/SKILL.md` | Phase 8 update note thêm CI fixes + 77 test | +| `CLAUDE.md` (root) | Test count 71 → 77 + folder structure update + CI/CD pipeline 3 fix | +| `~/.claude/.../memory/project_solution_erp.md` | Session 5 summary + workflow user mới | + +## E. Skill audit cron (định kỳ) + +`solution-erp-skill-audit-monthly` cron next fire **2026-05-01** (2 ngày sau session này). +Workflow đã setup từ session trước (commit `b904a25`). Cron survives across sessions. + +Setup ban đầu document tại `docs/rules.md §9.4`. Khi fire sẽ: +- Cross-check 6 skill hiện có với STATUS / gotchas / migration-todos +- Auto-refresh stale entries +- Đề xuất add/archive cho human approve +- Log vào `docs/changelog/skill-audit-2026-05.md` +- ABORT nếu repo dirty + +## F. Stats sau session 5 final + +| | Trước S5 | Sau S5 final | +|---|---:|---:| +| BE LOC | ~11750 | ~13050 (+1300) | +| DB tables | 51 | **52** (+1) | +| Migrations | 14 | **15** | +| API endpoints | ~124 | **~128** | +| FE pages | ~26 | **~31** (+5) | +| **Tests** | 0 | **77** (Phase 1+2+3mini) | +| **Gotchas** | 38 | **41** (+3 CI) | +| CI/CD pipeline | 1 step build | **Test gate (3 step) + path filter + manual checkout** | +| Demo user | 30 | 30 | +| Commits S5 | ~75 | **~91** (+16 session 5 incl chốt + CI iterations) | + +## G. Cảnh báo session 6 + +1. **CI test gate active** — workflow user: code → `dotnet test` local → commit → push. Test fail = NO deploy. +2. **Path filter live** — commit chỉ touch `docs/**`/`**/*.md`/`.claude/skills/**`/`.gitignore` → CI SKIP hoàn toàn (saving ~196s). +3. **Manual checkout từ Gitea** — không phụ thuộc github.com. Nếu nâng cấp act_runner version → check xem hỗ trợ uses: actions/* tốt hơn không (lúc đó có thể switch back). +4. **npm cache CHƯA work** (gotcha #40) — debug session sau với robocopy hoặc act_runner built-in cache server. +5. **Skill audit cron** next 2026-05-01 — kiểm tra log audit khi fire. +6. **Tests Phase 3 full + 4 + 5** pending — chỉ làm khi gặp bug recurring để justify ROI. +7. **Login email** vẫn `admin@solutionerp.local` — chưa rebrand `@solutions.com.vn`. +8. **win-acme** scheduled task unhealthy — fix trước cert expire 2026-06-18. +9. **Hard blockers ops** — UAT thật, SMTP, rotate creds, SQL backup schedule. + +## H. Lessons learned final S5 + +1. **CI debug strategy "log first, ROI second"** — incident #108/#109 spent 50min debugging. Cuối cùng fix là 30 dòng workflow YAML thay 1 line `uses: actions/checkout@v4`. Đáng vì workflow ổn định lâu dài. + +2. **Path filter ROI cực cao cho solo dev** — 30% commit là docs-only (chốt MD, session log, planning). Skip CI hoàn toàn = 100% saving cho commit đó. Implementation 2 dòng YAML. + +3. **npm cache với junction tricky on Windows** — Move-Item của node_modules chứa nested junctions có thể disrupt .bin/ structure. PowerShell stream capture ở act_runner cũng có quirk khó debug. Better path: robocopy hoặc act_runner built-in cache. + +4. **Tests Phase 3 incremental approach** — full handler tests cần Identity DI heavy. Mini scope (handler không phụ thuộc UserManager) như CreatePeWorkflowDefinition là sweet spot cho ROI nhanh. 6 test thêm trong 30 phút. + +5. **Workflow user mới `dotnet test` local trước commit** — catch bug local nhanh hơn đợi CI fail. Thói quen tốt cho solo dev. + +6. **Gotcha doc khi gặp 1 lần là đủ** — 3 gotcha CI mới documented chi tiết để session sau hiểu ngay context, không phải tái-debug. + +## I. Files touched session 5 final + +``` +.gitea/workflows/deploy.yml (mod multi: manual checkout + path filter + npm cache thử rồi rollback) + +tests/SolutionErp.Infrastructure.Tests/Application/PeWorkflowAdminTests.cs (NEW — 6 test) + +docs/STATUS.md (mod: Phase 8 header + 3 row Recently Done CI fixes + cumulative) +docs/HANDOFF.md (mod: TL;DR + CI optimize section + Phase status + gotcha count) +docs/changelog/migration-todos.md (mod: Phase 8 §E updated) +docs/rules.md (mod: §7 Testing rewrite full) +docs/architecture.md (mod: §11 update + CI optimization sub-section) +docs/gotchas.md (mod: + #39 #40 #41 + checklist 18-20) +.claude/skills/ef-core-migration/SKILL.md (mod: Phase 8 update note) +CLAUDE.md (root) (mod: 77 test + CI/CD pipeline 3 fix) +docs/changelog/sessions/2026-04-29-2300-chot-final-ci-tests-gotchas.md (NEW — file này) +~/.claude/.../memory/project_solution_erp.md (mod: session 5 summary + workflow user mới) +``` diff --git a/docs/gotchas.md b/docs/gotchas.md index 420abe5..e4c6438 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -463,6 +463,111 @@ await userManager.UpdateAsync(u); **Bonus:** Check conflict trước khi rename (user khác đã có email mới) → skip để tránh duplicate. +### 39. act_runner v0.2.13 fetch `actions/checkout` từ github.com timeout 21s + +**Triệu chứng:** Run #108/#109 fail trong **22s** với: +``` +Get "https://github.com/actions/checkout/info/refs?service=git-upload-pack": + dial tcp 20.205.243.166:443: connectex: A connection attempt failed + because the connected party did not properly respond... +``` + +Test gate (Domain + Infra) chưa kịp chạy. Build/deploy không tới. + +**Nguyên nhân:** act_runner mỗi run đều `git fetch` action source code từ +github.com (kiểm tra update `actions/checkout@v4`). Khi VPS → github.com +TCP có vấn đề (intermittent firewall/network), 21s timeout → toàn job fail +TRƯỚC step nào của workflow chạy. + +**Fix:** Thay `uses: actions/checkout@v4` bằng manual git checkout từ Gitea +internal — bypass github.com hoàn toàn. + +```yaml +- name: Checkout (manual git, bypass github.com) + shell: powershell + run: | + git config --global --add safe.directory '*' + git init -q + git remote add origin "https://gitea-actions:${{ github.token }}@git.baocaogiaoduc.vn/${{ github.repository }}.git" + $ref = "${{ github.ref }}" + if ($ref -like "refs/heads/*") { $ref = $ref.Substring(11) } + git fetch --depth=30 origin $ref + git checkout --quiet "${{ github.sha }}" +``` + +Tương tự với `actions/upload-artifact@v4` — bỏ vì cũng phụ thuộc github.com. +TRX file vẫn save local trong `test-results/` cho debug. + +**Long-term option:** config `github_mirror` trong gitea-runner config.yaml +mirror github.com → Gitea internal repo. Hoặc pre-cache `.cache/act//` +manually 1 lần. + +**Reference:** Run #108 commit `52999f3` fail, run #110 commit `14b7d18` fix pass. + +### 40. npm junction cache `tsc not found` sau Move-Item — chưa xác định root cause + +**Triệu chứng:** Implement npm cache strategy bằng junction (mklink /J) + +Move-Item node_modules → cache dir → fail `'tsc' is not recognized` ở step +`npm run build`. Log NO Write-Host "cache MISS" output, NO npm install +output. Timing 1.6s từ end-of-BE-build → start-of-fe-admin npm run build +(impossible cho npm install 49s). + +**Hypothesis:** +- (A) Move-Item của `node_modules` chứa nested junctions/symlinks → .bin/ + relative paths broken sau move +- (B) act_runner PowerShell stream capture có quirk với cache MISS branch → + output bị silenced +- (C) `Test-Path` trả về stale TRUE từ một state khác + +**Workaround tạm:** Rollback về fresh `npm install` mỗi run (49s + 33s = 82s). +Path filter docs-only skip CI là alternative win lớn hơn. + +**TODO khi debug session sau:** +- Thử `robocopy /MIR` thay `Move-Item` (handle symlinks tốt hơn) +- Hoặc Copy-Item với `-Force -Recurse` (slower nhưng safer) +- Hoặc dùng act_runner built-in `cache.host` server (có sẵn trong config.yaml) + +**Reference:** Run #111 commit `29eb5d9` fail, rollback ở `a21790d`. + +### 41. Gitea Actions `paths-ignore` — workflow file change vẫn trigger + +**Triệu chứng:** Setup `paths-ignore: ['docs/**', '**/*.md']` để skip CI +khi commit MD-only. Tự nhiên commit `.gitea/workflows/deploy.yml` (chính +workflow file) cũng bị skip → không thể test workflow change. + +**Nguyên nhân:** `paths-ignore` evaluate set của file thay đổi. Nếu TẤT CẢ +file thay đổi match patterns → skip. Workflow file `.gitea/workflows/**` +không trong list ignore → trigger normal. **OK behavior.** + +**Edge case ngược:** commit thay đổi cả `docs/STATUS.md` + `src/Backend/...cs` +→ NOT skip vì có file ngoài ignore patterns. **Cũng OK.** + +**Verify:** Commit chỉ touch `docs/STATUS.md` → check Gitea Actions UI → +phải KHÔNG có run mới trigger. Test với `curl /api/v1/.../runs/` +trả `Not found` cho run-id tiếp theo. + +**Pattern hiện áp dụng:** +```yaml +on: + push: + branches: [main] + paths-ignore: + - 'docs/**' + - '**/*.md' + - '.claude/skills/**' + - '.gitignore' + - 'scripts/**.md' +``` + +KHÔNG ignore: `.gitea/workflows/**`, `*.cs`, `*.tsx`, `*.ts`, `*.csproj`, +`*.json`, `*.slnx`, `tests/**`. + +**Saving:** ~196s/commit cho ~30% commit thuộc loại docs-only (chốt MD, +session log, etc). + +**Reference:** Commit `29eb5d9` add filter, verify ở commit `512880c` +(docs-only) → Gitea NO trigger run #113. + ## Checklist debug bug mới 1. Build pass không? → fail → check using + package version compat @@ -482,3 +587,6 @@ skip để tránh duplicate. 15. Nếu FE gọi API sai URL sau đổi env → rebuild + clear bundle cache (#36) 16. Nếu .ps1 fail parser trên PS 5.1 → ASCII-only, grep multi-byte chars (#30, #37) 17. Nếu rename email Identity vẫn 401 → update 4 field NormalizedEmail/UserName (#38) +18. Nếu CI fail TCP timeout 21s ở "Set up job" → bypass github.com, manual checkout từ Gitea (#39) +19. Nếu npm install caching fail `tsc not found` → KHÔNG dùng junction Move-Item, thử robocopy/Copy-Item (#40) +20. Nếu CI vẫn trigger khi commit MD-only → paths-ignore trong on:push không match patterns đúng (#41) diff --git a/docs/rules.md b/docs/rules.md index 1bf8589..1e71a1f 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -270,16 +270,74 @@ Co-Authored-By: Claude Opus 4.7 (1M context) - Thêm pattern → update skill tương ứng ở `.claude/skills/` - Phase đổi → update [`STATUS.md`](STATUS.md) + [`HANDOFF.md`](HANDOFF.md) + [`changelog/migration-todos.md`](changelog/migration-todos.md) -## 7. Testing (hiện chưa có test tự động) +## 7. Testing (Phase 8 active — 77 test pass + CI gate live) -**Phase 1-4 — manual test:** -- E2E qua curl/Postman trong mỗi session log -- Build + TS check mỗi commit +### Stack đã apply -**Phase 5 — tự động (chưa làm):** -- Unit test: xUnit cho BE, Vitest cho FE -- Integration test: TestContainer SQL Server cho BE -- E2E test: Playwright cho FE +- **xUnit 2.9.3** + **FluentAssertions 7.2** (pin trước v8 commercial license) +- **Microsoft.EntityFrameworkCore.Sqlite 10** (in-memory testing — supports transactions, không như InMemory provider) +- **Custom `TestApplicationDbContext`** override `nvarchar(max) → TEXT` cho SQLite compat (xem `tests/.../Common/SqliteDbFixture.cs`) +- **NO mock framework** (tránh mock-heavy, prefer EF in-memory hoặc fake data) + +### Test pyramid (bottom-heavy, KHÔNG E2E) + +``` +✅ Phase 1: Domain policy 54 test (pure functions, no DB) +✅ Phase 2: Infra code generator 17 test (SQLite in-mem) +✅ Phase 3 mini: Application versioning 6 test (SQLite, no UserManager) +⏸️ Phase 3 full: Application handlers w/ Identity (cần UserManager DI helper) +⏸️ Phase 4: API smoke (WebApplicationFactory) +⏸️ Phase 5: FE Vitest cho lib utility +❌ E2E (Playwright) — KHÔNG làm (brittle cho solo dev) +``` + +### Quy tắc bổ sung mỗi feature mới + +- **Domain entity / enum** → 0 test (compile check đủ) +- **Domain policy method** → 2-3 test (positive + negative + edge case) +- **CQRS handler có guard logic** → 1 test mỗi guard branch +- **Migration mới** → smoke test "DbInitializer chạy được + seed không lỗi" +- **API endpoint mới** → 1 happy path smoke (cho endpoint critical) +- **Bug found in production / staging** → 1 regression test BEFORE merge fix + +Pattern: **"1 bug found → 1 regression test added"**. + +### Workflow user end-of-task + +```bash +# Sau mỗi task code (BẮT BUỘC trước commit code): +dotnet test SolutionErp.slnx # local verify ~3s +git add -A && git commit -m "..." +git push # CI tự run test gate trước build/deploy +``` + +Test fail local → fix trước khi push. Test fail trên CI → no deploy. + +### CI gate behavior + +`.gitea/workflows/deploy.yml`: + +```yaml +# Path filter: skip CI hoàn toàn cho commit MD-only +on: + push: + paths-ignore: + - 'docs/**' + - '**/*.md' + - '.claude/skills/**' + +# 2 step test gate trước build/deploy (fail → exit → no deploy) +- name: Run unit tests (Domain) +- name: Run integration tests (Infrastructure) +``` + +### Pending (Phase 3-5 future) + +- Application handler tests cần Identity UserManager DI setup helper +- API smoke tests via `WebApplicationFactory` + SQLite +- FE Vitest cho `lib/queryMatches`, `lib/cn`, money formatter + +Detail: `docs/architecture.md §11 Testing strategy`. ## 8. Security baseline diff --git a/tests/SolutionErp.Infrastructure.Tests/Application/PeWorkflowAdminTests.cs b/tests/SolutionErp.Infrastructure.Tests/Application/PeWorkflowAdminTests.cs new file mode 100644 index 0000000..136f7ac --- /dev/null +++ b/tests/SolutionErp.Infrastructure.Tests/Application/PeWorkflowAdminTests.cs @@ -0,0 +1,167 @@ +using SolutionErp.Application.PurchaseEvaluations; +using SolutionErp.Domain.Contracts; // WorkflowApproverKind reuse +using SolutionErp.Domain.Identity; +using SolutionErp.Domain.PurchaseEvaluations; +using SolutionErp.Infrastructure.Tests.Common; + +namespace SolutionErp.Infrastructure.Tests.Application; + +// Tests cho PeWorkflowAdminFeatures (PE Workflow Designer admin UI — session 5). +// Mục tiêu: chống regression Versioning logic (auto-increment + deactivate cũ), +// vì invariant "AT MOST ONE IsActive=true per EvaluationType" rất quan trọng. +public class CreatePeWorkflowDefinitionCommandHandlerTests +{ + private static CreatePeWorkflowDefinitionCommand BuildCmd( + PurchaseEvaluationType type = PurchaseEvaluationType.DuyetNcc, + string code = "QT-DN-A-TEST", + string name = "Test Workflow") + { + return new CreatePeWorkflowDefinitionCommand( + EvaluationType: type, + Code: code, + Name: name, + Description: null, + Steps: new List + { + new(Order: 1, Phase: (int)PurchaseEvaluationPhase.DangSoanThao, Name: "Soạn", SlaDays: 3, + Approvers: new List + { + new(Kind: (int)WorkflowApproverKind.Role, AssignmentValue: AppRoles.Drafter), + }), + new(Order: 2, Phase: (int)PurchaseEvaluationPhase.ChoCCM, Name: "CCM duyệt", SlaDays: 2, + Approvers: new List + { + new(Kind: (int)WorkflowApproverKind.Role, AssignmentValue: AppRoles.CostControl), + }), + }); + } + + [Fact] + public async Task Create_FirstVersion_PersistsWith_Version1_And_IsActiveTrue() + { + using var fix = new SqliteDbFixture(); + var handler = new CreatePeWorkflowDefinitionCommandHandler(fix.Db); + + var id = await handler.Handle(BuildCmd(), CancellationToken.None); + + var def = fix.Db.PurchaseEvaluationWorkflowDefinitions + .Where(d => d.Id == id) + .Single(); + def.Version.Should().Be(1); + def.IsActive.Should().BeTrue(); + def.ActivatedAt.Should().NotBeNull(); + def.Code.Should().Be("QT-DN-A-TEST"); + } + + [Fact] + public async Task Create_SecondVersionSameCode_AutoIncrements_AndDeactivatesPrevious() + { + using var fix = new SqliteDbFixture(); + var handler = new CreatePeWorkflowDefinitionCommandHandler(fix.Db); + + var id1 = await handler.Handle(BuildCmd(name: "v1"), CancellationToken.None); + var id2 = await handler.Handle(BuildCmd(name: "v2"), CancellationToken.None); + + var def1 = fix.Db.PurchaseEvaluationWorkflowDefinitions.Single(d => d.Id == id1); + var def2 = fix.Db.PurchaseEvaluationWorkflowDefinitions.Single(d => d.Id == id2); + + def1.IsActive.Should().BeFalse("Version cũ phải bị deactivate khi tạo version mới"); + def1.Version.Should().Be(1); + + def2.IsActive.Should().BeTrue(); + def2.Version.Should().Be(2); + } + + [Fact] + public async Task Create_DifferentEvaluationType_DoesNotAffect_Other() + { + using var fix = new SqliteDbFixture(); + var handler = new CreatePeWorkflowDefinitionCommandHandler(fix.Db); + + // Type A active + var idA = await handler.Handle( + BuildCmd(type: PurchaseEvaluationType.DuyetNcc, code: "QT-DN-A"), + CancellationToken.None); + + // Type B active — không được tắt Type A + var idB = await handler.Handle( + BuildCmd(type: PurchaseEvaluationType.DuyetNccPhuongAn, code: "QT-DN-B"), + CancellationToken.None); + + var defA = fix.Db.PurchaseEvaluationWorkflowDefinitions.Single(d => d.Id == idA); + var defB = fix.Db.PurchaseEvaluationWorkflowDefinitions.Single(d => d.Id == idB); + + defA.IsActive.Should().BeTrue("Type A không liên quan tới Type B"); + defB.IsActive.Should().BeTrue(); + } + + [Fact] + public async Task Create_PersistsAllSteps_OrderedByOrderField() + { + using var fix = new SqliteDbFixture(); + var handler = new CreatePeWorkflowDefinitionCommandHandler(fix.Db); + + var id = await handler.Handle(BuildCmd(), CancellationToken.None); + + var steps = fix.Db.PurchaseEvaluationWorkflowSteps + .Where(s => s.PurchaseEvaluationWorkflowDefinitionId == id) + .OrderBy(s => s.Order) + .ToList(); + + steps.Should().HaveCount(2); + steps[0].Order.Should().Be(1); + steps[0].Phase.Should().Be(PurchaseEvaluationPhase.DangSoanThao); + steps[0].Name.Should().Be("Soạn"); + steps[1].Order.Should().Be(2); + steps[1].Phase.Should().Be(PurchaseEvaluationPhase.ChoCCM); + } + + [Fact] + public async Task Create_PersistsApprovers_PerStep() + { + using var fix = new SqliteDbFixture(); + var handler = new CreatePeWorkflowDefinitionCommandHandler(fix.Db); + + var id = await handler.Handle(BuildCmd(), CancellationToken.None); + + var stepIds = fix.Db.PurchaseEvaluationWorkflowSteps + .Where(s => s.PurchaseEvaluationWorkflowDefinitionId == id) + .Select(s => s.Id) + .ToList(); + var approvers = fix.Db.PurchaseEvaluationWorkflowStepApprovers + .Where(a => stepIds.Contains(a.PurchaseEvaluationWorkflowStepId)) + .ToList(); + + approvers.Should().HaveCount(2); + approvers.Should().Contain(a => a.Kind == WorkflowApproverKind.Role + && a.AssignmentValue == AppRoles.Drafter); + approvers.Should().Contain(a => a.Kind == WorkflowApproverKind.Role + && a.AssignmentValue == AppRoles.CostControl); + } + + [Fact] + public async Task Create_ThirdVersion_DeactivatesV2_Increments_To_V3() + { + // Tránh edge case: deactivate logic có gặp v2 active không, hay vẫn xét v1? + using var fix = new SqliteDbFixture(); + var handler = new CreatePeWorkflowDefinitionCommandHandler(fix.Db); + + await handler.Handle(BuildCmd(name: "v1"), CancellationToken.None); + await handler.Handle(BuildCmd(name: "v2"), CancellationToken.None); + var id3 = await handler.Handle(BuildCmd(name: "v3"), CancellationToken.None); + + var allDefs = fix.Db.PurchaseEvaluationWorkflowDefinitions + .Where(d => d.Code == "QT-DN-A-TEST") + .OrderBy(d => d.Version) + .ToList(); + + allDefs.Should().HaveCount(3); + allDefs[0].Version.Should().Be(1); + allDefs[0].IsActive.Should().BeFalse(); + allDefs[1].Version.Should().Be(2); + allDefs[1].IsActive.Should().BeFalse(); + allDefs[2].Version.Should().Be(3); + allDefs[2].IsActive.Should().BeTrue(); + allDefs[2].Id.Should().Be(id3); + } +}