nub/docs/TESTING.md
Tom a97013d876 initial commit
- v 0.1.0 siehe CHANGELOG.md
2026-07-25 11:08:01 +02:00

14 KiB

Testing

Für Endnutzer-Doku siehe die README und FEATURES.md. Diese Seite ist für die Weiterentwicklung von nub selbst: automatisierte Tests, manuelle Testrezepte pro Feature, und was aktuell (noch) nicht abgedeckt ist.

Automatisierte Tests

go build ./...
go vet ./...
go test ./...

Deckt ab: SSE-Fragmentierung/Tool-Call-Akkumulation (Golden File), Agent-Loop gegen einen Fake-Provider (Multi-Turn, Tool-Roundtrip, Stream-Fehler), Datei-Tools inkl. Pfad-Escape via ../ und Symlink, bash-Timeout mit Prozessgruppen-Kill, Architektur-Test für E-04 (kein Kern-Paket importiert internal/ui), Session-Baum (Resume-Roundtrip, Rewind+Branch, Dateirechte, Versions-Fehlerfall), Config-Layering-Präzedenz, Context Assembler (walk_up-Reihenfolge, Budget-Truncation+Warnung, Repomap-Staleness), Auto-Compaction (Trigger, Turn-Grenzen-Logik, Rewind auf einen Knoten vor der Compaction), Token-Kalibrierung, todo-Tool, Skill-Discovery (Index ohne Body, Präzedenz projekt-vor-global), MCP-Bridge (echter In-Process-Server- Roundtrip über mcp.NewInMemoryTransports, Allowlist, Laufzeitfehler ohne Panic), Profile-Filterung, TUI (Slash-Command-Dispatch ohne Backend-Kontakt, Steering- vs. Follow-up-Queue-Semantik, Tree-/Sessions-Modal-Auswahl inkl. Branch/Wechsel/Löschen mit Rückfrage, Diff-/Fehler-Rendering, /help- Formatierung, Skills-als-Slash-Commands, Zeilenumbruch, historische Token-Wiederherstellung), Permission-Policy (Modus-Auflösung, deny_paths/ deny_bash überstimmen auto/ask), Agent-Loop-Permission-Integration (deny/ask blockieren die Tool-Ausführung nachweislich, nil-Hook -> harter Fehler), TUI-Rückfrage-Modal (blockiert bis zur Antwort, Kontextabbruch löst sauber auf), Scaffold/nub init (legt an, überschreibt bestehende Dateien nie, generierte Config parst und rundtripped korrekt, Git-Exclude wird gesetzt), question-Tool (nil-Hook -> harter Fehler, leere Frage abgelehnt, Antwort-Roundtrip inkl. Options, Ask-Fehler landet als IsError), TUI-Rückfrage-Block für question (blockiert bis zur Antwort, Kontextabbruch löst sauber auf, Eingabe während offener Frage wird als Antwort statt als Chat-Nachricht/Slash-Command behandelt, kein Overlay).

