mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
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>
185 lines
6.0 KiB
Go
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é")
|
|
}
|
|
}
|