Strategi test untuk workflow patch Git P2P tidak bisa disamakan dengan testing aplikasi CRUD biasa. Masalah utamanya bukan hanya benar atau salah pada satu node, tetapi apakah state tetap konsisten ketika patch, issue, dan event direplikasi antar-peer yang bisa offline, terlambat sinkron, atau menerima urutan event berbeda.

Jika tujuan Anda adalah mencegah regresi, fokus pengujian harus diarahkan ke beberapa risiko inti: validasi format data patch/issue, idempotensi operasi saat event terkirim ulang, konflik state akibat replikasi yang berbeda urutan, serta ketahanan test terhadap kondisi jaringan yang tidak stabil. Konteks seperti Radicle berguna sebagai referensi arsitektur Git P2P, tetapi artikel ini berfokus pada prinsip engineering yang bisa diterapkan pada sistem serupa, bukan pada produk tertentu.

Mengapa workflow patch Git P2P rawan regresi

Pada workflow terpusat, biasanya ada satu sumber kebenaran. Pada sistem Git P2P, sumber kebenaran bersifat tersebar: setiap peer menyimpan repositori, metadata kolaborasi, dan riwayat sinkronisasinya sendiri. Akibatnya, regresi sering muncul bukan karena logika bisnis utama berubah, melainkan karena asumsi implisit tentang urutan replikasi, konektivitas, atau bentuk data tidak lagi berlaku.

Beberapa kelas bug yang paling sering muncul:

  • Sinkronisasi antar-peer tidak konvergen: dua peer menerima event yang sama tetapi berakhir dengan state berbeda.
  • Verifikasi patch tidak konsisten: patch valid di satu peer tetapi ditolak di peer lain karena metadata, referensi commit, atau aturan validasi tidak diperlakukan sama.
  • Konflik state: misalnya issue ditutup di satu peer dan dikomentari di peer lain sebelum sinkronisasi.
  • Duplikasi event: event yang sama diproses dua kali akibat retry jaringan atau replay log.
  • Flaky test: test kadang gagal bukan karena bug logika, tetapi karena timeout, race condition, atau asumsi urutan event yang terlalu ketat.

Karena itu, strategi test yang baik harus menguji invariant sistem, bukan hanya output satu fungsi.

Bangun test pyramid yang sesuai untuk sistem P2P

Untuk workflow patch dan issue berbasis Git P2P, test pyramid tetap relevan, tetapi komposisinya harus menekankan validasi domain dan replikasi. Jangan langsung mengandalkan end-to-end test untuk semua kasus, karena test jenis ini mahal, lambat, dan paling mudah menjadi flaky.

1. Unit test: jaga logika domain tetap deterministik

Unit test cocok untuk aturan yang bisa dievaluasi tanpa jaringan atau proses sinkronisasi penuh. Contohnya:

  • validasi status transisi issue, misalnya open -> closed valid, tetapi deleted -> commented tidak valid;
  • normalisasi payload event sebelum disimpan;
  • deduplikasi berdasarkan event ID atau hash konten;
  • aturan verifikasi patch seperti commit target harus ada, author metadata wajib lengkap, atau signature field tidak boleh kosong jika sistem mewajibkannya.

Target utama unit test adalah memastikan fungsi-fungsi kecil tetap murni, deterministik, dan mudah didiagnosis saat gagal.

2. Contract test: kunci format data patch dan issue

Pada sistem P2P, perubahan kecil pada format data bisa menjadi sumber regresi besar. Contract test memastikan setiap peer menghasilkan dan mengonsumsi payload dengan aturan yang sama. Ini penting bila patch, issue, komentar, atau event sinkronisasi diserialisasi ke JSON, file metadata, atau object Git tertentu.

Yang perlu diuji dalam contract test:

  • field wajib dan opsional;
  • tipe data dan format nilai, misalnya timestamp, hash, atau identifier peer;
  • kompatibilitas mundur saat field baru ditambahkan;
  • aturan canonicalization, misalnya urutan field tidak penting tetapi hash dihitung dari bentuk yang sudah dinormalisasi.