Architektur-Nachweis M6 (TUI ist ein reiner Consumer, entfernbar): TestArchitecture_NoUIImportBelowAgent (internal/agent/arch_test.go) verbietet jedem Kern-Paket den Import von internal/ui/*, also auch internal/ui/tui — automatisiert bei jedem Testlauf geprüft. Zusätzlich manuell verifiziert: internal/ui/tui entfernt, go build ./internal/... bleibt grün, cmd/nub baut nach Entfernen der TUI-Verzweigung ebenfalls, -p verhält sich danach identisch.

Manuelle Testrezepte

Die Tests unten gehen von nub -p aus (die beschriebenen /-Zeilen auf stderr sind Print-Modus-Ausgabe). Dieselben Szenarien lassen sich genauso in der TUI (nub, ohne -p) durchspielen — dort erscheinen Tool-Aufrufe direkt im Transkript statt auf stderr, und Ctrl+C/Rewind laufen über die im README beschriebenen Tastenkombinationen/Kommandos.

Aus dem Konzept, Abschnitt 6:

Ein mehrstufiger Task ("lies X, ändere Y, führe die Tests aus") läuft gegen einen echten Endpoint durch. Ctrl+C bricht mitten in einem bash-Aufruf sauber ab und tötet die Prozessgruppe.

Test 1 — Mehrstufiger Task

In einem kleinen Testrepo (z.B. mit ein paar .go-Dateien und go test lauffähig):

Lies main.go, füge einen Kommentar über der main-Funktion hinzu der "entry point" sagt, und führe danach go build ./... aus um zu prüfen dass es noch kompiliert.

Erwartet: Das Modell ruft nacheinander read, edit und bash auf (sichtbar an den /-Zeilen auf stderr), am Ende eine Textantwort mit dem Ergebnis. Danach Ctrl+D, dann nub sessions — die eben gelaufene Session sollte mit korrekter Node-Anzahl auftauchen. nub resume <id> und eine weitere Nachricht schicken sollte den Kontext (z.B. den Dateinamen) noch kennen, ohne dass du ihn wiederholst.

Test 1b — Rewind/Branch

Nach Test 1: die Node-ID kurz vor der letzten Antwort ermitteln (siehe README, Abschnitt „Sessions") und damit fortsetzen:

nub resume <session-id> --branch <node-id>
Mach stattdessen etwas anderes: ...

Erwartet: nub schreibt ab hier einen zweiten Ast. cat .nub/sessions/<id>.jsonl | wc -l zeigt, dass die alten Zeilen erhalten bleiben (nur neue kommen dazu). Mit dem --branch-Trick lässt sich sowohl der alte als auch der neue Ast wieder aufrufen.

Test 1c — Auto-Compaction

Eine echte Session lang genug laufen zu lassen, um 75% des Modell-Fensters zu füllen, dauert. Stattdessen max_context künstlich klein setzen, um den Trigger nach wenigen Nachrichten auszulösen — in .nub/config.toml:

version = 1
[[endpoint]]
name = "openai"
base_url = "https://api.openai.com/v1"
api_key  = "env:OPENAI_API_KEY"
  [endpoint.caps]
  max_context = 500

Dann nub starten und 4-5 kurze Nachrichten hintereinander schicken. Erwartet: Nach ein paar Nachrichten erscheint auf stderr compacted N nodes into a M-token summary. nub läuft danach normal weiter (das Modell kennt den zusammengefassten Kontext noch grob).

Dann Ctrl+D, Node-ID der allerersten Nachricht ermitteln (siehe README, „Sessions") und:

nub resume <session-id> --branch <node-id>

Erwartet: Das funktioniert weiterhin, obwohl dieser Knoten längst wegkompaktiert wurde — die Original-Nodes bleiben im Baum erhalten. .nub/config.toml danach wieder auf einen realistischen max_context zurücksetzen (oder den Override entfernen).

Test 2 — Ctrl+C mitten in bash

Führe "sleep 30" aus.

Während → bash {...} läuft: Ctrl+C drücken. Erwartet:

  • nub beendet sich zügig (nicht erst nach 30s)
  • stderr zeigt aborted, Exit-Code ist 3
  • Kein Zombie-Prozess bleibt übrig — prüfen mit ps aux | grep sleep direkt danach, sollte leer sein

Test 3 — Mehrdeutiger edit-Treffer

Ersetze in main.go den Text "return" durch "return " (falls "return" mehrfach vorkommt)

Erwartet: Falls return mehrfach in der Datei steht, meldet das edit-Tool einen Fehler mit Trefferzahl statt zu raten — sichtbar als ← edit [error] auf stderr, das Modell sollte daraufhin entweder replace_all nutzen oder den old_string präzisieren.

Test 4 — Unbekanntes Tool / Fehlerpfad

Kein manueller Test nötig, ist per Fake-Provider automatisiert abgedeckt (go test ./...).

Test 5 — Kaputter MCP-Server verhindert den Start nicht

version = 1
[[mcp]]
name    = "broken"
command = "this-binary-does-not-exist"
nub mcp

Erwartet: warning: mcp server "broken": ... auf stderr, Exit-Code 0, kein Absturz. Danach echtes nub starten (mit derselben Config) — muss trotz des kaputten Servers normal hochkommen.

Test 6 — Skill wird erst bei Bedarf geladen

Ein Skill wie im README (Abschnitt „Skills") anlegen, dann im laufenden nub fragen: Welche Skills hast du verfügbar, und was steht in "refactoring"? — erwartet: Das Modell nennt den Skill aus dem Index, ruft danach sichtbar → read_skill {"name":"refactoring"} auf, um den Body zu laden — nicht schon beim Start.

Test 7 — Steering vs. Follow-up, Rewind über die TUI

Nur in der TUI (nub, ohne -p):

  1. Einen Task starten, der mehrere Tool-Aufrufe braucht (wie Test 1). Während running: ... in der Statuszeile steht, eine neue Nachricht eingeben und Enter drücken (Steering). Erwartet: Sie wird eingeschleust, sobald der laufende Tool-Aufruf fertig ist, ohne dass du warten musst.
  2. Denselben Versuch mit Alt+Enter. Erwartet: Statuszeile zeigt queued: 1, die Nachricht erscheint im Transkript mit -Präfix und wird erst gesendet, wenn der Turn ganz fertig ist (kein tool_use mehr offen).
  3. /tree (oder /branch) öffnen — Liste aller Nodes erscheint, aktueller Head markiert. Mit ↑/↓ einen früheren Knoten wählen, Enter drücken. Erwartet: Head springt zurück, eine neue Nachricht hängt sich dort an (zweiter Ast, wie in Test 1b, nur ohne CLI-Flag).
  4. /export ausführen, geschriebene Datei öffnen — Transkript sollte vollständig als Markdown lesbar sein, inklusive Diffs.
  5. /help ausführen — jedes Kommando sollte in einer eigenen Zeile mit kurzer Beschreibung erscheinen, nicht als ein langer Fließtext.

Test 8 — Prompt-Cache-Verifikation (M7)

Nur mit echtem OpenAI-Endpoint sinnvoll (lokale Server cachen meist nicht). OpenAIs automatisches Prompt-Caching greift erst ab >1024 Tokens stabilem Prefix — ein AGENTS.md/REPOMAP.md im Kontext hilft, diese Schwelle zuverlässig zu überschreiten.

  1. In einem Repo mit ein paar hundert Zeilen Kontext (nub map vorher laufen lassen, damit REPOMAP.md existiert) nub -p starten.
  2. Zwei, drei kurze Nachrichten nacheinander schicken (z.B. "Was macht go.mod?", dann "Und main.go?").
  3. Auf stderr nach jedem Turn erscheint jetzt usage: input=X output=Y cache_read=Z.

Erwartet: Beim ersten Turn ist cache_read meist 0 (nichts zum Cachen vorhanden). Ab dem zweiten Turn sollte cache_read deutlich über 0 liegen und nah an input herankommen, weil System-Prompt + bisherige Historie als stabiler Prefix erneut gesendet werden. In der TUI ist dasselbe in der Statuszeile sichtbar: tokens: N (cached: M).

Falls cache_read dauerhaft bei 0 bleibt, obwohl der Kontext >1024 Tokens hat: prüfen, ob Caps.UsageInStream für den aktiven Endpoint tatsächlich true ist (nub context bzw. die Modell-Registry in internal/llm/registry) — ohne stream_options.include_usage liefert OpenAI gar keine Usage-Daten, dann bleibt der Wert immer 0, unabhängig davon ob gecacht wurde.

Test 9 — Permissions: ask, deny, deny_paths

In .nub/config.toml:

[permissions]
edit = "ask"
bash = "deny"
deny_paths = ["**/secrets.txt"]
  1. TUI, eine Nachricht schicken, die edit auf einer normalen Datei auslöst. Erwartet: ein umrandeter ⚠ Rückfrage: edit-Block erscheint im Transkript (kein Vollbild — Nachrichtenliste, Eingabefeld und Statuszeile bleiben sichtbar), Statuszeile zeigt zusätzlich ⚠ permission required. Mit n/Esc ablehnen — Modell bekommt einen Fehler-tool_result und macht sinnvoll weiter (fragt nach, versucht etwas anderes). Erneut versuchen, mit y bestätigen — Edit läuft normal.
  2. Eine Nachricht schicken, die bash auslösen würde. Erwartet: sofortiger Fehler permission denied: bash is not allowed by policy, kein Modal (deny fragt nie).
  3. Eine Datei secrets.txt anlegen, Modell bitten sie zu lesen/editieren. Erwartet: Fehler durch deny_paths, unabhängig davon dass read/edit sonst auf auto/ask stehen — Deny-Pfade überstimmen immer.
  4. Dasselbe im Print-Modus (nub -p) mit edit = "ask": Erwartet sofortiger Fehler requires confirmation ('ask'), which this mode does not support statt eines hängenden Prozesses — ask ist dort laut Konzept (E-11) ein harter Fehler, keine Rückfrage über stdin.

