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

321 lines
14 KiB
Markdown

# Testing
Für Endnutzer-Doku siehe die [README](../README.md) und
[FEATURES.md](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
```bash
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:
```bash
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`:
```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:
```bash
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
```toml
version = 1
[[mcp]]
name = "broken"
command = "this-binary-does-not-exist"
```
```bash
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`:
```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`):
```bash
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