Gunakan fixture payload yang eksplisit, dan simpan versi valid serta invalid. Ini membantu mendeteksi kapan parser terlalu longgar atau terlalu ketat.

3. Integration test: pusat utama untuk workflow patch Git P2P

Integration test adalah lapisan paling penting untuk topik ini. Di sinilah Anda menggabungkan repositori Git nyata, storage metadata, dan proses sinkronisasi antar-peer tanpa harus menjalankan seluruh stack produksi. Fokusnya adalah membuktikan bahwa komponen yang terpisah tetap konvergen saat bekerja bersama.

Contoh integration test yang bernilai tinggi:

  • peer A membuat patch, peer B sinkron, memverifikasi, lalu menambahkan review;
  • peer A offline saat issue diperbarui beberapa kali, lalu online kembali dan harus mendapatkan state akhir yang benar;
  • event yang sama dikirim dua kali ke peer penerima dan state akhir tetap sama;
  • patch mereferensikan commit yang belum ada, lalu diterima ulang setelah commit tersedia.

4. End-to-end test: sedikit, tetapi kritis

End-to-end test dipakai untuk memverifikasi alur paling penting dari perspektif pengguna atau operator sistem. Jumlahnya sebaiknya terbatas. Contohnya:

  • membuat patch dari repo lokal, mengiklankannya ke peer lain, menerima review, lalu mengubah status patch;
  • membuka issue pada peer A, sinkron ke peer B, mengomentari dari peer B, dan memverifikasi hasil akhir pada peer A.

Jangan gunakan E2E untuk seluruh kombinasi kondisi jaringan. Variasi seperti replay event, peer offline, atau urutan replikasi berbeda biasanya lebih stabil bila diuji pada integration test.

Prinsip desain test untuk mencegah regresi

Uji invariant, bukan urutan internal yang rapuh

Pada sistem terdistribusi, urutan internal sering berubah tanpa mengubah hasil yang benar. Karena itu, assert sebaiknya berbasis invariant seperti:

  • state akhir identik pada semua peer setelah sinkronisasi selesai;
  • event duplikat tidak menambah efek samping;
  • patch yang invalid selalu ditolak dengan alasan yang konsisten;
  • issue yang telah ditutup tetap memiliki histori lengkap setelah replikasi.

Hindari assert yang terlalu spesifik seperti urutan log internal, jumlah retry persis, atau waktu sinkronisasi absolut, kecuali memang bagian dari kontrak sistem.

Utamakan idempotensi sebagai properti yang diuji

Operasi seperti apply event, import patch, sync from peer, dan merge metadata idealnya idempotent. Dalam praktiknya, jaringan bisa menyebabkan retransmisi atau replay. Jika operasi tidak idempotent, event duplikat dapat melahirkan komentar ganda, status yang salah, atau pointer referensi yang rusak.

Contoh pseudo-code test untuk idempotensi:

test("apply event is idempotent", () => {
  const state1 = applyEvent(initialState, patchAcceptedEvent)
  const state2 = applyEvent(state1, patchAcceptedEvent)

  expect(state2).toEqual(state1)
})

Meski sederhana, pola ini sangat efektif untuk mencegah regresi yang umum pada replikasi.

Pisahkan deterministic state dari nondeterministic transport

Sering kali bug berasal dari dua area berbeda:

  • state machine: bagaimana patch/issue berubah status;
  • transport/replikasi: kapan dan dalam urutan apa event dikirim.

Pisahkan keduanya dalam desain test. Jika state machine dapat diuji secara deterministik dengan input event yang eksplisit, Anda akan lebih mudah membedakan bug domain dari bug jaringan.

Fixture repo yang realistis lebih penting daripada mock berlebihan

Untuk workflow patch Git P2P, mock penuh terhadap Git biasanya kurang memadai. Banyak regresi hanya muncul ketika commit graph, refs, atau metadata repo benar-benar ada. Karena itu, buat fixture repo yang kecil tetapi realistis.