Test 10 — nub init / /init

In einem leeren Testverzeichnis (kein .nub/, kein AGENTS.md):

nub init

Erwartet: .nub/config.toml (mit [permissions]-Beispiel) und AGENTS.md werden angelegt, beide Pfade auf stdout gemeldet. .git/info/exclude enthält danach .nub/ (git-Repo vorausgesetzt). Erneut nub init ausführen — erwartet: beide Dateien werden als skipped (already exists) gemeldet, Inhalt bleibt unverändert (vorher manuell etwas in AGENTS.md ändern und danach verifizieren, dass es erhalten bleibt).

Dasselbe in der TUI mit /init — meldet dieselben zwei Pfade als Command-Output im Transkript statt auf stdout.

Test 11 — question-Tool

  1. TUI: Modell bitten, vor einer mehrdeutigen Aktion nachzufragen (z.B. "frag mich per question-Tool, welches Ausgabeformat ich will, mit den Optionen json und yaml"). Erwartet: ein umrandeter ❓ Rückfrage:-Block erscheint im Transkript (kein Vollbild), Statuszeile zeigt ❓ question pending, Eingabefeld-Placeholder wechselt auf den Antwort-Hinweis. Text eintippen und Enter drücken — landet nicht als neue Chat-Nachricht oder Slash-Kommando, sondern als Antwort des Tools; Transkript zeigt danach Frage: … / Antwort: …, Placeholder wechselt zurück, Modell macht mit der Antwort sinnvoll weiter.
  2. Print-Modus (nub -p): Modell dasselbe fragen lassen. Erwartet sofortiger Fehler ("interactive questions are not supported in this mode…") statt eines hängenden Prozesses — analog zu ask-Permissions ist auch das question-Tool im Print-Modus bewusst kein Rückfrage- Mechanismus über stdin, da Print headless laufen soll (E-11).
  3. Skill mit einer Anweisung testen, die das Modell explizit zum question-Tool greifen lässt (z.B. "frage vor dem Löschen von Dateien immer erst per question-Tool nach") — Verhalten identisch zu 1., da das Tool unabhängig davon nutzbar ist, ob Modell oder Skill-Anweisung den Aufruf veranlasst.

