mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-19 05:07:20 +02:00
Lot 7 : outils fichiers et shell exécutés par la façade
Quand la façade tient la conversation (LOKI_CHAT=rust), read, grep, glob, write, edit, bash et ask s'exécutent dans rust/loki-rs/src/tools au lieu de repartir au Go par /internal/v1/tool/exec : mêmes garde-fous dans le même ordre (commande catastrophique, « & » final en mode Code, bac à sable du mode Code, dossier mémoire interdit, lu avant d'écrire), même diff pour l'UI, même nettoyage de la sortie du shell. Le jeton de l'API interne est retiré de l'environnement du shell de l'agent. L'état partagé reste au Go, en un seul exemplaire (sous-agents et tâches le lisent aussi) : POST /internal/v1/tool/context (dossier de travail, dossier mémoire, cible distante), /tool/tracker (lecture notée, contrôle avant écriture), /tool/wrote (note après écriture, diagnostics LSP). Repli sur le Go pour une cible distante ou un contexte indisponible. Parité : internal/loki/parity_tools_test.go passe 58 appels à execTool sur une arborescence de test et range le journal dans tests/parity_tools ; tests/tools.rs le retrouve à l'identique avec exec_local. Écarts assumés consignés dans docs/superpowers/specs/2026-10-15-rust-migration-lot7-outils-design.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
18 files changed
+3560
-2
No files matched your search
@@ -0,0 +1,264 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// parity_tools_test.go — journal de référence des outils fichiers et du shell
|
||||
// (lot 7 de la migration Rust).
|
||||
//
|
||||
// Une arborescence de test, une suite d'appels d'outils passés à execTool tels
|
||||
// que la boucle les passe (libellé compris), dans l'ordre : leurs résultats,
|
||||
// diffs et questions, puis l'arborescence finale, sont rangés dans
|
||||
// rust/loki-rs/tests/parity_tools/outils.json. Le test Rust (tests/tools.rs)
|
||||
// rejoue les mêmes appels dans tools::exec_local et doit retrouver ce journal.
|
||||
// Chemins réécrits en {{ROOT}}, durées du shell en {{DUR}}.
|
||||
//
|
||||
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteOutils
|
||||
|
||||
const parityToolsFile = "../../rust/loki-rs/tests/parity_tools/outils.json"
|
||||
|
||||
type parityFile struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
// Âge en secondes (glob trie par date de modification).
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
type parityToolCase struct {
|
||||
Name string `json:"name"`
|
||||
Tool string `json:"tool"`
|
||||
Args map[string]any `json:"args"`
|
||||
Code bool `json:"code,omitempty"`
|
||||
// Unix : commande shell propre à bash.
|
||||
Unix bool `json:"unix,omitempty"`
|
||||
|
||||
Label string `json:"label"`
|
||||
Result string `json:"result"`
|
||||
Diff []DiffLine `json:"diff,omitempty"`
|
||||
Added int `json:"added,omitempty"`
|
||||
Removed int `json:"removed,omitempty"`
|
||||
Ask *AskEvent `json:"ask,omitempty"`
|
||||
}
|
||||
|
||||
type parityToolsFixture struct {
|
||||
// Dossier de travail et dossier mémoire, relatifs à {{ROOT}}.
|
||||
Cwd string `json:"cwd"`
|
||||
MemoryDir string `json:"memory_dir"`
|
||||
Files []parityFile `json:"files"`
|
||||
Cases []parityToolCase `json:"cases"`
|
||||
After map[string]string `json:"after"`
|
||||
}
|
||||
|
||||
func parityToolTree() []parityFile {
|
||||
long := strings.Repeat("é", 600)
|
||||
many := strings.Repeat("aiguille\n", 70)
|
||||
return []parityFile{
|
||||
{Path: "ws/a.txt", Content: "ligne 1\nligne 2\nligne 3\n", Age: 50},
|
||||
{Path: "ws/b.txt", Content: "alpha\nbeta\n", Age: 40},
|
||||
{Path: "ws/vide.txt", Content: "", Age: 30},
|
||||
{Path: "ws/long.txt", Content: long + "\nfin\n", Age: 20},
|
||||
{Path: "ws/crlf.txt", Content: "un\r\ndeux\r\ntrois\r\n", Age: 10},
|
||||
{Path: "ws/bin.dat", Content: "PK\x00\x01binaire", Age: 60},
|
||||
{Path: "ws/src/main.go", Content: "package main\n\nfunc main() { x := 1 }\n", Age: 5},
|
||||
{Path: "ws/src/lib/util.go", Content: "package lib\n// x marque\n", Age: 3},
|
||||
{Path: "ws/src/lib/notes.md", Content: "x dans les notes\n", Age: 4},
|
||||
{Path: "ws/node_modules/dep/index.js", Content: "ligne cachée\n", Age: 70},
|
||||
{Path: "ws/beaucoup.txt", Content: many, Age: 80},
|
||||
{Path: "dehors.txt", Content: "hors du dossier\n", Age: 90},
|
||||
}
|
||||
}
|
||||
|
||||
func parityToolCases() []parityToolCase {
|
||||
c := func(name, tool string, args map[string]any) parityToolCase {
|
||||
return parityToolCase{Name: name, Tool: tool, Args: args}
|
||||
}
|
||||
code := func(p parityToolCase) parityToolCase { p.Code = true; return p }
|
||||
unix := func(p parityToolCase) parityToolCase { p.Unix = true; return p }
|
||||
return []parityToolCase{
|
||||
c("lecture", "read", map[string]any{"file": "a.txt"}),
|
||||
c("lecture_fenetre", "read", map[string]any{"file": "a.txt", "offset": 2, "limit": 1}),
|
||||
c("lecture_fenetre_decimale", "read", map[string]any{"file": "a.txt", "offset": 2.7, "limit": "3"}),
|
||||
c("lecture_chemin_vide", "read", map[string]any{"file": " "}),
|
||||
c("lecture_absent", "read", map[string]any{"file": "absent.txt"}),
|
||||
c("lecture_dossier", "read", map[string]any{"file": "src"}),
|
||||
c("lecture_binaire", "read", map[string]any{"file": "bin.dat"}),
|
||||
c("lecture_au_dela", "read", map[string]any{"file": "a.txt", "offset": 99}),
|
||||
c("lecture_ligne_longue", "read", map[string]any{"file": "long.txt"}),
|
||||
c("lecture_fichier_vide", "read", map[string]any{"file": "vide.txt"}),
|
||||
c("lecture_absolue", "read", map[string]any{"file": "{{ROOT}}/dehors.txt"}),
|
||||
c("lecture_relative_sortante", "read", map[string]any{"file": "src/../../dehors.txt"}),
|
||||
code(c("lecture_code_hors_dossier", "read", map[string]any{"file": "../dehors.txt"})),
|
||||
code(c("lecture_code", "read", map[string]any{"file": "b.txt"})),
|
||||
|
||||
c("grep", "grep", map[string]any{"pattern": "ligne [12]"}),
|
||||
c("grep_filtre", "grep", map[string]any{"pattern": "x", "glob": "*.go"}),
|
||||
c("grep_filtre_classe", "grep", map[string]any{"pattern": "x", "glob": "[mn]*"}),
|
||||
c("grep_fichier", "grep", map[string]any{"pattern": "beta", "path": "b.txt"}),
|
||||
c("grep_sous_dossier", "grep", map[string]any{"pattern": "package", "path": "src/lib"}),
|
||||
c("grep_point", "grep", map[string]any{"pattern": "alpha", "path": "."}),
|
||||
c("grep_aucun", "grep", map[string]any{"pattern": "zzz"}),
|
||||
c("grep_motif_vide", "grep", map[string]any{"pattern": " "}),
|
||||
c("grep_plafond", "grep", map[string]any{"pattern": "aiguille"}),
|
||||
c("grep_absent", "grep", map[string]any{"pattern": "a", "path": "nulle-part"}),
|
||||
code(c("grep_code_hors_dossier", "grep", map[string]any{"pattern": "a", "path": "{{ROOT}}"})),
|
||||
|
||||
c("glob_go", "glob", map[string]any{"pattern": "**/*.go"}),
|
||||
c("glob_racine", "glob", map[string]any{"pattern": "*.txt"}),
|
||||
c("glob_point", "glob", map[string]any{"pattern": "./src/*"}),
|
||||
c("glob_aucun", "glob", map[string]any{"pattern": "*.rs"}),
|
||||
c("glob_vide", "glob", map[string]any{"pattern": ""}),
|
||||
|
||||
c("question", "ask", map[string]any{"question": "Lequel ?", "options": []any{"a", " ", "b", "c", "d", "e"}}),
|
||||
c("question_vide", "ask", map[string]any{"question": ""}),
|
||||
|
||||
c("ecriture_neuve", "write", map[string]any{"file": "neuf/n.txt", "content": "un\ndeux\n"}),
|
||||
c("ecriture_existant", "write", map[string]any{"file": "b.txt", "content": "remplacé\n"}),
|
||||
c("ecriture_memoire", "write", map[string]any{"file": "{{MEM}}/x.md", "content": "x\n"}),
|
||||
c("ecriture_chemin_vide", "write", map[string]any{"file": "", "content": "x"}),
|
||||
code(c("ecriture_code_non_lu", "write", map[string]any{"file": "src/lib/notes.md", "content": "x\n"})),
|
||||
code(c("ecriture_code_hors_dossier", "write", map[string]any{"file": "../evade.txt", "content": "x\n"})),
|
||||
code(c("ecriture_code_neuve", "write", map[string]any{"file": "code.txt", "content": "c\n"})),
|
||||
|
||||
c("edition", "edit", map[string]any{"file": "a.txt", "old": "ligne 2", "new": "ligne deux"}),
|
||||
c("edition_deja_faite", "edit", map[string]any{"file": "a.txt", "old": "ligne 2", "new": "ligne deux"}),
|
||||
c("edition_introuvable", "edit", map[string]any{"file": "a.txt", "old": "zzz", "new": "y"}),
|
||||
c("edition_ambigue", "edit", map[string]any{"file": "a.txt", "old": "ligne", "new": "l"}),
|
||||
c("edition_crlf", "edit", map[string]any{"file": "crlf.txt", "old": "deux\ntrois", "new": "2\n3"}),
|
||||
c("edition_old_vide", "edit", map[string]any{"file": "a.txt", "old": "", "new": "y"}),
|
||||
c("edition_fichier_absent", "edit", map[string]any{"file": "absent.txt", "old": "a", "new": "b"}),
|
||||
code(c("edition_code_non_lu", "edit", map[string]any{"file": "src/main.go", "old": "x := 1", "new": "x := 2"})),
|
||||
code(c("edition_code_apres_lecture", "edit", map[string]any{"file": "b.txt", "old": "remplacé", "new": "rem"})),
|
||||
|
||||
unix(c("shell", "bash", map[string]any{"command": "echo bonjour"})),
|
||||
unix(c("shell_sortie_et_erreurs", "bash", map[string]any{"command": "echo out; echo err >&2; exit 3"})),
|
||||
unix(c("shell_tail", "bash", map[string]any{"command": "printf 'a\\nb\\nc\\n' | tail -2"})),
|
||||
unix(c("shell_ansi", "bash", map[string]any{"command": "printf '\\033[31mrouge\\033[0m\\n'"})),
|
||||
unix(c("shell_dossier", "bash", map[string]any{"command": "ls src"})),
|
||||
unix(c("shell_longue_sortie", "bash", map[string]any{"command": "seq 1 3000"})),
|
||||
unix(c("shell_delai", "bash", map[string]any{"command": "echo avant; sleep 3", "timeout": 1})),
|
||||
c("shell_dangereux", "bash", map[string]any{"command": "rm -rf /"}),
|
||||
unix(c("shell_memoire", "bash", map[string]any{"command": "cat {{MEM}}/x.md"})),
|
||||
unix(code(c("shell_code_arriere_plan", "bash", map[string]any{"command": "sleep 1 &"}))),
|
||||
}
|
||||
}
|
||||
|
||||
// subst remplace {{ROOT}} et {{MEM}} dans les chaînes des arguments.
|
||||
func paritySubst(v any, root, mem string) any {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return strings.NewReplacer("{{MEM}}", mem, "{{ROOT}}", root).Replace(x)
|
||||
case map[string]any:
|
||||
out := map[string]any{}
|
||||
for k, e := range x {
|
||||
out[k] = paritySubst(e, root, mem)
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, len(x))
|
||||
for i, e := range x {
|
||||
out[i] = paritySubst(e, root, mem)
|
||||
}
|
||||
return out
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
var parityDurRe = regexp.MustCompile(`(exit: -?\d+) · [0-9.]+[a-zµ]+(?:[0-9.]+[a-zµ]+)*`)
|
||||
|
||||
func parityNorm(s, root string) string {
|
||||
s = strings.ReplaceAll(s, root, "{{ROOT}}")
|
||||
return parityDurRe.ReplaceAllString(s, "$1 · {{DUR}}")
|
||||
}
|
||||
|
||||
func TestPariteOutils(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("journal tiré sous Unix")
|
||||
}
|
||||
root := testHome(t)
|
||||
if r, err := filepath.EvalSymlinks(root); err == nil {
|
||||
root = r // macOS : /var → /private/var, comme le verraient les outils
|
||||
}
|
||||
t.Setenv("LOKI_HOME", root)
|
||||
ws := filepath.Join(root, "ws")
|
||||
now := time.Now()
|
||||
for _, f := range parityToolTree() {
|
||||
p := filepath.Join(root, filepath.FromSlash(f.Path))
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(f.Content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mt := now.Add(-time.Duration(f.Age) * time.Second)
|
||||
if err := os.Chtimes(p, mt, mt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
setTaskWorkspace(ws)
|
||||
t.Cleanup(func() { setTaskWorkspace("") })
|
||||
mem := memoryDir()
|
||||
relMem, err := filepath.Rel(root, mem)
|
||||
if err != nil || strings.HasPrefix(relMem, "..") {
|
||||
t.Fatalf("dossier mémoire hors de la racine : %s", mem)
|
||||
}
|
||||
|
||||
fx := parityToolsFixture{Cwd: "ws", MemoryDir: filepath.ToSlash(relMem), Files: parityToolTree()}
|
||||
for _, pc := range parityToolCases() {
|
||||
args := paritySubst(pc.Args, root, mem).(map[string]any)
|
||||
// Les nombres passent par JSON, comme ceux d'un vrai appel.
|
||||
b, _ := json.Marshal(args)
|
||||
args = map[string]any{}
|
||||
_ = json.Unmarshal(b, &args)
|
||||
label := toolCallLabel(pc.Tool, args)
|
||||
res := execTool(context.Background(), pc.Tool, args, label, Caps{Code: pc.Code})
|
||||
pc.Label = parityNorm(label, root)
|
||||
pc.Result = parityNorm(res.Result, root)
|
||||
pc.Diff, pc.Added, pc.Removed, pc.Ask = res.Diff, res.Added, res.Removed, res.Ask
|
||||
fx.Cases = append(fx.Cases, pc)
|
||||
}
|
||||
fx.After = map[string]string{}
|
||||
_ = filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
rel, _ := filepath.Rel(root, p)
|
||||
rel = filepath.ToSlash(rel)
|
||||
if !strings.HasPrefix(rel, "ws/") && rel != "dehors.txt" && rel != "evade.txt" {
|
||||
return nil // base, configuration : propres au Go
|
||||
}
|
||||
b, _ := os.ReadFile(p)
|
||||
fx.After[rel] = string(b)
|
||||
return nil
|
||||
})
|
||||
got, err := parityJSON(fx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
|
||||
if err := os.MkdirAll(filepath.Dir(parityToolsFile), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(parityToolsFile, got, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
want, err := os.ReadFile(parityToolsFile)
|
||||
if err != nil {
|
||||
t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Errorf("les outils ne produisent plus le journal rangé dans %s.\n"+
|
||||
"Si c'est voulu : LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteOutils,\n"+
|
||||
"puis cargo test --test tools dit si le port Rust suit.\n--- obtenu ---\n%s", parityToolsFile, got)
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,12 @@ func withInternalAPI(next http.Handler) http.Handler {
|
||||
handleInternalOAIEnd(w, r)
|
||||
case "/internal/v1/tool/exec":
|
||||
handleInternalToolExec(w, r)
|
||||
case "/internal/v1/tool/context":
|
||||
handleInternalToolContext(w, r)
|
||||
case "/internal/v1/tool/tracker":
|
||||
handleInternalToolTracker(w, r)
|
||||
case "/internal/v1/tool/wrote":
|
||||
handleInternalToolWrote(w, r)
|
||||
case "/internal/v1/crypt/seal":
|
||||
handleInternalCrypt(w, r, true)
|
||||
case "/internal/v1/crypt/open":
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// API interne de l'état des outils (lot 7 de la migration Rust).
|
||||
//
|
||||
// La façade exécute elle-même les outils fichiers et le shell (read, grep,
|
||||
// glob, write, edit, bash, ask) quand elle tient la conversation. Ce qu'ils
|
||||
// partagent avec le reste de Loki reste ici, en un seul exemplaire : le
|
||||
// dossier de travail de la discussion, le dossier mémoire interdit au shell,
|
||||
// la cible distante, le suivi « lu avant d'écrire » (lu aussi par les
|
||||
// sous-agents et les tâches, qui tournent ici) et les diagnostics LSP.
|
||||
//
|
||||
// POST /internal/v1/tool/context {}
|
||||
// → {"cwd":"…","memory_dir":"…","target":"slug|"}
|
||||
// POST /internal/v1/tool/tracker {"op":"read","path":"/abs"} → {}
|
||||
// POST /internal/v1/tool/tracker {"op":"check","path":"/abs","for_write":true}
|
||||
// → {"refusal":"[refusé] …"|""}
|
||||
// POST /internal/v1/tool/wrote {"path":"/abs","caps":{…}}
|
||||
// → {"lsp":"\n\n[diagnostics LSP] …"|""}
|
||||
//
|
||||
// wrote : après une écriture réussie (write/edit) — note du tracker, puis le
|
||||
// bloc des diagnostics LSP que la boucle ajoute au résultat de l'outil.
|
||||
|
||||
func handleInternalToolContext(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct{}
|
||||
if !readInternalJSON(w, r, &in) {
|
||||
return
|
||||
}
|
||||
sendJSON(w, http.StatusOK, map[string]any{
|
||||
"cwd": agentCwd(),
|
||||
"memory_dir": memoryDir(),
|
||||
"target": agentTargetSlug(),
|
||||
})
|
||||
}
|
||||
|
||||
func handleInternalToolTracker(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
ForWrite bool `json:"for_write"`
|
||||
}
|
||||
if !readInternalJSON(w, r, &in) {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.Path) == "" {
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "chemin manquant"})
|
||||
return
|
||||
}
|
||||
switch in.Op {
|
||||
case "read":
|
||||
trackerNoteRead(in.Path)
|
||||
sendJSON(w, http.StatusOK, map[string]any{})
|
||||
case "check":
|
||||
sendJSON(w, http.StatusOK, map[string]any{"refusal": trackerCheck(in.Path, in.ForWrite)})
|
||||
default:
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "op inconnue"})
|
||||
}
|
||||
}
|
||||
|
||||
func handleInternalToolWrote(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Path string `json:"path"`
|
||||
Caps Caps `json:"caps"`
|
||||
}
|
||||
if !readInternalJSON(w, r, &in) {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(in.Path) == "" {
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "chemin manquant"})
|
||||
return
|
||||
}
|
||||
trackerNoteWrite(in.Path)
|
||||
sendJSON(w, http.StatusOK, map[string]any{"lsp": lspDiagBlock(in.Path, in.Caps)})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Le contexte des outils dit à la façade où travaille l'agent, ce que le shell
|
||||
// n'a pas le droit de toucher, et s'il vise un poste distant.
|
||||
func TestOutilsInterneContexte(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/tool/context", `{}`)
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("contexte : %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
got := leaseJSON(t, w.Body.String())
|
||||
if got["cwd"] != agentCwd() || got["memory_dir"] != memoryDir() || got["target"] != "" {
|
||||
t.Fatalf("contexte : %v", got)
|
||||
}
|
||||
if w := appelOAIInterne(t, "GET", "/internal/v1/tool/context", ``); w.Code != 405 {
|
||||
t.Fatalf("GET : %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Une lecture notée par la façade autorise l'écriture, exactement comme une
|
||||
// lecture faite ici ; une écriture notée aussi.
|
||||
func TestOutilsInterneTracker(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
f := filepath.Join(t.TempDir(), "a.txt")
|
||||
if err := os.WriteFile(f, []byte("x\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check := func() string {
|
||||
t.Helper()
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/tool/tracker", `{"op":"check","path":`+jsonQuote(f)+`,"for_write":true}`)
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("check : %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
r, _ := leaseJSON(t, w.Body.String())["refusal"].(string)
|
||||
return r
|
||||
}
|
||||
if r := check(); !strings.Contains(r, "pas lu") {
|
||||
t.Fatalf("fichier jamais lu accepté : %q", r)
|
||||
}
|
||||
if w := appelOAIInterne(t, "POST", "/internal/v1/tool/tracker", `{"op":"read","path":`+jsonQuote(f)+`}`); w.Code != 200 {
|
||||
t.Fatalf("read : %d", w.Code)
|
||||
}
|
||||
if r := check(); r != "" {
|
||||
t.Fatalf("lecture notée mais refus : %q", r)
|
||||
}
|
||||
g := filepath.Join(filepath.Dir(f), "b.txt")
|
||||
if err := os.WriteFile(g, []byte("y\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/tool/wrote", `{"path":`+jsonQuote(g)+`,"caps":{"Code":true}}`)
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("wrote : %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
if trackerCheck(g, false) != "" {
|
||||
t.Fatal("écriture notée mais edit refusé")
|
||||
}
|
||||
for _, body := range []string{"", "{", `{"op":"read"}`, `{"op":"x","path":"/a"}`} {
|
||||
if w := appelOAIInterne(t, "POST", "/internal/v1/tool/tracker", body); w.Code != 400 {
|
||||
t.Fatalf("%q : %d", body, w.Code)
|
||||
}
|
||||
}
|
||||
if w := appelOAIInterne(t, "POST", "/internal/v1/tool/wrote", `{}`); w.Code != 400 {
|
||||
t.Fatalf("wrote sans chemin : %d", w.Code)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user