use super::*;
fn enabled_ttsr_settings() -> crate::config::TtsrSettings {
crate::config::TtsrSettings {
enabled: true,
rules: Vec::new(),
}
}
fn ttsr_run_request<'a, 'sink>(
prompt: &'a str,
cwd: &'a std::path::Path,
session: Option<&'a crate::sessions::Session>,
output_sink: Option<&'sink mut dyn AgentOutputSink>,
) -> AgentRunRequest<'a, 'sink> {
AgentRunRequest {
prompt,
tools: None,
hooks: None,
session,
cwd,
output_sink,
cancellation: AgentCancellation::default(),
session_title_job: None,
semantic_progress_timeout: None,
invocation_mode: crate::output::InvocationMode::Print,
agent_id: None,
initial_instructions: &[],
ttsr: enabled_ttsr_settings(),
herdr_reporter: None,
}
}
#[test]
fn ttsr_stream_match_blocks_delta_before_sink() {
let temp = tempfile::TempDir::new().unwrap();
let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let provider = ScriptedProvider::new(vec![vec![text("safe "), text("rm -rf / "), done()]]);
let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
let mut sink = CapturingOutputSink::default();
let output = agent
.run_print_with_tools_streaming_output_cancellable(
&provider,
ttsr_run_request("say", temp.path(), Some(&session), Some(&mut sink)),
)
.unwrap();
assert_eq!(output.text, "safe ");
assert_eq!(sink.text, "safe ");
assert!(!assistant_chunk_text(&session.read_events().unwrap()).contains("rm -rf"));
assert!(event_types(&session).contains(&"ttsr_injection".to_string()));
}
#[test]
fn ttsr_match_aborts_and_retries_with_reminder() {
let temp = tempfile::TempDir::new().unwrap();
let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let provider = ScriptedProvider::new(vec![
vec![text("git reset --hard"), done()],
text_done(" safe retry"),
]);
let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
let output = agent
.run_print_with_tools_streaming_output_cancellable(
&provider,
ttsr_run_request("say", temp.path(), Some(&session), None),
)
.unwrap();
assert_eq!(output.text, " safe retry");
assert_eq!(provider.requests().len(), 2);
assert!(provider.requests()[1].messages().iter().any(|message| {
message.role == crate::providers::MessageRole::System
&& message.content.contains("Git safety reminder")
}));
let events = session.read_events().unwrap();
assert!(
events
.iter()
.any(|event| event.event_type == "ttsr_injection")
);
assert!(!events.iter().any(|event| {
event.event_type == "turn_status" && event.payload["status"] == "cancelled"
}));
}
#[test]
fn ttsr_request_cancellation_does_not_poison_outer_user_cancellation() {
let temp = tempfile::TempDir::new().unwrap();
let provider = ScriptedProvider::new(vec![
vec![text("git reset --hard"), done()],
text_done("safe after retry"),
]);
let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
let output = agent
.run_print_with_tools_streaming_output_cancellable(
&provider,
ttsr_run_request("say", temp.path(), None, None),
)
.unwrap();
assert_eq!(output.text, "safe after retry");
assert_eq!(provider.requests().len(), 2);
}
#[test]
fn ttsr_completed_tool_call_arguments_are_checked_before_tool_lifecycle() {
let temp = tempfile::TempDir::new().unwrap();
let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let provider = ScriptedProvider::new(vec![
vec![bash_call("bad", "rm -rf / "), done()],
text_done("no tool"),
]);
let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
let output = agent
.run_print_with_tools_streaming_output_cancellable(
&provider,
ttsr_run_request("use tool", temp.path(), Some(&session), None),
)
.unwrap();
assert_eq!(output.text, "no tool");
assert!(!event_types(&session).contains(&"tool_call".to_string()));
assert!(event_types(&session).contains(&"ttsr_injection".to_string()));
}
#[test]
fn ttsr_retry_exhaustion_records_failed_turn() {
let temp = tempfile::TempDir::new().unwrap();
let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let provider = ScriptedProvider::new(vec![
vec![text("git clean -fd"), done()],
vec![text("git clean -fd"), done()],
vec![text("git clean -fd"), done()],
vec![text("git clean -fd"), done()],
]);
let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
let error = agent
.run_print_with_tools_streaming_output_cancellable(
&provider,
ttsr_run_request("say", temp.path(), Some(&session), None),
)
.unwrap_err()
.to_string();
assert!(
error.contains("TTSR") || error.contains("interrupted"),
"{error}"
);
let events = session.read_events().unwrap();
assert_eq!(
events
.iter()
.filter(|event| event.event_type == "ttsr_injection")
.count(),
3
);
assert!(
events.iter().any(|event| {
event.event_type == "turn_status" && event.payload["status"] == "failed"
})
);
}
#[test]
fn invalid_ttsr_config_fails_before_durable_turn_start() {
let temp = tempfile::TempDir::new().unwrap();
let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let provider = ScriptedProvider::new(vec![text_done("unreachable")]);
let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
let mut request = ttsr_run_request("say", temp.path(), Some(&session), None);
request.ttsr.rules.push(crate::config::TtsrRuleSetting {
pattern: "[".to_string(),
reminder: "invalid".to_string(),
});
let error = agent
.run_print_with_tools_streaming_output_cancellable(&provider, request)
.unwrap_err();
assert!(error.to_string().contains("regex"));
assert!(provider.requests().is_empty());
assert!(session.read_events().unwrap().is_empty());
}