Aturan saya untuk setiap commit perbaikan adalah suite tesnya harus hijau dulu. Alasannya dua. Commit di atas suite merah adalah klaim palsu di riwayat, dan kalau commit itu sampai di-push, yang ikut terkirim adalah regresi. Gerbangnya saya tulis sebagai satu rantai shell yang kelihatannya sudah menjaga semua itu:
pnpm test:int | grep Tests && git commitJalankan suite, cari baris ringkasannya, baru commit. Pada 26 September 2026 rantai ini meloloskan dua commit perbaikan ketika pnpm test:int sedang punya tes yang gagal. Keduanya ketahuan dan di-amend sebelum ada push sama sekali, jadi versi yang salah tidak pernah sampai ke remote.
&& membaca exit code grep, bukan exit code tes
&& hanya menanyakan satu hal ke perintah di sebelah kirinya, yaitu apakah perintah itu keluar dengan 0. Di rantai tadi, yang ada di sebelah kirinya adalah sebuah pipeline, dan exit code sebuah pipeline adalah exit code perintah terakhirnya. Perintah terakhir itu grep, bukan pnpm test:int.
grep keluar dengan 0 kalau menemukan setidaknya satu baris yang cocok. Baris ringkasan suite muncul di keluaran entah hasilnya hijau atau merah, jadi grep sukses pada baris itu apa pun hasil tesnya. Exit code dari pnpm test:int sendiri tidak pernah ditanyakan siapa pun. Rantai itu memeriksa keluaran grep, bukan exit code dari test run, dan lewat celah itulah dua commit tadi masuk di atas suite yang gagal.
Polanya bisa diulang tanpa proyek apa pun. Contoh minimal di bawah memakai fungsi tiruan yang mencetak satu baris ringkasan lalu keluar dengan 1, seperti suite yang gagal:
# contoh minimal dengan nama netral, bukan skrip proyek
tes_palsu() { echo "Tests: 1 gagal"; return 1; }
tes_palsu | grep Tests && echo "commit jalan"
# Tests: 1 gagal
# commit jalanHasilnya sama di bash maupun zsh. Tidak ada peringatan apa pun, dan di contoh ini kata gagal tercetak persis di atas baris yang mewakili commit-nya.
Ada opsi pipefail di bash maupun zsh yang membuat pipeline melaporkan kegagalan dari perintah mana pun di dalamnya, dan dengan opsi itu contoh tadi berhenti sebelum echo. Jalan lain yang paling dekat adalah membaca exit code perintah pertama lewat PIPESTATUS. Yang kedua ini sudah pernah menipu tes saya, tiga belas hari sebelum dua commit tadi.
PIPESTATUS[0] kosong di zsh
Pada 13 September 2026, guard yang saya tulis untuk menghentikan deployment yang rusak ternyata akan menggagalkan setiap build lokal. Itu baru ketahuan karena guard-nya dijalankan. Tes pertama untuk guard yang sama lebih licin lagi, karena tes itu melaporkan tiga kelulusan yang sama sekali tidak memverifikasi apa pun. Catatan saya tentang hari itu menulis sebabnya dalam satu kalimat: PIPESTATUS[0] kosong di zsh, yang array-nya mulai dari satu, sehingga setiap exit code di tes itu terbaca nol.
Kalimat itu benar, tapi ceritanya sedikit lebih lebar, dan sisanya bisa dicek siapa pun. PIPESTATUS adalah array milik bash. Isinya exit code tiap perintah di pipeline terakhir, dimulai dari indeks 0. Di zsh, hal yang sama disimpan di array bernama pipestatus dengan huruf kecil, dan array zsh memang mulai dari indeks 1. PIPESTATUS dengan huruf besar tidak ada di zsh, jadi indeks berapa pun yang dibaca dari situ menghasilkan string kosong tanpa error, kecuali shell-nya berjalan dengan set -u.
# contoh minimal: perintah pertama gagal, perintah kedua sukses
false | true
echo "PIPESTATUS[0]=[${PIPESTATUS[0]}] pipestatus[1]=[${pipestatus[1]}]"
# bash : PIPESTATUS[0]=[1] pipestatus[1]=[]
# zsh : PIPESTATUS[0]=[] pipestatus[1]=[1]Catatan saya tidak merinci bagaimana kosong itu berubah jadi nol, jadi bentuk perbandingan di dalam tes itu tidak saya tuliskan di sini. Yang bisa ditunjukkan adalah betapa mudahnya hal itu terjadi di zsh. Perbandingan angka yang paling biasa saja sudah memperlakukan string kosong sebagai nol:
# contoh minimal, bukan isi tes proyek
[ "" -eq 0 ] && echo "dianggap nol"
# zsh : dianggap nol
# bash : [: : integer expression expectedDi bash, perbandingan yang sama setidaknya berteriak. Di zsh, string kosong dari variabel yang tidak pernah ada lolos begitu saja sebagai nol.
Gerbang yang membaca exit code tes itu sendiri
Aturan yang saya tulis dari kejadian 26 September bunyinya pendek: commit hanya ketika perintah tes itu sendiri keluar dengan 0. Caranya, jalankan suite ke sebuah log, simpan R=$?, lalu commit hanya di dalam if [ $R -eq 0 ]. Bentuk yang disederhanakan, dengan nama berkas log yang netral:
pnpm test:int > uji.log 2>&1
R=$?
if [ $R -eq 0 ]; then
git commit
fiTidak ada pipe di antara suite dan pembacaan exit code-nya, jadi $? di baris kedua milik pnpm test:int, bukan milik grep, dan pipefail tidak diperlukan karena tidak ada pipe yang perlu dijaga. R=$? harus berada tepat di baris berikutnya, karena perintah apa pun yang jalan di antaranya menimpa $?. Keluaran suite tidak hilang, cuma pindah ke uji.log, jadi tes mana yang gagal tetap bisa dibaca dari sana. R selalu berisi angka dari $?, sehingga perbandingan di if tidak pernah bertemu string kosong seperti contoh tadi, dan hasilnya sama di bash maupun zsh.
Ada satu batas yang harus saya tulis apa adanya. Aturan ini tertulis sebagai cara kerja ke depan, bukan skrip yang sudah terpasang dan teruji. Catatan saya belum memuat satu run pun yang menunjukkan bentuk ini benar-benar menahan commit di atas suite merah.
Tes yang belum pernah terlihat gagal
Catatan tentang 13 September memuat aturan untuk memastikan sebuah tes memang bisa gagal sebelum mempercayai bahwa tes itu lulus. Catatan tentang 26 September mengulang hal yang sama dengan cara yang lebih konkret: sebelum sebuah tes dipercaya, tunjukkan dulu tes itu gagal tanpa perbaikannya, dengan men-stash source-nya. Bentuk yang disederhanakan, dengan path netral:
git stash push -- path/ke/berkas-perbaikan
pnpm test:int > uji.log 2>&1
echo "tanpa perbaikan: $?" # harus bukan 0
git stash popgit stash push dengan path hanya menyimpan perubahan di path itu, jadi tes barunya tetap di tempat sementara perbaikannya disingkirkan sebentar. Kalau suite tetap keluar dengan 0 tanpa perbaikan, tes baru itu tidak sedang menguji perbaikannya. Tes yang membaca setiap exit code sebagai nol akan lulus dengan atau tanpa perbaikan, dan langkah seperti inilah tempat tes semacam itu ketahuan.
Sisa data dari run yang berhenti di tengah
Catatan tentang 26 September memuat satu masalah lagi, terpisah dari soal grep. Tes yang memakai stempel tetap meninggalkan baris data ketika sebuah run gagal di tengah jalan, lalu run berikutnya gagal pada slug unik. Stempel tetap berarti setiap run membawa penanda yang sama, jadi baris yang tertinggal dari satu run bisa bertabrakan dengan run sesudahnya.
Aturan yang saya tulis untuk itu ada dua. Tes baru diberi stempel yang unik untuk setiap run, dan sesudah run merah, sisa datanya dibersihkan dulu sebelum run berikutnya. Ini juga berkaitan dengan gerbangnya. Gerbang yang membaca exit code dengan jujur akan menahan commit untuk merah jenis apa pun, termasuk merah yang datang dari sisa run sebelumnya, bukan dari kode yang sedang diperbaiki.
Pemeriksa juga perlu dijalankan
Rantai grep dan tes guard ternyata keliru dengan cara yang sama. Keduanya alat yang tugasnya memeriksa hal lain, dan keduanya menyimpulkan lulus dari exit code yang salah: yang satu dari grep, yang lain dari variabel yang tidak ada di zsh. Yang keluar dari keduanya cuma nol.
Catatan tentang 13 September menyimpan prinsipnya dalam satu kalimat, bahwa alat yang belum dijalankan hanyalah keyakinan tentang alat itu. Aturan turunannya adalah menjalankan apa pun yang dibangun sebelum menyatakan itu berfungsi, termasuk alat yang memeriksa hal lain.
Gerbang R=$? tadi juga alat yang memeriksa hal lain. Dengan ukuran saya sendiri, gerbang itu masih berupa keyakinan sampai terlihat menahan commit di atas suite yang memang merah, dan cara mengujinya sama dengan cara menguji tes mana pun. Lihat dulu gerbang itu menolak, baru percaya ketika dia mengizinkan.