Skip to content

Nhịp thư: Tests ​

PRD · SDD · Kế hoạch

npm run test:orchestrator (api.conan.school/tests/email-orchestrator.test.mts, bước "Gác Nhịp thư" trong job api của CI) kiểm phần thuần: chấm điểm, cổng, trần, link có chữ ký, mẫu thư, người gửi. 570 phép kiểm (14.10.2026, sau khi thêm journey_day; lúc viết đầu là 529 trong 12 nhóm), chia 13 nhóm, mỗi nhóm khoá một bất biến của SDD hoặc một lỗi mà rà soát thiết kế 14.10.2026 tìm ra trước khi nó kịp xảy ra. Phần chạm D1 và Cloudflare được giữ bằng ràng buộc CSDL (khoá chính, UPDATE có điều kiện) và kiểm tay; bảng dưới nói rõ dòng nào là gác thật, dòng nào chỉ là đọc mã.

Nhóm trong file testKiểm gì
1. Người gửiMọi địa chỉ của ALL_SENDER_ADDRESSES có trong allowed_sender_addresses của wrangler.toml và ngược lại (48 địa chỉ, hai miền); nha + slug ra alias cộng đồng, không slug rơi về hello; miền tin.conan.school; người gửi của từng loại nằm trong danh sách khoá
2. Nhóm gắn kết và bản saoTừng loại của Nhịp thư là thư gắn kết, không sinh bản sao, có mẫu trong EMAIL_CATALOG; bảng kế hoạch không là thư gắn kết và không bị CC; login_code vẫn không bao giờ có bản sao; đúng hai hộp thư duyệt
3. Cài đặtMặc định an toàn (preview, ramp_pct = 0, live_classes = quan_he, daily_quota = 0, legacy_cold = skip); giá trị lạ bị từ chối (chế độ, trần âm, loại lạ, nhóm lạ, miền lạ); dòng hỏng rơi về mặc định; cap_max ≥ cap_start
4. Thời gianGiờ VN, ngày qua tháng, tuần ISO, giờ tròn kế tiếp
5. TầngTài khoản mới, nóng, 3 thư liền không đọc tụt một tầng, 6 thư + im > 30 ngày thì dừng, dòng kế hoạch chưa gửi không tính vào chuỗi im lặng, người hệ cũ im 6 tháng là ngủ
6. Xác suấtBấm làm p tăng, nhận mà không đọc làm p giảm, người hệ cũ từng mở có p cao hơn, bộ sinh có hạt giống lặp lại được, Beta(2,8) trung bình ≈ 0,2, tiên nghiệm loại chưa có dữ liệu = trung bình toàn hệ, tiên nghiệm co về trung bình
7. Cổng và chọnĐã huỷ, nhịp none, việc của chính họ thắng câu chuyện chung, ứng viên thua được ghi, Thompson cùng dữ liệu cùng kết quả, dưới ngưỡng thì im; người hệ cũ chỉ reconnect, bấm một link không phải đồng ý, bấm "Có" thì qua; nhóm tắt, nhịp important, ưu đãi cần đồng ý, người chưa đủ 16 tuổi, tầng ngủ, loại bị tắt
8. Hai ngày liềnDòng kế hoạch hôm qua chưa gửi vẫn chặn cùng câu chuyện, vẫn tính cooldown, vẫn tính nhịp 1 thư/tuần (lỗi chặn của rà soát)
9. TrầnNgười đang tương tác xếp trước người ngủ đông; reconnect ≤ 25% trần; phần còn lại over_cap; công thức trần (preview, ngày đầu, ramp 0, ramp 40%, chạm hạn mức); ba điều kiện "không khoẻ"; ngưỡng tầng tăng dần
10. Link có chữ kýChữ ký 32 ký tự, sai mục đích hay sai người không qua, không có secret thì không ký; đích bấm chỉ https://*.conan.school (từ chối conan.school.evil.com, evilconan.school, http, javascript:); viết lại link nội dung nhưng giữ /api/ và mailto:; chỗ giữ trong thư mẫu không mang chữ ký người thật
11. Mẫu thư mớiCả 14 loại mới + bảng kế hoạch, bản có tên và không tên: không lộ undefined/null/NaN, tiêu đề ≤ 80 ký tự, không dấu gạch dài, đúng một nút chính, có dòng xem trước, mọi link tokyo/www khớp một route thật (đọc từ com.conan.school/app/main.tsx và www.conan.school/app/routes.ts), thư mẫu không chạm người nhận thật, phần chữ thuần không còn chỗ giữ
12. Dấu vân tayỔn định với cùng mẫu; đổi khi dữ liệu đổi; đổi khi người gửi đổi
13. Chuỗi 30 thư làm quen (journey_day)Chuỗi đủ 30 thư (JOURNEY_DAYS.length === JOURNEY_LENGTH). Thư kế tiếp (nextJourneyDay): chưa nhận gì là thư 1, đã nhận 7 (kể cả dòng kế hoạch chưa gửi) là thư 8, đủ 30 là hết chuỗi, sổ có {1,2,3,4,6} thì là thư 5 (thư bị bỏ được gửi lại trước), email_sent_refs có 1-10 thì là thư 11. journeyOpenedRecently: dưới ba thư thì chưa kết luận, dòng kế hoạch chưa gửi không tính. KIND_META: nhóm Bản tin, tối đa 30 thư, nhóm tuỳ chọn riêng lam_quen. Rồi quét cả 30 thư × 16 biến thể dữ liệu (mẫu, không tên, không khoá, khoá mở, khoá mở im 90 ngày, khoá đóng im 70 ngày, tên khoá dài, có Pro, hồ sơ chưa xong, lịch 0 buổi, lịch 2 buổi, ba thư không tương tác, ở Conan 240 ngày, Retail, Hospitality, Sales), mỗi lần dựng kiểm: không lộ giá trị rỗng, không gạch dài, tiêu đề ≤ 70 ký tự và không cụm chào bán, đúng một nút, có dòng xem trước, mọi link khớp route tokyo thật, không dẫn người chưa xong hồ sơ vào /courses/ hay /books/, không dẫn tới tab Sự kiện khi lịch 0 buổi, không hứa "Học tiếp" hay "đi tiếp một bài" với khoá chưa mở trọn, không nói "cộng đồng thứ hai", chữ thuần không còn chỗ giữ