Apa isi fixture repo yang berguna

  • repositori dasar dengan beberapa commit dan satu branch utama;
  • satu branch fitur yang menghasilkan patch;
  • satu patch yang valid dan satu yang sengaja invalid;
  • issue dengan histori komentar dan perubahan status;
  • metadata peer atau identity minimal yang diperlukan sistem Anda.

Simpan fixture sebagai script pembentuk repo, bukan snapshot biner semata. Dengan begitu, perubahan format atau kebutuhan test lebih mudah dipelihara.

# contoh ide setup fixture repo
mkdir repo-a
cd repo-a
git init
echo "base" > README.md
git add README.md
git commit -m "initial commit"

git checkout -b feature/improve-patch
echo "change" >> README.md
git add README.md
git commit -m "update readme for patch"

Script seperti ini tidak perlu rumit. Tujuannya adalah menghasilkan graph commit yang bisa dipakai ulang oleh integration test.

Kapan mock tetap berguna

Mock tetap relevan untuk dependency non-inti seperti:

  • lapisan transport jaringan;
  • clock atau sumber waktu;
  • random ID generator;
  • penyimpanan sementara untuk memicu error path tertentu.

Namun untuk object yang memengaruhi validitas patch dan replikasi, seperti repo Git dan event store, lebih aman memakai implementasi nyata atau in-memory fake yang sangat dekat dengan perilaku asli.

Skenario uji penting untuk sinkronisasi antar-peer

1. Peer offline lalu online kembali

Ini skenario paling mendasar untuk sistem P2P. Yang diuji bukan hanya apakah peer tertinggal bisa mengejar ketertinggalan, tetapi juga apakah hasil akhirnya konvergen.

  1. Peer A membuat issue dan dua komentar baru.
  2. Peer B sedang offline.
  3. Peer A menutup issue.
  4. Peer B online kembali dan sinkron.
  5. Assert bahwa peer B memiliki issue, semua komentar, dan status akhir closed.

Varian penting: biarkan peer B juga membuat komentar saat offline, lalu uji bagaimana sistem menangani konflik saat sinkronisasi kembali.

2. Duplikasi event akibat retry

Simulasikan event yang sama dikirim dua atau tiga kali. Assert yang penting:

  • komentar tidak terduplikasi;
  • status patch tidak berubah bolak-balik;
  • counter atau audit log tidak bertambah secara salah jika kontraknya menuntut deduplikasi;
  • hash state akhir tetap sama.

3. Urutan replikasi berbeda, hasil tetap sama

Karena jaringan nyata tidak menjamin urutan global, sistem Anda harus tahan terhadap penerimaan event dalam urutan berbeda selama aturan kausalitas minimum terpenuhi.

Contoh:

  1. Peer A membuat patch.
  2. Peer B menerima review dulu, lalu metadata patch.
  3. Sistem seharusnya menahan review sebagai pending atau menolaknya dengan alasan yang eksplisit.
  4. Setelah patch dasar tersedia, state akhir harus benar dan konsisten.

Test seperti ini sering membuka bug pada kode yang terlalu berasumsi bahwa semua dependency selalu sudah ada.

4. Verifikasi patch terhadap referensi commit

Patch yang terlihat valid secara format bisa tetap tidak valid bila basis commit tidak dikenal atau objek Git belum tersedia. Test ini penting untuk mencegah acceptance yang salah.

test("reject patch when base commit is missing", async () => {
  const peer = createPeerWithEmptyRepo()
  const patch = loadFixture("patch-with-unknown-base.json")

  const result = await peer.importPatch(patch)

  expect(result.ok).toBe(false)
  expect(result.reason).toBe("missing-base-commit")
})

Nama error tidak harus sama dengan contoh, tetapi kategorinya harus stabil agar mudah diobservasi dan diuji.

5. Konflik state issue

Misalnya satu peer menutup issue, peer lain menambahkan komentar, lalu keduanya sinkron. Test harus memeriksa apakah:

  • komentar tetap tersimpan;
  • status akhir sesuai aturan sistem;
  • histori event tidak hilang;
  • hasil resolusi konflik deterministik.

