use super::*;
use crate::app::orchestrator::request::inject_attachments;
use crate::entities::attachment::{AttachMode, Attachment};
use crate::shared::api::ChatRequest;
use crate::shared::config::{AttachmentSettings, CompactionSettings};
fn ru() -> &'static crate::shared::i18n::Locale {
crate::shared::i18n::locale(crate::shared::i18n::Lang::Ru)
}
const NO_INDEX: &[uuid::Uuid] = &[];
fn att(name: &str, text: &str, mode: AttachMode) -> Attachment {
let bytes = text.len();
Attachment::new(name, format!("/tmp/{name}"), text.to_string(), bytes, mode)
}
fn request_of(chat: &Chat, compaction: &CompactionSettings, history_tools: bool) -> ChatRequest {
build_request(
chat,
SamplingConfig::default(),
vec![],
&PromptContext {
attachments: &AttachmentSettings::default(),
compaction,
indexed: NO_INDEX,
files: &[],
python_dirs: ("/w/in", "/w/out"),
history_tools,
offered_tools: &[],
loc: ru(),
},
)
}
#[test]
fn the_chat_files_block_names_handles_and_staged_names() {
let p = Profile::new("X", "Ты — X.");
let chat = Chat::from_profile(&p, "c");
let attached = att("report.pdf", "the extracted text", AttachMode::ByReference);
let items = crate::features::chat_inputs::items(
std::slice::from_ref(&attached),
&[],
&[],
std::path::Path::new("/d"),
);
let req = build_request(
&chat,
SamplingConfig::default(),
vec![],
&PromptContext {
attachments: &AttachmentSettings::default(),
compaction: &CompactionSettings::default(),
indexed: NO_INDEX,
files: &items,
python_dirs: ("/w/in", "/w/out"),
history_tools: false,
offered_tools: &[],
loc: ru(),
},
);
let system = req.system.unwrap_or_default();
assert!(system.contains("#1 report.pdf"), "{system}");
assert!(system.contains("/w/in/report.pdf.txt"), "{system}");
assert!(system.contains("python_exec"), "{system}");
let bare = request_of(&chat, &CompactionSettings::default(), false)
.system
.unwrap_or_default();
assert!(!bare.contains("/w/in"), "{bare}");
let local = build_request(
&chat,
SamplingConfig::default(),
vec![],
&PromptContext {
attachments: &AttachmentSettings::default(),
compaction: &CompactionSettings::default(),
indexed: NO_INDEX,
files: &items,
python_dirs: crate::shared::config::PythonMode::Local.dirs(),
history_tools: false,
offered_tools: &[],
loc: ru(),
},
)
.system
.unwrap_or_default();
assert!(local.contains("#1 report.pdf"), "{local}");
assert!(local.contains("in/report.pdf.txt"), "{local}");
assert!(!local.contains("/w/"), "{local}");
}
#[test]
fn build_request_puts_system_aside_and_maps_roles() {
let mut p = Profile::new("X", "Ты — X.");
p.greeting = Some("Здравствуйте!".into());
let mut chat = Chat::from_profile(&p, "c");
chat.push_message(Message::assistant("Здравствуйте!"));
chat.push_message(Message::user("привет"));
let req = request_of(&chat, &CompactionSettings::default(), true);
assert_eq!(req.system.as_deref(), Some("Ты — X."));
assert_eq!(req.messages.len(), 2);
}
#[test]
fn build_request_appends_attached_files_to_system() {
let p = Profile::new("X", "Ты — X.");
let mut chat = Chat::from_profile(&p, "c");
chat.push_message(Message::user("что в файле?"));
chat.attachments
.push(att("notes.md", "секретное число 4242", AttachMode::Inline));
let req = request_of(&chat, &CompactionSettings::default(), true);
let system = req.system.expect("system with the attachment block");
assert!(system.starts_with("Ты — X."), "{system}");
assert!(system.contains("notes.md"), "{system}");
assert!(system.contains("секретное число 4242"), "{system}");
assert_eq!(req.messages.len(), 1);
assert_eq!(req.messages[0].content, "что в файле?");
}
#[test]
fn inject_attachments_is_a_noop_without_attachments() {
let system = Some("Ты — X.".to_string());
assert_eq!(
inject_attachments(
system.clone(),
&[],
&AttachmentSettings::default(),
NO_INDEX,
ru()
),
system
);
assert_eq!(
inject_attachments(None, &[], &AttachmentSettings::default(), NO_INDEX, ru()),
None
);
}
#[test]
fn inline_carries_full_text_by_reference_only_an_excerpt() {
let cfg = AttachmentSettings {
excerpt_tokens: 20,
..Default::default()
};
let tail = "ХВОСТ-МАРКЕР";
let long = format!("начало документа, довольно длинное вступление… {tail}");
let items = vec![
att("small.txt", "короткий текст", AttachMode::Inline),
att("big.txt", &long, AttachMode::ByReference),
];
let out = inject_attachments(None, &items, &cfg, NO_INDEX, ru()).expect("a block");
assert!(out.contains("короткий текст"), "{out}");
assert!(out.contains("начало документа"), "{out}");
assert!(
!out.contains(tail),
"the by-reference tail must stay out of the prompt: {out}"
);
assert!(
out.contains("small.txt") && out.contains("big.txt"),
"{out}"
);
assert!(out.contains("ДАННЫЕ"), "{out}");
}
#[test]
fn by_reference_entry_tells_the_model_how_to_read_the_rest() {
let cfg = AttachmentSettings {
excerpt_tokens: 5,
page_tokens: 10,
..Default::default()
};
let long = "слово ".repeat(200);
let items = vec![att("big.txt", &long, AttachMode::ByReference)];
let out = inject_attachments(None, &items, &cfg, NO_INDEX, ru()).unwrap();
assert!(
out.contains("attachment_read"),
"the model must be told which tool reads the rest: {out}"
);
let pages = items[0].page_count(cfg.page_tokens);
assert!(pages > 1, "the fixture must span several pages");
assert!(
out.contains(&pages.to_string()),
"the page range must be stated ({pages} pages): {out}"
);
let inline = vec![att("small.txt", "коротко", AttachMode::Inline)];
let out = inject_attachments(None, &inline, &cfg, NO_INDEX, ru()).unwrap();
assert!(!out.contains("attachment_read"), "{out}");
}
#[test]
fn search_is_offered_only_for_an_indexed_file() {
let cfg = AttachmentSettings {
excerpt_tokens: 5,
page_tokens: 10,
..Default::default()
};
let items = vec![att(
"big.txt",
&"слово ".repeat(200),
AttachMode::ByReference,
)];
let without = inject_attachments(None, &items, &cfg, NO_INDEX, ru()).unwrap();
assert!(
!without.contains("attachment_search"),
"an unindexed file must not advertise search: {without}"
);
assert!(without.contains("attachment_read"), "{without}");
let with = inject_attachments(None, &items, &cfg, &[items[0].id], ru()).unwrap();
assert!(with.contains("attachment_search"), "{with}");
assert!(
with.contains("attachment_read"),
"the guaranteed path stays advertised: {with}"
);
}
#[test]
fn fence_widens_so_a_file_cannot_close_its_own_section() {
let hostile = "текст >>> и ещё >>> внутри";
let items = vec![att("evil.md", hostile, AttachMode::Inline)];
let out =
inject_attachments(None, &items, &AttachmentSettings::default(), NO_INDEX, ru()).unwrap();
assert!(
out.contains(hostile),
"the content is still delivered: {out}"
);
assert!(
out.contains(">>>>") && out.contains("<<<<"),
"the fence must widen past the content's own run: {out}"
);
}
#[test]
fn block_stands_alone_when_the_chat_has_no_system_message() {
let items = vec![att("a.txt", "содержимое", AttachMode::Inline)];
let out =
inject_attachments(None, &items, &AttachmentSettings::default(), NO_INDEX, ru()).unwrap();
assert!(out.starts_with('['), "the block leads: {out}");
assert!(out.contains("содержимое"));
}
#[test]
fn block_is_localized_for_all_langs() {
let items = vec![att("a.txt", "payload", AttachMode::ByReference)];
for &lang in crate::shared::i18n::Lang::ALL {
let loc = crate::shared::i18n::locale(lang);
let out = inject_attachments(None, &items, &AttachmentSettings::default(), NO_INDEX, loc)
.unwrap();
assert!(!out.contains('{') && !out.contains('}'), "{lang:?}: {out}");
if lang == crate::shared::i18n::Lang::En {
assert!(
!out.chars().any(|c| ('\u{0400}'..='\u{04FF}').contains(&c)),
"Cyrillic leaked into the en block: {out}"
);
}
}
}
fn compacted_chat(n: usize, upto: usize, summary: &str) -> Chat {
let p = Profile::new("X", "Ты — X.");
let mut chat = Chat::from_profile(&p, "c");
for i in 0..n {
chat.push_message(Message::user(format!("вопрос {i}")));
chat.push_message(Message::assistant(format!("ответ {i}")));
}
chat.compaction = Some(crate::entities::chat::Compaction {
summary: summary.into(),
upto,
boundary_id: chat.messages[upto].id,
compacted_at: chrono::Utc::now(),
rolls: 1,
});
chat
}
#[test]
fn compaction_replaces_the_prefix_with_a_summary_block() {
let chat = compacted_chat(5, 6, "Ранее: обсудили хранилище, выбрали SQLite.");
let req = request_of(&chat, &CompactionSettings::default(), true);
let system = req.system.expect("system with the summary block");
assert!(system.starts_with("Ты — X."), "{system}");
assert!(system.contains("выбрали SQLite"), "{system}");
assert_eq!(req.messages.len(), 4);
assert_eq!(chat.messages.len(), 10);
}
#[test]
fn the_block_names_the_read_back_tools_only_when_they_are_offered() {
let chat = compacted_chat(5, 6, "Ранее: выбрали SQLite.");
let block = |history_tools| {
request_of(&chat, &CompactionSettings::default(), history_tools)
.system
.expect("system with the summary block")
};
let with = block(true);
assert!(
with.contains("history_search") && with.contains("history_read"),
"{with}"
);
let without = block(false);
assert!(
!without.contains("history_search") && !without.contains("history_read"),
"a tool the model does not have must not be named: {without}"
);
for system in [&with, &without] {
assert!(system.contains("выбрали SQLite"), "{system}");
assert!(system.contains("ДАННЫЕ"), "{system}");
}
}
#[test]
fn the_master_switch_off_makes_compression_inert() {
let chat = compacted_chat(5, 6, "Ранее: выбрали SQLite.");
let off = CompactionSettings {
enabled: false,
..Default::default()
};
let req = request_of(&chat, &off, true);
assert_eq!(req.system.as_deref(), Some("Ты — X."));
assert_eq!(req.messages.len(), 10);
assert!(
chat.compaction.is_some(),
"the summary must survive the flip"
);
}
#[test]
fn a_vanished_boundary_falls_back_to_the_whole_history() {
let mut chat = compacted_chat(5, 6, "Ранее: выбрали SQLite.");
chat.messages.remove(6);
let req = request_of(&chat, &CompactionSettings::default(), true);
assert_eq!(req.system.as_deref(), Some("Ты — X."));
assert_eq!(req.messages.len(), 9);
}
#[test]
fn the_workspace_block_appears_only_with_a_project() {
use crate::app::orchestrator::request::inject_workspace;
use crate::entities::workspace::Workspace;
use crate::features::tools::code;
let readers: Vec<String> = vec![code::CODE_READ_ID.into(), code::CODE_GREP_ID.into()];
let ws = Workspace::new("D:/Projects/app");
let none = inject_workspace(Some("persona".into()), None, &readers, ru());
assert_eq!(
none,
Some("persona".into()),
"no project must leave the system prompt untouched"
);
let with = inject_workspace(Some("persona".into()), Some(&ws), &readers, ru()).unwrap();
assert!(
with.starts_with("persona"),
"the persona stays first: {with}"
);
assert!(with.contains("D:/Projects/app"), "{with}");
assert!(with.contains("app"), "the name is shown too: {with}");
assert!(with.contains("code_read"), "{with}");
assert!(
!with.contains("code_edit"),
"a tool this turn does not have must not be named: {with}"
);
let unreachable = inject_workspace(Some("persona".into()), Some(&ws), &[], ru()).unwrap();
assert!(unreachable.contains("D:/Projects/app"), "{unreachable}");
assert!(
!unreachable.contains("code_read"),
"a tool this turn does not have must not be named: {unreachable}"
);
}
#[test]
fn the_block_quotes_the_command_lines_it_can_run() {
use crate::app::orchestrator::request::inject_workspace;
use crate::entities::workspace::{CommandSlot, Workspace};
use crate::features::tools::code;
let mut ws = Workspace::new("/home/u/app");
ws.set_command(CommandSlot::Build, Some("cargo build --offline".into()));
ws.set_command(CommandSlot::Test, Some("cargo test --offline".into()));
let offered: Vec<String> = vec![code::CODE_BUILD_ID.into()];
let block = inject_workspace(None, Some(&ws), &offered, ru()).unwrap();
assert!(block.contains("cargo build --offline"), "{block}");
assert!(
!block.contains("cargo test"),
"a slot whose tool is not offered must not be described: {block}"
);
assert!(
!block.contains("code_test"),
"…and neither must its tool: {block}"
);
}
#[test]
fn a_chat_without_a_project_builds_an_unchanged_request() {
let p = Profile::new("X", "persona");
let mut chat = Chat::from_profile(&p, "c");
chat.push_message(Message::user("hi"));
assert!(chat.workspace.is_none());
let req = request_of(&chat, &CompactionSettings::default(), false);
assert!(
req.system.is_none() || !req.system.as_deref().unwrap().contains("code_read"),
"no project must add nothing: {:?}",
req.system
);
}
#[test]
fn a_notification_is_user_text_merged_into_the_next_user_message() {
let p = Profile::new("X", "Ты — X.");
let run = uuid::Uuid::new_v4();
let mut chat = Chat::from_profile(&p, "c");
chat.push_message(Message::user("delegate"));
chat.push_message(Message::assistant("started"));
chat.push_message(Message::notification(
run,
"[note] the run finished: harsh view",
));
chat.push_message(Message::user("thanks"));
let req = request_of(&chat, &CompactionSettings::default(), false);
let roles: Vec<_> = req.messages.iter().map(|m| m.role).collect();
assert_eq!(
roles,
[
crate::shared::api::contract::ApiRole::User,
crate::shared::api::contract::ApiRole::Assistant,
crate::shared::api::contract::ApiRole::User
],
"{:?}",
req.messages
);
assert_eq!(
req.messages[2].content,
"[note] the run finished: harsh view
thanks"
);
chat.messages.pop();
let req = request_of(&chat, &CompactionSettings::default(), false);
assert_eq!(req.messages.len(), 3);
assert_eq!(
req.messages[2].role,
crate::shared::api::contract::ApiRole::User
);
assert_eq!(
req.messages[2].content,
"[note] the run finished: harsh view"
);
chat.push_message(Message::assistant("the review is in"));
let req = request_of(&chat, &CompactionSettings::default(), false);
let roles: Vec<_> = req.messages.iter().map(|m| m.role).collect();
assert_eq!(
roles,
[
crate::shared::api::contract::ApiRole::User,
crate::shared::api::contract::ApiRole::Assistant,
crate::shared::api::contract::ApiRole::User,
crate::shared::api::contract::ApiRole::Assistant
]
);
chat.push_message(Message::new(MessageRole::System, "Director: wrap up"));
let req = request_of(&chat, &CompactionSettings::default(), false);
assert_eq!(req.messages.len(), 4);
}
#[test]
fn withheld_images_become_markers_that_name_them() {
use crate::app::orchestrator::request::withhold_images;
use crate::shared::api::{ApiImage, ApiMessage};
let loc = crate::shared::i18n::locale(crate::shared::i18n::Lang::En);
let label = |n: usize, name: &str| {
Some(loc.tf(
"prompt.images.label",
&[("n", &n.to_string()), ("name", name)],
))
};
let image = |label: Option<String>| ApiImage::new("image/png", "AAAA", label);
let mut messages = vec![
ApiMessage::user("what is this?").with_images(vec![
image(label(1, "figure.png")),
image(label(2, "chart.png")),
]),
ApiMessage::assistant("A blue field."),
ApiMessage::tool("c1", "rendered").with_images(vec![image(label(1, "tool-image-1.png"))]),
ApiMessage::user("").with_images(vec![image(None)]),
ApiMessage::user("no pictures here"),
];
assert_eq!(withhold_images(&mut messages, loc), 4);
assert!(messages.iter().all(|m| m.images.is_empty()));
let marker = |name: &str| loc.tf("prompt.images.withheld", &[("image", name)]);
assert_eq!(
messages[0].content,
format!(
"what is this?\n\n{}\n{}",
marker("Image #1 — \"figure.png\""),
marker("Image #2 — \"chart.png\"")
)
);
assert_eq!(messages[1].content, "A blue field.");
assert_eq!(
messages[2].content,
format!("rendered\n\n{}", marker("Image #1 — \"tool-image-1.png\""))
);
assert_eq!(
messages[3].content,
marker(loc.t("prompt.images.withheld_unnamed"))
);
assert_eq!(messages[4].content, "no pictures here");
assert_eq!(
marker("Image #1 — \"figure.png\""),
"[Image #1 — \"figure.png\" is not included: the current model does not accept images. \
You have not seen it — do not describe what it shows; say that you cannot see it.]",
"the wording the measurement ran with"
);
}