Files
solution-erp/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-test-specialist-0.md
2026-07-29 19:44:56 +07:00

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ền tests/**). 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àn N ≥ 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: ContractSigningPlanLevelOpinionAuditableEntity 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ận PurchaseEvaluation)
  • 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), PK Prefix

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)"

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"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 ContractSigningPlan từ QueryFilter=truefalse:
    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)
    
    ⇒ chứng bộ đo đọc ra True ở bảng CÓ filter.
  • Chiều B — đổi kỳ vọng ContractSigningPlanLevelOpinion từ falsetrue:
    Expected hasFilter to be True because AuditableEntity NHƯNG CỐ Ý KHÔNG filter — …, but found False.
    Failed! - Failed: 1, Passed: 0
    
    ⇒ chứng bộ đo đọc ra False ở bảng KHÔNG filter.

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ớ

  1. 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òn GetDeclaredQueryFilters(). 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ọn GetDeclaredQueryFilters() + ép kiểu (IReadOnlyEntityType) để khỏi dính obsolete/ambiguity. Repo KHÔNG bật TreatWarningsAsErrors (grep 0 hit) nên đây là chọn cho sạch, không phải bắt buộc.
  2. 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ẳng new ContractSigningPlanCodeGenerator(fix.Db, clock), KHÔNG sửa production.
  3. 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.
  4. KHÔNG dùng Remove() để đo xoá mềm: TestApplicationDbContext không wire AuditingInterceptorRemove() = xoá CỨNG (bài học S155). Test set cờ IsDeleted bằ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.