ข้ามไปยังเนื้อหา
Pre-MVP โค้ดเบส 1.0 ยังไม่ใช่ผลิตภัณฑ์ที่เผยแพร่ — ดู ขอบเขตผลิตภัณฑ์ 1.0

การทดสอบปลั๊กอิน

สิ่งที่ปลั๊กอินต้องพิสูจน์ก่อนจะได้รับอนุญาตให้เข้าใกล้เนื้อหาจริง วิธีพิสูจน์กับ core ปลอม (fake core) ใน test suite ของคุณเอง วิธีรันทั้งสองเซอร์วิสบนเครื่องเดียว และวิธีทำให้เกิดความล้มเหลวแต่ละแบบโดยตั้งใจ

ดู Protocol v1 สำหรับพื้นผิวที่อยู่ภายใต้การทดสอบ และ ความปลอดภัยของปลั๊กอิน สำหรับ invariant ที่กรณีทดสอบด้านล่างมีไว้ปกป้อง

ต่อไปนี้คือกรณีที่ปลั๊กอินต้องมี แต่ละกรณีเป็นคุณสมบัติของโปรโตคอล ไม่ใช่ของปลั๊กอินตัวใดตัวหนึ่ง ดังนั้นแต่ละกรณีจึงควรอยู่ใน 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)

อย่าชี้ 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 ใดก็ได้, header Retry-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 ได้

Terminal window
cd plugins/scheduling
npm test # vitest
npm run typecheck

เอกสาร 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 ด้วย:

Terminal window
npm run docs:plugins

backend/test/plugin-docs.test.ts จะล้มเหลวหากสำเนาที่ commit ไว้คลาดเคลื่อน ดังนั้นคำสั่งนี้จำเป็น เฉพาะหลังจากเปลี่ยนโปรโตคอลเท่านั้น

ทั้งสองเซอร์วิสบนเครื่องเดียว โดยไม่ต้องมีบัญชี Cloudflare และไม่ต้องมี credential จริง

Dee Wan จาก root ของ repository:

Terminal window
node scripts/install-dependencies.mjs
npm run local:migrate
npm run local:seed # prints the sign-in it creates
npm 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=development
PLUGIN_DEV_ORIGINS=http://localhost:8788
BACKEND_URL=http://localhost:8787

PLUGIN_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:

Terminal window
cd plugins/scheduling
npx wrangler d1 migrations apply dee-wan-scheduling --local
npx 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

วิธีทำให้เกิดแต่ละ 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 ถูกสรุปผลภายใต้ชื่อที่ถูกต้อง และ ไม่มีอะไรถูกส่ง ซ้ำสองครั้ง — รายการคำสั่งที่บันทึกไว้คือสิ่งที่พิสูจน์ข้อหลัง

รันจาก backend/:

Terminal window
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 install conformance suite ได้
  • การรัน acceptance ข้ามเซอร์วิสใน §16 ยังไม่ได้ทำเป็นอัตโนมัติที่นี่ suite ของปลั๊กอินอ้างอิง และ suite ของ core ต่างก็ใช้ตัวแทน (double) สำหรับอีกฝั่ง การรันทั้งสอง process คุยกัน คือชุดติดตั้งบนเครื่อง local ข้างต้น ซึ่งขับเคลื่อนด้วยมือ
  • ไม่มี browser journey รวมอยู่ในไฟล์เหล่านี้ gate ระดับหน้าจอสำหรับ Plugins UI ของ core เป็น นโยบายของ repository (SPEC.md §12 D-S) และเป็นหน้าที่ของ orchestrator ไม่ใช่ของผู้เขียนปลั๊กอิน