209 lines
6.7 KiB
Rust
209 lines
6.7 KiB
Rust
//! 后台任务的测试:运行、自动压缩、仅压缩(注入假 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() {
|
||
// 超过阈值(窗口 100,transcript 约 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,
|
||
}
|
||
}
|
||
}
|