focus/crates/focus-tui/tests/job_tests.rs

209 lines
6.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 后台任务的测试:运行、自动压缩、仅压缩(注入假 provider零网络
//! Background-job tests: running, auto-compaction, compact-only (injected fake
//! provider, zero network).
mod fake;
use focus_core::model::*;
use focus_tui::config::TuiConfig;
use focus_tui::job::{JobEvent, RunJob};
use std::path::PathBuf;
use std::sync::mpsc::{channel, Receiver};
fn ready_config(window: Option<u64>) -> TuiConfig {
TuiConfig {
api_key: "sk-test".into(),
model: "gpt-4o".into(),
context_window: window,
..TuiConfig::default()
}
}
/// 运行任务并收集事件,直到 Done。
/// Run the job and collect events until Done.
fn collect(job: RunJob) -> Vec<JobEvent> {
let (tx, rx): (_, Receiver<JobEvent>) = channel();
let handle = focus_tui::job::spawn(job, tx);
let mut events = Vec::new();
while let Ok(ev) = rx.recv() {
let done = matches!(ev, JobEvent::Done { .. });
events.push(ev);
if done {
break;
}
}
let _ = handle.join();
events
}
fn done_of(events: &[JobEvent]) -> (&Vec<Message>, &Vec<Message>, usize, Option<&str>) {
match events.iter().rev().find_map(|e| match e {
JobEvent::Done {
messages,
to_persist,
appended_count,
error,
} => Some((messages, to_persist, *appended_count, error.as_deref())),
_ => None,
}) {
Some(x) => x,
None => panic!("no Done event"),
}
}
fn long_transcript(n: usize) -> Vec<Message> {
(0..n)
.map(|i| Message::user_text(format!("msg {} {}", i, "x".repeat(200))))
.collect()
}
#[test]
fn run_appends_user_and_assistant() {
let job = RunJob {
transcript: Vec::new(),
user_text: Some("hi".into()),
config: ready_config(None),
cwd: PathBuf::from("."),
system_prompt: "sys".into(),
provider: Some(Box::new(fake::FakeProvider::new("agent reply", "SUMMARY"))),
};
let events = collect(job);
let (messages, to_persist, appended, error) = done_of(&events);
assert_eq!(error, None);
assert_eq!(messages.len(), 2);
assert!(matches!(&messages[0], Message::User(_)));
let last = messages.last().unwrap();
let text = match last {
Message::Assistant(a) => a.content[0].as_text().map(|t| t.text.clone()),
_ => None,
};
assert_eq!(text.as_deref(), Some("agent reply"));
// 用户消息由任务统一追加并计入 to_persist。
// The user message is appended by the job and counted in to_persist.
assert_eq!(to_persist.len(), 2);
assert_eq!(appended, 2);
// 转发了 agent 事件。
// Agent events were forwarded.
assert!(events.iter().any(|e| matches!(e, JobEvent::Agent(_))));
}
#[test]
fn auto_compaction_then_run() {
// 超过阈值(窗口 100transcript 约 1000+ token
// Over the threshold (window 100, transcript ~1000+ tokens).
let transcript = long_transcript(10);
let job = RunJob {
transcript,
user_text: Some("next".into()),
config: ready_config(Some(100)),
cwd: PathBuf::from("."),
system_prompt: "sys".into(),
provider: Some(Box::new(fake::FakeProvider::new(
"agent reply",
"SUMMARY TEXT",
))),
};
let events = collect(job);
let (messages, to_persist, appended, error) = done_of(&events);
assert_eq!(error, None);
// 最终 transcript 以摘要 user 消息开头。
// The final transcript starts with a summary user message.
let first = &messages[0];
let summary_text = match first {
Message::User(u) => u.content[0].as_text().map(|t| t.text.clone()),
_ => None,
};
assert!(
summary_text
.as_deref()
.unwrap_or("")
.contains("SUMMARY TEXT"),
"expected summary in transcript head, got {:?}",
summary_text
);
// 末尾是 user("next") + assistant("agent reply")。
// The tail is user("next") + assistant("agent reply").
let n = messages.len();
assert!(matches!(&messages[n - 2], Message::User(_)));
let last_text = match &messages[n - 1] {
Message::Assistant(a) => a.content[0].as_text().map(|t| t.text.clone()),
_ => None,
};
assert_eq!(last_text.as_deref(), Some("agent reply"));
// to_persist = 摘要 + 本轮追加user + assistant
// to_persist = summary + this run's appended (user + assistant).
assert_eq!(to_persist.len(), 3);
assert_eq!(appended, 2);
// 有压缩说明。
// A compaction note was emitted.
assert!(events
.iter()
.any(|e| matches!(e, JobEvent::Note(n) if n.contains("compacting"))));
}
#[test]
fn compact_only_produces_summary() {
let transcript = long_transcript(10);
let job = RunJob {
transcript,
user_text: None,
config: ready_config(Some(100)),
cwd: PathBuf::from("."),
system_prompt: "sys".into(),
provider: Some(Box::new(fake::FakeProvider::new(
"agent reply",
"SUMMARY TEXT",
))),
};
let events = collect(job);
let (messages, to_persist, appended, error) = done_of(&events);
assert_eq!(error, None);
// 摘要消息出现在头部且要被持久化。
// The summary message heads the transcript and is persisted.
assert!(messages[0]
.as_user_text()
.map(|t| t.contains("SUMMARY TEXT"))
.unwrap_or(false));
assert_eq!(to_persist.len(), 1);
assert_eq!(appended, 0);
}
#[test]
fn below_threshold_no_compaction() {
// 窗口极大 → 不触发压缩。
// Huge window → no compaction.
let transcript = long_transcript(3);
let job = RunJob {
transcript,
user_text: Some("hi".into()),
config: ready_config(Some(1_000_000)),
cwd: PathBuf::from("."),
system_prompt: "sys".into(),
provider: Some(Box::new(fake::FakeProvider::new("agent reply", "SUMMARY"))),
};
let events = collect(job);
assert!(!events
.iter()
.any(|e| matches!(e, JobEvent::Note(n) if n.contains("compacting"))));
let (messages, to_persist, appended, _) = done_of(&events);
assert_eq!(messages.len(), 5); // 3 user + user("hi") + assistant
assert_eq!(to_persist.len(), 2);
assert_eq!(appended, 2);
}
/// 为 `Message` 提供便捷的文本访问(测试辅助)。
/// Convenience text accessor on `Message` (test helper).
trait AsUserText {
fn as_user_text(&self) -> Option<String>;
}
impl AsUserText for Message {
fn as_user_text(&self) -> Option<String> {
match self {
Message::User(u) => u.content[0].as_text().map(|t| t.text.clone()),
_ => None,
}
}
}