การ์ดที่ฉันสร้างให้ Claude Code ระเบิดใส่ข้อความคอมมิตของตัวมันเอง — ลากเส้นแบ่งระหว่างผู้ตรวจกับสิ่งที่ถูกตรวจใหม่ ด้วยตัวเนื้อหาจริง

AI Claude Code Operations テスト設計 検証

บทนำ

ฉันเคยเขียนถึงการสร้าง “กลไกที่หยุดผู้ช่วย AI เขียนโค้ดก่อนที่มันจะลงมือ” ไว้แล้ว

สร้างกลไก “ไม่ให้ทำ” สำหรับผู้ช่วย AI เขียนโค้ด — จากคำแนะนำสู่การบังคับ แล้วพบว่าประตูนั้นเองก็มีช่องโหว่ข้าง

ส่วนขยายของเรื่องนั้นนำไปสู่การสร้างการ์ดอีกตัว เพราะ การพิมพ์หมายเลขพอร์ตการเชื่อมต่อจากความจำล้วน ๆ และการทำเครื่องหมายคำพูดของเชลล์พัง เกิดขึ้นซ้ำ ๆ ภายในช่วงงานเดียวกัน การ์ดเข้าไปตัดการทำงานแล้วตรวจเนื้อหาของคำสั่งที่กำลังจะรันด้วยเครื่องจักร ระหว่างสร้างมัน การ์ดเจอข้อบกพร่องที่ไม่หวือหวาแต่กวนใจถึงสองครั้ง: มันลากตัวเองเข้าไปอยู่ในขอบเขตการตรวจของตัวมันเอง บทความนี้บันทึกข้อบกพร่องเหล่านั้นและเทสต์เชิงปฏิปักษ์ที่เขียนขึ้นเพื่อแก้มัน


การ์ดตรวจอะไร

สิ่งที่ฉันสร้างเข้าไปตัดทันทีก่อนคำสั่งทำงาน แล้วตรวจค่าสองชนิด

  • A. หมายเลขพอร์ตที่ไม่มีที่มา ถ้ารูปแบบ host:port ปรากฏในคำสั่ง ให้ใช้ git grep ตรวจว่าหมายเลขพอร์ตนั้นเขียนอยู่ที่ไหนสักแห่งในซอร์สของเวิร์กสเปซจริงหรือไม่ ถ้าไม่เจอแม้แต่ครั้งเดียว ให้หยุดการทำงานด้วยความสงสัยว่ามันถูกพิมพ์จากความจำ แต่ถ้าตัวการตรวจเองล้มเหลว (การค้นหาใช้ไม่ได้) อย่าหยุด ให้ปล่อยผ่าน (fail-open) การถือว่า “ตรวจไม่ได้” เป็นการละเมิด จะทำให้ตัวงานเองหยุดในสภาพแวดล้อมที่เครื่องมือใช้ไม่ได้
  • B. โค้ดยาวที่ฝังอยู่ในเชลล์โดยตรง หยุดเมื่อมีโค้ดเกิน 200 อักขระฝังอยู่ในเครื่องหมายคำพูดของเชลล์โดยตรง ในรูปแบบอย่าง python -c "..." อุบัติเหตุจากเครื่องหมายคำพูดซ้อนและการขยายตัวแปรถูกป้องกันได้เกือบทั้งหมด เพียงแค่หยุดเรื่องนี้แล้วเขียนลงไฟล์แทน

อุบัติเหตุจริงที่จุดชนวนเรื่องนี้เป็นแบบนี้ ฉันเขียนหมายเลขพอร์ตการเชื่อมต่อจากความจำ และการตรวจการเชื่อมต่อทุกครั้งถูกปฏิเสธ ค่าที่ถูกต้องคือ หมายเลขที่ต่างกันอยู่อักขระเดียว และมันเขียนอยู่เป็นค่าคงที่ในอีกไฟล์หนึ่ง มันอยู่ในที่ที่การมองแวบเดียวหนึ่งวินาทีก็จบเรื่อง — และ ฉันเขียนมันจากความจำแทนที่จะไปดูซอร์ส


การ์ดตอบสนองต่อตัวเอง (1) — ข้อความอธิบายภายใน heredoc

ตอนคอมมิตการเปลี่ยนแปลงของการ์ดตัวนี้ ฉันเขียนคำอธิบายของการ์ดเองลงในเนื้อหาข้อความคอมมิตว่า “หยุดเมื่อมีโค้ดเกิน 200 อักขระฝังอยู่ในรูปแบบอย่าง python -c” ข้อความคอมมิตยาวหลายบรรทัด ฉันจึงเขียนมันในเชลล์ด้วยไวยากรณ์ที่ “ส่งบรรทัดต่อไปนี้ผ่านไปในฐานะข้อมูล” (heredoc)

