typeduck-codex-web 0.5.1

A standalone browser interface and Codex runtime
Documentation
use std::fs;

use pretty_assertions::assert_eq;
use tempfile::TempDir;

use super::AccountProfile;
use super::DaemonConfig;

#[test]
fn loads_ordered_accounts_and_tunnel() {
    let temp = TempDir::new().expect("temp dir");
    let first = temp.path().join("first");
    let second = temp.path().join("second");
    let identity = temp.path().join("id_ed25519");
    let config = temp.path().join("daemon.toml");
    fs::write(
        &config,
        format!(
            r#"
port = 36915
mcp_oauth_callback_url = "http://127.0.0.1:36915/oauth/callback"

[[accounts]]
name = "primary"
label = "Primary"
codex_home = {first:?}
proxy = "http://proxy.example:8080"

[[accounts]]
name = "backup"
codex_home = {second:?}
use_ssh_tunnel = true

[ssh_tunnel]
destination = "codex@example.com"
identity_file = {identity:?}
ssh_port = 2222
local_port = 1080
"#
        ),
    )
    .expect("write config");

    let loaded = DaemonConfig::load(Some(&config)).expect("load config");

    assert_eq!(loaded.port, Some(36915));
    assert_eq!(
        loaded.mcp_oauth_callback_url.as_deref(),
        Some("http://127.0.0.1:36915/oauth/callback")
    );
    assert_eq!(loaded.accounts.len(), 2);
    assert_eq!(loaded.accounts[0].name, "primary");
    assert_eq!(loaded.accounts[1].label, "backup");
    assert!(loaded.accounts[1].use_ssh_tunnel);
    assert_eq!(loaded.ssh_tunnel.expect("tunnel").local_port, Some(1080));
}

#[test]
fn rejects_invalid_persistent_callback_url() {
    let temp = TempDir::new().expect("temp dir");
    let config = temp.path().join("daemon.toml");
    fs::write(
        &config,
        r#"mcp_oauth_callback_url = "https://example.com/not-the-callback""#,
    )
    .expect("write config");

    let error = DaemonConfig::load(Some(&config)).expect_err("invalid callback URL");

    assert!(error.to_string().contains("must end at /oauth/callback"));
}

#[test]
fn rejects_duplicate_accounts_and_unsupported_proxies() {
    let temp = TempDir::new().expect("temp dir");
    let home = temp.path().join("home");
    let config = temp.path().join("daemon.toml");
    fs::write(
        &config,
        format!(
            r#"
[[accounts]]
name = "same"
codex_home = {home:?}
proxy = "ftp://proxy.example"

[[accounts]]
name = "same"
codex_home = {home:?}
"#
        ),
    )
    .expect("write config");

    let error = DaemonConfig::load(Some(&config)).expect_err("invalid config");

    assert!(error.to_string().contains("proxy URL scheme"));
}

#[test]
fn saves_account_profile_changes_without_losing_daemon_settings() {
    let temp = TempDir::new().expect("temp dir");
    let config = temp.path().join("daemon.toml");
    fs::write(
        &config,
        "port = 36915\nmcp_oauth_callback_url = \"http://127.0.0.1:36915/oauth/callback\"\n",
    )
    .expect("write config");
    let mut loaded = DaemonConfig::load(Some(&config)).expect("load config");

    loaded
        .save_accounts(vec![AccountProfile {
            name: "personal".to_string(),
            label: "Personal".to_string(),
            codex_home: temp.path().join("personal"),
            proxy: Some("socks5h://127.0.0.1:1080".to_string()),
            use_ssh_tunnel: false,
        }])
        .expect("save accounts");
    let reloaded = DaemonConfig::load(Some(&config)).expect("reload config");

    assert_eq!(reloaded.port, Some(36915));
    assert_eq!(reloaded.accounts, loaded.accounts);
    assert_eq!(
        reloaded.mcp_oauth_callback_url.as_deref(),
        Some("http://127.0.0.1:36915/oauth/callback")
    );
}