Files
Loki/rust/loki-rs/tests/tools.rs
T
Claude 76d9dce74a Lot 38 : l'outil criteria du mode Code exécuté par la façade
L'outil par lequel le modèle tient les critères d'acceptation de sa tâche
(poser, marquer faits, lister, effacer) partait au Go, qui renvoyait la
liste à la façade comme delta. Il s'exécute maintenant dans le tour de la
façade (chat/crittool.rs, port de toolCriteria) : mêmes messages, ids en
nombre ou en chaîne, vingt critères au plus, `passed` réservé à la passe de
vérification, rangement scellé (chat/crit:<id>), liste poussée à l'écran à
chaque changement par un nouveau champ `deltas` de l'hôte du tour.

Les critères sont rangés sous la discussion du tour (le Go prenait la
discussion active : elles ne diffèrent que si l'utilisateur en change
pendant la génération). see_image reste au Go (préparation de l'image).

Parité : journal de 22 appels tiré du Go (parity_criteria_test.go →
tests/parity_tools/criteria.json), rejoué par tests/tools.rs ; le contrat
du mode Code de tests/lot6.rs est désormais vérifié par la façade seule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
2026-10-16 04:01:03 +00:00

120 lines
4.7 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());
}
/// Lot 38 : l'outil criteria du mode Code, rejoué contre le journal du Go
/// (`internal/loki/parity_criteria_test.go` → `parity_tools/criteria.json`) :
/// résultat de chaque appel, critères rangés après coup (scellés), et la
/// liste poussée à l'écran quand elle change.
#[tokio::test]
async fn criteria_comme_le_go() {
use loki_rs::chat::crittool::{CRIT_MAX_COUNT, exec};
let fx: Value =
serde_json::from_str(include_str!("parity_tools/criteria.json")).expect("journal");
let t = tempfile::tempdir().unwrap();
let (_f, go, _stop) = common::fakego::start(t.path().into()).await;
let backend = go.backend.lock().unwrap().unwrap();
let store = loki_rs::store::Store::new(backend, common::fakego::JETON.into());
assert_eq!(CRIT_MAX_COUNT, 20);
let conv = "c1";
for st in fx.as_array().unwrap() {
let name = st["name"].as_str().unwrap();
if let Some(put) = st["put"].as_str() {
store
.put_seal("chat", &format!("crit:{conv}"), put.as_bytes().to_vec())
.await
.unwrap();
}
let args = st["args"].as_object().cloned().unwrap_or_default();
let verifier = st["verifier"].as_bool() == Some(true);
let before = store.get_open("chat", &format!("crit:{conv}")).await.unwrap();
let out = exec(&store, conv, &args, verifier).await;
assert_eq!(out.result, st["out"].as_str().unwrap(), "{name}");
let raw = store.get_open("chat", &format!("crit:{conv}")).await.unwrap();
let stored = raw
.as_deref()
.filter(|b| !b.is_empty())
.map(|b| serde_json::from_slice::<Value>(b).unwrap())
.unwrap_or(Value::Null);
assert_eq!(stored, st["stored"], "{name}");
// Scellés comme le contenu des discussions.
if let Some(sealed) = go.get("chat", &format!("crit:{conv}")) {
assert!(sealed.starts_with("SCELLE:"), "{name} : {sealed}");
}
// L'écran reçoit la liste quand elle change (vide : effacée).
match &out.notify {
Some(list) => {
let want = if stored.is_null() { json!([]) } else { stored.clone() };
assert_eq!(json!(list), want, "{name}");
}
None => assert_eq!(raw, before, "{name} : rien ne change sans notification"),
}
}
}