Files
Loki/internal/loki/code_tools_test.go
MichaelandClaude Opus 5.5 dfafca6fb2 Mode Code : git sur le dépôt cloné, budget de build, sous-agents moins rigides
Trois trous relevés en comparant avec OpenFox.

- git_status / git_diff tournaient à la racine de la discussion, qui
  n'est souvent pas un dépôt : git_clone range le projet dans un
  sous-dossier, et le vérificateur — dont la consigne commence par
  git_diff — tombait sur « not a git repository ». Ils visent maintenant
  le sous-dossier demandé (dir), la racine si c'est un dépôt, sinon
  l'unique dépôt présent ; plusieurs : on demande de choisir.
- Budget d'outils triplé en mode Code : lire, éditer, compiler, relancer
  les tests dépasse vite 24 appels sans tourner en rond, et le troisième
  rappel (« n'appelle plus d'outil ») coupait le build au milieu.
- Sous-agents : température 0.6 au lieu de 0 (en glouton, Qwen avec
  réflexion boucle vite ; le TEMP du preset l'emporte toujours), et seul
  le texte écrit après le dernier outil revient au builder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:36:01 +02:00

185 lines
6.0 KiB
Go

package loki
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// read numérote depuis 1, respecte offset/limit et annonce le reste.
func TestReadOffsetLimit(t *testing.T) {
ws := withWorkspace(t)
var lines []string
for i := 1; i <= 10; i++ {
lines = append(lines, "ligne")
}
if err := os.WriteFile(filepath.Join(ws, "f.txt"), []byte(strings.Join(lines, "\n")+"\n"), 0o644); err != nil {
t.Fatal(err)
}
out := toolRead(map[string]any{"file": "f.txt", "offset": float64(3), "limit": float64(2)}, true)
if !strings.Contains(out, "3: ligne") || !strings.Contains(out, "4: ligne") {
t.Fatalf("lignes 3-4 attendues :\n%s", out)
}
if strings.Contains(out, "5: ligne") {
t.Fatalf("limite ignorée :\n%s", out)
}
if !strings.Contains(out, "restantes") {
t.Fatalf("suite non annoncée :\n%s", out)
}
}
// read refuse un binaire et le dit, au lieu de déverser du bruit.
func TestReadBinaire(t *testing.T) {
ws := withWorkspace(t)
if err := os.WriteFile(filepath.Join(ws, "bin.dat"), []byte{0x00, 0x01, 0x02}, 0o644); err != nil {
t.Fatal(err)
}
if out := toolRead(map[string]any{"file": "bin.dat"}, true); !strings.Contains(out, "[binaire]") {
t.Fatalf("binaire non détecté : %s", out)
}
}
// read enregistre la lecture : le tracker doit ensuite autoriser l'écriture.
func TestReadNourritLeTracker(t *testing.T) {
ws := withWorkspace(t)
path := filepath.Join(ws, "suivi.txt")
if err := os.WriteFile(path, []byte("contenu"), 0o644); err != nil {
t.Fatal(err)
}
if msg := trackerCheck(path, true); msg == "" {
t.Fatal("écriture acceptée avant lecture")
}
toolRead(map[string]any{"file": "suivi.txt"}, true)
if msg := trackerCheck(path, true); msg != "" {
t.Fatalf("écriture refusée après lecture : %s", msg)
}
// Fichier modifié APRÈS la lecture : refus (lecture périmée).
future := time.Now().Add(2 * time.Second)
if err := os.Chtimes(path, future, future); err != nil {
t.Fatal(err)
}
if msg := trackerCheck(path, true); !strings.Contains(msg, "changé") {
t.Fatalf("lecture périmée non détectée : %q", msg)
}
}
// grep trouve, borne et ignore les dossiers de dépendances.
func TestGrep(t *testing.T) {
ws := withWorkspace(t)
writeFile(t, filepath.Join(ws, "a.go"), "package main\nfunc Cible() {}\n")
writeFile(t, filepath.Join(ws, "node_modules", "x.js"), "Cible aussi\n")
out := toolGrep(map[string]any{"pattern": "Cible"}, true)
if !strings.Contains(out, "a.go:2") {
t.Fatalf("correspondance manquée :\n%s", out)
}
if strings.Contains(out, "node_modules") {
t.Fatalf("node_modules aurait dû être ignoré :\n%s", out)
}
if out := toolGrep(map[string]any{"pattern": "IntrouvableXYZ"}, true); !strings.Contains(out, "aucun résultat") {
t.Fatalf("absence non signalée : %s", out)
}
}
// glob : ** traverse les dossiers, tri par modification décroissante.
func TestGlob(t *testing.T) {
ws := withWorkspace(t)
writeFile(t, filepath.Join(ws, "src", "a.ts"), "a")
writeFile(t, filepath.Join(ws, "src", "sub", "b.ts"), "b")
writeFile(t, filepath.Join(ws, "autre.txt"), "c")
out := toolGlob(map[string]any{"pattern": "**/*.ts"}, true)
if !strings.Contains(out, "src/a.ts") || !strings.Contains(out, "src/sub/b.ts") {
t.Fatalf("fichiers .ts manquants :\n%s", out)
}
if strings.Contains(out, "autre.txt") {
t.Fatalf("autre.txt ne matche pas **/*.ts :\n%s", out)
}
}
// globToRegexp : cas aux limites du **.
func TestGlobToRegexp(t *testing.T) {
cases := []struct {
pattern, path string
want bool
}{
{"*.go", "main.go", true},
{"*.go", "src/main.go", false},
{"**/*.go", "src/deep/main.go", true},
{"**/*.go", "main.go", true}, // **/ = zéro dossier ou plus
{"src/**", "src/a/b/c.txt", true},
{"src/*.ts", "src/a.ts", true},
{"src/*.ts", "src/sub/a.ts", false},
}
for _, c := range cases {
re, err := globToRegexp(c.pattern)
if err != nil {
t.Fatalf("%s : %v", c.pattern, err)
}
if got := re.MatchString(c.path); got != c.want {
t.Errorf("%s sur %s : %v, attendu %v", c.pattern, c.path, got, c.want)
}
}
}
// fileEdit préserve les fins de ligne CRLF d'un fichier quand le modèle
// envoie du LF (et n'introduit jamais de mélange).
func TestEditPreserveCRLF(t *testing.T) {
ws := withWorkspace(t)
path := filepath.Join(ws, "w.txt")
if err := os.WriteFile(path, []byte("un\r\ndeux\r\ntrois\r\n"), 0o644); err != nil {
t.Fatal(err)
}
out := fileEdit(path, "deux", "DEUX")
if !strings.HasPrefix(out, "[ok]") {
t.Fatalf("édition refusée : %s", out)
}
b, _ := os.ReadFile(path)
if string(b) != "un\r\nDEUX\r\ntrois\r\n" {
t.Fatalf("fins de ligne perdues : %q", b)
}
}
// fileEdit multi-lignes en LF sur un fichier CRLF : la normalisation couvre
// aussi le old qui contient des sauts de ligne.
func TestEditCRLFMultiligne(t *testing.T) {
ws := withWorkspace(t)
path := filepath.Join(ws, "m.txt")
if err := os.WriteFile(path, []byte("a\r\nb\r\nc\r\n"), 0o644); err != nil {
t.Fatal(err)
}
out := fileEdit(path, "a\nb", "a\nB")
if !strings.HasPrefix(out, "[ok]") {
t.Fatalf("édition refusée : %s", out)
}
b, _ := os.ReadFile(path)
if string(b) != "a\r\nB\r\nc\r\n" {
t.Fatalf("résultat inattendu : %q", b)
}
}
// git_status / git_diff trouvent le dépôt cloné dans un sous-dossier de la
// discussion, et demandent de choisir quand il y en a plusieurs.
func TestGitRepoDirDetecteLeClone(t *testing.T) {
ws := withWorkspace(t)
if d, err := gitRepoDir(""); err != nil || d != agentCwd() {
t.Fatalf("sans dépôt : %q, %v", d, err)
}
if err := os.MkdirAll(filepath.Join(agentCwd(), "projet", ".git"), 0o755); err != nil {
t.Fatal(err)
}
if d, err := gitRepoDir(""); err != nil || filepath.Base(d) != "projet" {
t.Fatalf("un clone : %q, %v (ws %s)", d, err, ws)
}
_ = os.MkdirAll(filepath.Join(agentCwd(), "autre", ".git"), 0o755)
if _, err := gitRepoDir(""); err == nil {
t.Fatal("deux dépôts : il fallait demander dir")
}
if d, err := gitRepoDir("autre"); err != nil || filepath.Base(d) != "autre" {
t.Fatalf("dir explicite : %q, %v", d, err)
}
if _, err := gitRepoDir("../dehors"); err == nil {
t.Fatal("dir hors de la discussion accepté")
}
}