ตรงนั้นการ์ดติดธงใส่ตัวเอง เนื้อหาของ heredoc ไม่ใช่โค้ดที่เชลล์จะรัน แต่คือข้อมูลธรรมดาที่ส่งให้คำสั่ง (ในที่นี้คือสตริงข้อความคอมมิต) — แต่การ์ดกลับรวมเนื้อหานั้นเข้าไปในการตรวจโค้ดของมัน ผลคือข้อความที่แค่อธิบายการเปลี่ยนแปลงของตัวเองถูกเข้าใจผิดว่าเป็น “โค้ดยาวที่ฝังอยู่ในเชลล์” และการทำงานก็หยุด

เส้นแบ่งของสิ่งที่ถูกตรวจ (แนวตั้ง = คำสั่งที่ผ่านการตรวจ,
                แนวนอน = การแตกกิ่งตามธรรมชาติของสตริง, การซ้อน = ก่อนและหลังการแก้)
 สตริงคำสั่ง

   ├─▶ นอก heredoc ── โค้ดที่เชลล์ตีความจริง
   │        ├─ ก่อน ── ถูกตรวจ ── ✅ ถูกต้อง
   │        └─ หลัง ── ถูกตรวจ ── ✅ ไม่เปลี่ยน ───────────────────┐
   │                                                                 │
   └─▶ ใน heredoc ── ข้อมูลธรรมดาที่ส่งให้คำสั่ง                     │
            ├─ ก่อน ── ถูกตรวจ ◀── ผลบวกลวง ①                      │
            │      └─ คำว่า "python -c" แค่บังเอิญ                  │
            │         ปรากฏในข้อความอธิบายของข้อความคอมมิต           │
            │              └─ ข้อความที่อธิบายการเปลี่ยนแปลงของตัวเอง │
            │                 หยุดการทำงานของตัวเอง                  │
            │                    │                                   │
            │                    └──(เส้นย้อนกลับ)──▶ ลากเส้นแบ่งใหม่
            │                        จาก "พาธ" ไปเป็น "ธรรมชาติของเนื้อหา"
            │                                          │             │
            └─ หลัง ── ตัดตั้งแต่เครื่องหมายเปิดถึงเครื่องหมายปิดออกนอกขอบเขต
                              │                                       │
                              └───────────┬───────────────────────────┘

                    แยกสตริงคำสั่งเดียวกันเป็นสองส่วนตามธรรมชาติ แล้วค่อยตรวจ

การแก้คือตัดช่วงตั้งแต่เครื่องหมายเปิดของ heredoc ถึงเครื่องหมายปิดที่คู่กัน ออกจากขอบเขตการตรวจก่อน นี่เป็นรูปแบบเดียวกับบทเรียนที่ฉันเคยสถาปนาไว้ก่อนหน้าในบริบทอื่น — “คำที่คุณกำลังสกัดออกมา อาจปรากฏในตำแหน่งที่ไม่ได้ตั้งใจภายในสตริงในเครื่องหมายคำพูดหรือในข้อความ แล้วจับคู่ผิด” บทเรียนที่ควรจะเรียนไปแล้วครั้งหนึ่ง กลับมาโดนอีกในการอิมพลีเมนต์คนละตัว


การ์ดตอบสนองต่อตัวเอง (2) — ค่าสำหรับตรวจสอบภายในโค้ดเทสต์

ผลบวกลวงอีกอันเกิดขึ้นระหว่างเขียนโค้ดเทสต์เชิงปฏิปักษ์เพื่อยืนยันความถูกต้องของการ์ด

เทสต์ที่ยืนยันว่า “หมายเลขพอร์ตที่ไม่มีที่มาถูกหยุด” จำเป็นต้องมี “หมายเลขพอร์ตที่ไม่ควรมีอยู่ที่ไหนเลยในเวิร์กสเปซ” อย่างจงใจ การเขียนหมายเลขนั้นเป็นค่าคงตัวในโค้ดเทสต์ ทำให้ — เพราะขอบเขตการค้นหาของการ์ดคือทั้งเวิร์กสเปซ (รวมไฟล์ที่โค้ดเทสต์เองอยู่) — การค้นหาของการ์ดหยิบเอาข้อความนั้นในโค้ดเทสต์ขึ้นมาเป็นผลลัพธ์ที่ตรงกัน เทสต์ที่อยากยืนยันว่า “พอร์ตนี้ไม่มีอยู่ที่ไหนเลย” ถูกการค้นหาของการ์ดเองตัดสินว่า “มีอยู่ (ในโค้ดเทสต์)” — และคุณสมบัติที่กำลังถูกทดสอบก็ถูกทำลาย