Jika sistem Anda menerapkan aturan seperti last writer wins, tulis test secara eksplisit untuk membuktikan konsekuensinya. Jika memakai merge berbasis event log, uji bahwa replay selalu menghasilkan hasil akhir yang sama.

Contract format data: lindungi kompatibilitas antar-peer

Pada workflow patch Git P2P, kontrak data adalah fondasi interoperabilitas. Sedikit perubahan pada serializer atau parser dapat memecahkan sinkronisasi dengan peer lain tanpa error yang jelas.

Apa yang perlu dikunci dalam kontrak

  • Identifier stabil: event ID, patch ID, issue ID, peer ID.
  • Representasi hash dan reference: huruf besar-kecil, prefix, atau bentuk pendek vs penuh.
  • Timestamp: format serialisasi harus konsisten; jika presisi waktu tidak penting, jangan membuat assert terlalu detail.
  • Unknown fields: putuskan apakah parser mengabaikan field tak dikenal atau menganggapnya error.
  • Backward compatibility: payload lama harus tetap dapat dibaca selama masa migrasi yang ditentukan.

Praktik yang umum adalah menyimpan fixture berikut:

  • payload valid minimal;
  • payload valid lengkap;
  • payload dengan field tambahan;
  • payload invalid karena field wajib hilang;
  • payload invalid karena referensi objek tidak sah.

Catatan: jika Anda menghitung hash dari payload, dokumentasikan dengan jelas apakah whitespace, urutan field, atau nilai default ikut memengaruhi hash. Banyak regresi deduplikasi berawal dari canonicalization yang tidak konsisten.

Simulasi peer offline/online dan jaringan tidak stabil

Flaky test sering muncul ketika simulasi jaringan dibuat terlalu realistis pada level yang salah. Kuncinya adalah memilih level abstraksi yang tepat: cukup realistis untuk memicu race dan retry, tetapi cukup terkendali agar hasil test dapat dijelaskan.

Gunakan scheduler atau event queue yang bisa dikontrol

Daripada benar-benar bergantung pada delay acak jaringan, lebih baik gunakan komponen simulasi yang memungkinkan Anda mengatur:

  • pesan ditahan sementara;
  • pesan digandakan;
  • pesan dibuang;
  • urutan pengiriman diubah.

Dengan model ini, Anda bisa membuat test yang tetap deterministik walau meniru kondisi jaringan bermasalah.

const network = createTestNetwork()
network.dropNextMessage(fromPeerA, toPeerB)
network.duplicateNextMessage(fromPeerA, toPeerB)
network.pausePeer(peerB)
network.resumePeer(peerB)

Contoh di atas adalah pseudo-API, tetapi idenya penting: kontrol eksplisit lebih baik daripada mengandalkan timeout acak.

Batasi penggunaan sleep dan timeout tetap

Salah satu penyebab flaky terbesar adalah pola seperti "tunggu 2 detik lalu assert". Ini rapuh karena kecepatan CI dan lokal berbeda. Lebih aman gunakan mekanisme:

  • polling dengan batas waktu wajar dan kondisi yang jelas;
  • hook ketika replikasi selesai;
  • event acknowledgement untuk test harness;
  • virtual clock bila memungkinkan.

Gate CI: pisahkan test deterministik dan non-deterministik

Jika semua test digabung dalam satu gate CI yang sama, pipeline akan mudah tidak stabil. Untuk workflow patch Git P2P, praktik yang lebih aman adalah memisahkan lapisan test berdasarkan determinisme dan biaya eksekusi.

Lapisan gate yang disarankan

  • Gate cepat dan wajib: unit test, contract test, integration test deterministik.
  • Gate menengah: integration test dengan simulasi urutan replikasi, replay event, dan peer offline/online yang masih terkendali.
  • Gate observasional/non-blocking atau terjadwal: stress test, chaos-like test, dan skenario nondeterministik yang sengaja mengeksplor race lebih luas.

Pemisahan ini membantu dua hal:

  1. regresi nyata tertangkap cepat oleh test yang stabil;
  2. eksperimen kondisi jaringan tetap berjalan tanpa merusak kepercayaan tim pada CI.