Bekannte Grenzen (Entwicklungsstand)

  • Bilder werden nicht gemappt, auch wenn der Endpoint sie unterstützt
  • Permissions/deny_paths/deny_bash gelten nur für die sechs eingebauten Tools (read/glob/grep/write/edit/bash) — MCP- Tools, todo und read_skill laufen immer als auto und lassen sich aktuell nicht einzeln einschränken
  • Compaction nutzt immer dasselbe Modell für die Zusammenfassung — keine Möglichkeit, dafür ein günstigeres Modell zu konfigurieren
  • Token-Kalibrierung ist rein In-Process — kein persistenter Zustand über Sessions hinweg
  • MCP: kein Reconnect zur Laufzeit (E-06 — würde die Tool-Liste ändern und den Prompt-Cache brechen), kein OAuth-Login-Flow getestet, MCP-Prompts/ -Resources werden nicht angebunden, nur Tools
  • Skills: kein nub skill init o.ä., Skills müssen von Hand angelegt werden
  • TUI: kein Model-Auswahl-Picker (/model <name> setzt nur direkt, kein Modal mit Liste), /tree zeigt Nodes flach sortiert nach Erstellzeit statt als echten Baum mit Einrückung, bash-Streaming-Output läuft nur in die Statuszeile statt live ins Transkript (erst das fertige Ergebnis landet dort), Bildschirm-Resize während eines Modals kann die Breite nicht neu berechnen bis zum nächsten Tastendruck