use std::sync::Arc;
use serde_json::json;
use uuid::Uuid;
use super::testkit::{RoleRecorder, ctx_with_backends};
use super::{Tool, ToolContext};
use crate::shared::api::EmbedRole;
use crate::shared::api::mock::MockBackend;
use crate::shared::storage::Storage;
fn ctx() -> (
tempfile::TempDir,
Arc<Storage>,
ToolContext,
Arc<RoleRecorder>,
) {
let rec = Arc::new(RoleRecorder::new());
let (dir, storage, ctx) = ctx_with_backends(
Uuid::new_v4(),
Arc::new(MockBackend::scripted(vec![])),
rec.clone(),
);
(dir, storage, ctx, rec)
}
#[tokio::test]
async fn rag_search_embeds_the_query_as_a_query() {
let (_d, _s, ctx, rec) = ctx();
super::rag::RagAdd
.invoke(
&ctx,
json!({"text": "столица Франции — Париж", "source": "f"}),
)
.await
.unwrap();
assert!(
rec.all_were(EmbedRole::Passage),
"indexing writes passages: {:?}",
rec.roles()
);
rec.calls.lock().unwrap().clear();
super::rag::RagSearch
.invoke(&ctx, json!({"query": "какая столица?"}))
.await
.unwrap();
assert_eq!(rec.roles(), vec![EmbedRole::Query]);
}
#[tokio::test]
async fn note_recall_embeds_the_query_as_a_query() {
let (_d, _s, ctx, rec) = ctx();
super::notes::NoteSave
.invoke(&ctx, json!({"content": "пользователь ценит краткость"}))
.await
.unwrap();
rec.calls.lock().unwrap().clear();
super::notes::NoteRecall
.invoke(&ctx, json!({"query": "что известно про ответы?"}))
.await
.unwrap();
let queries = rec
.roles()
.iter()
.filter(|r| **r == EmbedRole::Query)
.count();
assert_eq!(queries, 1, "exactly one query: {:?}", rec.roles());
}
#[tokio::test]
async fn stored_text_is_always_embedded_as_a_passage() {
let (_d, _s, ctx, rec) = ctx();
super::rag::RagAdd
.invoke(&ctx, json!({"text": "чанк базы знаний", "source": "s"}))
.await
.unwrap();
super::notes::NoteSave
.invoke(&ctx, json!({"content": "заметка"}))
.await
.unwrap();
assert!(
rec.all_were(EmbedRole::Passage),
"every stored vector is a passage: {:?}",
rec.roles()
);
}
#[tokio::test]
async fn note_revise_re_embeds_as_a_passage() {
let (_d, _s, ctx, rec) = ctx();
let saved = super::notes::NoteSave
.invoke(&ctx, json!({"content": "первая версия"}))
.await
.unwrap();
let id = saved
.result
.split("id=")
.nth(1)
.and_then(|s| s.split(|c: char| !c.is_ascii_hexdigit() && c != '-').next())
.unwrap()
.to_string();
rec.calls.lock().unwrap().clear();
super::notes::NoteRevise
.invoke(&ctx, json!({"id": id, "content": "вторая версия"}))
.await
.unwrap();
assert!(
rec.all_were(EmbedRole::Passage),
"a rewritten note replaces a stored vector: {:?}",
rec.roles()
);
}
#[tokio::test]
async fn the_note_save_duplicate_gate_is_symmetric() {
let (_d, _s, ctx, rec) = ctx();
super::notes::NoteSave
.invoke(&ctx, json!({"content": "пользователь ценит краткость"}))
.await
.unwrap();
super::notes::NoteSave
.invoke(&ctx, json!({"content": "пользователь любит лаконичность"}))
.await
.unwrap();
assert!(
rec.all_were(EmbedRole::Passage),
"the duplicate gate compares stored notes: {:?}",
rec.roles()
);
}
#[tokio::test]
async fn the_add_insight_gate_probes_with_a_passage_not_a_query() {
let (_d, _s, ctx, rec) = ctx();
super::self_model::AddInsight
.invoke(
&ctx,
json!({"text": "я склонен объяснять слишком подробно"}),
)
.await
.unwrap();
super::self_model::AddInsight
.invoke(
&ctx,
json!({"text": "я часто даю больше деталей, чем нужно"}),
)
.await
.unwrap();
assert!(
rec.all_were(EmbedRole::Passage),
"the observation gate is passage-to-passage: {:?}",
rec.roles()
);
}
#[tokio::test]
async fn the_trait_gate_embeds_both_sides_alike() {
let (_d, _s, ctx, rec) = ctx();
super::self_model::UpdateUserModel
.invoke(&ctx, json!({"add_traits": ["ценит краткость"]}))
.await
.unwrap();
rec.calls.lock().unwrap().clear();
super::self_model::UpdateUserModel
.invoke(&ctx, json!({"add_traits": ["предпочитает лаконичность"]}))
.await
.unwrap();
assert!(
rec.all_were(EmbedRole::Passage),
"trait comparison is symmetric: {:?}",
rec.roles()
);
}