10 KiB
sub-test-specialist-0 — W1 KHKK Mig 69: schema-invariant test + codegen format test
Ghi TỪNG MỤC trong lúc làm (chống #53). Run:
2026-07-29-S161-khkk-w1-schema. Vai: test-specialist (WRITE độc quyềntests/**). KHÔNG sửa production, KHÔNG commit.
§0 — Tiền đề đọc từ ĐĨA (trước khi viết 1 dòng test)
Spec đọc: .claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w1-schema-designer-29-07-2026.md
- §③-B dòng 35: test PIN tên
ContractSigningPlanSchemaTests.BayBang_MoiBang_CoAuditFields_VaQueryFilter, sànN ≥ 2, số-sau ≥ số-trước + 2. - §③-B dòng 36: test PIN tên
SigningPlanCodeGenerator_Format_KhkkYearSeqD3— regex^KHKK/\d{4}/\d{3}$+ 2 lần gọi seq tăng.
7 entity — base class ĐO THẬT trên đĩa (KHÔNG đoán)
| # | Entity | File | Base | Query filter (config) |
|---|---|---|---|---|
| 1 | ContractSigningPlan |
Domain/ContractSigningPlans/ContractSigningPlan.cs:16 |
AuditableEntity |
✅ ContractSigningPlanConfiguration.cs:47 |
| 2 | ContractSigningPlanLine |
ContractSigningPlanLine.cs:14 |
AuditableEntity |
✅ ContractSigningPlanLineConfiguration.cs:36 |
| 3 | ContractSigningPlanDossierItem |
ContractSigningPlanDossierItem.cs:31 |
AuditableEntity |
✅ ...DossierItemConfiguration.cs:29 |
| 4 | ContractSigningPlanAttachment |
ContractSigningPlanAttachment.cs:24 |
AuditableEntity |
✅ ...AttachmentConfiguration.cs:39 |
| 5 | ContractSigningPlanLevelOpinion |
ContractSigningPlanLevelOpinion.cs:24 |
AuditableEntity |
❌ CỐ Ý KHÔNG có |
| 6 | ContractSigningPlanChangelog |
ContractSigningPlanChangelog.cs:26 |
BaseEntity |
❌ (đúng — no IsDeleted) |
| 7 | ContractSigningPlanApproval |
ContractSigningPlanApproval.cs:19 |
BaseEntity |
❌ (đúng — no IsDeleted) |
🔴 LỆCH so mô-tả trong TASK-prompt (prompt liệt LevelOpinion = AuditableEntity có IsDeleted ⇒ ngụ ý phải có query filter). ĐĨA nói khác: ContractSigningPlanLevelOpinion LÀ AuditableEntity nhưng CỐ Ý KHÔNG HasQueryFilter, có lý do viết sẵn 2 chỗ:
- entity
ContractSigningPlanLevelOpinion.cs:20-23: "CỐ Ý KHÔNG có HasQueryFilter (parity 7 twin còn lại): site 'dọn chữ ký MỒ CÔI' (ApprovalWorkflowV2AdminFeatures ExecuteDeleteAsync) phải THẤY được row đã soft-delete, nếu bị filter che thì row mồ côi sống sót và FK Restrict lại nổ 500." - config
ContractSigningPlanLevelOpinionConfiguration.cs:12-16: cùng nội dung.
⇒ Áp luật MEMORY "spec drift detection BEFORE write (S34)": test theo CODE, không theo prompt. Nhưng KHÔNG bỏ qua — assert LevelOpinion KHÔNG có filter như một invariant CÓ CHỦ Ý (nếu ai đó thêm filter sau này ⇒ test ĐỎ ⇒ buộc đọc lại lý do). Đây là assert có RĂNG, không phải né.
Codegen
src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanCodeGenerator.cs
- ctor
(IApplicationDbContext db, IDateTime dateTime)—:12 GenerateAsync(CancellationToken ct = default)— KHÔNG nhận entity (khác PE nhậnPurchaseEvaluation)- prefix
$"KHKK/{year}", return$"{prefix}/{seq.LastSeq:D3}"—:20,:45 BeginTransactionAsync(IsolationLevel.Serializable, ct)—:24-25- sequence sống ở
db.WorkflowAppCodeSequences(bảng DÙNG CHUNG Office), PKPrefix
Tiền lệ SQLite + Serializable — ĐÃ CÓ, không cần chế mới
tests/SolutionErp.Infrastructure.Tests/Common/SqliteDbFixture.cs:36-42 ghi rõ:
"SQLite hỗ trợ transactions thật (BEGIN/COMMIT/ROLLBACK), tuy nhiên IsolationLevel enum value bị provider mapping gracefully (no exception, just default behavior)"
Và PurchaseEvaluationCodeGeneratorTests.cs:17 gọi thẳng new PurchaseEvaluationCodeGenerator(fix.Db, dt) — generator PE cũng Serializable, test cũ chạy XANH nhiều session ⇒ dùng y khuôn, KHÔNG đụng production.
FixedDateTime stub: SqliteDbFixture.cs:71.
§1 — Test đã viết
File MỚI duy nhất: tests/SolutionErp.Infrastructure.Tests/Common/ContractSigningPlanSchemaTests.cs
Đặt ở Common/ cạnh ContractV2SchemaPersistenceTests.cs (sibling gần nhất về NGHĨA: schema-invariant
của họ *LevelOpinion + workflow V2), namespace SolutionErp.Infrastructure.Tests.Common.
4 test (sàn spec là N ≥ 2):
| # | Test | Nội dung | Vế chống vacuous |
|---|---|---|---|
| 1 | BayBang_MoiBang_CoAuditFields_VaQueryFilter (PIN spec) |
Quét EF model theo namespace SolutionErp.Domain.ContractSigningPlans → phải ra ĐÚNG 7 entity; mỗi bảng: tên bảng vật lý + 4 cột audit + bộ-3 soft-delete + có/không query filter |
(a) BeEquivalentTo bộ 7 tên ⇒ thêm bảng #8 mà quên khai = ĐỎ; (b) soft-delete assert 2 chiều (BaseEntity phải KHÔNG có IsDeleted); (c) đuôi test seed 1 phiếu IsDeleted=true → filter phải giấu (0) mà IgnoreQueryFilters vẫn thấy (1) ⇒ chứng filter là BIỂU THỨC THẬT, không chỉ "có khai" |
| 2 | SigningPlanCodeGenerator_Format_KhkkYearSeqD3 (PIN spec) |
2 lần GenerateAsync() → regex ^KHKK/\d{4}/\d{3}$ + giá trị tuyệt đối KHKK/2026/001 → /002 + Seq(code2)==Seq(code1)+1 + row WorkflowAppCodeSequences.Prefix=="KHKK/2026" có LastSeq==2 |
đẩy đồng hồ sang 2027 → đòi KHKK/2027/001 (reset) + hàng 2026 giữ LastSeq==2. Không có vế này thì \d{4} vẫn xanh dù năm bị hardcode |
| 3 | Line_UniqueFilteredTheoIsDeleted_ChoXoaMemRoiThemLaiCungNcc |
gotcha #57: xoá mềm 1 dòng NCC rồi thêm LẠI đúng NCC đó ⇒ KHÔNG được nổ UNIQUE; active==1, IgnoreQueryFilters==2 |
🔴 vế đối chứng bắt buộc: 2 dòng CÒN SỐNG cùng (PlanId, SupplierId) ⇒ PHẢI ném DbUpdateException. Không có vế này thì "không nổ" cũng đúng trong thế-giới index chưa từng được tạo trên SQLite |
| 4 | ApplicableType_ContractSigningPlan_PinSlot10_KhongTrungSlot |
(int)ApprovalWorkflowApplicableType.ContractSigningPlan == 10 + không member nào khác chiếm slot 10 |
enum này HasConversion<int> (ApprovalWorkflowConfiguration.cs:17) ⇒ số ĐÃ ghi xuống DB; đánh số lại = quy trình đã pin trỏ sai module, hỏng IM LẶNG |
Tiền lệ tái dùng, KHÔNG chế hạ tầng mới: SqliteDbFixture + FixedDateTime (Common/SqliteDbFixture.cs:43,71).
KHÔNG cần IdentityFixture (không seed User/Level ⇒ tránh FK Restrict ApprovalWorkflowLevel→User).
§2 — Số đo
Baseline ĐO THẬT ngay trước khi thêm test (không lấy số nghe kể)
Passed! - Failed: 0, Passed: 45, Total: 45 — SolutionErp.Domain.Tests.dll
Passed! - Failed: 0, Passed: 517, Total: 517 — SolutionErp.Infrastructure.Tests.dll (1 m 17 s)
⇒ 562 = 45 + 517 — KHỚP con số spec ghi (562).
Filtered run 4 test mới
Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4, Duration: 1 s
--filter "FullyQualifiedName~ContractSigningPlanSchemaTests"
🔬 Fault-injection chứng test CÓ RĂNG (bơm lỗi vào BẢNG KỲ VỌNG của chính test — file tests/, KHÔNG đụng production)
Áp luật "fault-injection proves teeth": một detector luôn-trả-True cũng cho 4/4 xanh, nên phải ép ĐỎ cả 2 chiều.
- Chiều A — đổi kỳ vọng
ContractSigningPlantừQueryFilter=true→false:⇒ chứng bộ đo đọc ra True ở bảng CÓ filter.Expected hasFilter to be False because header AuditableEntity — HasQueryFilter(!IsDeleted) `ContractSigningPlanConfiguration.cs:47`, but found True. Failed! - Failed: 1, Passed: 0 (ContractSigningPlanSchemaTests.cs:line 108) - Chiều B — đổi kỳ vọng
ContractSigningPlanLevelOpiniontừfalse→true:⇒ chứng bộ đo đọc ra False ở bảng KHÔNG filter.Expected hasFilter to be True because AuditableEntity NHƯNG CỐ Ý KHÔNG filter — …, but found False. Failed! - Failed: 1, Passed: 0
Hai chiều ⇒ ((IReadOnlyEntityType)et).GetDeclaredQueryFilters().Any() phân biệt được hai thế giới, không phải hằng số. Cả 2 lần bơm đã revert về đúng trạng thái đĩa ngay sau khi đo.
Full suite sau khi thêm
[LEAD điền on-behalf @S161 — vai chết #53 lúc đợi job nền; số do lead chạy TƯƠI]
Passed! - Failed: 0, Passed: 45, Total: 45 — SolutionErp.Domain.Tests.dll
Passed! - Failed: 0, Passed: 521, Total: 521 — SolutionErp.Infrastructure.Tests.dll (50s)
⇒ 566 = 562 + 4 (sàn spec ≥ 564 VƯỢT). 0 fail.
§3 — Ghi chú kỹ thuật đáng nhớ
- EF Core 10 đổi API query-filter. EF 10 có filter CÓ TÊN ⇒ ngoài
GetQueryFilter()(cũ, ném khi có nhiều filter tên) cònGetDeclaredQueryFilters(). Probe XML doc gói thật đang restore (~/.nuget/packages/microsoft.entityframeworkcore/10.0.6/lib/net10.0/*.xml) thấy CẢ HAI còn sống; chọnGetDeclaredQueryFilters()+ ép kiểu(IReadOnlyEntityType)để khỏi dính obsolete/ambiguity. Repo KHÔNG bậtTreatWarningsAsErrors(grep 0 hit) nên đây là chọn cho sạch, không phải bắt buộc. - Serializable trên SQLite — KHÔNG phải vấn đề, đã có tiền lệ ghi sẵn ở
SqliteDbFixture.cs:36-42("IsolationLevel enum bị provider map gracefully") vàPurchaseEvaluationCodeGeneratorTests.cs:17. ⇒ gọi thẳngnew ContractSigningPlanCodeGenerator(fix.Db, clock), KHÔNG sửa production. HasFilter("[IsDeleted] = 0")viết kiểu SQL Server VẪN chạy trên SQLite — SQLite chấp nhận định danh trong[...]và có partial index. Test 3 xanh cả 2 vế ⇒ index filtered được tạo THẬT.- KHÔNG dùng
Remove()để đo xoá mềm:TestApplicationDbContextkhông wireAuditingInterceptor⇒Remove()= xoá CỨNG (bài học S155). Test set cờIsDeletedbằng tay — đúng thứ cần đo.
§4 — Verdict
[LEAD điền on-behalf] PASS — 4/4 test mới xanh trong full-suite 566/0 (lead chạy tươi); fault-inject 2 chiều đã chứng răng (§2); baseline khớp đĩa. Vai chết #53 SAU khi mọi việc thật xong — sub-md ghi-từng-mục cứu trọn lần 2 trong wave này.