Skip to content

Sisa Baris Run yang Dibunuh: Ganti Jaring Jadi Sweep Prefix

Adityo Guni Waluyo

Assert zero-left gagal karena baris e2e dari run yang dibunuh; jaring pembersihan berbasis prefix menutup kebocorannya.

Ringkasan

Dulu tes e2e suka gagal karena run yang dibunuh paksa ninggalin baris sisa, dan afterAll nggak pernah jalan kalau prosesnya kena SIGKILL. Solusinya: hapus berdasarkan prefix e2e-parity-% langsung ke database, plus set event_date biar baris uji nggak kegeser di listing. Catatan: jangan paralel penuh, soalnya sweep bisa ngehapus data run lain yang masih jalan.

Assert zero-left gagal sebelum tes pertama sempat jalan

Gate e2e parity berhenti di assert zero-left. Query sederhana itu seharusnya mengembalikan 0 baris, tetapi yang muncul lima baris sisa dengan prefix e2e-parity-%, lengkap dengan tanggal kegiatan beberapa hari ke belakang. Padahal run di depannya tuntas bersih: informasi 7/7 dalam 2,9 menit, agenda 3/3 dalam 51,8 detik. Tidak ada error, tidak ada laporan gagal.

Saya cek log run sebelumnya. Prosesnya dihentikan paksa di tengah jalan, bukan gagal karena assertion. Dari situlah polanya mulai terlihat: semua baris sisa itu milik run yang tidak pernah sempat menyelesaikan dirinya sendiri.

Tebakan pertama saya keliru. Saya mengira cukup menyimpan createdIds per run, lalu menghapusnya di afterAll. Skema itu valid untuk run yang berjalan sampai selesai, dan itulah yang selama ini terjadi. Masalahnya, identitas berbasis daftar ID hanya lengkap kalau run sempat melaporkan apa yang ia buat. Run yang dibunuh tidak pernah melapor.

Dua fakta dari dokumentasi Playwright menjelaskan kenapa afterAll tidak menolong di kasus ini. afterAll dijalankan sekali per worker setelah semua tes di worker itu selesai [10]. Ketika sebuah tes gagal, Playwright membuang seluruh worker beserta browser-nya dan menjalankan worker baru [11]. Itu pun masih di dalam mekanisme yang punya kesempatan bersih-bersih.

SIGKILL atau Ctrl-C di tengah run tidak masuk mekanisme mana pun. Tidak ada afterAll, tidak ada flush, tidak ada laporan createdIds. Yang tertinggal hanya baris di database. Dokumentasi resmi memang menyarankan isolasi dan kontrol data uji sebagai praktik terbaik [9], tetapi saran itu mengasumsikan teardown tetap punya kesempatan berjalan.

Konsekuensinya bukan cuma kotor. Rubric listing agenda memakai ORDER BY event_date DESC NULLS LAST dengan LIMIT 9. Baris sisa yang ber-tanggal lama akan mengisi slot di dalam limit itu, menggeser baris uji yang baru dibuat, lalu menghancurkan assert zero-left milik run berikutnya. Satu run mati merusak run berikutnya, dua kali.

Jadi jaring pembersihan tidak boleh bergantung pada ingatan proses yang bisa mati kapan saja. Identitasnya harus terbaca dari bentuk barisnya sendiri, bukan dari daftar ID yang hidup di memori sebuah proses. Pola prefix adalah satu-satunya hal yang tetap ada setelah prosesnya hilang.

Sweep berbasis pattern, lalu seed event_date

Perbaikannya ada dua bagian: sweep menyeluruh di teardown, dan seed tanggal yang disengaja di tes agenda.

Pertama, ganti penghapusan berbasis createdIds dengan sweep prefix langsung ke database. Prefix yang saya gunakan sengaja sempit, e2e-parity-%, supaya baris di luar skema uji tidak ikut tersapu.

// tests/e2e/teardown.ts
test.afterAll(async ({ request }) => {
  const rows = await db.query(
    `SELECT id FROM agenda WHERE id LIKE 'e2e-parity-%'`
  );

  for (const row of rows) {
    // kontrak delete dua langkah: trash lebih dulu
    await request.delete(`/api/agenda/${row.id}`);
    await request.delete(`/api/agenda/${row.id}?permanent=1`);
  }
});

Catatan kontraknya: delete bersifat dua langkah, trash lalu permanent. Permintaan permanent tanpa trash lebih dulu mengembalikan 404 pada DELETE /api/agenda/{id}?permanent=1. Kalau muncul 404 di log teardown, urutan itulah yang salah, bukan datanya.

Kedua, tes agenda sekarang menyimpan event_date secara eksplisit, dengan nilai hari ini saat tes dijalankan, bukan mengandalkan nilai default atau NULL. Baris uji berada di puncak urutan event_date DESC dan tidak tergeser oleh sisa lama sekalipun sebagian baris belum sempat tersapu. Tes sched-nya jadi deterministik terhadap urutan listing.

Verifikasi setelah perubahan: jalankan ulang suite, lalu cek langsung ke database.

SELECT count(*) FROM agenda WHERE id LIKE 'e2e-parity-%';

Hasil yang diharapkan setelah run selesai adalah 0. Kalau masih ada angka di atas nol setelah run normal, sweep-nya tidak berjalan atau prefix baris uji tidak cocok dengan pola di query. Hasil rerun pada kasus saya: informasi 7/7 dalam 2,9 menit, agenda 3/3 dalam 51,8 detik, assert zero-left lolos tanpa baris sisa.

Satu trade-off yang tidak boleh dilewatkan. Sweep prefix punya sisi tajam: pada fullyParallel, ia dapat menghapus baris milik run paralel yang masih berjalan. Regresi ini pernah terverifikasi pada penelitian terdahulu, dengan penyebab yang sama persis. Mitigasi saat ini adalah prefix yang lebih sempit dan kesepakatan bahwa dua run parity tidak berjalan bersamaan terhadap database yang sama. Kalau suatu saat suite harus berjalan paralel penuh, sweep ini perlu batas waktu tambahan, misalnya hanya menyapu baris yang dibuat sebelum RUN_STARTED_AT. Tanpa batas itu, pattern sweep akan berubah dari jaring menjadi pemotong.

Sources:

[9] Playwright Best Practices

[10] test.test.afterAll – Playwright API

[11] Retries – Playwright (worker discard)

Artikel terkait