การทดสอบปลั๊กอิน
สิ่งที่ปลั๊กอินต้องพิสูจน์ก่อนจะได้รับอนุญาตให้เข้าใกล้เนื้อหาจริง วิธีพิสูจน์กับ core ปลอม (fake core) ใน test suite ของคุณเอง วิธีรันทั้งสองเซอร์วิสบนเครื่องเดียว และวิธีทำให้เกิดความล้มเหลวแต่ละแบบโดยตั้งใจ
ดู Protocol v1 สำหรับพื้นผิวที่อยู่ภายใต้การทดสอบ และ ความปลอดภัยของปลั๊กอิน สำหรับ invariant ที่กรณีทดสอบด้านล่างมีไว้ปกป้อง
สัญญา (contract)
หัวข้อที่มีชื่อว่า “สัญญา (contract)”ต่อไปนี้คือกรณีที่ปลั๊กอินต้องมี แต่ละกรณีเป็นคุณสมบัติของโปรโตคอล ไม่ใช่ของปลั๊กอินตัวใดตัวหนึ่ง ดังนั้นแต่ละกรณีจึงควรอยู่ใน suite ของคุณ ไม่ว่าปลั๊กอินของคุณจะทำอะไรก็ตาม
| กรณี | สิ่งที่ต้องเกิดขึ้น |
|---|---|
| Activation | โค้ดแบบใช้ครั้งเดียวถูกใช้ได้ครั้งเดียวเท่านั้น โค้ดที่ไม่รู้จัก ใช้ไปแล้ว หรือหมดอายุจะถูกปฏิเสธ token ดิบถูกเก็บแบบเข้ารหัสและไม่เคยถูกบันทึกลง log |
| Rotation | kind: rotate แทนที่ token ที่เก็บไว้ token เก่าเลิกถูกใช้งาน rotation ที่ถูกปฏิเสธจะคง token เก่าไว้ตามเดิม |
| Launch | โค้ดถูกแลกเปลี่ยนได้ครั้งเดียว ผ่านแบ็กเอนด์ของปลั๊กอิน การแลกเปลี่ยนครั้งที่สองล้มเหลว โค้ดที่ออกให้ installation อื่นล้มเหลว |
| Session | browser session ของปลั๊กอินเองถูกสร้างตอนแลกเปลี่ยน เป็น cookie ของมันเอง และไม่มี credential ของ Dee Wan อยู่ในนั้น |
| Pinning | การเผยแพร่ pin current_version_id การยกเลิกการเผยแพร่ pin published_version_id ทั้งสองอย่าง pin workflow.revision และ (kind, to) ที่เลือกต้องมาจากการอ่านผ่าน delegation |
| Idempotency | key ได้มาจาก record และ revision ของมัน เหมือนเดิมทุกครั้งที่ retry และเปลี่ยนไปเมื่อ record ถูกตั้งเวลาใหม่ |
| At most once | worker สองตัวไม่สามารถ execute record เดียวกันได้: ใช้ lease ที่มี compare-and-set บวกกับ key เป็นตัวป้องกันชั้นที่สองของ core |
| Replay | idempotent_replay ถูกสรุปผลเป็นความสำเร็จ ไม่ใช่ผลกระทบใหม่ |
| การปฏิเสธแบบ terminal | invalid_request, plugin_unauthorized, capability_refused, requester_unauthorized, content_not_found, scheduled_target_stale, workflow_conflict, dependent_content, idempotency_key_reused, transition_unavailable และ transition_refused แต่ละตัวถูกสรุปผลโดยไม่มีการ retry ภายใต้ชื่อของตัวเอง |
| Retries | rate_limited เคารพ Retry-After ภายในค่าสูงสุด temporary_failure และความล้มเหลวระดับ transport ถอยเวลา (back off) แบบมี jitter ทั้งคู่หยุดเมื่อถึงเพดานจำนวนครั้ง และ (AND) เพดานอายุ |
| Response ที่ไม่น่าเชื่อถือ | status ที่ไม่ตรงกับ outcome.code, body ที่ไม่ใช่ envelope, 3xx หรือ status ที่ไม่รู้จัก เป็นการปฏิเสธแบบ terminal ไม่มีวันเป็นความสำเร็จ และไม่มีวันเป็นการ retry ไม่รู้จบ |
| Isolation | ทุกตารางและทุก query ถูกจำกัดขอบเขตตาม installation installation หนึ่งไม่สามารถอ่าน ยกเลิก หรือ execute record ของอีก installation ได้ |
| Revocation | หลัง uninstall งานที่ค้างอยู่จะถูกปฏิเสธด้วย plugin_unauthorized และปลั๊กอินรายงานเช่นนั้นแทนที่จะ retry |
| Secrets | ไม่มี test fixture, บรรทัด log, error body หรือภาพหน้าจอใดที่มี token, launch code หรือ activation code |
suite ของปลั๊กอินอ้างอิง (reference plugin) เป็นตัวอย่างที่ทำจริงของทุกแถว: plugins/scheduling/test/
(activation, launch, schedules, runner, time, isolation, flow)
พิสูจน์กับ core ปลอม
หัวข้อที่มีชื่อว่า “พิสูจน์กับ core ปลอม”อย่าชี้ suite ของคุณไปที่ Dee Wan ที่กำลังรันอยู่ core ปลอมให้ผลแน่นอน (deterministic) สามารถแทรกความล้มเหลวที่ core จริง ไม่สร้างให้ตามต้องการ และรันเสร็จในระดับมิลลิวินาที
plugins/scheduling/test/helpers/fake-core.ts คือตัวหนึ่ง และเป็นจุดเริ่มต้นที่ตั้งใจไว้สำหรับ
ปลั๊กอินใน repository อื่น มันเป็น implementation ของ fetch ที่:
- ยืนยันตัวตน bearer token ต่อ installation และตอบ
plugin_unauthorizedในกรณีอื่น - ตอบ
/context,/content/:idและ/launch/exchangeด้วย body ที่มีรูปแบบตามโปรโตคอล รวมถึง บล็อกworkflowและ transition ที่ delegation อนุญาต - ตรวจ pin และ workflow revision ของคำสั่ง และตอบ
scheduled_target_staleหรือworkflow_conflictเมื่อเงื่อนไขเหล่านั้นไม่เป็นจริงอีกต่อไป - เก็บ idempotency record ต่อ key ดังนั้น replay ที่ตรงกันทุกประการจะตอบ
idempotent_replayและ key ที่ถูกใช้ซ้ำ กับ input ที่ต่างออกไปจะตอบidempotency_key_reused - ใช้ launch code ได้ครั้งเดียว และปฏิเสธเมื่อใช้กับ installation อื่น
- แทรกความล้มเหลวตามต้องการ — network error, status ใดก็ได้,
outcome.codeใดก็ได้, headerRetry-Afterหรือ body ที่จงใจทำให้ผิดรูปแบบ - บันทึกทุกคำสั่งที่ได้รับ เพื่อให้ test ยืนยันได้ว่าไม่มีอะไรถูกส่งซ้ำสองครั้ง
สองสิ่งที่มันไม่ใช่ (NOT): มันไม่ใช่ core และสตริง message ของมันเป็นเพียงตัวยึดตำแหน่ง ให้ assert กับ
outcome.code และ HTTP status เท่านั้น อย่า assert กับข้อความสำหรับมนุษย์ — ข้อความของ core มีไว้สำหรับคนอ่าน และ
ไม่คงที่
ควบคู่กันนั้น plugins/scheduling/test/helpers/d1.ts คือ shim ขนาด 50 บรรทัดบน better-sqlite3 ที่มีพื้นผิว
prepare/bind/first/all/run/batch แบบเดียวกับ D1 เพื่อให้ SQL จริงของปลั๊กอิน — รวมถึง
การ claim แบบ compare-and-set และ partial unique index — รันใน suite ได้
cd plugins/schedulingnpm test # vitestnpm run typecheckตรวจสอบกับ schema ที่เผยแพร่ไว้
หัวข้อที่มีชื่อว่า “ตรวจสอบกับ schema ที่เผยแพร่ไว้”เอกสาร JSON Schema สองไฟล์ถูกสร้างจาก definition ที่ core ใช้ validate และถูก commit ไว้ใน repository:
schemas/manifest-v1.schema.json— validate manifest ของคุณใน CI ของคุณเอง ก่อนที่ผู้ดูแลจะดึงมันไปใช้schemas/protocol-v1.schema.json— รูปแบบ request และ response ภายใต้$defsบวกกับตารางoutcomesที่เครื่องอ่านได้ (HTTP status และ retry flag ต่อ code),capabilities,installation_statesและlimits
ตาราง outcomes คือส่วนที่คุ้มค่าจะเชื่อมเข้าไป: ปลั๊กอินที่เก็บสำเนาของตัวเอง — ปลั๊กอินอ้างอิงทำเช่นนั้น
ใน plugins/scheduling/src/protocol.ts เพราะมัน import จาก core ไม่ได้ — ควร
assert ว่าสำเนาของตนเท่ากับฉบับที่เผยแพร่ backend/test/plugin-contract.test.ts ทำสิ่งนั้นพอดี
สำหรับปลั๊กอินอ้างอิง และจะล้มเหลวหากทั้งสองไม่ตรงกันในเรื่อง status หรือ retry flag
สร้างเอกสารทั้งสองใหม่ รวมถึง protocol-v1.md ด้วย:
npm run docs:pluginsbackend/test/plugin-docs.test.ts จะล้มเหลวหากสำเนาที่ commit ไว้คลาดเคลื่อน ดังนั้นคำสั่งนี้จำเป็น
เฉพาะหลังจากเปลี่ยนโปรโตคอลเท่านั้น
ชุดติดตั้งแบบสองเซอร์วิสบนเครื่อง local
หัวข้อที่มีชื่อว่า “ชุดติดตั้งแบบสองเซอร์วิสบนเครื่อง local”ทั้งสองเซอร์วิสบนเครื่องเดียว โดยไม่ต้องมีบัญชี Cloudflare และไม่ต้องมี credential จริง
Dee Wan จาก root ของ repository:
node scripts/install-dependencies.mjsnpm run local:migratenpm run local:seed # prints the sign-in it createsnpm run local:api # admin Worker on http://localhost:8787 (leave running)npm run local:admin # admin UI on http://localhost:5173 (leave running)admin Worker ต้องการการตั้งค่าสามอย่างก่อนจะยอมคุยกับปลั๊กอินบน http:// เพราะ URL ของปลั๊กอินทุกตัว
เป็น HTTPS เท่านั้นใน production ใน backend/.dev.vars:
ENVIRONMENT=developmentPLUGIN_DEV_ORIGINS=http://localhost:8788BACKEND_URL=http://localhost:8787PLUGIN_DEV_ORIGINS คือ allowlist แบบ origin ตรงตัว — ไม่มี wildcard ไม่มีทางลัดสำหรับช่วง private — และ
จะถูกอ่านเฉพาะเมื่อ ENVIRONMENT=development เท่านั้น BACKEND_URL คือค่าที่ core ส่งเป็น
dee_wan_base_url เพื่อให้ปลั๊กอินของคุณรู้ว่าจะเรียกกลับไปที่ใด
ปลั๊กอินของคุณ ให้บริการบน origin ที่คุณเพิ่งอนุญาต เช่น http://localhost:8788 และยอมรับ
base URL นั้นเป็นการตอบแทน: ปลั๊กอินอ้างอิงมี ENVIRONMENT=development และ
DEE_WAN_DEV_ORIGINS=http://localhost:8787 ของตัวเองด้วยเหตุผลเดียวกันนี้ สำหรับ Worker:
cd plugins/schedulingnpx wrangler d1 migrations apply dee-wan-scheduling --localnpx wrangler dev --port 8788 --test-scheduledจากนั้น ใน admin UI: Settings → Plugins → install โดยใช้
http://localhost:8788/dee-wan/manifest.json และโค้ดแบบใช้ครั้งเดียวที่ปลั๊กอินของคุณออกให้ มอบ
ชุด capability และเลือกโมเดล เปิดรายการเนื้อหาหนึ่งรายการแล้วใช้ action ที่ manifest ของคุณประกาศไว้
ทำการติดตั้งผ่าน UI แทนการทำด้วยมือ: route ด้านการดูแลระบบต้องการ session จริง
header ของไซต์ และ site:plugins และ UI มีครบทั้งสามอย่างอยู่แล้ว
ข้อควรระวังสองข้อบนเครื่อง local ซึ่งเกิดขึ้นจริงทั้งคู่:
- session cookie แบบ
__Host-/Secureต้องการ HTTPS หรือlocalhostตรงตัว — ไม่ใช่127.0.0.1 - cron trigger จะไม่ทำงานใน
wrangler devเว้นแต่คุณจะสั่ง เมื่อใช้--test-scheduledคำสั่งcurl "http://localhost:8788/__scheduled?cron=*+*+*+*+*"จะรันหนึ่ง tick
Fixture สำหรับความล้มเหลว
หัวข้อที่มีชื่อว่า “Fixture สำหรับความล้มเหลว”วิธีทำให้เกิดแต่ละ outcome โดยตั้งใจ คอลัมน์ซ้ายคือสิ่งที่ test ของคุณทำกับ core ปลอม คอลัมน์ขวาคือวิธีที่สถานะเดียวกันเกิดขึ้นกับ core จริง เมื่อคุณต้องการยืนยันแบบ end to end
| Outcome | กับ core ปลอม | กับ Dee Wan บนเครื่อง local |
|---|---|---|
invalid_request |
ส่ง body key ที่ไม่รู้จัก หรือไม่ใส่ Idempotency-Key |
เหมือนกัน |
plugin_unauthorized |
ใช้ token ที่ไม่รู้จัก | ปิดใช้งานหรือ uninstall installation แล้วปล่อยให้ record ถึงกำหนดเวลา |
capability_refused |
ลบ capability ออกจาก installation | จำกัด grant ให้แคบลงใน Settings → Plugins |
requester_unauthorized |
ทำเครื่องหมายว่าบุคคลของ delegation ไม่ได้รับอนุญาตอีกต่อไป | ลบบทบาท (role) ของบุคคลนั้น หรือปิดใช้งานบัญชี หลังจากสร้าง record แล้ว |
content_not_found |
ขอ instance ที่ไม่รู้จัก | ใช้ id จากไซต์อื่น หรือลบโมเดลออกจาก grant |
scheduled_target_stale |
เปลี่ยน current_version_id ของเนื้อหา |
แก้ไขและบันทึก draft หลังจากสร้าง record แล้ว |
workflow_conflict |
เพิ่มค่า revision ของเนื้อหา |
เผยแพร่หรือยกเลิกการเผยแพร่รายการด้วยมือหลังจากสร้าง record แล้ว |
dependent_content |
ตอบ dependent_content ด้วยการแทรก |
เชื่อมรายการที่เผยแพร่แล้วอีกรายการเข้ากับมัน แล้วลองยกเลิกการเผยแพร่ |
idempotency_key_reused |
ใช้ key ซ้ำกับ body ที่ต่างออกไป | เหมือนกัน |
transition_unavailable |
ขอ to ที่สถานะนั้นไม่มีให้ |
ขอยกเลิกการเผยแพร่สิ่งที่ยังไม่ได้เผยแพร่ |
rate_limited |
แทรก 429 พร้อม Retry-After ทั้งแบบวินาทีและแบบ HTTP date |
120 request ในหนึ่งนาทีสำหรับ installation เดียว |
temporary_failure |
แทรก 500, 502, 503 และ 504 | ทำให้เกิดได้ยาก ให้เชื่อกรณีที่แทรก |
| ความล้มเหลวระดับ transport | แทรก network error และ timeout | หยุด Dee Wan Worker ระหว่างทำงาน |
| response ที่ไม่น่าเชื่อถือ | แทรก 200 ที่ outcome.code เป็น code ของ 409, body ที่ไม่ใช่ JSON และ 3xx |
สร้างขึ้นไม่ได้ นั่นคือเหตุผลที่ต้องแทรก |
| การกู้คืน lease | ทิ้ง record ไว้ในสถานะ running พร้อม lease ที่หมดอายุ |
kill ปลั๊กอินระหว่างคำสั่ง แล้วปล่อยให้ tick ถัดไป reclaim มัน |
assert สองอย่างในทุกการปฏิเสธ: record ถูกสรุปผลภายใต้ชื่อที่ถูกต้อง และ ไม่มีอะไรถูกส่ง ซ้ำสองครั้ง — รายการคำสั่งที่บันทึกไว้คือสิ่งที่พิสูจน์ข้อหลัง
สิ่งที่ suite ของ core เองครอบคลุม
หัวข้อที่มีชื่อว่า “สิ่งที่ suite ของ core เองครอบคลุม”รันจาก backend/:
npx vitest run test/plugin-manifest.test.ts test/plugin-installation.test.ts \ test/plugin-transition.test.ts test/plugin-schema.test.ts test/plugin-contract.test.ts \ test/plugin-docs.test.ts| ไฟล์ | สิ่งที่มันตรึงไว้ |
|---|---|
plugin-manifest.test.ts |
ตัว parse manifest, นโยบาย URL, เพดานขนาด body, รูปแบบ token และการ hash |
plugin-installation.test.ts |
การติดตั้ง, activation ที่ล้มเหลว, การปฏิเสธ redirect, state machine, ช่วงซ้อนทับและการหมดอายุของ rotation, การจำกัด grant ให้แคบลง, ขอบเขตของ request, rate limit, launch ที่ใช้ได้ครั้งเดียวและการหมดอายุ |
plugin-transition.test.ts |
การเผยแพร่ทั้งกลุ่ม, การระบุว่าเป็นการกระทำของปลั๊กอิน, replay, concurrency, การอ่านสิทธิ์ใหม่ และการปฏิเสธแบบ terminal ทุกตัวที่มีชื่อ |
plugin-schema.test.ts |
schema ที่เผยแพร่ตรงกับการตรวจ runtime ที่เขียนเอง ทีละกรณี |
plugin-contract.test.ts |
สำเนาโปรโตคอลของปลั๊กอินอ้างอิงตรงกับของ core |
plugin-docs.test.ts |
เอกสารทั้งสี่นี้: ไฟล์ที่สร้างขึ้นตรงกัน และทุกตัวอย่างคือสิ่งที่ route จริงตอบกลับ |
ช่องว่างที่ระบุไว้อย่างตรงไปตรงมา
หัวข้อที่มีชื่อว่า “ช่องว่างที่ระบุไว้อย่างตรงไปตรงมา”- ไม่มีแพ็กเกจ contract ที่เผยแพร่ไว้ SPEC-PLUGINS.md §14 บันทึกช่องว่างเดียวกันไว้ สิ่งที่
มีอยู่ในวันนี้คือ JSON Schema สองไฟล์, core ปลอมใน
plugins/scheduling/test/helpers/และ test ฝั่ง core ที่ทำให้ตารางโปรโตคอลของปลั๊กอินอ้างอิงถูกต้องอยู่เสมอ ปลั๊กอินใน repository อื่นคัดลอก core ปลอมไปใช้และ validate กับ schema ยังไม่สามารถnpm installconformance suite ได้ - การรัน acceptance ข้ามเซอร์วิสใน §16 ยังไม่ได้ทำเป็นอัตโนมัติที่นี่ suite ของปลั๊กอินอ้างอิง และ suite ของ core ต่างก็ใช้ตัวแทน (double) สำหรับอีกฝั่ง การรันทั้งสอง process คุยกัน คือชุดติดตั้งบนเครื่อง local ข้างต้น ซึ่งขับเคลื่อนด้วยมือ
- ไม่มี browser journey รวมอยู่ในไฟล์เหล่านี้ gate ระดับหน้าจอสำหรับ Plugins UI ของ core เป็น นโยบายของ repository (SPEC.md §12 D-S) และเป็นหน้าที่ของ orchestrator ไม่ใช่ของผู้เขียนปลั๊กอิน