Quét 30 × 16 dựng 480 lá thư nhưng chỉ cộng một phép kiểm vào tổng khi xanh (jcheck chỉ ghi khi hỏng, mỗi lỗi một dòng có số thư và tên biến thể). Nên 570 không nói gì về độ phủ của quét: đọc dòng "cả 30 thư × 16 biến thể dựng sạch". Trước quét này, CI chỉ dựng thư mẫu (thư 1) của chuỗi, và 29 thư còn lại không có gác nào.

Theo story ​

MãCách kiểmTrạng thái
COMM-ORCH-01PK (plan_date, email_key) (CSDL cưỡng chế); already_mailed_today đọc mã dispatch.ts; kiểm tay SELECT COUNT(*), COUNT(DISTINCT email_key) FROM email_plan_items WHERE plan_date=? và SELECT COUNT(*), COUNT(DISTINCT lower(recipient)) FROM email_deliveries WHERE plan_date=? AND audit_of IS NULLCó gác (CSDL); cổng "đã nhận thư hôm nay" chưa có test
COMM-ORCH-02Nhóm 6 + 7: EV cao nhất thắng, dưới ngưỡng thì im, ứng viên thua được ghi, Thompson tất địnhCó gác
COMM-ORCH-03Nhóm 5: tụt tầng mỗi 3 thư, dung sau 6 thư + 30 ngày, dòng ảo không tínhCó gác
COMM-ORCH-04Nhóm 7 + 8: nhóm tắt, important, ưu đãi cần đồng ý, nhịp tuần tính cả dòng ảo; đọc lại lúc gửi và ở cổng gửi (prefsBlock) đọc mãCó gác phần chấm; cổng gửi chưa có test
COMM-ORCH-05Cổng ở dispatch.ts (preview_ok, 24 giờ, fingerprint, markStale) đọc mã; nhóm 10 + 11: thư mẫu không chạm người thật; nhóm 12: fingerprintMột phần (cổng đọc mã; fingerprint và chỗ giữ có test)
COMM-ORCH-06Nhóm 10: chữ ký sai mục đích bị từ chối; holdFormatProblem, holdExpired là hàm thuần chưa có test; GET/POST kiểm tayCó gác chữ ký; GET/POST kiểm tay
COMM-ORCH-07Nhóm 1: mọi loại có người gửi trong danh sách khoá, SENDERS khớp allowed_sender_addresses từng địa chỉCó gác; giữ loại khi E_SENDER_* đọc mã
COMM-ORCH-08Nhóm 7: chỉ reconnect, bấm link không phải đồng ý, "Có" thì qua; reconnectAttempt (ấm 2 lần, lạnh skip/once) chưa có testCó gác phần chính sách
COMM-ORCH-09Nhóm 9: công thức trần và "khoẻ"; tính lại trần lúc gửi (selectDue) và tripToPreview đọc mãCó gác công thức
COMM-ORCH-10Nhóm 10: đích ngoài *.conan.school bị từ chối, /api/ không bị viết lại; isBotClick là hàm thuần chưa có testCó gác đích + viết lại
COMM-ORCH-11Đọc mã tick.ts, stop-all; kiểm tay ở adminChưa có gác
COMM-ORCH-12Đọc mã daily.ts (orchestratorOwnsDaily)Chưa có gác
COMM-ORCH-13Nhóm 8 (lịch sử ảo); email_sent_refs PK (CSDL)Có gác (test + CSDL)
COMM-ORCH-14Nhóm 7: người chưa đủ 16 tuổi không nhận bản tin, vẫn nhận nhắc họcCó gác phần cổng; truy vấn minor trong audience.ts đọc mã
COMM-ORCH-15Trần mềm, phần hạn mức, nhịp 30 thư/phút, thử lại một lần ở send.ts: đọc mãChưa có gác
COMM-ORCH-16Đọc mã email-orch.ts; kiểm tay: GET link huỷ không ghi, curl -X POST -d 'List-Unsubscribe=One-Click' '<URL trong header>' trả 200Chưa có gác (RFC 8058 chưa được ghim bằng test)
COMM-ORCH-17Nhóm 3: mặc định live_classes; validateSettingsPatch (live cần hạn mức) là hàm thuần export sẵn nhưng chưa có testMột phần
COMM-ORCH-18Nhóm 11: mọi link khớp một route thậtMột phần: route /events/:slug của tokyo CÓ THẬT (trang chuỗi), nên gác route không bắt được link sự kiện sai; số unit theo vị trí được stillValid so lại lúc gửi, chưa có test
COMM-ORCH-19Nhóm 13: thứ tự thư, thư bị bỏ được chọn lại, đủ 30 thì hết, nhóm lam_quen riêng, Bản tin; quét 30 × 16. Lọc "chỉ tài khoản có cộng đồng nhà" (homeCommunity, generators/journey.ts) và stillValid (thư N cần journey:<N-1> ở email_sent_refs) đọc mãCó gác phần thuần; lọc người nhận và stillValid chạm D1, chưa có test

