Cumulative S35 3 commit + 3 CI Run #242/#243/#244 ALL PASS: - `c3cd343` FE inline forms 5 satellite × 2 app cookie-cutter (+1758 LOC) - `909655c` G-H2 BE CRUD HrmConfig 16 endpoint (+576 LOC NEW) - `021674a` G-H2 FE Admin HrmConfigsPage declarative (+1388 LOC) ## Updates this commit (docs CI skip per gotcha #41) - docs/STATUS.md S35 wrap header (cumulative 3 chunk + Multi-agent ROI ~250K) - docs/HANDOFF.md S35 brief + S36 backlog 6 option - docs/gotchas.md +#53 sub-agent truncation/stall pattern S35 × 3 occurrence + Quick reference 28 - docs/changelog/sessions/2026-05-28-s35-fe-inline-forms-g-h2.md NEW session log - 4 sub-agent MEMORY auto-updated entry (Implementer + CICD + Reviewer + Investigator S35 spawns) ## Patterns reinforced cumulative S35 - Pattern 12-ter (within-module N-satellite) 6× cumulative - Pattern 12-bis (cross-module catalog mega) 3× cumulative - Pattern 16-bis (4-place mirror cross-app) 6× — staticMap 4th place mandatory (gotcha #50) - Smart Friend 9× cumulative clean (S22+S25+S29×2+S33×2+S35×3) - NEW: Declarative KIND_CONFIG Record pattern (single-page multi-kind CRUD reuse) ## Smart Friend Implementer 3 catch S35 (anti-pattern prevention) 1. Chunk 2 MaxLength validator vs EF config mismatch → aligned EF source-of-truth 2. Chunk 2 HRM entities NO HasQueryFilter → explicit .Where(!IsDeleted) 8 site 3. Chunk 3 em main spec gap Layout staticMap miss → Implementer enforced Pattern 16-bis 4-place ## State chốt S35 - 35 mig unchanged · 71 tables · ~185 endpoints (+16 HRM Configs) - 43 FE pages (+1 HrmConfigsPage) · 130 test PASS unchanged - 53 gotcha (+1 #53) · 27 memory user-level · 6 skills · 4 sub-agents ## Multi-agent ROI S35 ~250K - Implementer 3 spawn ~80K (3 cookie-cutter chunk + Smart Friend × 3 catch) - Investigator 1 spawn ~8K (G-H2 BE CRUD pre-flight + NamGroup MISS verdict) - Reviewer 3 spawn ~60K (Smart Friend 9× clean, 2 truncated + 1 tight brief PASS) - CICD 4 spawn ~70K (warm-up + 3 deploy verify, 1 stalled em main fallback) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1031 lines
56 KiB
Markdown
1031 lines
56 KiB
Markdown
# Gotchas — SOLUTION_ERP
|
||
|
||
> Bẫy/pitfall đã gặp + cách xử lý. Đọc trước khi debug tương tự để không mất thời gian. Cập nhật liên tục khi gặp bug mới.
|
||
|
||
## Tech stack constraints (.NET 10 + TS 6 + Vite 8)
|
||
|
||
### 1. MediatR 14.x không tương thích → pin 12.4.1
|
||
|
||
**Triệu chứng:** `Unable to resolve service for type 'MediatR.IMediator'` — `AddMediatR` vẫn chạy nhưng không register IMediator.
|
||
|
||
**Fix:** Pin `MediatR 12.4.1`. Khi đó `RequestHandlerDelegate<TResponse>` là delegate không tham số (v14 có thêm CancellationToken).
|
||
|
||
### 2. Swashbuckle 10.x + Microsoft.OpenApi 2.x breaking change
|
||
|
||
**Triệu chứng:** Build fail `The type or namespace 'Models' does not exist in 'Microsoft.OpenApi'`. Swagger 404.
|
||
|
||
**Fix:**
|
||
- Remove `Microsoft.AspNetCore.OpenApi` khỏi Api
|
||
- Downgrade Swashbuckle về `6.9.0`
|
||
|
||
### 3. TypeScript 6 `erasableSyntaxOnly` cấm `enum`
|
||
|
||
**Fix:** Dùng `const + as const + typeof[keyof]` pattern:
|
||
|
||
```ts
|
||
export const SupplierType = { NhaCungCap: 1 } as const
|
||
export type SupplierType = typeof SupplierType[keyof typeof SupplierType]
|
||
```
|
||
|
||
### 4. TypeScript 6 deprecate `baseUrl`
|
||
|
||
**Fix:** Bỏ `baseUrl` trong tsconfig, chỉ giữ `paths`. Paths resolve relative tsconfig location.
|
||
|
||
### 5. Node 22 local vs CI pin 20
|
||
|
||
**Bài học NamGroup:** CI build fail trên Node latest.
|
||
|
||
**Fix:**
|
||
- `package.json` engines: `">=20"` (min, không upper)
|
||
- `.nvmrc` = `20` cho CI
|
||
- GitHub/Gitea Actions: `actions/setup-node@v4` với `node-version: '20.x'`
|
||
|
||
## EF Core 10
|
||
|
||
### 6. Expression tree không support switch expression
|
||
|
||
**Triệu chứng:** `CS8514: An expression tree may not contain a switch expression`.
|
||
|
||
**Fix:** Tách switch ra ngoài LINQ:
|
||
|
||
```csharp
|
||
var hasPermission = action switch
|
||
{
|
||
"Read" => await query.AnyAsync(p => p.CanRead),
|
||
"Create" => await query.AnyAsync(p => p.CanCreate),
|
||
_ => false,
|
||
};
|
||
```
|
||
|
||
### 7. Design-time DbContext resolve fail
|
||
|
||
**Triệu chứng:** `dotnet ef migrations add` → `Unable to resolve service for type 'DbContextOptions<T>'`.
|
||
|
||
**Fix:** Tạo `IDesignTimeDbContextFactory<ApplicationDbContext>` trong Infrastructure.
|
||
|
||
### 8. `AddDefaultTokenProviders()` không có trong `AddIdentityCore`
|
||
|
||
**Fix:** Bỏ call nếu chưa cần password reset. Khi cần, chuyển `AddIdentity` hoặc add package `Microsoft.AspNetCore.Identity.UI`.
|
||
|
||
## OpenXml / ClosedXML
|
||
|
||
### 9. `SpaceProcessingModeValues` namespace
|
||
|
||
**Fix:** Full path + wrap `EnumValue<>`:
|
||
|
||
```csharp
|
||
textElement.Space = new DocumentFormat.OpenXml.EnumValue<
|
||
DocumentFormat.OpenXml.SpaceProcessingModeValues>(
|
||
DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve);
|
||
```
|
||
|
||
### 10. Placeholder `{{field}}` bị split runs
|
||
|
||
**Vấn đề:** Word hay split text thành nhiều `<w:t>` — placeholder miss khi regex replace.
|
||
|
||
**Fix:** Iterate Paragraph, gom text tất cả `<w:t>` → replace → gán lại text đầu + clear rest. Đã implement trong `DocxRenderer`.
|
||
|
||
### 11. Word COM `SaveAs` PowerShell type conversion
|
||
|
||
**Fix:** Dùng `SaveAs2`:
|
||
|
||
```powershell
|
||
$doc.SaveAs2($outPath, 16) # 16 = wdFormatDocumentDefault
|
||
```
|
||
|
||
### 12. Word COM stuck
|
||
|
||
**Fix:**
|
||
- `$word.DisplayAlerts = 0`
|
||
- Nếu stuck → `Get-Process WINWORD | Stop-Process -Force`
|
||
- Fallback: LibreOffice headless `soffice --headless --convert-to docx`
|
||
|
||
## System.Text.Json
|
||
|
||
### 13. Record deserialization fail với Unicode qua CLI
|
||
|
||
**Triệu chứng:** POST JSON tiếng Việt từ Windows bash/curl → 400 "JSON value could not be converted".
|
||
|
||
**Fix:** Dùng `curl --data-binary @file.json` (file UTF-8). API handle đúng qua axios/Swagger.
|
||
|
||
## File operations
|
||
|
||
### 14. Dropbox sync có thể revert file đang edit
|
||
|
||
**Triệu chứng:** Write thành công, build pass, runtime chạy code cũ.
|
||
|
||
**Fix:** Sau Write quan trọng → Read lại verify. Nếu revert → Write lại.
|
||
|
||
### 15. `.gitignore` wwwroot rules
|
||
|
||
- `wwwroot/uploads/` → **ignore** (user files)
|
||
- `wwwroot/templates/` → **commit** (source of truth)
|
||
- `wwwroot/exports/` → ignore (temp)
|
||
|
||
## Dev workflow
|
||
|
||
### 16. Port conflict khi restart dev server
|
||
|
||
**Fix:** `TaskStop` task cũ, hoặc `netstat -ano | findstr :8082` → `taskkill /F /PID <pid>`.
|
||
|
||
### 17. EF migration 3-file rule
|
||
|
||
Mỗi migration tạo: `{name}.cs` + `{name}.Designer.cs` + `ApplicationDbContextModelSnapshot.cs`. Commit đủ 3.
|
||
|
||
## Claude Code harness quirks
|
||
|
||
### 18. Edit tool "File not read" sau system-reminder
|
||
|
||
**Triệu chứng:** Edit file vừa Read, lỗi "File has not been read yet".
|
||
|
||
**Nguyên nhân:** System reminder interrupt reset read-cache.
|
||
|
||
**Fix:** Read lại file rồi Write/Edit. Hoặc dùng Write (ghi đè full) thay Edit.
|
||
|
||
### 19. Build pass nhưng DI thiếu registration
|
||
|
||
**Triệu chứng:** `dotnet build` → 0 errors nhưng runtime throw `Unable to resolve service`.
|
||
|
||
**Nguyên nhân:** C# compiler chỉ check type, không check DI graph.
|
||
|
||
**Fix:** Sau thêm interface mới + impl → luôn add `services.AddScoped<IX, X>()` trong `DependencyInjection.cs`. Test API start up là OK check.
|
||
|
||
## Contract workflow
|
||
|
||
### 20. Mã HĐ gen 2 lần sau reject → approve lại
|
||
|
||
**Fix:** Check `if (contract.MaHopDong is null)` trước khi gen. Đã implement trong `ContractWorkflowService.TransitionAsync`.
|
||
|
||
### 21. ~~BE adjacency vs FE NEXT_PHASES sync~~ (RESOLVED)
|
||
|
||
**Đã xử lý:** FE không còn hardcode `NEXT_PHASES` nữa. BE expose `contract.workflow.nextPhases` trong `ContractDetailDto` từ `WorkflowPolicyRegistry.ForContract(contract)`. FE render dynamic từ đó — single source of truth.
|
||
|
||
Nếu đổi policy BE: chỉ cần update `WorkflowPolicies.Standard` hoặc `WorkflowPolicies.SkipCcm` trong `Domain/Contracts/WorkflowPolicy.cs`. FE tự reflect.
|
||
|
||
### 22. Race condition gen mã HĐ khi 2 user cùng transition tới DangDongDau
|
||
|
||
**Fix:** `IsolationLevel.Serializable` transaction trong `ContractCodeGenerator`. Không skip.
|
||
|
||
### 42. Dual schema workflow V1 vs V2 — Service phải branch theo pin field (Session 17)
|
||
|
||
**Symptom:** Phiếu PE pin V2 (`ApprovalWorkflowId` set qua workspace Select) nhưng Service vẫn match approver theo schema cũ (`Dept+PositionLevel`). Approver V2 không duyệt được, button Duyệt báo Forbidden.
|
||
|
||
**Root cause:** Sau Mig 23-24 entity PE có 2 field workflow pin:
|
||
- `WorkflowDefinitionId` (Mig 21 V1 legacy) — pin schema flat cũ
|
||
- `ApprovalWorkflowId` (Mig 23 V2 mới) — pin schema 3-table mới
|
||
|
||
Service trước đó chỉ đọc `WorkflowDefinitionId` → bỏ qua V2.
|
||
|
||
**Fix (`b41484b`):** `PurchaseEvaluationWorkflowService.TransitionAsync` branch:
|
||
```csharp
|
||
if (evaluation.ApprovalWorkflowId is Guid awId)
|
||
await ApproveV2Async(evaluation, awId, ...); // iterate ApprovalWorkflowSteps + Levels match ApproverUserId
|
||
else
|
||
await ApproveV1LegacyAsync(evaluation, ...); // iterate WorkflowSteps match Dept+PositionLevel
|
||
```
|
||
|
||
**Pattern reusable** khi wire schema mới song song schema cũ (Contract V2 sắp tới): pin field flag để rẽ logic, KHÔNG drop legacy ngay (giữ backward compat phiếu cũ).
|
||
|
||
### 43. Step.Order ≠ index 0-based — không thể EF query trực tiếp (Session 17)
|
||
|
||
**Symptom:** Implement `ResolveV2InboxIdsAsync` (V2-aware Inbox) bằng EF query thẳng:
|
||
```csharp
|
||
.Where(s => s.Order == e.CurrentWorkflowStepIndex.Value + 1) // FAIL — Step.Order là logical, không phải position
|
||
```
|
||
Logic sai: `WorkflowStep.Order` không phải position 0-based mà là số sort thứ tự (vd 5, 10, 20). `Steps.OrderBy(s => s.Order).ToList()[idx]` mới đúng.
|
||
|
||
**Fix:** Precompute candidates EF query → in-memory sort by Order → array index access:
|
||
```csharp
|
||
var candidates = await db.PE.Where(...).ToListAsync(ct); // Step 1: EF lấy phiếu V2 pending
|
||
var workflows = await db.AW.Include(...).ToDictionaryAsync(w => w.Id, ct);
|
||
foreach (var c in candidates) {
|
||
var steps = wf.Steps.OrderBy(s => s.Order).ToList(); // Step 2: in-memory sort
|
||
var step = steps[c.CurrentWorkflowStepIndex.Value]; // Step 3: array index
|
||
...
|
||
}
|
||
```
|
||
|
||
Trade-off: scalable đến vài trăm phiếu pending, không ngon cho >10k. Optimize sau nếu cần.
|
||
|
||
## Permission matrix
|
||
|
||
### 23. Permission update không real-time
|
||
|
||
**Triệu chứng:** Admin tick permission cho role X → user X vẫn thấy menu cũ.
|
||
|
||
**Nguyên nhân:** FE cache menu trong `localStorage`, không auto refetch.
|
||
|
||
**Fix:** User phải logout/login. Phase 3 iteration 2 có thể thêm SignalR push "permission-changed" → FE tự refetch `/menus/me`.
|
||
|
||
### 24. MenuKey typo — không check type
|
||
|
||
**Fix:** Luôn dùng `MenuKeys.Contracts` const (BE) + `MenuKeys.Contracts` (FE `menuKeys.ts`). Không hardcode string.
|
||
|
||
## IIS / Windows Server
|
||
|
||
### 25. `Install-WindowsFeature Web-WebSockets` khóa section `<webSocket>` ở applicationHost
|
||
|
||
**Triệu chứng:** Sau khi install WebSocket feature → TẤT CẢ IIS site có `<webSocket enabled="true" />` trong web.config trả về HTTP 500.19 với error code `0x80070021` "configuration section cannot be used at this path" — kể cả site khác project không liên quan.
|
||
|
||
**Nguyên nhân:** Feature install thêm `<webSocket>` section vào `applicationHost.config` với `overrideModeDefault="Deny"`. Site web.config override section đó → fail.
|
||
|
||
**Fix:** Unlock section ở server level:
|
||
```powershell
|
||
& "$env:SystemRoot\system32\inetsrv\appcmd.exe" unlock config -section:system.webServer/webSocket
|
||
```
|
||
|
||
Tương tự khi dùng URL Rewrite `<serverVariables>` cần unlock `system.webServer/rewrite/allowedServerVariables`.
|
||
|
||
**Cảnh báo co-existence:** Trên VPS shared với project khác, enable feature mới qua `Install-WindowsFeature` có thể làm sập site project khác. Luôn test all site sau mỗi enable.
|
||
|
||
## SignalR / Realtime
|
||
|
||
### 26. SignalR WebSocket không cho custom Authorization header
|
||
|
||
**Triệu chứng:** `new HubConnectionBuilder().withUrl('/hubs/x', { headers: { Authorization: ... } })` — WebSocket transport vẫn 401.
|
||
|
||
**Nguyên nhân:** Browser WebSocket API không cho set custom headers cho handshake. Chỉ 2 transport khác (SSE / LongPolling) mới dùng headers.
|
||
|
||
**Fix:**
|
||
- FE: dùng `accessTokenFactory: () => token` — SignalR client tự append `?access_token=` query cho WebSocket
|
||
- BE: Wire JWT bearer `OnMessageReceived` để đọc token từ query khi path matches `/hubs/*`:
|
||
```csharp
|
||
options.Events = new JwtBearerEvents {
|
||
OnMessageReceived = ctx => {
|
||
var accessToken = ctx.Request.Query["access_token"];
|
||
var path = ctx.HttpContext.Request.Path;
|
||
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
|
||
ctx.Token = accessToken;
|
||
return Task.CompletedTask;
|
||
}
|
||
};
|
||
```
|
||
|
||
### 27. SignalR SaveChangesInterceptor — capture Added ở SavingChanges, push ở SavedChanges
|
||
|
||
**Lý do:** SavedChanges chỉ có entries sau commit thành công. Nhưng ở SavedChanges thì `EntityEntry.State` đã về `Unchanged` → không thể filter `Added`.
|
||
|
||
**Fix:** 2-phase pattern:
|
||
```csharp
|
||
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(...) {
|
||
_pending = eventData.Context.ChangeTracker.Entries<Notification>()
|
||
.Where(e => e.State == EntityState.Added)
|
||
.Select(e => e.Entity).ToList();
|
||
return base.SavingChangesAsync(...);
|
||
}
|
||
|
||
public override async ValueTask<int> SavedChangesAsync(..., int result, ...) {
|
||
foreach (var n in _pending) await _realtimeNotifier.PushAsync(n);
|
||
_pending.Clear();
|
||
return result;
|
||
}
|
||
```
|
||
|
||
## DevOps / CI/CD
|
||
|
||
### 28. LibreOffice download URL 404 khi pin wrong version
|
||
|
||
**Triệu chứng:** `Invoke-WebRequest https://download.documentfoundation.org/libreoffice/stable/25.2.7/...` → 404.
|
||
|
||
**Nguyên nhân:** LibreOffice mirror chỉ giữ vài version mới nhất. 25.2.7, 24.8.7 không có. Chỉ 25.8.6 tồn tại tại thời điểm cài.
|
||
|
||
**Fix:** Check mirror URL trước khi pin. Dùng `Invoke-WebRequest -Method Head` verify trước download thật.
|
||
|
||
### 29. PowerShell 5.1 `>> $GITHUB_PATH` ghi UTF-16 → NUL byte crash Gitea Actions
|
||
|
||
**Triệu chứng:** Gitea Actions job fail với "NUL byte in PATH". `echo "C:\\dotnet" >> $env:GITHUB_PATH`.
|
||
|
||
**Nguyên nhân:** PS 5.1 default encoding UTF-16 LE BOM khi redirect `>>`. Gitea reads PATH as UTF-8 → NUL byte xuất hiện sau mỗi ASCII char.
|
||
|
||
**Fix:** Dùng `Out-File -Encoding utf8 -Append`:
|
||
```powershell
|
||
"C:\dotnet" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||
```
|
||
|
||
Hoặc drop step GITHUB_PATH hoàn toàn nếu NSSM PATH đã có sẵn dotnet+node.
|
||
|
||
### 30. PS 5.1 scripts với Vietnamese diacritics → parser error
|
||
|
||
**Triệu chứng:** `Cannot parse script: Unexpected character` khi chạy PS script có text tiếng Việt inline.
|
||
|
||
**Nguyên nhân:** PS 5.1 đọc file script với ANSI codepage (Windows-1258 hoặc default 1252), không phải UTF-8.
|
||
|
||
**Fix (1):** Save script với BOM UTF-8 (Write-Host có dấu vẫn work):
|
||
```powershell
|
||
[System.IO.File]::WriteAllText($path, $content, [System.Text.Encoding]::UTF8)
|
||
```
|
||
|
||
**Fix (2, safer):** Rewrite script ASCII-only. Text tiếng Việt nằm trong log messages thay dùng code:
|
||
```powershell
|
||
Write-Host "Setup IIS sites done" # thay vi "Hoan tat"
|
||
```
|
||
|
||
## TypeScript / FE
|
||
|
||
### 31. Dialog `size="xl"` TS2322 nếu variant không khai báo
|
||
|
||
**Triệu chứng:** `<Dialog size="xl">` → `Type '"xl"' is not assignable to type '"sm" | "md" | "lg"'`.
|
||
|
||
**Fix:** Sửa usage về `"lg"`, hoặc add `"xl"` vào `DialogSize` type union trong `components/ui/Dialog.tsx`. Đừng lazy `as any`.
|
||
|
||
## FE architecture
|
||
|
||
### 32. NavLink `end` prop cho query-param URL variants
|
||
|
||
**Triệu chứng:** `/contracts?type=1` highlight cả `/contracts` lẫn `/contracts?type=2` cùng lúc.
|
||
|
||
**Nguyên nhân:** Default NavLink `startsWith` match. Query string không parse distinct paths.
|
||
|
||
**Fix:** `end={path.includes('?')}` trong resolvePath để query-variants match exact:
|
||
```tsx
|
||
<NavLink to={path} end={path.includes('?')}>
|
||
```
|
||
|
||
## IIS / Windows Server (continued)
|
||
|
||
### 33. IPv4/IPv6 port hijack trên VPS shared (G-084)
|
||
|
||
**Triệu chứng:** `git.baocaogiaoduc.vn` trả về homepage Next.js của VietReport
|
||
thay vì Gitea UI. Headers lộ `x-nextjs-cache: HIT` + `X-Powered-By: ARR/3.0`
|
||
(request đã qua IIS ARR proxy rồi mới hit Next.js).
|
||
|
||
**Root cause:** Next.js app (NSSM service) được deploy lên VPS shared với
|
||
Gitea, ignore env `PORT=3001 HOSTNAME=127.0.0.1` và bind `0.0.0.0:3000`.
|
||
Gitea bind `0.0.0.0:3000` trước đó bị Windows fallback xuống IPv6-only
|
||
`[::]:3000` (default `IPV6_V6ONLY=1`). IIS ARR rewrite `http://localhost:3000`
|
||
→ Windows DNS resolve IPv4 first → hit Next.js → leak homepage cho TẤT CẢ
|
||
subdomain có ARR proxy về `:3000`.
|
||
|
||
**Fix (VietReport applied):**
|
||
1. Next.js NSSM env `PORT=3001 HOSTNAME=127.0.0.1` — bind loopback IPv4
|
||
2. Gitea `HTTP_ADDR=127.0.0.1` — bind loopback IPv4 explicit
|
||
3. IIS `web.config` rewrite URL dùng `127.0.0.1` thay `localhost`
|
||
4. NSSM `DependOnService=gitea` — boot order tránh race
|
||
|
||
**3 rules rút ra — áp dụng mọi service trên VPS shared:**
|
||
- Reverse-proxy luôn **IP literal `127.0.0.1`**, KHÔNG dùng `localhost`
|
||
- Backend services bind **loopback IPv4 explicit**, KHÔNG `0.0.0.0`
|
||
- Service dependency cho boot order khi nhiều service cùng port family
|
||
|
||
**SOLUTION_ERP relevance:**
|
||
- API host trong IIS app pool out-of-process (ANCM tự quản lý port Kestrel ephemeral) → risk THẤP
|
||
- FE gọi trực tiếp `https://api.solutions.com.vn` (không ARR proxy) → risk THẤP
|
||
- **NHƯNG** nếu tương lai thêm ARR reverse proxy (fe-admin/user `/api` proxy) hoặc
|
||
deploy Kestrel standalone qua NSSM → PHẢI apply 3 rules trên
|
||
- Scripts + skill doc đã update `localhost` → `127.0.0.1` để đồng bộ
|
||
|
||
## FE routing + state (Phase 6)
|
||
|
||
### 34. React Router NavLink `isActive` chỉ match pathname, không query string
|
||
|
||
**Triệu chứng:** 2 NavLink cùng pathname (`/purchase-evaluations?type=2` vs
|
||
`/purchase-evaluations?type=2&pendingMe=1`) cùng highlight khi URL là một
|
||
trong 2. User thấy menu "Danh sách" + "Duyệt" active đồng thời.
|
||
|
||
**Nguyên nhân:** React Router v6 `NavLink`'s built-in `isActive` chỉ so
|
||
pathname. `end` prop chỉ thêm exact-match cho pathname segment, không check
|
||
query string.
|
||
|
||
**Fix:** Custom `isActive` với `queryMatches` helper (URLSearchParams set
|
||
equality). Xem `Layout.tsx` cả 2 FE:
|
||
|
||
```tsx
|
||
function queryMatches(current: string, target: string): boolean {
|
||
const a = new URLSearchParams(current)
|
||
const b = new URLSearchParams(target)
|
||
const aKeys = [...a.keys()].sort()
|
||
const bKeys = [...b.keys()].sort()
|
||
if (aKeys.length !== bKeys.length) return false
|
||
return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k))
|
||
}
|
||
|
||
function MenuLeaf({ node }: { node: MenuNode }) {
|
||
const location = useLocation()
|
||
const path = resolvePath(node.key)
|
||
const [targetPath, targetQuery = ''] = path.split('?')
|
||
const isActive = location.pathname === targetPath
|
||
&& queryMatches(location.search.replace(/^\?/, ''), targetQuery)
|
||
return <NavLink to={path} className={isActive ? 'active' : ''}>...</NavLink>
|
||
}
|
||
```
|
||
|
||
### 35. Menu tree inheritance phải extend khi thêm root mới
|
||
|
||
**Triệu chứng:** Admin/role đã grant `PurchaseEvaluations.Read` (inherit parent)
|
||
nhưng menu children `Pe_DuyetNcc_List` / `Pe_DuyetNcc_Create` không hiển thị.
|
||
Chỉ thấy root `PurchaseEvaluations` ở Layout sidebar.
|
||
|
||
**Nguyên nhân:** `GetMyMenuTreeQuery` hardcode 2 inherit root: `Contracts` và
|
||
`Workflows`. Descendant Ct_*/Wf_* auto-inherit CRUD flags từ parent qua switch
|
||
statement. Khi thêm root mới (`PurchaseEvaluations`, `PeWorkflows`) — không có
|
||
trong switch → children mặc định (false,false,false,false) → filter
|
||
`HasAccess` hide children.
|
||
|
||
**Fix:** Extend switch + `nextInherit` propagation:
|
||
|
||
```csharp
|
||
var contractsFlags = GetFlags(MenuKeys.Contracts);
|
||
var workflowsFlags = GetFlags(MenuKeys.Workflows);
|
||
var peFlags = GetFlags(MenuKeys.PurchaseEvaluations); // NEW
|
||
var peWorkflowsFlags = GetFlags(MenuKeys.PeWorkflows); // NEW
|
||
|
||
// Trong BuildChildren:
|
||
if (inheritFromKey is not null && !resolved.ContainsKey(m.Key))
|
||
{
|
||
flags = inheritFromKey switch
|
||
{
|
||
var k when k == MenuKeys.Contracts => contractsFlags,
|
||
var k when k == MenuKeys.Workflows => workflowsFlags,
|
||
var k when k == MenuKeys.PurchaseEvaluations => peFlags, // NEW
|
||
var k when k == MenuKeys.PeWorkflows => peWorkflowsFlags, // NEW
|
||
_ => flags,
|
||
};
|
||
}
|
||
|
||
var nextInherit = inheritFromKey
|
||
?? (m.Key == MenuKeys.Contracts ? MenuKeys.Contracts
|
||
: m.Key == MenuKeys.Workflows ? MenuKeys.Workflows
|
||
: m.Key == MenuKeys.PurchaseEvaluations ? MenuKeys.PurchaseEvaluations
|
||
: m.Key == MenuKeys.PeWorkflows ? MenuKeys.PeWorkflows
|
||
: null);
|
||
```
|
||
|
||
**Rule:** Khi thêm 1 root mới có child leaves (vd `PeWorkflows` → `PeWf_*`) —
|
||
PHẢI update cả 3 chỗ: (1) MenuKeys.All, (2) GetMyMenuTreeQuery GetFlags + switch,
|
||
(3) nextInherit propagation.
|
||
|
||
### 36. Vite env var embed compile-time — đổi `.env.production` phải rebuild FE
|
||
|
||
**Triệu chứng:** Đổi `VITE_API_BASE_URL=...` trong `.env.production` nhưng FE
|
||
vẫn gọi URL cũ. Hot reload không giúp.
|
||
|
||
**Nguyên nhân:** Vite inline `import.meta.env.VITE_*` tại build time vào JS
|
||
bundle (minified). File `.env*` chỉ đọc khi `vite build` — không runtime.
|
||
|
||
**Fix:** Sau đổi env:
|
||
1. Rebuild: `cd fe-admin ; npm run build`
|
||
2. Deploy dist mới lên IIS
|
||
3. Clear CDN/browser cache (Ctrl+Shift+R)
|
||
|
||
Verify bundle có URL mới: `curl dist/assets/index-*.js | grep -oE 'https://[^"]+api'`.
|
||
|
||
## Deploy / Production (continued)
|
||
|
||
### 37. PowerShell 5.1 diacritics trong script — gotcha #30 tái phát
|
||
|
||
**Triệu chứng (bis):** `migrate-domains.ps1` viết với "Phương Án", "→",
|
||
em-dash → PS 5.1 parser fail `Missing closing ''`, `Unexpected character`.
|
||
|
||
**Fix:** Luôn ASCII-only cho .ps1 — rule lặp lại gotcha #30. Cách phát hiện:
|
||
grep file cho UTF-8 multi-byte chars trước khi deploy:
|
||
```bash
|
||
grep -P '[\x80-\xff]' scripts/*.ps1
|
||
# Nếu có match → rewrite ASCII-only
|
||
```
|
||
|
||
## Email / Users
|
||
|
||
### 38. Email rename Identity user — 4 field cần update đồng thời
|
||
|
||
**Triệu chứng:** Đổi `user.Email` xong login với email mới vẫn 401. Hoặc
|
||
UserManager.FindByEmail trả null.
|
||
|
||
**Nguyên nhân:** Identity lookup qua `NormalizedEmail` (uppercase), không
|
||
`Email`. Username cũng dùng email. 4 field phải sync:
|
||
|
||
```csharp
|
||
u.Email = newEmail;
|
||
u.NormalizedEmail = newEmail.ToUpperInvariant();
|
||
u.UserName = newEmail;
|
||
u.NormalizedUserName = newEmail.ToUpperInvariant();
|
||
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/<hash>/`
|
||
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/<id>`
|
||
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.
|
||
|
||
### 44. Silent 403 từ class-level `[Authorize(Policy = ...)]` quá strict (Session 18)
|
||
|
||
**Triệu chứng:** UAT 2026-05-08 — Drafter `nv.test` Workspace tạo phiếu B, dropdown "Quy trình duyệt" empty silent. Admin Designer cùng URL endpoint thấy data đầy đủ. Không có toast error / network panel hint.
|
||
|
||
**Root cause:** `ApprovalWorkflowsV2Controller` class-level `[Authorize(Policy = "Workflows.Read")]` → non-admin role (Drafter chỉ có `PurchaseEvaluations.Read`) bị 403 Forbidden khi GET `/api/approval-workflows-v2`. TanStack Query catch HTTP error trả `data=undefined`, FE component render dropdown empty không có "loading" / "error" state visible.
|
||
|
||
**Fix (`f77ea38`):** Tách policy theo action — class-level `[Authorize]` only (any authenticated), action-level chỉ POST/DELETE giữ `[Authorize(Policy = "Workflows.Create")]`:
|
||
```csharp
|
||
[ApiController]
|
||
[Route("api/approval-workflows-v2")]
|
||
[Authorize] // ← any authenticated, không hardcode policy
|
||
public class ApprovalWorkflowsV2Controller(IMediator mediator) : ControllerBase
|
||
{
|
||
[HttpGet] // ← Drafter pick workflow lúc create — read-only OK
|
||
public async Task<...> Overview(...) { ... }
|
||
|
||
[HttpPost]
|
||
[Authorize(Policy = "Workflows.Create")] // ← admin Designer
|
||
public async Task<...> Create(...) { ... }
|
||
}
|
||
```
|
||
|
||
**Pattern reusable:** Endpoint dùng cho nhiều use case (admin Designer + user list-pick) — split policy per action thay vì class-level uniform. Read-only list workflow KHÔNG nhạy cảm (chỉ là cấu hình quy trình, không expose business data).
|
||
|
||
**Phòng tránh tương lai:** Khi controller class-level `[Authorize(Policy)]`, audit role nào cần access từng action. Nếu GET cần broader role hơn POST → split policy. Default `[Authorize]` (any authenticated) cho list-pick endpoint.
|
||
|
||
**FE diagnostic improvement:** TanStack Query error nên hiển thị warning UI (toast hoặc banner) thay vì silent. Hiện tại `useQuery` catch silent → debug khó. Future: wire `onError` handler global show generic error toast.
|
||
|
||
### 45. PE "Trả về nhưng hệ thống vẫn duyệt" — FE button label vs decision payload mismatch (Session 21 turn 3)
|
||
|
||
**Triệu chứng:** UAT 2026-05-12 — User bro screenshot button labeled `← Trả lại` trong PE Workflow Panel (menu "Duyệt"), nhấn vào nhưng phiếu KHÔNG về phase TraLai — ngược lại tiến qua Cấp tiếp theo (hệ thống ghi nhận approve). User mô tả hành vi: "Trả về nhưng hệ thống vẫn duyệt".
|
||
|
||
**Root cause:** `PeWorkflowPanel.tsx` có 3 chỗ check transition type với logic KHÔNG sync giữa nhau:
|
||
|
||
- **L205-207** `isSendBack` (button label color): include cả `DangSoanThao` lẫn `TraLai` từ phase trung gian → label hiển thị `← Trả lại` đúng.
|
||
- **L64-66** `isReject` (payload `decision` gửi BE): CHỈ check `DangSoanThao`, **thiếu `TraLai`** → khi target=TraLai (98), `isReject=false` → payload `decision: 1` (Approve) thay vì `2` (Reject).
|
||
- **L247-248** dialog `isSendBack` (title + warning): CHỈ check `DangSoanThao`, **thiếu `TraLai`** → dialog title fallback `'✓ Duyệt → Trả lại'` (sai semantic) + KHÔNG hiển thị amber warning "Phiếu sẽ về Đang soạn thảo".
|
||
|
||
BE `PurchaseEvaluationWorkflowService.TransitionAsync`:
|
||
- L51 `if (decision == Reject)` branch → set Phase=TraLai correctly khi decision=Reject.
|
||
- L97 `APPROVE STEP` branch khi decision=Approve + fromPhase=ChoDuyet → `ApproveV2Async` UPSERT opinion = "đã duyệt" + advance Cấp.
|
||
- → Khi FE gửi `decision=1` (do bug `isReject`), BE đi vào nhánh APPROVE thay vì REJECT → phiếu được ghi nhận approve dù user định trả lại.
|
||
|
||
**Severity:** 🔴 CRITICAL — data integrity issue. NV nhấn "Trả lại" sẽ vô tình "duyệt" phiếu sang Cấp tiếp theo + UPSERT opinion vĩnh viễn vào `PurchaseEvaluationLevelOpinions` (Mig 26). Khó rollback vì BE đã `SaveChangesAsync`.
|
||
|
||
**Fix Chunk A (`de00887` BE defense-in-depth):**
|
||
```csharp
|
||
// PurchaseEvaluationWorkflowService.cs sau set isAdmin/isSystem (L48), trước REJECT branch (L51)
|
||
if ((targetPhase == PurchaseEvaluationPhase.TraLai
|
||
|| targetPhase == PurchaseEvaluationPhase.TuChoi)
|
||
&& decision != ApprovalDecision.Reject)
|
||
{
|
||
throw new ConflictException(
|
||
$"Transition tới {targetPhase} BẮT BUỘC decision=Reject (nhận {decision}). " +
|
||
"Báo lỗi caller — payload mismatch giữa target phase và decision.");
|
||
}
|
||
```
|
||
|
||
Boundary protection cho mọi caller tương lai (API client / mobile / cron retry). 3 regression test:
|
||
- `TransitionAsync_TargetTraLai_WithApproveDecision_Throws_AndDoesNotMutateState` (bug reproduce)
|
||
- `TransitionAsync_TargetTuChoi_WithApproveDecision_Throws_AndDoesNotMutateState` (consistency cover)
|
||
- `TransitionAsync_TargetTraLai_WithRejectDecision_SetsPhaseTraLai` (happy path control)
|
||
|
||
**Fix Chunk B (`4b29d00` FE mirror 2 app):**
|
||
```typescript
|
||
// PeWorkflowPanel.tsx (fe-user + fe-admin) — 3 chỗ × 2 app
|
||
|
||
// Chỗ 1: isReject payload (line 64-66)
|
||
const isReject = target === PurchaseEvaluationPhase.TuChoi
|
||
|| (target === PurchaseEvaluationPhase.DangSoanThao
|
||
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao)
|
||
|| (target === PurchaseEvaluationPhase.TraLai // ← THÊM
|
||
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai)
|
||
|
||
// Chỗ 2: dialog isSendBack (line 247-248)
|
||
const isSendBack = (target === PurchaseEvaluationPhase.DangSoanThao
|
||
|| target === PurchaseEvaluationPhase.TraLai) // ← THÊM
|
||
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao
|
||
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai // ← THÊM
|
||
```
|
||
|
||
Chỗ 3 (button label `isSendBack` L205-207) đã đúng từ S17, KHÔNG đụng.
|
||
|
||
**Pattern reusable — invariant check khi viết FE workflow transition:**
|
||
1. Button label condition (visual) phải SYNC với payload decision (semantic).
|
||
2. Dialog title/warning condition phải SYNC với button label + payload.
|
||
3. Tốt nhất: extract `isReject(target, currentPhase)` thành 1 helper FE + BE share semantic — KHÔNG duplicate logic giữa 3 chỗ.
|
||
|
||
**Phòng tránh tương lai:**
|
||
- Khi spec mới có thêm phase terminal/intermediate (vd Session 17 thêm TraLai làm Phase RIÊNG thay vì DangSoanThao revert), audit grep TOÀN BỘ logic check `=== DangSoanThao` để xem chỗ nào cần thêm `|| === NewPhase`.
|
||
- BE guard early invariant `(targetPhase ∈ terminalSet) ⇔ (decision == Reject)` thay vì trust FE payload.
|
||
- Test-before bug fix BẮT BUỘC §7 — 3 test cover bug reproduce + consistency + happy path.
|
||
|
||
**References:**
|
||
- Commit fix: `de00887` (BE Chunk A) + `4b29d00` (FE Chunk B)
|
||
- Spec Session 17: `feedback_n_stage_workflow_pattern` DEPRECATED + spec mới trong `PurchaseEvaluationWorkflowService.cs` comment L15-19
|
||
- State machine 5 trạng thái: Nháp / Đã gửi duyệt / **Trả lại (98) — Phase RIÊNG** / Từ chối / Đã duyệt
|
||
|
||
### 46. Gitea Actions API path `/tasks` not `/runs` + cache stale ~2 min (CICD Monitor S21 t4 discovery)
|
||
|
||
**Triệu chứng:** CICD Monitor sub-agent S21 t4 run đầu poll Gitea Actions runs sau push → `GET https://git.baocaogiaoduc.vn/api/v1/repos/vietreport-admin/solution-erp/actions/runs?limit=5` → **404 Not Found**. Tưởng repo không có Actions enabled hoặc API endpoint sai. Debug 10 phút retry path/auth/header trước khi tìm ra đúng path.
|
||
|
||
**Root cause:** Gitea API v1 spec dùng `/actions/tasks` (NOT `/actions/runs`). Naming khác GitHub Actions API (GitHub: `/actions/runs`). Public no-auth read OK cho repo public.
|
||
|
||
**Endpoint đúng:**
|
||
```bash
|
||
GET https://git.baocaogiaoduc.vn/api/v1/repos/vietreport-admin/solution-erp/actions/tasks?limit=5
|
||
```
|
||
|
||
**Response fields:**
|
||
- `id` (task internal ID)
|
||
- `run_number` (display number Run #N)
|
||
- `head_sha` (commit triggered task)
|
||
- `status` (queued / running / success / failure / cancelled)
|
||
- `conclusion` (final state when status=success/failure)
|
||
- `created_at`, `updated_at`
|
||
- `display_title` (commit message summary)
|
||
|
||
**Match task to commit:** Filter `head_sha == $commitSha` thay vì rely on order.
|
||
|
||
**Bonus gotcha — cache stale:** `updated_at` field caches ~2 phút sau khi deploy thật xong (act_runner ghi log final, Gitea API chưa update DB ngay). Cross-check VPS file timestamps khi cần time-sensitive verify:
|
||
```powershell
|
||
ssh vietreport-vps "Get-Item C:\inetpub\solution-erp\admin\index.html | Select LastWriteTime"
|
||
ssh vietreport-vps "Get-Item C:\inetpub\solution-erp\api\SolutionErp.Api.dll | Select LastWriteTime"
|
||
```
|
||
|
||
File LastWriteTime VPS = thực sự deploy completion time (NSSM copy + IIS recycle xong).
|
||
|
||
**Phòng tránh tương lai:**
|
||
- CICD Monitor system prompt + Bash command preset đã update sang `/actions/tasks` (saved trong MEMORY S21 t4).
|
||
- Khi setup automation script với Gitea API → đọc rõ Gitea API spec v1.20+ (`https://docs.gitea.com/api/`) thay vì assume GitHub naming.
|
||
- Time-sensitive verify (vd "deploy xong chưa, có an toàn trigger task tiếp không"): KHÔNG trust API status timestamp đơn lẻ → cross với VPS file mtime hoặc curl bundle hash live.
|
||
|
||
### 47. `.claude/agent-memory/**` paths-ignore — hypothesis disproven, preventive note cho non-.md state files (S22 discovery + S22 chốt revise)
|
||
|
||
**Triệu chứng ban đầu (em main HYPOTHESIS S22):** Cuối session flush 3-4 sub-agent MEMORY.md drift patch + commit dạng `[CLAUDE] Docs: chốt S22 ...` — em main đoán push trigger Gitea Actions full deploy ~3.5min waste vì `.claude/agent-memory/**` thiếu trong paths-ignore.
|
||
|
||
**Verify thực tế (CICD Monitor Run #193 S22 chốt — 2026-05-13 23:16):**
|
||
|
||
Hypothesis **DISPROVEN**. `paths-ignore` của workflow file đã có pattern `**/*.md` — glob này match **mọi `.md` file ở mọi độ sâu** (kể cả `.claude/agent-memory/{investigator,implementer,reviewer,cicd-monitor}/MEMORY.md`).
|
||
|
||
Push `cc8a7d3` (Docs S22 chốt + 4 agent MEMORY flush, 8 files tất cả là `.md`) → CI **correctly SKIPPED**. Run #193 cuối cùng cho code change là `b04a11a` (Mig 30 BE+FE), KHÔNG phải `cc8a7d3` Docs.
|
||
|
||
**Cross-check:** `git show --name-only cc8a7d3` → 8 files với extension `.md` only → match `**/*.md` glob → SKIPPED đúng.
|
||
|
||
**Preventive note cho future:** Nếu tương lai add **non-`.md` state files** vào `.claude/agent-memory/` (vd `archive.json`, `metrics.log`, `cache.bin`) — sẽ trigger CI vì KHÔNG match `**/*.md`. Khi đó add `.claude/agent-memory/**` vào paths-ignore explicit:
|
||
|
||
```yaml
|
||
paths-ignore:
|
||
- 'docs/**'
|
||
- '**/*.md'
|
||
- '.claude/skills/**'
|
||
- '.claude/agent-memory/**' # ← preventive nếu add non-.md state files
|
||
```
|
||
|
||
**Severity:** Informational (hiện tại KHÔNG có vấn đề thật vì all MEMORY files là `.md`). KHÔNG cần fix `.gitea/workflows/deploy.yml`.
|
||
|
||
**Cross-ref:** Gotcha #41 paths-ignore docs-only skip pattern.
|
||
|
||
**Lesson:** Verify hypothesis qua actual Gitea API task list TRƯỚC KHI claim CI waste. Em main S22 đoán nhầm — CICD Monitor catch sai.
|
||
|
||
**References:**
|
||
- CICD Monitor Run #186 (S21 t4 2026-05-13 19:13) — first discovery
|
||
- CICD Monitor Run #187 (S21 t5 2026-05-13 20:12) — confirmed pattern + cache stale bonus
|
||
- Memory `feedback_multi_agent_setup` Plan G Trial Week 1 evidence
|
||
|
||
### 48. Multi-Changelog.Add() trong cùng SaveChangesAsync → SQLite frozen-clock tie-break → tests `OrderByDescending(CreatedAt).First()` non-deterministic (Session 25 Plan AB + Run #215 catch)
|
||
|
||
**Triệu chứng:** Plan AB Chunk A `cdfd542` add SECOND Changelog.Add() entry vào `ApplyReturnModeAsync` (cover Bug 2 — Return mode log) end-of-function. Caller `TransitionAsync:100` đã có sẵn `LogTransitionAsync` add FIRST Changelog entry (Action=Transition + ContextNote=comment chứa "không lùi được"). 2 entries cùng `SaveChangesAsync` transaction → SQLite test fixture frozen clock → CreatedAt **identical microseconds** cho cả 2 rows.
|
||
|
||
Plan M edge case tests (S23 t3) query `.OrderByDescending(c => c.CreatedAt).FirstAsync()` assert `ContextNote.Contains("không lùi được")` — sau Plan AB, SQLite tie-break non-deterministic, pick Plan AB row (EntityType=Workflow, Action=Update, ContextNote=null) → `Expected ContextNote not to be <null>` FAIL.
|
||
|
||
**CI Run #215 sha=cdfd542** test_infra FAIL 2/53 (51 PASS, 2 FAIL):
|
||
- `ApplyReturnMode_OneStep_AtStep1_ResetsToBuoc1Cap1_KeepsChoDuyet` (line 350)
|
||
- `ApplyReturnMode_OneLevel_AtStep1Level1_ResetsToBuoc1Cap1_KeepsChoDuyet` (line 308)
|
||
|
||
Test gate caught regression → deploy never reached → prod spared broken state.
|
||
|
||
**Fix Option A (chốt):** Test query filter by `Summary.Contains("Chuyển phase")` để pick đúng LogTransition entry. Plan AB BE code stays clean.
|
||
|
||
```csharp
|
||
// Trước Plan AB Chunk A2:
|
||
var changelog = await db.PurchaseEvaluationChangelogs
|
||
.Where(c => c.PurchaseEvaluationId == pe.Id)
|
||
.OrderByDescending(c => c.CreatedAt)
|
||
.FirstAsync();
|
||
|
||
// Sau Plan AB Chunk A2 fix (commit 8c05947):
|
||
var changelog = await db.PurchaseEvaluationChangelogs
|
||
.Where(c => c.PurchaseEvaluationId == pe.Id && c.Summary!.Contains("Chuyển phase"))
|
||
.OrderByDescending(c => c.CreatedAt)
|
||
.FirstAsync();
|
||
```
|
||
|
||
**Pattern reusable:** Khi handler/service add NEW Changelog row trong existing flow đã có LogTransition row, tests query audit table MUST filter EntityType / Action / Summary keyword **discriminator** thay vì raw OrderByDescending timestamp. Cross-ref Contract V2 test setup tương lai.
|
||
|
||
**Severity:** Major — caught by CI before prod ship, no user impact. Lesson reinforced UAT mode `feedback_uat_skip_verify` skip `dotnet test` per chunk RISK khi BE refactor > 100 LOC + signature change → em main resumed local test verify post Plan AB Chunk A2.
|
||
|
||
**References:**
|
||
- Plan AB Chunk A commit `cdfd542` (Run #215 FAIL)
|
||
- Plan AB Chunk A2 fix commit `8c05947` (Run #216 PASS)
|
||
- Memory `feedback_uat_skip_verify` lesson reinforced S25
|
||
- File: `tests/SolutionErp.Infrastructure.Tests/Services/PurchaseEvaluationWorkflowServiceReturnModeTests.cs:304-310, 346-352`
|
||
|
||
### 49. UI dual-phase badge `fromPhase → toPhase` gây nhầm khi 3/4 Reject mode giữ Phase=ChoDuyet (Session 25 Plan AD)
|
||
|
||
**Triệu chứng:** Bro UAT 2026-05-19 Plan AC deploy: panel "Lịch sử duyệt" 6 entries TẤT CẢ hiện `Đã gửi duyệt → Đã gửi duyệt` (vì 3 mode Return OneLevel/OneStep/Assignee giữ Phase=ChoDuyet sau Mig 28 — chỉ Drafter mode set TraLai). Reject entry visually IDENTICAL Approve entry → user nhầm "Đã trả lại nhưng vẫn hiện đã duyệt".
|
||
|
||
**Fix Plan AD (commit `0aaf2df`):**
|
||
|
||
1. **Drop fromPhase → toPhase badges entirely** trong ApprovalsTab (cả `fe-user` + `fe-admin` mirror §3.9). Visual confusion gỡ bỏ.
|
||
|
||
2. **Thay bằng next-target hint parse từ comment** via helper `extractNextTargetHint(decision, toPhase, comment)`:
|
||
- **Approve:** Summary "sang Cấp X" → "→ Cấp X", "sang Bước Y" → "→ Bước Y (Cấp 1)", "Duyệt vượt cấp" → "→ Vượt cấp tới Cấp cuối", toPhase=DaDuyet(20) → "→ Đã duyệt hoàn tất"
|
||
- **Reject:** ContextNote "Người chỉ định" → "→ Trả về Người chỉ định (Bước X Cấp Y)" parse regex, "Người soạn thảo"/"Drafter" → "→ Trả về Người soạn thảo", "không lùi được" → "→ Không lùi được", "Trả về 1 Cấp"/"Trả về Cấp X" → "→ Lùi về Cấp X", toPhase=TuChoi(99) → "→ Từ chối hoàn toàn"
|
||
|
||
3. **Decision badge** (Plan AC `a734bf2` đã add): Duyệt emerald / Trả lại amber / Từ chối rose — phân biệt Action level KHÔNG dựa vào phase.
|
||
|
||
**Pattern reusable cross-project:** UI audit history KHÔNG nên render dual-phase badge khi state machine self-loop (e.g. ChoDuyet → ChoDuyet là advance pointer trong cùng phase, KHÔNG phải transition). Thay bằng Decision badge + semantic next-target hint parse từ structured comment.
|
||
|
||
**Severity:** UX confusion (KHÔNG functional bug). Bro UAT phản hồi sau Plan AC deploy.
|
||
|
||
**References:**
|
||
- Plan AD commit `0aaf2df` (Run #219 PASS)
|
||
- File: `fe-user/src/components/pe/PeDetailTabs.tsx:1995-2070` (ApprovalsTab + decisionBadge + extractNextTargetHint)
|
||
- Mirror: `fe-admin/src/components/pe/PeDetailTabs.tsx`
|
||
|
||
### 51. INFRASTRUCTURE seed vs DEMO seed phân biệt — `DemoSeed:Disabled` flag gate trap (Session 29 Plan B Chunk A2 Hotfix CICD)
|
||
|
||
**Triệu chứng:** Plan B Chunk A2 Implementer scaffold `SeedSampleContractWorkflowV2Async` mirror PE `SeedSampleApprovalWorkflowsV2Async` pattern — nested inside `if (!demoSeedDisabled)` branch trong `DbInitializer.cs:105-111`. Prod has `DemoSeed:Disabled=true` (Plan T S23 t10 — UAT permanent clean slate). Run #231 PASS deploy NHƯNG `QT-HD-V2-001` workflow KHÔNG seed prod. CICD Monitor (agentId a2ea2e3a) verify Stage 4 catch smoking gun log: `"DemoSeed:Disabled=true → skip workflow + contracts + PE + sample V2 seed (Plan T S23 t10 + Plan B Chunk A2 Contract V2)"`. → Drafter Workspace dropdown V2 EMPTY → V2 contract path **BLOCKED end-to-end UAT**.
|
||
|
||
**Root cause:** Implementer mirror PE pattern (which IS gated for valid reason — PE workflow A/B already seeded V1 historically) → áp dụng nguyên xi cho Contract V2 (no V1 sample existed) → V2 path không có default workflow để Drafter pick.
|
||
|
||
**Fix Hotfix CICD (`38f1c4d`):** PROMOTE `SeedSampleContractWorkflowV2Async` ra ngoài `DemoSeed` gate:
|
||
```csharp
|
||
if (!demoSeedDisabled)
|
||
{
|
||
await SeedDemoContractsAsync(...);
|
||
await SeedDemoPurchaseEvaluationsAsync(...);
|
||
await SeedSampleApprovalWorkflowsV2Async(...); // PE: gated (historical V1 sample existed)
|
||
}
|
||
|
||
// [Plan B S29 2026-05-22 Hotfix CICD] OUT of gate
|
||
await SeedSampleContractWorkflowV2Async(...); // INFRASTRUCTURE — V2 path requires
|
||
```
|
||
|
||
**Pattern reusable — Seed classification table:**
|
||
|
||
| Category | Examples | Gate? |
|
||
|---|---|---|
|
||
| **INFRASTRUCTURE always run** | Roles, Departments, Catalogs, MenuTree, AdminPermissions, ContractTemplates, **SampleWorkflowV2 (V2 path requires)** | NO gate |
|
||
| **DEMO gated** | DemoUsers (30 sample, Plan T disabled prod), DemoContracts ([DEMO]), DemoPE ([DEMO]), SampleApprovalWorkflowsV2 (historical PE A/B legacy) | `if (!demoSeedDisabled)` |
|
||
|
||
**Decision tree khi add new Seed method:**
|
||
1. Production có cần seed này để work end-to-end không?
|
||
- YES → INFRASTRUCTURE (no gate)
|
||
- NO → DEMO (gated)
|
||
2. Có safe để admin edit/delete/disable không?
|
||
- YES → INFRASTRUCTURE OK (admin sửa qua Designer khi cần)
|
||
- NO → architect re-think (immutable seed = anti-pattern)
|
||
|
||
**Bonus — Smart Friend ROI:** CICD Monitor catch BEFORE bro UAT 401/empty experience. Cumulative pattern proven 4× S22 #44 + S25 #48 + S29 Plan B Reviewer #ApplicableType + S29 Plan B CICD #DemoSeed.
|
||
|
||
**Phòng tránh tương lai:** Khi Implementer mirror PE V2 pattern cho new module (Budget V2, Notification V2), explicit check question: "PE pattern gated/ungated, có applicable cho new module không?" — KHÔNG copy nguyên xi DemoSeed gate placement.
|
||
|
||
**References:**
|
||
- Hotfix CICD commit: `38f1c4d` (Run #232 PASS 3m33s)
|
||
- File: `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs:105-122`
|
||
- Cross-ref: Plan T S23 t10 DemoSeed flag (gotcha implicit)
|
||
- Memory cross-ref: `feedback_demo_seed_flag_disable.md` (Plan T pattern)
|
||
|
||
### 50. Page move cross-app — `Layout.tsx` `resolvePath` staticMap missed mirror → silent sidebar drop (Session 29 Plan CA Hotfix 1)
|
||
|
||
**Triệu chứng:** Plan CA move 4 master pages từ fe-admin → fe-user (commit `06a441c` Implementer Case 2). BE `/api/menus/me` return tree đầy đủ (Master + 4 children + Catalogs sub-tree 4 children, ALL CanRead=true). Admin login eoffice → sidebar group "DANH MỤC" expand chỉ thấy "Danh mục chi tiết" (Catalogs), **3 leaf Suppliers/Projects/Departments + 4 sub-catalogs biến mất silent**.
|
||
|
||
**Root cause:** `fe-user/src/components/Layout.tsx:55-94` `resolvePath(key)` staticMap chỉ có 7 entries cho `Dashboard/Contracts/PE/Budgets/Bg_*`. THIẾU 7 entries cho master/catalog leaf mới move:
|
||
- `Suppliers`, `Projects`, `Departments`
|
||
- `CatalogUnits`, `CatalogMaterials`, `CatalogServices`, `CatalogWorkItems`
|
||
|
||
Component `MenuLeaf` line 238: `if (!path) return null` → khi `resolvePath` trả null, component returns null → React render nothing → **silent drop khỏi DOM**, không có error toast/console warn.
|
||
|
||
**Fix Hotfix 1 (`e55d96b`):** Thêm 7 entries vào fe-user `staticMap` mirror EXACT từ fe-admin `Layout.tsx:33-47`:
|
||
```typescript
|
||
const staticMap: Record<string, string> = {
|
||
Dashboard: '/dashboard',
|
||
// ... existing 6 entries
|
||
Suppliers: '/master/suppliers',
|
||
Projects: '/master/projects',
|
||
Departments: '/master/departments',
|
||
CatalogUnits: '/master/catalogs/units',
|
||
CatalogMaterials: '/master/catalogs/materials',
|
||
CatalogServices: '/master/catalogs/services',
|
||
CatalogWorkItems: '/master/catalogs/work-items',
|
||
}
|
||
```
|
||
|
||
**Pattern reusable — 4-place mirror checklist khi Implementer Case 2 cookie-cutter copy page cross-app:**
|
||
1. ✅ Page file (`pages/<dir>/*.tsx`) — copy nguyên content
|
||
2. ✅ `App.tsx` Routes — add `<Route path="..." element={...} />`
|
||
3. ✅ `lib/menuKeys.ts` constants — mirror BE `MenuKeys.cs`
|
||
4. ⚠️ **`components/Layout.tsx` `resolvePath` staticMap** — KEY mapping → route path. **DỄ MISS** vì khác file scope với pages directory.
|
||
|
||
**Bonus:** fe-admin có `staticMap` đầy đủ từ trước Plan CA → mirror dễ. fe-user trước Plan CA KHÔNG có master keys → Implementer cookie-cutter copy page nhưng quên check staticMap diff.
|
||
|
||
**Phòng tránh tương lai:** Implementer Case 2 cross-app page move task prompt MUST list 4 places explicit. Reviewer Cat 1 wire claim verify SHOULD include: "Sidebar menu visible end-to-end test post-build".
|
||
|
||
**References:**
|
||
- Commit Hotfix 1: `e55d96b` (Run #230 PASS, fe-user bundle `DVBLmZlt` → `Dgn1iU9E`)
|
||
- File: `fe-user/src/components/Layout.tsx:55-94` (resolvePath staticMap + line 238 MenuLeaf null guard)
|
||
- Original Plan CA Chunk B commit: `06a441c` (Implementer missed point 4)
|
||
- Mirror: `fe-admin/src/components/Layout.tsx:33-53`
|
||
|
||
### 52. `qdrant-client` 1.18 xóa `search()` API — `except Exception: continue` nuốt lỗi silent → vector search luôn trả `[]` (Session 31 RAG eval diagnosis)
|
||
|
||
**Triệu chứng:** `search_memory` MCP tool chỉ trả kết quả cho queries có BM25 exact-match tốt (có tất cả token trong cùng 1 chunk). Queries dùng ngữ nghĩa / multi-hop concept → **0 results** dù Qdrant `points_count=2949` green.
|
||
|
||
**Root cause:** `qdrant-client 1.18.0` removed `QdrantClient.search()` method hoàn toàn. `retrieval.py` vẫn gọi `_qdrant.search(...)`:
|
||
```python
|
||
# OLD (broken in 1.18):
|
||
hits = _qdrant.search(
|
||
collection_name=c,
|
||
query_vector=query_vector,
|
||
limit=top_k,
|
||
with_payload=True,
|
||
)
|
||
# AttributeError: 'QdrantClient' object has no attribute 'search'
|
||
```
|
||
Exception bị `except Exception: continue` nuốt silent → `vec_results = []` mọi lúc → pipeline chỉ có BM25.
|
||
|
||
**Kết quả trước fix:** RAG eval v1.1 recall@5 = 0.455 (chỉ BM25 queries). v1.0 = 0.455 (same reason). BM25 strict AND-match: ALL tokens phải cùng chunk → multi-token query 8+ tokens fail.
|
||
|
||
**Fix (retrieval.py):**
|
||
```python
|
||
# NEW (qdrant-client 1.12+ query_points API):
|
||
resp = _qdrant.query_points(
|
||
collection_name=c,
|
||
query=query_vector, # ← param renamed: query_vector → query
|
||
limit=top_k,
|
||
with_payload=True,
|
||
)
|
||
for h in resp.points: # ← .points không phải iterable trực tiếp
|
||
```
|
||
|
||
**Kết quả sau fix:** recall@5 = 1.000 (11/11), avg rerank = 0.847. PASS gate.
|
||
|
||
**Phòng tránh:** Pin `qdrant-client` version trong deps (`==1.x.y`). Hoặc thêm health-check startup: `assert hasattr(_qdrant, 'query_points'), "upgrade qdrant-client"`. **KHÔNG dùng `except Exception: continue` che-mờ lỗi API** — ít nhất log warning.
|
||
|
||
### 53. Sub-agent truncation / stall pattern khi heavy MEMORY update phase end-of-task — Reviewer + CICD bị cut mid-sentence ở Update MEMORY.md step (Session 35 × 3 occurrence)
|
||
|
||
**Triệu chứng:** Sub-agent (Reviewer + CICD spawn ~100K token budget, ~30+ tool uses) chạy adversarial checks / smoke verify hoàn chỉnh, returning verdict PASS qua snippet visible, NHƯNG output bị truncate ở final "Update MEMORY.md BEFORE stop" step. Em main không nhận structured verdict đầy đủ.
|
||
|
||
**Pattern empirical S35:**
|
||
- Reviewer FE forms (1200 LOC + 60 mutation): Cat 1 "wire BE PERFECT" + 33 tool uses → truncated mid-MEMORY append
|
||
- Reviewer BE CRUD (576 LOC + 16 endpoint): "MEMORY size warning 24.6KB exceeds 24.4KB. Let me append concise entry but trim verbose..." → truncated mid-trim
|
||
- CICD Run #244 verify (FE Admin deploy): "VPS mtime cross-check confirms ship at 10:05" → stalled 600s watchdog timeout
|
||
|
||
**Root cause hypothesis:**
|
||
1. Sub-agent context window approaches limit khi cumulative tool output (Read MEMORY ~25KB initial + Read references + Bash output + grep results) + 100K spawn budget
|
||
2. MEMORY.md size ~25-31KB borderline triggers Edit/Write large operation late-stage → token overflow during streaming
|
||
3. Stream watchdog 600s timeout không recover (CICD case) — process hung internally
|
||
|
||
**Mitigation S35 verified:**
|
||
- **A. Tight brief scope** — Reviewer FE Admin spawn (~5K token brief, 4 cat tight, "concise findings only") → PASS clean 5K return không truncated. Pattern: brief budget < 8K + scope ≤ 4 cat + explicit "DO NOT curate MEMORY heavy — short append only this time"
|
||
- **B. Em main manual verify post-truncation** — Cat 2-6 grep-based verify (SHA256 diff exit code + grep count match) takes ~5 phút em main, faster than re-spawn
|
||
- **C. Curate MEMORY pre-spawn nếu > 25KB** — agent MEMORY > threshold trigger truncation risk. Em main curate proxy archive q3.md trước spawn heavy.
|
||
- **D. Avoid forcing MEMORY heavy update in agent spec** — phase "Update MEMORY.md BEFORE stop with detailed findings" → switch to "short append 1 entry FIFO most-recent-first, KHÔNG curate old"
|
||
|
||
**Cumulative occurrences S35:** 3/8 sub-agent spawn (Reviewer × 2 + CICD × 1 = 37.5% truncation rate at borderline ~25-31KB MEMORY). Heavy task + large MEMORY = correlation point.
|
||
|
||
**References:**
|
||
- S33 Implementer truncation pattern 2/3 (memory `feedback_implementer_truncation_mitigation` user-level — heavy scaffold ≥30 file)
|
||
- S35 Reviewer FE forms (Session 35 push #1 verify): output cut after Cat 1 PERFECT statement
|
||
- S35 Reviewer BE CRUD (Session 35 push #2 verify): cut mid-MEMORY trim
|
||
- S35 CICD Run #244 (Session 35 push #3 verify): stalled 600s watchdog after VPS mtime cross-check
|
||
|
||
**Phòng tránh:**
|
||
1. Tight brief scope ≤ 8K tokens cho Reviewer/CICD nếu task verifiable qua grep/diff em main
|
||
2. MEMORY pre-spawn audit: nếu > 25KB → curate proxy archive trước spawn
|
||
3. Agent spec ghi rõ "short append MEMORY only, NO curate", remove "BEFORE stop with detailed" directive khi MEMORY borderline
|
||
4. Em main backup verify Cat 2-6 manual grep nếu Reviewer truncated mid-verdict
|
||
|
||
**References:**
|
||
- AI_INFRA: `claude-rag/lib/retrieval.py` `vector_search()` function — fixed 2026-05-26 S31
|
||
- Eval run: `eval/runs/2026-05-26-baseline-v1.1-final.json`
|
||
- Diagnosis: Qdrant REST `/collections/proj_solution_erp` green (2949 points), `bm25.db` 2949 chunks → pipeline broken, not data. Confirmed via `python -c "from qdrant_client import QdrantClient; client.search(...)"` → AttributeError.
|
||
|
||
---
|
||
|
||
## Checklist debug bug mới
|
||
|
||
1. Build pass không? → fail → check using + package version compat
|
||
2. DI register đủ? → runtime error "Unable to resolve" → add `AddScoped/Singleton`
|
||
3. API log startup có error ẩn? → `tail` output file
|
||
4. File đã persist đúng chưa? → `head -5` verify sau Write
|
||
5. Nếu package exotic → thử downgrade về stable trước
|
||
6. Nếu TS error → check `erasableSyntaxOnly`, `verbatimModuleSyntax`
|
||
7. Nếu EF expression tree → tách logic ra ngoài query
|
||
8. Nếu Unicode CLI → dùng file payload
|
||
9. Nếu workflow 403 → check FE `workflow.nextPhases` sync từ BE pinned policy
|
||
10. Nếu SignalR 401 → dùng `accessTokenFactory` + BE OnMessageReceived hook (#26)
|
||
11. Nếu PS 5.1 script fail → check encoding UTF-8 / BOM / ASCII-only (#30)
|
||
12. Nếu subdomain trả sai content / bị hijack → check IPv4/IPv6 port collision trên VPS shared (#33)
|
||
13. Nếu 2 NavLink cùng active / không đúng highlight → custom isActive match query string (#34)
|
||
14. Nếu menu item có quyền nhưng không hiện → check GetMyMenuTreeQuery inheritance extend (#35)
|
||
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)
|
||
21. Nếu user phàn nàn "feature work cho admin nhưng user empty/403 silent" → check class-level Authorize policy có over-restrict cho non-admin không, split per action (#44)
|
||
22. Nếu button workflow label nói "Trả lại" nhưng phiếu vẫn tiến approve → audit FE `isReject` payload condition vs button `isSendBack` label condition vs dialog `isSendBack` warning condition — phải sync 3 chỗ với CÙNG set target phase. BE thêm guard `(target ∈ terminalSet) ⇔ (decision=Reject)` chặn caller mismatch (#45)
|
||
23. Nếu Gitea Actions API trả 404 trên `/actions/runs` → đúng path là `/actions/tasks` (Gitea naming khác GitHub). Cache `updated_at` stale ~2 min → cross-check VPS file LastWriteTime cho time-sensitive verify (#46)
|
||
24. Nếu test `OrderByDescending(CreatedAt).First()` query audit table fail sau add Changelog mới → SQLite frozen-clock tie-break, MUST filter Summary/EntityType discriminator (#48)
|
||
25. Nếu page move cross-app (Implementer Case 2) nhưng menu leaf KHÔNG hiện sidebar dù BE trả permission OK → check `Layout.tsx` `resolvePath` staticMap miss key mapping → MenuLeaf null guard silent drop (#50). 4-place mirror checklist: page + Routes + menuKeys.ts + Layout.tsx staticMap
|
||
26. Nếu new Seed method KHÔNG chạy prod dù dotnet build PASS + deploy SUCCESS → check nested inside `if (!demoSeedDisabled)` gate (Plan T S23 t10 flag enabled prod) → INFRASTRUCTURE seed phải PROMOTE OUT of DemoSeed gate (#51). Decision tree: production cần để work end-to-end? YES → ungate
|
||
25. Nếu UI audit list show `Đã gửi duyệt → Đã gửi duyệt` lặp gây nhầm → drop dual-phase badge khi state machine self-loop, thay Decision badge + next-target hint parse từ comment (#49)
|
||
27. Nếu RAG `search_memory` trả 0 results dù Qdrant green + BM25 có data → `qdrant-client` upgrade xóa `search()` method, bị nuốt silent. Test: `python -c "from qdrant_client import QdrantClient; c=QdrantClient(url='http://127.0.0.1:6333'); c.search"`. Fix: dùng `query_points(query=...).points` (#52)
|
||
28. Nếu sub-agent (Reviewer/CICD) return PASS verdict bị cut mid-sentence ở "Update MEMORY.md" step → MEMORY > 25KB triggers truncation risk. Mitigation: tight brief ≤ 8K + em main grep verify manual + curate MEMORY pre-spawn nếu > 25KB (#53)
|