ขอบเขตที่ค้นหาที่มา (แนวตั้ง = การค้นหาที่กำลังรัน, แนวนอน = ไฟล์ไหนนับเป็นที่มา,
                การซ้อน = ค่าที่วางไว้ตรงนั้นแท้จริงคืออะไร)
 หยิบหมายเลขพอร์ตมาหนึ่งตัว

   └─▶ git grep ทั่วทั้งเวิร์กสเปซ

          ├─▶ โค้ดโปรดักชัน ── ค่าที่ถูกใช้เป็นปลายทางการเชื่อมต่อจริง
          │        └─ ตรงกัน ─▶ ✅ ผ่านได้ในฐานะ "ค่าที่ยืนยันแล้ว" ────────┐
          │                                                                 │
          ├─▶ ไฟล์ตั้งค่า ── ค่าที่ถูกใช้จริงเช่นกัน                          │
          │        └─ ตรงกัน ─▶ ✅ ผ่านได้ ─────────────────────────────────┤
          │                                                                 │
          └─▶ โค้ดเทสต์ ── ค่าปลอมที่ดูเหมือนจริง เขียนไว้                    │
                   │        อย่างจงใจเพื่อยืนยันว่า "ไม่มีที่มา"              │
                   ├─ ก่อน ── ตรงกัน ─▶ ⛔ ผลบวกลวง ② ─────────────────────┤
                   │      └─ เทสต์ที่อยากยืนยันว่า "ไม่มีอยู่ที่ไหนเลย"
                   │         ยืนยันตัวเอง โดยเอาข้อความของตัวเองเป็นที่มา
                   │              └─ เทสต์นั้นผ่านตลอดไปนับจากนั้น           │
                   │                    (= คุณสมบัติที่ถูกทดสอบหายไป)
                   │                       │                                │
                   │                       └──(เส้นย้อนกลับ)──▶ ลากนิยามของ
                   │                           ขอบเขตการค้นหาเองใหม่
                   │                                                        │
                   └─ หลัง ── กันไฟล์ที่ตรงกับธรรมเนียมการตั้งชื่อเทสต์ออก ───┤

                                    เฉพาะสิ่งที่ถูกใช้จริงเท่านั้นที่เรียกได้ว่า "ที่มา"

การแก้คือกันไฟล์เทสต์ (ไฟล์ที่วางอยู่ใต้ธรรมเนียมการตั้งชื่ออย่าง test_*.py) ออกจากขอบเขตการค้นหาอย่างชัดเจน ค่าที่เขียนอยู่ในโค้ดเทสต์ไม่ใช่ที่มาของค่าที่ถูกใช้เป็นปลายทางการเชื่อมต่อจริง


สิ่งที่ผลบวกลวงทั้งสองมีร่วมกัน

ทั้งคู่มีรากเดียวกัน ฉันลากเส้นแบ่งระหว่าง “ผู้ตรวจ” กับ “สิ่งที่ถูกตรวจ” ด้วยพาธไฟล์และไดเรกทอรีล้วน ๆ โดยไม่ดูธรรมชาติของเนื้อหา (โค้ดที่จะถูกรัน หรือข้อมูลธรรมดา; ค่าโปรดักชัน หรือค่าปลอมที่เขียนไว้เพื่อตรวจสอบ) เนื้อหาของ heredoc “ดูเหมือนโค้ดแต่เป็นข้อมูล” ค่าคงตัวในโค้ดเทสต์ “ดูเหมือนจริงแต่เป็นของปลอม” ทั้งคู่ผิวเผินดูเหมือนเป็นส่วนหนึ่งของสตริงคำสั่งหรือไฟล์ซอร์ส แต่บทบาทจริงของมันต่างกัน


ยืนยันทั้งสองทิศทางด้วยเทสต์เชิงปฏิปักษ์

หลังการแก้ ฉันเขียนการตรวจ 24 รายการที่ครอบคลุมทั้งสองทิศทาง: สิ่งที่ควรผ่านถูกหยุดผิดหรือไม่ (ตรวจจับเกิน) และสิ่งที่ควรถูกหยุดถูกปล่อยหลุดหรือไม่ (ตรวจจับขาด) รันในสภาพแวดล้อมที่ติดตั้งใช้งานจริง ผลลัพธ์คือ:

