
Bagaimana cara refactoring kode tanpa test?
Anda mewarisi seratus ribu baris kode legacy. Tidak ada test. Ingin refactoring, tapi tidak tahu apa yang akan rusak kalau disentuh. Untuk menulis test, perlu memahami kodenya; untuk memahami kode, perlu dokumentasi — yang juga tidak ada.
Tidak ada yang berani menyentuh. Semakin membusuk.
Semua kode legacy di dunia terjebak dalam kebuntuan ini. Perusahaan Fortune 500 menghabiskan 60–80% anggaran IT mereka untuk pemeliharaan sistem legacy. 42% waktu developer dihabiskan untuk menangani utang teknis.
Bagaimana kalau LLM bisa menulis test untuk Anda?
Masalah saat menyerahkan test ke LLM
Ketika Anda meminta LLM “tulis test untuk fungsi ini”, sesuatu memang keluar. Masalahnya ada tiga:
Pertama, tidak tahu harus mulai dari mana. Dengan 527 fungsi — mulai dari nomor 1 secara berurutan? Dari yang paling penting? Tidak ada kriteria.
Kedua, kualitas test tidak bisa diverifikasi. LLM menulis test dan lolos. Tapi apakah test itu benar-benar memverifikasi perilaku fungsi, atau hanya memanggil tanpa assert — cangkang kosong? Harus dibaca manual satu per satu.
Ketiga, tanpa feedback, test dari LLM berhenti di 60–70%. Hanya “test fungsi ini” saja tidak cukup untuk mencapai 100% branch coverage. Harus diberitahu branch mana yang terlewat agar bisa dilengkapi.
LLM bukan tidak mampu menulis test. Masalahnya adalah tidak ada struktur yang memberitahu apa yang harus ditulis dan seberapa baik hasilnya.
tsma: rel test yang berjalan dengan satu perintah
tsma adalah alat CLI yang mengindeks semua fungsi dalam proyek, mendeteksi keberadaan test, mengukur coverage, dan memberikan feedback akurat kepada agen LLM.
Perintah yang perlu diketahui agen hanya satu:
$ tsma next
Satu perintah ini menggerakkan seluruh loop:
$ tsma next # menampilkan fungsi berikutnya yang belum punya test
→ tulis test-nya
$ tsma next # mendeteksi test baru, menjalankan, mengukur coverage
→ 100%? PASS, lanjut ke fungsi berikutnya
→ <100%? menampilkan branch yang belum ter-cover beserta nomor baris
$ tsma next # mengukur ulang test yang sudah diperbaiki
→ membaik atau tidak, ditandai DONE dan lanjut
Diulang sampai muncul “All functions complete!”.
Divalidasi pada 527 fungsi
tsma diterapkan pada proyek Go nyata (527 fungsi).
| Hasil | Jumlah | Persentase |
|---|---|---|
| PASS (100% branch coverage) | 246 | 46,7% |
| DONE (best-effort) | 281 | 53,3% |
| TODO (belum diproses) | 0 | 0% |
246 fungsi mencapai 100% branch coverage. 281 sisanya tidak mencapai 100%, tetapi test ditulis sejauh yang memungkinkan.
Mengapa ada fungsi yang tidak bisa mencapai 100%?
Fungsi yang mencapai 100% dan yang tidak
Apakah sebuah fungsi bisa mencapai 100% branch coverage bergantung pada bagaimana ia menerima dependensinya.
Interface (mockable) — 100% bisa dicapai:
type Handler struct {
svc AuthSvc // interface — bisa diganti dengan mock
}
Dengan meng-inject mock dalam test, semua jalur bisa dikontrol:
svc := mocks.NewMockAuthSvc(ctrl)
svc.EXPECT().Login(...).Return(result, nil) // jalur sukses
svc.EXPECT().Login(...).Return(nil, err) // jalur gagal
Tipe konkret (not mockable) — 100% tidak mungkin:
type Handler struct {
svc *service.SMSImportService // pointer struct — tidak bisa diganti
}
Implementasi asli berjalan dengan dependensi internal seperti database dan API eksternal. Tidak bisa memicu error tertentu atau memaksa hasil tertentu. Branch yang bergantung pada hasil tersebut tidak bisa dijangkau oleh unit test.
Respons tsma: setelah feedback tentang branch yang belum ter-cover, mencoba sekali lagi. Kalau tetap tidak tercapai, diterima sebagai DONE. Ini bukan keterbatasan alat, melainkan cerminan testability kode. Memperkenalkan interface (DI) membuat 100% menjadi mungkin, tapi itu berarti memodifikasi kode asli.
Feedback mengubah test LLM secara drastis
Nilai inti tsma bukan indexing atau pengukuran coverage. Melainkan menunjuk branch yang belum ter-cover secara tepat dengan nomor baris.
Tanpa feedback:
"Tulis test untuk fungsi ListContracts"
→ LLM hanya menguji happy path
→ coverage 60–70%
Dengan feedback:
"Tulis test untuk fungsi ListContracts"
→ coverage 65% (11/17)
→ UNCOVERED:
line 41 — if params.Status != nil
line 44 — if params.BuildingId != nil
line 70 — if err != nil (CountSummary)
→ LLM menambahkan test yang meng-cover branch tersebut secara tepat
→ coverage 100%
LLM yang sama. Perbedaannya hanya ada atau tidaknya feedback. Tiga baris nomor baris memisahkan 60% dan 100%.
Agen mati pun, progres tetap tersimpan
Agen LLM bisa crash. Batas token, error jaringan, sesi terputus. Tidak mungkin memproses 527 fungsi dalam satu sesi.
tsma menyimpan status progres secara persisten di .tsma/session.json.
$ tsma status
527 functions
PASS: 246 (46.7%)
DONE: 281 (53.3%)
TODO: 0 (0.0%)
Agen berhenti di fungsi ke-200? Agen baru menjalankan tsma next dan melanjutkan dari ke-201. session.json adalah checkpoint.
Beberapa agen bisa bergantian bekerja tanpa konflik. Operasi bersifat atomik di level fungsi.
Session adalah cache, file sumber adalah kebenaran
Salah satu prinsip desain tsma: session adalah cache dan file sumber adalah source of truth.
Jika Anda menghapus file test, meskipun session.json mencatatnya sebagai PASS, fungsi tersebut kembali menjadi TODO. Session tidak terlepas dari kenyataan.
Prinsip:
meskipun session.json mengatakan "PASS"
jika file test tidak ada → TODO
jika file sumber berubah → diukur ulang
Instruksi untuk agen LLM
Agen hanya butuh 6 baris:
1. Jalankan tsma next
2. Jika TODO — baca fungsinya dan tulis test
3. Jika test gagal — baca error-nya dan perbaiki test
4. Jika branch belum ter-cover muncul — tambahkan test yang meng-cover-nya
5. Jika PASS/DONE — fungsi berikutnya muncul otomatis
6. Ulangi sampai muncul "All functions complete!"
Perintah yang perlu diketahui agen hanya tsma next. Sisanya dikontrol oleh CLI.
Kereta dan rel
Vibe coding adalah kereta. Cepat. Tapi tanpa rel, tergelincir.
Semua alat AI coding fokus pada membuat kereta lebih cepat. Model lebih besar, agen lebih pintar, prompt lebih baik. Tapi semakin cepat keretanya, semakin besar kerusakan saat tergelincir.
tsma adalah relnya. LLM menghasilkan test (Neural), dan CLI mendefinisikan “sampai sini saja” (Symbolic Constraint). Kreativitas LLM tetap bebas, tapi kualitas hasil dipaksakan oleh mesin.
| Konvensional | tsma | |
|---|---|---|
| Penulisan test | Manusia (lambat) atau LLM (kacau) | LLM menulis, CLI memverifikasi |
| Mulai dari mana? | Manusia memutuskan | CLI menentukan urutan |
| Pemeriksaan kualitas | Manusia me-review | CLI mengukur coverage |
| Feedback | Tidak ada | Nomor baris branch yang belum ter-cover |
| Pelacakan progres | Tidak ada | session.json otomatis |
LLM menghasilkan dengan bebas. Tapi hanya berjalan di atas rel tsma next.
Dukungan bahasa
| Bahasa | Indexer | Test runner | Coverage |
|---|---|---|---|
| Go | go/ast | go test | go test -coverprofile |
| TypeScript | regex | npx vitest / npx jest | c8 / istanbul |
| Python | regex | pytest | coverage.py |
Go menggunakan parser AST untuk ekstraksi fungsi yang akurat. TypeScript dan Python berbasis ekspresi reguler.
File yang di-generate (*_gen.go, *.pb.go), file test, dan vendor/node_modules secara otomatis dikecualikan dari indexing.
Instalasi dan menjalankan
make install
cd your-legacy-project
tsma next
Itu saja.
MIT License. github.com/park-jun-woo/tsma