tsma — קו ההגנה נגד רגרסיות בקוד ישן

איך מבצעים refactoring לקוד בלי טסטים?

קיבלתם בירושה 100,000 שורות קוד ישן. בלי טסטים. רוצים לעשות refactoring, אבל לא יודעים מה ישבר אם נוגעים. כדי לכתוב טסטים צריך להבין את הקוד, וכדי להבין את הקוד צריך תיעוד — שלא קיים.

אף אחד לא נוגע. הקוד ממשיך להירקב.

כל קוד ישן בעולם תקוע בקיפאון הזה. 60–80% מתקציב ה-IT של חברות Fortune 500 מוקדש לתחזוקת מערכות ישנות. 42% מזמן המפתחים מוקדש לטיפול בחוב טכני.

מה אם LLM יכול לכתוב את הטסטים במקומכם?


הבעיות כשמפקידים טסטים בידי LLM

כשאומרים ל-LLM “כתוב טסט לפונקציה הזו”, משהו יוצא. הבעיה משולשת.

ראשית, לא יודעים מאיפה להתחיל. כשיש 527 פונקציות — לפי הסדר מהראשונה? הכי חשובה קודם? אין קריטריון.

שנית, אי אפשר לאמת את איכות הטסטים. הטסט שה-LLM כתב עבר. אבל האם הטסט הזה באמת מוודא את התנהגות הפונקציה, או שזה סתם קריאה לפונקציה בלי assert אחד? צריך לקרוא כל טסט ידנית כדי לדעת.

שלישית, בלי משוב ה-LLM נתקע ב-60–70%. רק עם “כתוב טסט לפונקציה הזו” לא מגיעים ל-100% branch coverage. צריך לומר ל-LLM אילו ענפים חסרים כדי שישלים את השאר.

ה-LLM לא חסר יכולת לכתוב טסטים. הבעיה היא שאין מבנה שאומר ל-LLM מה לבדוק וכמה טוב הוא בדק.


tsma: מסילת טסטים שרצה בפקודה אחת

tsma הוא כלי CLI שמאנדקס את כל הפונקציות בפרויקט, מזהה קיום טסטים, מודד coverage ונותן ל-LLM agent משוב מדויק.

הפקודה היחידה שה-agent צריך לדעת:

$ tsma next

הפקודה הזו מניעה את כל הלולאה:

$ tsma next          # מציגה את הפונקציה הבאה ללא טסט
  → כותבים טסט
$ tsma next          # מזהה את הטסט החדש, מריצה, מודדת coverage
  → 100%? PASS, הלאה לפונקציה הבאה
  → <100%? מציגה ענפים לא מכוסים עם מספרי שורות
$ tsma next          # מודדת מחדש את הטסט המתוקן
  → השתפר או לא, מסמנת DONE וממשיכה הלאה

חוזרים עד שמופיע “All functions complete!”.


אומת על 527 פונקציות

tsma הופעל על פרויקט Go אמיתי עם 527 פונקציות.

תוצאהמספראחוז
PASS (100% branch coverage)24646.7%
DONE (best-effort)28153.3%
TODO (לא טופל)00%

246 פונקציות הגיעו ל-100% branch coverage. 281 הנותרות לא הגיעו ל-100%, אבל טסטים נכתבו עד כמה שניתן.

למה יש פונקציות שלא מגיעות ל-100%?


פונקציות שמגיעות ל-100% — ואלה שלא

האם פונקציה יכולה להגיע ל-100% branch coverage תלוי באיך היא מקבלת את התלויות שלה.

Interface (mockable) — 100% אפשרי:

type Handler struct {
    svc AuthSvc              // interface — ניתן להחליף ב-mock
}

בטסט מזריקים mock ושולטים בכל הנתיבים:

svc := mocks.NewMockAuthSvc(ctrl)
svc.EXPECT().Login(...).Return(result, nil)   // נתיב הצלחה
svc.EXPECT().Login(...).Return(nil, err)      // נתיב כישלון

טיפוס קונקרטי (not mockable) — 100% בלתי אפשרי:

type Handler struct {
    svc *service.SMSImportService    // מצביע ל-struct — לא ניתן להחלפה
}

המימוש האמיתי רץ עם תלויות פנימיות כמו בסיס נתונים או API חיצוני. אי אפשר לגרום לשגיאה ספציפית או לתוצאה ספציפית. ענפים שתלויים בתוצאות כאלה לא נגישים ב-unit test.

