Files
Loki/rust/loki-rs/tests/tools.rs
T
Claude f11f667a4c Lot 7 : outils git exécutés par la façade
git_status, git_diff et git_clone (rust/loki-rs/src/tools/git.rs) quand la
façade tient la conversation : dépôt deviné comme le Go (sous-dossier
demandé, racine, unique sous-dossier dépôt), sorties mêlées et bornées,
jamais d'invite, clone limité à https:// et git@hôte: et rangé dans le
dossier de la discussion. Toujours sur cette machine, cible distante ou
non, comme le Go.

Parité : TestPariteOutilsGit (deux dépôts, l'un modifié, l'autre sans
commit ; aucun appel réseau) range tests/parity_tools/git.json ;
tests/tools_git.rs le retrouve à l'identique. Le rejeu des journaux des
outils est partagé (tests/common/toolsfx.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
2026-10-15 12:19:07 +00:00

70 lines
2.3 KiB
Rust

//! Parité des outils fichiers et du shell avec le Go (lot 7).
//!
//! `tests/parity_tools/outils.json` : journal produit par `execTool`
//! (`internal/loki/parity_tools_test.go`, régénéré par
//! `LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteOutils`). La
//! même arborescence est recréée ici et les mêmes appels passent, dans le même
//! ordre, par `tools::exec_local` (`common::toolsfx`) : libellés, résultats,
//! diffs, questions et arborescence finale doivent être ceux du Go.
mod common;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Mutex;
use serde_json::{Value, json};
use common::toolsfx::{FakeState, replay};
use loki_rs::chat::stream::tool_call_label;
use loki_rs::tools::{ToolCtx, exec_local};
#[tokio::test]
async fn outils_comme_le_go() {
let diffs = replay("outils").await;
assert!(
diffs.is_empty(),
"{} écart(s) avec le Go :\n\n{}",
diffs.len(),
diffs.join("\n\n")
);
}
/// Outils non portés, ou cible distante : c'est au Go de les exécuter.
#[tokio::test]
async fn le_reste_va_au_go() {
let st = FakeState {
ctx: ToolCtx {
cwd: PathBuf::from("/nulle-part"),
memory_dir: PathBuf::from("/nulle-part/memory"),
target: "poste".into(),
code: false,
},
reads: Mutex::new(HashMap::new()),
};
let a = |v: Value| v.as_object().unwrap().clone();
for (name, args) in [
("mem_read", json!({"file":"x"})),
("web_search", json!({"query":"x"})),
("write", json!({"file":"a","content":"x"})),
("edit", json!({"file":"a","old":"x","new":"y"})),
("bash", json!({"command":"ls"})),
] {
let args = a(args);
let label = tool_call_label(name, &args);
assert!(
exec_local(&st, false, name, &args, &label).await.is_none(),
"{name}"
);
}
// Une commande catastrophique est refusée ici, cible distante comprise.
let args = a(json!({"command":"rm -rf /"}));
let out = exec_local(&st, false, "bash", &args, "rm -rf /")
.await
.unwrap();
assert!(out.result.starts_with("[refusé] Commande bloquée"));
// read/grep/glob lisent toujours cette machine.
let args = a(json!({"pattern":"*"}));
assert!(exec_local(&st, false, "glob", &args, "*").await.is_some());
}