mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
Repris du pré-vol d'OpenFox (tool-preflight, 2.0.154). Un write sur un fichier existant jamais lu était refusé par le tracker… une fois tout le fichier généré. builder.md demande des fichiers écrits en entier : un refus pouvait coûter des minutes de décodage pour rien. - Les schémas write, edit, mem_add et mem_edit annoncent le chemin AVANT le contenu. Une map Go sort ses clés par ordre alphabétique : le modèle, qui suit l'ordre du schéma, déroulait tout le contenu avant le chemin. L'interface nomme aussi le fichier dès le début de la frappe. - Dès que le chemin d'un write/edit est complet dans le flux, les gardes du mode Code (fichier lu, chemin permis) sont vérifiées. Refus : la génération est coupée, l'appel réduit à son chemin entre dans l'historique avec le refus pour résultat, et le modèle repart (lire le fichier d'abord). Rien n'est écrit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
2.9 KiB
Go
87 lines
2.9 KiB
Go
package loki
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Le schéma de write annonce le chemin AVANT le contenu.
|
|
func TestSchemaWriteCheminDAbord(t *testing.T) {
|
|
b, _ := json.Marshal(writeTool().Function.Parameters)
|
|
s := string(b)
|
|
if i, j := strings.Index(s, `"file"`), strings.Index(s, `"content"`); i < 0 || j < 0 || i > j {
|
|
t.Fatalf("ordre des propriétés : %s", s)
|
|
}
|
|
}
|
|
|
|
// write sur un fichier existant jamais lu : refusé dès que le chemin est
|
|
// complet, sans attendre le contenu ; le fichier reste intact.
|
|
func TestPreVolCoupeLEcritureRefusee(t *testing.T) {
|
|
withWorkspace(t)
|
|
cible := filepath.Join(agentCwd(), "main.go")
|
|
if err := os.WriteFile(cible, []byte("package main\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var n int32
|
|
var second []Message
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.WriteHeader(200)
|
|
if atomic.AddInt32(&n, 1) == 1 {
|
|
chunk := func(args string) {
|
|
esc := strings.ReplaceAll(args, `"`, `\"`)
|
|
_, _ = w.Write([]byte(`data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"w1","type":"function","function":{"name":"write","arguments":"` + esc + `"}}]}}]}` + "\n\n"))
|
|
w.(http.Flusher).Flush()
|
|
}
|
|
chunk(`{"file":"main.go",`)
|
|
chunk(`"content":"package main // début`)
|
|
time.Sleep(300 * time.Millisecond)
|
|
chunk(` FIN-JAMAIS-LUE"}`)
|
|
_, _ = w.Write([]byte(`data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}]}` + "\n\n"))
|
|
return
|
|
}
|
|
var body struct {
|
|
Messages []Message `json:"messages"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
second = body.Messages
|
|
_, _ = w.Write([]byte(sseChunk("je lis d'abord")))
|
|
_, _ = w.Write([]byte(`data: {"choices":[{"delta":{},"finish_reason":"stop"}]}` + "\n\ndata: [DONE]\n\n"))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
u, _ := url.Parse(srv.URL)
|
|
if err := SetConfigKey("PORT", u.Port()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var results []string
|
|
if _, err := runChat(context.Background(), []Message{{Role: "user", Content: "réécris main.go"}}, 0.7, Caps{Agent: true, Code: true}, func(ev StreamEvent) bool {
|
|
if ev.ToolUsed != nil && ev.ToolUsed.Done {
|
|
results = append(results, ev.ToolUsed.Result)
|
|
}
|
|
if ev.ToolUsed != nil && strings.Contains(ev.ToolUsed.Body, "FIN-JAMAIS-LUE") {
|
|
t.Error("le contenu a continué d'arriver après le refus")
|
|
}
|
|
return true
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(results) != 1 || !strings.Contains(results[0], "[refusé]") || !strings.Contains(results[0], "interrompue") {
|
|
t.Fatalf("résultats = %q", results)
|
|
}
|
|
if b, _ := os.ReadFile(cible); string(b) != "package main\n" {
|
|
t.Fatalf("fichier modifié : %q", b)
|
|
}
|
|
if len(second) < 2 || second[len(second)-1].Role != "tool" || !strings.Contains(msgText(second[len(second)-1]), "[refusé]") {
|
|
t.Fatalf("le refus n'a pas été rendu au modèle : %+v", second)
|
|
}
|
|
}
|