thndrs 0.1.0

Terminal AI pair programmer with local tools, sessions, MCP, and ACP support
Documentation
use super::*;
use crate::skills::{SkillMetadata, SkillSource};

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::path::Path;

pub fn with_home<T>(home: &Path, f: impl FnOnce() -> T) -> T {
    let _guard = crate::test_env::lock();
    let old_home = std::env::var_os("HOME");

    unsafe {
        std::env::set_var("HOME", home);
    }

    let result = f();

    unsafe {
        if let Some(old_home) = old_home {
            std::env::set_var("HOME", old_home);
        } else {
            std::env::remove_var("HOME");
        }
    }

    result
}

pub fn with_provider_env_removed<T>(f: impl FnOnce() -> T) -> T {
    let _guard = crate::test_env::lock();
    let old_umans = std::env::var_os(auth::UMANS_API_KEY_ENV);
    let old_opencode = std::env::var_os(auth::OPENCODE_GO_KEY_ENV);
    let old_opencode_zen = std::env::var_os(auth::OPENCODE_ZEN_KEY_ENV);
    let old_chatgpt = std::env::var_os(auth::CHATGPT_CODEX_ACCESS_TOKEN_ENV);

    unsafe {
        std::env::remove_var(auth::UMANS_API_KEY_ENV);
        std::env::remove_var(auth::OPENCODE_GO_KEY_ENV);
        std::env::remove_var(auth::OPENCODE_ZEN_KEY_ENV);
        std::env::remove_var(auth::CHATGPT_CODEX_ACCESS_TOKEN_ENV);
    }

    let result = f();

    unsafe {
        if let Some(value) = old_umans {
            std::env::set_var(auth::UMANS_API_KEY_ENV, value);
        } else {
            std::env::remove_var(auth::UMANS_API_KEY_ENV);
        }
        if let Some(value) = old_opencode {
            std::env::set_var(auth::OPENCODE_GO_KEY_ENV, value);
        } else {
            std::env::remove_var(auth::OPENCODE_GO_KEY_ENV);
        }
        if let Some(value) = old_opencode_zen {
            std::env::set_var(auth::OPENCODE_ZEN_KEY_ENV, value);
        } else {
            std::env::remove_var(auth::OPENCODE_ZEN_KEY_ENV);
        }
        if let Some(value) = old_chatgpt {
            std::env::set_var(auth::CHATGPT_CODEX_ACCESS_TOKEN_ENV, value);
        } else {
            std::env::remove_var(auth::CHATGPT_CODEX_ACCESS_TOKEN_ENV);
        }
    }

    result
}

pub fn with_setup_home<T>(home: &Path, f: impl FnOnce() -> T) -> T {
    let _guard = crate::test_env::lock();
    let old_home = std::env::var_os("HOME");
    let old_umans = std::env::var_os(auth::UMANS_API_KEY_ENV);
    let old_opencode = std::env::var_os(auth::OPENCODE_GO_KEY_ENV);
    let old_opencode_zen = std::env::var_os(auth::OPENCODE_ZEN_KEY_ENV);
    let old_chatgpt = std::env::var_os(auth::CHATGPT_CODEX_ACCESS_TOKEN_ENV);

    unsafe {
        std::env::set_var("HOME", home);
        std::env::remove_var(auth::UMANS_API_KEY_ENV);
        std::env::remove_var(auth::OPENCODE_GO_KEY_ENV);
        std::env::remove_var(auth::OPENCODE_ZEN_KEY_ENV);
        std::env::remove_var(auth::CHATGPT_CODEX_ACCESS_TOKEN_ENV);
    }

    let result = f();

    unsafe {
        if let Some(value) = old_home {
            std::env::set_var("HOME", value);
        } else {
            std::env::remove_var("HOME");
        }
        for (name, value) in [
            (auth::UMANS_API_KEY_ENV, old_umans),
            (auth::OPENCODE_GO_KEY_ENV, old_opencode),
            (auth::OPENCODE_ZEN_KEY_ENV, old_opencode_zen),
            (auth::CHATGPT_CODEX_ACCESS_TOKEN_ENV, old_chatgpt),
        ] {
            if let Some(value) = value {
                std::env::set_var(name, value);
            } else {
                std::env::remove_var(name);
            }
        }
    }

    result
}

pub fn key(code: KeyCode, modifiers: KeyModifiers) -> Msg {
    Msg::Key(KeyEvent::new(code, modifiers))
}

pub fn fresh_app() -> App {
    let dir = tempfile::tempdir().expect("create temp dir");
    let cwd = dir.path().to_path_buf();
    auth::set_credential(
        &auth::project_credentials_path(&cwd),
        auth::UMANS_API_KEY_ENV,
        "test-umans-key",
    )
    .expect("seed test credential");
    auth::set_credential(
        &auth::project_credentials_path(&cwd),
        auth::OPENCODE_ZEN_KEY_ENV,
        "test-zen-key",
    )
    .expect("seed test Zen credential");
    let _kept = dir.keep();
    let cli = Cli { cwd, model: "umans-coder".to_string(), ..Cli::default() };
    let mut app = App::from_cli(&cli);
    app.session_writer = None;
    app.first_run_recovery = None;
    app
}

