tsma — Garis Pertahanan Regresi untuk Kode Legacy

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).

HasilJumlahPersentase
PASS (100% branch coverage)24646,7%
DONE (best-effort)28153,3%
TODO (belum diproses)00%

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.

Konvensionaltsma
Penulisan testManusia (lambat) atau LLM (kacau)LLM menulis, CLI memverifikasi
Mulai dari mana?Manusia memutuskanCLI menentukan urutan
Pemeriksaan kualitasManusia me-reviewCLI mengukur coverage
FeedbackTidak adaNomor baris branch yang belum ter-cover
Pelacakan progresTidak adasession.json otomatis

LLM menghasilkan dengan bebas. Tapi hanya berjalan di atas rel tsma next.


Dukungan bahasa

BahasaIndexerTest runnerCoverage
Gogo/astgo testgo test -coverprofile
TypeScriptregexnpx vitest / npx jestc8 / istanbul
Pythonregexpytestcoverage.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