Jangan campur definisi kegagalan

Test deterministik harus gagal hanya jika ada bug atau perubahan kontrak. Test non-deterministik sebaiknya dikategorikan berbeda, misalnya nightly atau scheduled run, lalu dianalisis sebagai sinyal kualitas, bukan blocker utama setiap commit.

Kapan memakai integration test dibanding end-to-end test

Pertanyaan ini penting karena banyak tim terlalu cepat menaikkan skenario ke E2E, padahal integration test lebih murah dan lebih presisi untuk debugging.

Pakai integration test jika:

  • Anda ingin menguji sinkronisasi antar-peer dengan repositori nyata tetapi tanpa seluruh stack produksi.
  • Anda perlu mengontrol urutan event, replay, duplikasi, atau peer offline secara presisi.
  • Anda ingin memverifikasi konvergensi state, idempotensi, dan validasi patch dengan diagnosis yang jelas.

Pakai end-to-end test jika:

  • Anda perlu membuktikan alur pengguna lengkap dari pembuatan patch/issue sampai sinkronisasi dan pembaruan state.
  • Anda ingin memverifikasi integrasi antarproses, konfigurasi runtime, atau packaging yang tidak terlihat di integration test.
  • Anda sedang menguji jalur kritis yang paling sering dipakai dan paling berisiko dari sudut operasional.

Aturan praktisnya: uji sebanyak mungkin perilaku replikasi di integration test, dan sisakan E2E untuk validasi perjalanan sistem yang benar-benar penting.

Checklist anti-flaky untuk test workflow patch Git P2P

  • Hindari sleep tetap; tunggu berdasarkan kondisi atau event.
  • Gunakan identifier deterministik dalam test bila memungkinkan.
  • Bekukan atau virtualisasikan waktu jika timestamp memengaruhi hasil.
  • Pastikan fixture repo dibuat ulang per test atau benar-benar terisolasi.
  • Jangan assert urutan event kecuali memang kontrak sistem mensyaratkannya.
  • Uji idempotensi untuk operasi impor, sinkronisasi, dan replay event.
  • Simpan log diagnostik yang cukup: event ID, peer ID, state hash, dan referensi commit.
  • Gunakan retry hanya untuk harness yang jelas-jelas nondeterministik, bukan untuk menutupi bug.
  • Pisahkan test cepat deterministik dari test stress atau chaos.
  • Jika test gagal sesekali, investigasi akar masalahnya; jangan langsung menambah timeout.

Contoh matriks skenario uji yang layak diprioritaskan

  1. Patch valid, sinkron normal: baseline untuk memastikan jalur sehat selalu terjaga.
  2. Patch invalid, base commit hilang: mencegah acceptance yang merusak state.
  3. Issue diperbarui saat peer offline: uji catch-up dan konvergensi.
  4. Event duplikat: uji idempotensi.
  5. Urutan replikasi berbeda: uji robust terhadap asinkroni.
  6. Konflik status dan komentar: uji resolusi konflik domain.
  7. Parser menerima field tambahan: uji kompatibilitas antar-peer.
  8. Replay event log dari awal: uji determinisme rekonstruksi state.

Penutup

Strategi test untuk workflow patch Git P2P tanpa regresi berpusat pada satu prinsip: pastikan sistem tetap konvergen, dapat mengulang operasi dengan aman, dan tahan terhadap variasi replikasi. Unit test menjaga aturan domain, contract test melindungi format data, integration test memverifikasi sinkronisasi yang nyata, dan end-to-end test memastikan jalur kritis tetap bekerja dari ujung ke ujung.

Jika Anda hanya mengambil beberapa langkah awal, mulai dari tiga hal ini: buat fixture repo yang realistis, tulis test idempotensi untuk semua operasi replikasi utama, dan pisahkan gate CI untuk test deterministik vs non-deterministik. Tiga langkah tersebut biasanya sudah cukup untuk menurunkan regresi paling mahal pada sistem kolaborasi Git P2P.