pub fn picker_from_paths(paths: Vec<String>) -> PickerState {
    PickerState::new(
        paths.into_iter().map(|path| PickerItem::new(path, "")).collect(),
        LARGE_PICKER_LIMIT,
    )
}

pub fn test_skill(path: std::path::PathBuf, markdown: &str) -> SkillMetadata {
    std::fs::create_dir_all(path.parent().expect("skill parent")).expect("create skill dir");
    std::fs::write(&path, markdown).expect("write skill");
    SkillMetadata {
        name: "example-skill".to_string(),
        description: "Helps test the skills picker.".to_string(),
        root: path.parent().expect("skill root").to_path_buf(),
        path,
        content_hash: tools::hash_content(markdown),
        byte_count: markdown.len(),
        source: SkillSource::Project,
        allowed_tools: Vec::new(),
        license: None,
        compatibility: None,
        metadata: None,
        references: Vec::new(),
    }
}

pub fn test_chatgpt_device_code() -> auth::ChatGptCodexDeviceCode {
    auth::ChatGptCodexDeviceCode {
        device_auth_id: "device-auth-secret-from-test".to_string(),
        user_code: "USER-CODE".to_string(),
        verification_uri: Some("https://auth.example.test/device".to_string()),
        verification_uri_complete: None,
        expires_in: Some(900),
        interval: Some(1),
    }
}

pub fn test_chatgpt_credentials() -> auth::ChatGptCodexCredentials {
    auth::ChatGptCodexCredentials {
        access_token: "access-token-secret-from-test".to_string(),
        refresh_token: "refresh-token-secret-from-test".to_string(),
        expires_at_ms: 999_999,
        account_id: "acct_test".to_string(),
    }
}

pub fn oauth_request_ok() -> Result<auth::ChatGptCodexDeviceCode, auth::AuthError> {
    Ok(test_chatgpt_device_code())
}

pub fn oauth_request_fail() -> Result<auth::ChatGptCodexDeviceCode, auth::AuthError> {
    Err(auth::AuthError::ChatGptCodex(
        "device_auth_id device-auth-secret-from-test unavailable".to_string(),
    ))
}

pub fn oauth_poll_pending(_: &auth::ChatGptCodexDeviceCode) -> Result<auth::ChatGptCodexDevicePoll, auth::AuthError> {
    Ok(auth::ChatGptCodexDevicePoll::Pending)
}

pub fn oauth_poll_slow_down(_: &auth::ChatGptCodexDeviceCode) -> Result<auth::ChatGptCodexDevicePoll, auth::AuthError> {
    Ok(auth::ChatGptCodexDevicePoll::SlowDown)
}

pub fn oauth_poll_authorized(
    _: &auth::ChatGptCodexDeviceCode,
) -> Result<auth::ChatGptCodexDevicePoll, auth::AuthError> {
    Ok(auth::ChatGptCodexDevicePoll::Authorized(test_chatgpt_credentials()))
}

pub fn oauth_poll_fail(_: &auth::ChatGptCodexDeviceCode) -> Result<auth::ChatGptCodexDevicePoll, auth::AuthError> {
    Err(auth::AuthError::ChatGptCodex(
        "access_token access-token-secret-from-test rejected".to_string(),
    ))
}

pub fn oauth_write_ok(_: &auth::ChatGptCodexCredentials) -> Result<(), auth::AuthError> {
    Ok(())
}

pub fn oauth_write_fail(_: &auth::ChatGptCodexCredentials) -> Result<(), auth::AuthError> {
    Err(auth::AuthError::ChatGptCodex(String::from(
        "credential store unavailable",
    )))
}

pub fn oauth_browser_start() -> Result<auth::ChatGptCodexBrowserLogin, auth::AuthError> {
    Ok(auth::test_chatgpt_codex_browser_login())
}

pub fn oauth_browser_open(_: &str) -> Result<(), auth::AuthError> {
    Ok(())
}

pub fn oauth_browser_pending(
    _: &mut auth::ChatGptCodexBrowserLogin,
) -> Result<auth::ChatGptCodexBrowserPoll, auth::AuthError> {
    Ok(auth::ChatGptCodexBrowserPoll::Pending)
}

pub fn oauth_browser_complete(
    _: &auth::ChatGptCodexBrowserLogin, _: &str,
) -> Result<auth::ChatGptCodexCredentials, auth::AuthError> {
    Ok(test_chatgpt_credentials())
}

pub fn working_app_with_streaming() -> App {
    let mut app = fresh_app();
    app.run_state = RunState::Working;
    app.transcript.push(Entry::User { text: "do the thing".to_string() });
    app.transcript
        .push(Entry::Agent { text: "working on it".to_string(), streaming: true });
    app
}

pub fn pending_permission(tx: mpsc::Sender<PermissionDecision>) -> PendingPermission {
    PendingPermission {
        tool_call_id: "call_1".to_string(),
        title: "Edit file".to_string(),
        options: vec![
            PermissionOptionView {
                id: "reject".to_string(),
                name: "Reject".to_string(),
                kind: PermissionKindView::RejectOnce,
            },
            PermissionOptionView {
                id: "allow".to_string(),
                name: "Allow".to_string(),
                kind: PermissionKindView::AllowOnce,
            },
        ],
        selected: 0,
        responder: tx,
    }
}