mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 22:48:11 +02:00
Lot 19 : export, mode Code et critères tenus par la façade (avec LOKI_CHAT=rust)
Quand la façade tient la conversation, /api/chat/export, /api/chat/mode et /api/chat/criteria rejoignent ses routes (chatx.rs, facadeChatRoutes côté Go), sans nouvelle variable. L'export Markdown ou JSON part de la conversation vivante (le Go relisait la copie enregistrée, en retard d'un tour au besoin) : raisonnement replié, outils dans leur état final, compaction signalée, export partiel annoncé, JSON indenté et HTML échappé, sans mise à jour de contexte ni image en base64. Le mode et les critères (scellés, vingt au plus, statuts ramenés) vont sur le fil de la façade. Le reste du mode Code (indice, travaux de fond, outil criteria) reste au Go et lit les mêmes clés. Parité : parity_chatx_test.go tire du Go l'export d'une conversation riche sous 22 jeux d'options et 23 appels au mode et aux critères (base et fil après chacun, erreurs JSON imbriquées comprises), rejoués octet pour octet par tests/chatx.rs ; tests du relais mis à jour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
11 files changed
+2625
-11
No files matched your search
@@ -0,0 +1,186 @@
|
||||
//! Lot 19 : export, mode Code et critères de la discussion tenus par la
|
||||
//! façade quand elle tient la conversation (`LOKI_CHAT=rust`).
|
||||
//!
|
||||
//! Le journal tiré du Go (`internal/loki/parity_chatx_test.go` →
|
||||
//! `tests/parity_chatx/chatx.json`) est rejoué : l'export Markdown et JSON de
|
||||
//! la même conversation sous chaque jeu d'options, puis les appels aux routes
|
||||
//! du mode et des critères par la façade, avec la base et les événements du
|
||||
//! fil après chacun.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::fakego::{self, CryptMode};
|
||||
use loki_rs::chat::persist::decode_state;
|
||||
use loki_rs::chatx::{self as cx, ExportOpts};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn fixture() -> Value {
|
||||
serde_json::from_str(include_str!("parity_chatx/chatx.json")).expect("journal")
|
||||
}
|
||||
|
||||
fn s(v: &Value) -> &str {
|
||||
v.as_str().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parite_de_l_export() {
|
||||
let fx = fixture();
|
||||
// La conversation VIVANTE, images comprises (decode_state, comme le
|
||||
// rechargement du Go, les retire d'un fil relu).
|
||||
let c = &fx["conversation"];
|
||||
let conv = decode_state(c.to_string().as_bytes());
|
||||
let messages: Vec<loki_rs::chat::types::Message> =
|
||||
serde_json::from_value(c["messages"].clone()).unwrap();
|
||||
let log: Vec<loki_rs::chat::types::LogEvent> =
|
||||
serde_json::from_value(c["log"].clone()).unwrap();
|
||||
assert_eq!(conv.log, log);
|
||||
for c in fx["exports"].as_array().unwrap() {
|
||||
let q = s(&c["query"]);
|
||||
let o = cx::opts_from_query(q);
|
||||
let want = &c["opts"];
|
||||
assert_eq!(
|
||||
o,
|
||||
ExportOpts {
|
||||
format: s(&want["format"]).into(),
|
||||
reasoning: want["reasoning"].as_bool().unwrap(),
|
||||
tools: want["tools"].as_bool().unwrap(),
|
||||
results: want["results"].as_bool().unwrap(),
|
||||
turns: want["turns"].as_i64().unwrap(),
|
||||
},
|
||||
"options de {q:?}"
|
||||
);
|
||||
let body = if o.format == "json" {
|
||||
String::from_utf8(cx::export_json(
|
||||
&messages,
|
||||
&log,
|
||||
conv.ctx_used,
|
||||
&o,
|
||||
"{{DATE}}",
|
||||
"{{VERSION}}",
|
||||
))
|
||||
.unwrap()
|
||||
} else {
|
||||
cx::export_markdown(&log, &o, "{{DATE}}", "{{VERSION}}")
|
||||
};
|
||||
assert_eq!(body, s(&c["body"]), "export de {q:?}");
|
||||
}
|
||||
let o = ExportOpts::default();
|
||||
assert_eq!(
|
||||
cx::export_markdown(&[], &o, "{{DATE}}", "{{VERSION}}"),
|
||||
s(&fx["empty_md"])
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8(cx::export_json(&[], &[], 0, &o, "{{DATE}}", "{{VERSION}}")).unwrap(),
|
||||
s(&fx["empty_json"])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn parite_du_mode_et_des_criteres() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (facade, go, _stop, chat) = fakego::start_with(
|
||||
tmp.path().to_path_buf(),
|
||||
fakego::Opts {
|
||||
chat: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let chat = chat.unwrap();
|
||||
*go.crypt_mode.lock().unwrap() = CryptMode::Plain;
|
||||
let conv = chat.sessions.conv();
|
||||
let id = conv.id();
|
||||
let log_len = || conv.with_state(|st| st.log.len());
|
||||
for st in fixture()["steps"].as_array().unwrap() {
|
||||
let name = s(&st["name"]);
|
||||
let before = log_len();
|
||||
let (code, body) = fakego::send(
|
||||
facade,
|
||||
s(&st["method"]),
|
||||
s(&st["path"]),
|
||||
&[],
|
||||
st["body"].as_str().unwrap_or(""),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
code.as_u16() as i64,
|
||||
st["status"].as_i64().unwrap(),
|
||||
"{name}"
|
||||
);
|
||||
let out: Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(out, st["out"], "{name}");
|
||||
assert_eq!(
|
||||
go.get("chat", &format!("mode:{id}")).unwrap_or_default(),
|
||||
s(&st["mode"]),
|
||||
"{name} : mode"
|
||||
);
|
||||
let crit: Value = go
|
||||
.get("chat", &format!("crit:{id}"))
|
||||
.map(|v| serde_json::from_str(&v).unwrap())
|
||||
.unwrap_or(Value::Null);
|
||||
assert_eq!(crit, st["criteria"], "{name} : critères");
|
||||
let deltas: Vec<Value> = conv.with_state(|s| {
|
||||
s.log[before..]
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let mut d = e.delta.clone();
|
||||
d.remove("ts");
|
||||
Value::Object(d)
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
let want = st["deltas"].as_array().cloned().unwrap_or_default();
|
||||
assert_eq!(deltas, want, "{name} : fil");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn export_par_la_route() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (facade, _go, _stop, chat) = fakego::start_with(
|
||||
tmp.path().to_path_buf(),
|
||||
fakego::Opts {
|
||||
chat: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let chat = chat.unwrap();
|
||||
let conv = chat.sessions.conv();
|
||||
let mut d = serde_json::Map::new();
|
||||
d.insert("user".into(), json!("Salut"));
|
||||
conv.append_delta(conv.epoch(), d);
|
||||
let mut d = serde_json::Map::new();
|
||||
d.insert("content".into(), json!("Bonjour !"));
|
||||
conv.append_delta(conv.epoch(), d);
|
||||
let (code, headers, body) =
|
||||
fakego::get(facade, "/api/chat/export?format=json&reasoning=0", &[]).await;
|
||||
assert_eq!(code, 200);
|
||||
assert_eq!(headers["content-type"], "application/json; charset=utf-8");
|
||||
let disp = headers["content-disposition"].to_str().unwrap();
|
||||
assert!(
|
||||
disp.starts_with("attachment; filename=\"loki-conversation-") && disp.ends_with(".json\""),
|
||||
"{disp}"
|
||||
);
|
||||
assert_eq!(headers["content-length"], body.len().to_string().as_str());
|
||||
let v: Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(v["log"].as_array().unwrap().len(), 2);
|
||||
assert!(v["exported_at"].as_str().unwrap().contains('T'));
|
||||
let (_, headers, body) = fakego::get(facade, "/api/chat/export", &[]).await;
|
||||
assert_eq!(headers["content-type"], "text/markdown; charset=utf-8");
|
||||
let md = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(
|
||||
md.contains("## Vous\n\nSalut\n") && md.contains("## Loki\n\nBonjour !\n"),
|
||||
"{md}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sans_la_conversation_ces_routes_restent_au_go() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (facade, _go, _stop) = fakego::start(tmp.path().to_path_buf()).await;
|
||||
for p in ["/api/chat/export", "/api/chat/mode", "/api/chat/criteria"] {
|
||||
let (_, _, b) = fakego::get(facade, p, &[]).await;
|
||||
assert_eq!(&b[..], format!("go:{p}").as_bytes());
|
||||
}
|
||||
}
|
||||
@@ -1352,8 +1352,8 @@ async fn routes_du_chat_par_la_facade() {
|
||||
let (_, list) = call(facade, "GET", "/api/conversations", "").await;
|
||||
assert_eq!(list["conversations"][0]["title"], "Mon fil");
|
||||
|
||||
// Mode Code : servi par le Go, qui pousse son événement au journal d'ici
|
||||
// par l'API interne de la façade (jeton interne exigé).
|
||||
// Mode Code : servi par la façade (lot 19), son événement au journal
|
||||
// d'ici, sans passer par le Go.
|
||||
assert_eq!(
|
||||
call(facade, "POST", "/api/chat/mode", r#"{"mode":"code"}"#)
|
||||
.await
|
||||
@@ -1361,11 +1361,14 @@ async fn routes_du_chat_par_la_facade() {
|
||||
200
|
||||
);
|
||||
assert!(
|
||||
go.relayed
|
||||
!go.relayed
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&"/api/chat/mode".to_string())
|
||||
);
|
||||
assert_eq!(deltas(&chat, "mode"), [serde_json::json!("code")]);
|
||||
// Le Go pousse encore ses propres événements (indice de code, outil
|
||||
// criteria) par l'API interne de la façade (jeton interne exigé).
|
||||
let (code, _) = call(
|
||||
facade,
|
||||
"POST",
|
||||
@@ -1376,7 +1379,10 @@ async fn routes_du_chat_par_la_facade() {
|
||||
assert_eq!(code, 404, "sans le jeton, l'API interne n'existe pas");
|
||||
let code = call_internal(facade, "/internal/facade/v1/delta", r#"{"mode":"code"}"#).await;
|
||||
assert_eq!(code, 204);
|
||||
assert_eq!(deltas(&chat, "mode"), [serde_json::json!("code")]);
|
||||
assert_eq!(
|
||||
deltas(&chat, "mode"),
|
||||
[serde_json::json!("code"), serde_json::json!("code")]
|
||||
);
|
||||
// Fil déjà rempli : le déverrouillage ne le recharge pas.
|
||||
assert_eq!(
|
||||
call_internal(facade, "/internal/facade/v1/reload", "").await,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user