[OK ] 許可: ソースに存在するポート(<実在する番号>)  rc=0
[OK ] 拒否: ソースに存在しないポート(<非在の番号>)  rc=2 contains=True
[OK ] 許可: well-known ポート(443)  rc=0
[OK ] 誤爆しない: 時刻表記 12:34  rc=0
[OK ] 誤爆しない: バージョン番号 1.5.8  rc=0
[OK ] 誤爆しない: Windows パス C:\Users  rc=0
[OK ] 拒否: python -c に長いコードを埋めている  rc=2 contains=True
[OK ] 誤爆しない: メッセージ内に 'python -c' の語が出るだけ  rc=0
[OK ] 誤爆しない: heredoc 本体の説明文に 'python -c' と長文が入っている  rc=0
[OK ] 誤爆しない: heredoc 本体にソース非在のポート番号が語として出てくる  rc=0
[OK ] 拒否: heredoc の外側にある実行対象のインラインコードは検査される  rc=2 contains=True
[OK ] 空 stdin で落ちない  rc=0
[OK ] 壊れた JSON で落ちない  rc=0
[OK ] command 欠落で落ちない  rc=0

failures=0

(ตัดมาจากเอาต์พุตจริง มีเพียงหมายเลขพอร์ตที่มีอยู่จริงที่ถูกปิดบังด้วย <…> ทั้ง 24 รายการแบ่งเป็น: ยืนยันสิ่งที่ควรอนุญาต, ยืนยันสิ่งที่ควรปฏิเสธ, ยืนยันว่าไม่มีสัญญาณเตือนลวง, ยืนยันเส้นแบ่งของ heredoc และยืนยันความทนทานต่ออินพุตที่ผิดรูป)

สิ่งที่ฉันคำนึงตรงนี้ไม่ใช่แค่การเรียง “กรณีที่ใช้ได้” แต่คือ การทำให้รูปร่างของผลบวกลวงแต่ละอันที่ฉันเคยโดน กลายเป็นชื่อของรายการเทสต์ เทสต์ที่ว่า “หมายเลขพอร์ตที่ไม่มีในซอร์สบังเอิญปรากฏเป็นคำในเนื้อ heredoc” ถูกเขียนขึ้นเพื่อสร้างข้อบกพร่อง ② ขึ้นมาใหม่พอดี มันทำหน้าที่เป็นเทสต์ป้องกันการถดถอย ไม่ให้ฝังข้อบกพร่องเดิมลงไปอีก


ขีดจำกัดของการออกแบบนี้

การ์ดเลือกการออกแบบที่ “ถ้าการค้นหาล้มเหลว อย่าหยุด” (fail-open) นั่นเป็นการตัดสินที่ทำไปเพื่อไม่ให้ตัวงานเองหยุด แต่ด้านกลับคือ เมื่อการค้นหาล้มเหลว มันยกสัญญาณเตือนขึ้นมาจริง แต่ไม่ออกแรงหยุดด้วยเครื่องจักรเลย เส้นทางนี้ถูกเดินบ่อยแค่ไหนยังไม่ได้รวมยอด ความจริงจึงยังไม่รู้

อีกอย่าง สิ่งที่ฉันพบและซ่อมตรงนี้มีเพียงการอ้างถึงตัวเองสองจุดที่ฉันโดนและสังเกตเห็นระหว่างทำงานจริงเท่านั้น ว่าข้อบกพร่องรูปแบบเดียวกันยังเหลืออยู่ที่อื่นหรือไม่ ยืนยันได้เพียงภายในขอบเขตของเทสต์เหล่านี้ เส้นแบ่งระหว่างผู้ตรวจกับสิ่งที่ถูกตรวจกลายเป็นสิ่งที่ลากใหม่ได้ทุกครั้งที่คุณเจอมันเท่านั้น

ส่งข้อความได้ตามสบาย

ไม่ว่าจะเป็นการว่าจ้างงาน แนะนำโปรเจกต์ ความคิดเห็น หรือคำถาม ยินดีรับทั้งหมด ฉันหวังเป็นอย่างยิ่งว่าจะได้เชื่อมต่อกับผู้ที่มีอุดมการณ์อันสูงส่งเช่นเดียวกัน ฉันจะมุ่งมั่นท้าทายในสิ่งที่ทุ่มเททั้งชีวิตต่อไป ขอขอบคุณและฝากเนื้อฝากตัวด้วยครับ/ค่ะ