use super::*;
pub(super) fn test_area() -> ratatui::layout::Rect {
ratatui::layout::Rect::new(0, 0, 100, 30)
}
pub(super) fn reap_owned_worker(
app: &mut MissionControlApp,
ui_state: &mut state::MissionControlState,
reap: impl Fn(&mut MissionControlApp, &mut state::MissionControlState) -> bool,
) {
let deadline = Instant::now() + Duration::from_secs(5);
while !reap(app, ui_state) {
assert!(Instant::now() < deadline, "owned worker did not finish");
thread::sleep(Duration::from_millis(1));
}
}
pub(super) fn reap_provider_worker(
app: &mut MissionControlApp,
ui_state: &mut state::MissionControlState,
receiver: &Receiver<TuiEvent>,
) {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
while let Ok(event) = receiver.try_recv() {
let mut result = DrainResult::default();
apply_control_event_to_state(ui_state, event, &mut result);
for (provider_id, provider_result) in result.custom_provider_finished {
if provider_result.is_ok() {
app.refresh_runtime_after_custom_provider(&provider_id, ui_state);
}
}
}
if app.finish_worker_if_ready(ui_state) {
return;
}
assert!(Instant::now() < deadline, "provider worker did not finish");
thread::yield_now();
}
}
pub(super) fn reap_model_selection_worker(
app: &mut MissionControlApp,
ui_state: &mut state::MissionControlState,
receiver: &Receiver<TuiEvent>,
) {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let worker_reaped = app.reap_model_selection_workers(ui_state);
let mut drain = DrainResult::default();
while let Ok(event) = receiver.try_recv() {
apply_control_event_to_state(ui_state, event, &mut drain);
}
let completion_seen = !drain.model_selection_finished.is_empty();
app.handle_model_selection_drain(ui_state, &drain);
if worker_reaped {
return;
}
if completion_seen {
let expected_errors: Vec<_> = drain
.model_selection_finished
.iter()
.filter_map(|finished| {
finished.result.as_ref().err().map(|error| {
format!(
"model selection worker {} failed: {}",
finished.request_id, error
)
})
})
.collect();
let errors = app.join_model_selection_workers_for_cleanup();
assert_eq!(errors, expected_errors, "model selection worker cleanup");
return;
}
assert!(
Instant::now() < deadline,
"model selection worker did not finish"
);
thread::yield_now();
}
}
pub(super) fn reap_rewind_worker(
app: &mut MissionControlApp,
ui_state: &mut state::MissionControlState,
receiver: &Receiver<TuiEvent>,
) {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let mut drain = DrainResult::default();
while let Ok(event) = receiver.try_recv() {
apply_control_event_to_state(ui_state, event, &mut drain);
}
let completion_seen = !drain.rewind_finished.is_empty();
app.handle_rewind_drain(ui_state, &drain);
let worker_reaped = app.reap_rewind_workers(ui_state);
if completion_seen {
let errors = app.join_rewind_workers_for_cleanup();
assert!(errors.is_empty(), "rewind worker cleanup: {errors:?}");
return;
}
if worker_reaped {
return;
}
assert!(Instant::now() < deadline, "rewind worker did not finish");
thread::yield_now();
}
}
pub(super) fn reap_export_worker(
app: &mut MissionControlApp,
ui_state: &mut state::MissionControlState,
receiver: &Receiver<TuiEvent>,
) {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let mut drain = DrainResult::default();
while let Ok(event) = receiver.try_recv() {
apply_control_event_to_state(ui_state, event, &mut drain);
}
let completion_seen = !drain.export_finished.is_empty();
app.handle_export_drain(ui_state, &drain);
let worker_reaped = app.reap_export_workers(ui_state);
if completion_seen {
let errors = app.join_export_workers_for_cleanup();
assert!(errors.is_empty(), "export worker cleanup: {errors:?}");
return;
}
if worker_reaped {
return;
}
assert!(Instant::now() < deadline, "export worker did not finish");
thread::yield_now();
}
}
pub(super) fn herdr_statuses(
lines: &std::sync::Arc<std::sync::Mutex<Vec<String>>>,
) -> Vec<(String, String)> {
lines
.lock()
.unwrap()
.iter()
.map(|line| {
let value: serde_json::Value = serde_json::from_str(line).unwrap();
(
value["params"]["state"].as_str().unwrap().to_string(),
value["params"]["message"].as_str().unwrap().to_string(),
)
})
.collect()
}
pub(super) fn test_paths(root: &std::path::Path) -> crate::config::McPaths {
let mut paths =
crate::config::McPaths::from_root_and_project_dir(root.join("mc"), root.join("project"));
paths.user_agents = root.join("mc/agents");
paths
}
pub(super) fn start_sse_server(summary: &'static str) -> String {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(2)));
let mut buffer = [0_u8; 4096];
let _ = std::io::Read::read(&mut stream, &mut buffer);
let payload = format!("data: {{\"delta\":\"{summary}\"}}\n\ndata: [DONE]\n\n");
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
payload.len(),
payload
);
let _ = std::io::Write::write_all(&mut stream, response.as_bytes());
});
format!("http://{addr}/v1")
}
pub(super) fn write_compaction_override_settings(
paths: &crate::config::McPaths,
provider: &str,
model: &str,
base_url: &str,
) {
let settings = crate::config::Settings {
compaction: crate::config::CompactionSettings {
provider: Some(provider.to_string()),
model: Some(model.to_string()),
..crate::config::CompactionSettings::default()
},
custom_providers: std::collections::BTreeMap::from([(
provider.to_string(),
crate::config::make_custom_provider_config("Local", base_url, "").unwrap(),
)]),
..crate::config::Settings::default()
};
crate::config::write_settings(paths, &settings).unwrap();
}
pub(super) fn test_app(temp: &tempfile::TempDir, sender: Sender<TuiEvent>) -> MissionControlApp {
test_app_with_active_session(temp, sender, None)
}
pub(super) fn test_app_with_active_session(
temp: &tempfile::TempDir,
sender: Sender<TuiEvent>,
active_session: Option<Session>,
) -> MissionControlApp {
let mut app = MissionControlApp::new(
TuiSessionConfig {
config: EffectiveConfig {
provider: Some("openai".to_string()),
model: Some("test-model".to_string()),
no_color: false,
file_autocomplete_respects_gitignore: true,
custom_providers: std::collections::BTreeMap::new(),
thinking_level: crate::thinking::ThinkingLevel::Default,
auth: None,
paths: test_paths(temp.path()),
},
settings: crate::config::Settings::default(),
appearance: crate::appearance::RuntimeAppearance::default(),
theme_cli_override: false,
initial_prompt: None,
instructions: Vec::new(),
discovered_skills: SkillDiscovery::default(),
skills: SkillDiscovery::default(),
commands: CommandRegistry::mvp(),
manager: SessionManager::new(temp.path().join("sessions")),
active_session,
cwd: temp.path().to_path_buf(),
herdr_reporter: None,
},
sender,
);
app.startup_readiness = StartupReadiness::Ready;
app.subagent_profile_discovery =
crate::subagents::profiles::discover_subagent_profiles(&app.config.paths.subagents);
app
}
pub(super) fn write_codex_auth(paths: &crate::config::McPaths) {
let mut auth = crate::config::Auth::default();
auth.providers.insert(
crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
crate::config::AuthProviderRecord::OAuth {
access: "access".into(),
refresh: Some("refresh".into()),
expires: None,
account_id: Some("acct".into()),
},
);
crate::config::write_auth(paths, &auth).unwrap();
}
pub(super) fn ready_codex_app(
temp: &tempfile::TempDir,
sender: Sender<TuiEvent>,
) -> MissionControlApp {
let mut app = test_app(temp, sender);
app.config.provider = Some(crate::providers::OPENAI_CODEX_PROVIDER.to_string());
app.config.auth = Some(crate::config::ProviderCredential::OAuth {
access: "access".into(),
account_id: Some("acct".into()),
});
app.state.config = Some(app.config.clone());
app.state.auth_state = crate::config::AuthState::Ready {
provider: crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
credential: crate::config::ProviderCredential::OAuth {
access: "access".into(),
account_id: Some("acct".into()),
},
};
write_codex_auth(&app.config.paths);
app.settings = crate::config::read_settings(&app.config.paths).unwrap();
app
}
pub(super) fn ready_anthropic_app(
temp: &tempfile::TempDir,
sender: Sender<TuiEvent>,
) -> MissionControlApp {
let mut app = test_app(temp, sender);
let credential = crate::config::ProviderCredential::ApiKey {
key: "stored-test-key".to_string(),
};
app.config.provider = Some(crate::providers::ANTHROPIC_PROVIDER.to_string());
app.config.model = Some(crate::providers::DEFAULT_ANTHROPIC_MODEL.to_string());
app.config.auth = Some(credential.clone());
app.state.config = Some(app.config.clone());
app.state.auth_state = crate::config::AuthState::Ready {
provider: crate::providers::ANTHROPIC_PROVIDER.to_string(),
credential,
};
app
}
pub(super) fn apply_fast_mode_completion(
app: &mut MissionControlApp,
ui_state: &mut state::MissionControlState,
receiver: &Receiver<TuiEvent>,
) {
let event = receiver
.recv_timeout(Duration::from_secs(1))
.expect("fast mode persistence completion");
let mut result = DrainResult::default();
apply_control_event_to_state(ui_state, event, &mut result);
assert_eq!(result.fast_mode_persisted.len(), 1);
assert!(app.handle_fast_mode_persistence_drain(ui_state, &result));
}