התגובה של tsma: אחרי משוב על ענפים לא מכוסים, ניסיון נוסף. אם עדיין לא מגיעים — מסומן כ-DONE. זו לא מגבלה של הכלי אלא שיקוף של יכולת הבדיקה של הקוד. עם interfaces (DI) אפשר להגיע ל-100% — אבל זה דורש שינוי בקוד המקורי.


משוב משנה את הטסטים של LLM באופן דרמטי

הערך המרכזי של tsma הוא לא האינדוקס ולא מדידת ה-coverage. הערך הוא הצגת ענפים לא מכוסים עם מספרי שורות מדויקים.

בלי משוב:

"כתוב טסט לפונקציה ListContracts"
→ LLM בודק רק את ה-happy path
→ coverage 60–70%

עם משוב:

"כתוב טסט לפונקציה 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 מוסיף טסטים שמכסים בדיוק את הענפים האלה
→ coverage 100%

אותו LLM בדיוק. ההבדל היחיד הוא המשוב. שלוש שורות עם מספרי שורות מפרידות בין 60% ל-100%.


כשה-agent קורס, ההתקדמות נשמרת

LLM agents קורסים. מגבלת tokens, שגיאת רשת, ניתוק סשן. אי אפשר לטפל ב-527 פונקציות בסשן אחד.

tsma שומר את מצב ההתקדמות בקובץ .tsma/session.json.

$ tsma status

527 functions
PASS:  246 (46.7%)
DONE:  281 (53.3%)
TODO:    0 (0.0%)

ה-agent קרס בפונקציה ה-200? agent חדש מקליד tsma next וממשיך מה-201. session.json הוא ה-checkpoint.

כמה agents יכולים לעבוד לסירוגין בלי התנגשויות. כל פונקציה היא יחידה אטומית.


הסשן הוא cache, קובץ המקור הוא האמת

עיקרון תכנוני של tsma: הסשן הוא cache וקובץ המקור הוא ה-source of truth.

אם מוחקים קובץ טסט, הפונקציה חוזרת ל-TODO — גם אם session.json מסמן אותה כ-PASS. הסשן לעולם לא מתנתק מהמציאות.

עיקרון:
  session.json אומר "PASS"?
  אין קובץ טסט → TODO
  קובץ המקור השתנה → מדידה מחדש

הוראות ל-LLM agent

ה-agent צריך 6 שורות הוראות:

1. להריץ tsma next
2. TODO → לקרוא את הפונקציה ולכתוב טסט
3. טסט נכשל → לקרוא את השגיאה ולתקן את הטסט
4. ענפים לא מכוסים מוצגים → להוסיף טסטים שמכסים אותם
5. PASS/DONE → הפונקציה הבאה מוצגת אוטומטית
6. לחזור עד שמופיע "All functions complete!"

ה-agent צריך לדעת פקודה אחת בלבד: tsma next. השאר נכפה על ידי ה-CLI.


הרכבת והמסילה

Vibe coding זו רכבת. מהירה. אבל בלי מסילה היא יוצאת מהפסים.

כל כלי ה-AI coding מתמקדים בלהפוך את הרכבת למהירה יותר. מודלים גדולים יותר, agents חכמים יותר, prompts טובים יותר. אבל ככל שהרכבת מהירה יותר, הנזק מיציאה מהפסים גדול יותר.

tsma היא המסילה. ה-LLM מייצר טסטים (Neural), ה-CLI מגדיר “עד כאן ולא יותר” (Symbolic Constraint). היצירתיות של ה-LLM נשארת כמות שהיא, אבל איכות התוצאה נכפית על ידי המכונה.

קודםtsma
כתיבת טסטיםאדם (איטי) או LLM (כאוטי)LLM כותב, CLI מאמת
מאיפה מתחילים?אדם מחליטCLI קובע את הסדר
בדיקת איכותאדם עושה reviewCLI מודד coverage
משובאיןענפים לא מכוסים עם מספרי שורות
מעקב התקדמותאיןsession.json אוטומטי

ה-LLM מייצר בחופשיות. אבל הוא רץ רק על המסילה של tsma next.


תמיכה בשפות

שפהindexertest runnercoverage
Gogo/astgo testgo test -coverprofile
TypeScriptregexnpx vitest / npx jestc8 / istanbul
Pythonregexpytestcoverage.py

Go משתמש ב-AST parser לחילוץ פונקציות מדויק. TypeScript ו-Python מבוססים על ביטויים רגולריים.

קבצים שנוצרו אוטומטית (*_gen.go, *.pb.go), קבצי טסט ו-vendor/node_modules מוחרגים אוטומטית מהאינדוקס.


התקנה והרצה

make install
cd your-legacy-project
tsma next

זה הכל.

MIT License. github.com/park-jun-woo/tsma