Gác dùng chung áp lên code mới: npm run test:email (cả 34 loại của EMAIL_CATALOG: không undefined/null/NaN, primary chỉ ở nút, link tokyo khớp router, có bản không tên), typecheck api (cổng tuyệt đối 0 lỗi), check:emdash, check:colors, qc:scan --check.

Khoảng trống ​

  • Đồng thời thật (hai instance gửi cùng giờ, một bước workflow chạy lại) chỉ được giữ bằng UPDATE … WHERE status='planned' và PK email_sent_refs; không có test chạy D1 thật.
  • Các hàm thuần đã export mà chưa có test: validateSettingsPatch, isBotClick, holdFormatProblem, reconnectAttempt, winBackItems, proPriceStep, orderReminderNumber. Thêm test cho chúng là việc rẻ nhất để nâng các dòng "đọc mã" ở trên thành gác.
  • Hợp đồng RFC 8058 của POST /unsubscribe (u, t ở query, không cookie, 200 không chuyển hướng) chưa được ghim; một lần "dọn" handler đọc thân form là Gmail huỷ một chạm hỏng im lặng.
  • Bố cục 375px của các mẫu mới kiểm tay như các mẫu cũ.
  • Dấu nội dung cả chuỗi của journey_day (<!-- journey-content … -->) chưa có test riêng: không phép kiểm nào sửa một thư khác thư mẫu rồi đòi dấu vân tay đổi. Quét 30 × 16 kiểm CHỮ của mọi thư, không kiểm cổng xem trước của chúng.
  • Các truy vấn D1 của chuỗi (loadCourses, loadCompleteProfiles, loadUpcomingEvents, loadSentRefs) chỉ được diễn tập trên production bằng SELECT, không có test.

Tài liệu nội bộ nền tảng Conan School.