mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 14:38:12 +02:00
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
This commit is contained in:
9 files changed
+976
-224
No files matched your search
@@ -3,6 +3,7 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod fakego;
|
||||
pub mod toolsfx;
|
||||
|
||||
use bytes::Bytes;
|
||||
use http_body_util::Full;
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
//! Rejeu des journaux de référence des outils (`tests/parity_tools/*.json`,
|
||||
//! produits par `internal/loki/parity_tools_test.go`) dans
|
||||
//! `tools::exec_local`. Partagé par `tests/tools.rs` et `tests/tools_git.rs`.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use loki_rs::chat::stream::tool_call_label;
|
||||
use loki_rs::chat::types::{AskEvent, DiffLine};
|
||||
use loki_rs::tools::{ToolCtx, ToolState, exec_local};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GitCmd {
|
||||
dir: String,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FileFx {
|
||||
path: String,
|
||||
content: String,
|
||||
age: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CaseFx {
|
||||
name: String,
|
||||
tool: String,
|
||||
args: Map<String, Value>,
|
||||
#[serde(default)]
|
||||
code: bool,
|
||||
#[serde(default)]
|
||||
unix: bool,
|
||||
label: String,
|
||||
result: String,
|
||||
#[serde(default)]
|
||||
diff: Vec<DiffLine>,
|
||||
#[serde(default)]
|
||||
added: i64,
|
||||
#[serde(default)]
|
||||
removed: i64,
|
||||
#[serde(default)]
|
||||
ask: Option<AskEvent>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Fixture {
|
||||
cwd: String,
|
||||
memory_dir: String,
|
||||
files: Vec<FileFx>,
|
||||
#[serde(default)]
|
||||
git_setup: Vec<GitCmd>,
|
||||
#[serde(default)]
|
||||
edits: Vec<FileFx>,
|
||||
cases: Vec<CaseFx>,
|
||||
after: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
/// Le suivi « lu avant d'écrire » du Go (`code_tracker.go`), en mémoire.
|
||||
pub struct FakeState {
|
||||
pub ctx: ToolCtx,
|
||||
pub reads: Mutex<HashMap<PathBuf, SystemTime>>,
|
||||
}
|
||||
|
||||
impl ToolState for FakeState {
|
||||
async fn context(&self) -> Result<ToolCtx, String> {
|
||||
Ok(self.ctx.clone())
|
||||
}
|
||||
|
||||
async fn note_read(&self, path: &Path) {
|
||||
self.reads
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(path.to_path_buf(), SystemTime::now());
|
||||
}
|
||||
|
||||
async fn check(&self, path: &Path, _for_write: bool) -> Option<String> {
|
||||
let mtime = std::fs::metadata(path).ok()?.modified().ok()?;
|
||||
let p = path.display();
|
||||
match self.reads.lock().unwrap().get(path) {
|
||||
None => Some(format!(
|
||||
"[refusé] {p} existe mais tu ne l'as pas lu dans cette discussion. Lis-le d'abord (outil read), puis modifie-le."
|
||||
)),
|
||||
Some(at) if mtime > *at => Some(format!(
|
||||
"[refusé] {p} a changé sur le disque depuis ta lecture. Relis-le (outil read) avant de le modifier."
|
||||
)),
|
||||
Some(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn wrote(&self, path: &Path) -> String {
|
||||
self.note_read(path).await;
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn subst(v: &Value, root: &str, mem: &str) -> Value {
|
||||
match v {
|
||||
Value::String(s) => Value::String(s.replace("{{MEM}}", mem).replace("{{ROOT}}", root)),
|
||||
Value::Object(m) => Value::Object(
|
||||
m.iter()
|
||||
.map(|(k, e)| (k.clone(), subst(e, root, mem)))
|
||||
.collect(),
|
||||
),
|
||||
Value::Array(a) => Value::Array(a.iter().map(|e| subst(e, root, mem)).collect()),
|
||||
_ => v.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn norm(s: &str, root: &str) -> String {
|
||||
static DUR: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
|
||||
Regex::new(r"(exit: -?\d+) · [0-9.]+[a-zµ]+(?:[0-9.]+[a-zµ]+)*").unwrap()
|
||||
});
|
||||
DUR.replace_all(&s.replace(root, "{{ROOT}}"), "$1 · {{DUR}}")
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn tree(root: &Path) -> BTreeMap<String, String> {
|
||||
fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<String, String>) {
|
||||
for e in std::fs::read_dir(dir).unwrap().flatten() {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
walk(root, &p, out);
|
||||
continue;
|
||||
}
|
||||
let rel = p
|
||||
.strip_prefix(root)
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
if rel.contains("/.git/") {
|
||||
continue;
|
||||
}
|
||||
if rel.starts_with("ws/") || rel == "dehors.txt" || rel == "evade.txt" {
|
||||
out.insert(
|
||||
rel,
|
||||
String::from_utf8_lossy(&std::fs::read(&p).unwrap()).into_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = BTreeMap::new();
|
||||
walk(root, root, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Rejoue `tests/parity_tools/<name>.json` et rend les écarts avec le Go.
|
||||
pub async fn replay(name: &str) -> Vec<String> {
|
||||
let path =
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("tests/parity_tools/{name}.json"));
|
||||
let fx: Fixture = serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = std::fs::canonicalize(tmp.path()).unwrap();
|
||||
let root_s = root.display().to_string();
|
||||
let now = SystemTime::now();
|
||||
for f in &fx.files {
|
||||
let p = root.join(&f.path);
|
||||
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
|
||||
std::fs::write(&p, &f.content).unwrap();
|
||||
std::fs::File::options()
|
||||
.write(true)
|
||||
.open(&p)
|
||||
.unwrap()
|
||||
.set_modified(now - Duration::from_secs(f.age))
|
||||
.unwrap();
|
||||
}
|
||||
for g in &fx.git_setup {
|
||||
let out = std::process::Command::new("git")
|
||||
.args(&g.args)
|
||||
.current_dir(root.join(&g.dir))
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"git {:?} : {}",
|
||||
g.args,
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
for f in &fx.edits {
|
||||
let p = root.join(&f.path);
|
||||
std::fs::write(&p, &f.content).unwrap();
|
||||
std::fs::File::options()
|
||||
.write(true)
|
||||
.open(&p)
|
||||
.unwrap()
|
||||
.set_modified(now - Duration::from_secs(f.age))
|
||||
.unwrap();
|
||||
}
|
||||
let mem = root.join(&fx.memory_dir);
|
||||
let st = FakeState {
|
||||
ctx: ToolCtx {
|
||||
cwd: root.join(&fx.cwd),
|
||||
memory_dir: mem.clone(),
|
||||
target: String::new(),
|
||||
code: false,
|
||||
},
|
||||
reads: Mutex::new(HashMap::new()),
|
||||
};
|
||||
let mem_s = mem.display().to_string();
|
||||
let mut diffs = Vec::new();
|
||||
for c in &fx.cases {
|
||||
if c.unix && cfg!(windows) {
|
||||
continue;
|
||||
}
|
||||
let args = match subst(&Value::Object(c.args.clone()), &root_s, &mem_s) {
|
||||
Value::Object(m) => m,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let label = tool_call_label(&c.tool, &args);
|
||||
let out = exec_local(&st, c.code, &c.tool, &args, &label)
|
||||
.await
|
||||
.unwrap_or_else(|| panic!("{} : non exécuté ici", c.name));
|
||||
let got = json!({
|
||||
"label": norm(&label, &root_s),
|
||||
"result": norm(&out.result, &root_s),
|
||||
"diff": out.diff,
|
||||
"added": out.added,
|
||||
"removed": out.removed,
|
||||
"ask": out.ask,
|
||||
});
|
||||
let want = json!({
|
||||
"label": c.label,
|
||||
"result": c.result,
|
||||
"diff": c.diff,
|
||||
"added": c.added,
|
||||
"removed": c.removed,
|
||||
"ask": c.ask,
|
||||
});
|
||||
if got != want {
|
||||
diffs.push(format!("{} :\n obtenu {got}\n attendu {want}", c.name));
|
||||
}
|
||||
}
|
||||
let after = tree(&root);
|
||||
if after != fx.after {
|
||||
diffs.push(format!(
|
||||
"arborescence finale :\n obtenu {after:?}\n attendu {:?}",
|
||||
fx.after
|
||||
));
|
||||
}
|
||||
diffs
|
||||
}
|
||||
Reference in new issue
Block a user