321 lines
14 KiB
Markdown
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
|