ralph-agent-loop 0.3.1

A Rust CLI for managing AI agent loops with a structured JSON task queue
Documentation
//! Agent settings resolution tests for run command.

use super::{LoggerState, base_task, resolved_with_agent_defaults, take_logs};
use crate::agent::AgentOverrides;
use crate::commands::run::context::task_context_for_prompt;
use crate::commands::run::iteration::{
    apply_followup_reasoning_effort, resolve_iteration_settings,
};
use crate::contracts::{
    Model, ModelEffort, ReasoningEffort, Runner, RunnerCliOptionsPatch, TaskAgent,
};
use crate::runner;

#[test]
fn resolve_run_agent_settings_task_agent_overrides_config() -> anyhow::Result<()> {
    let resolved = resolved_with_agent_defaults(
        Some(Runner::Codex),
        Some(Model::Gpt53Codex),
        Some(ReasoningEffort::Medium),
    );

    let mut task = base_task();
    task.agent = Some(TaskAgent {
        runner: Some(Runner::Opencode),
        model: Some(Model::Gpt53),
        model_effort: ModelEffort::High,
        phases: None,
        iterations: None,
        followup_reasoning_effort: None,
        runner_cli: None,
        phase_overrides: None,
    });

    let overrides = AgentOverrides::default();
    let settings = crate::commands::run::resolve_run_agent_settings(&resolved, &task, &overrides)?;
    assert_eq!(settings.runner, Runner::Opencode);
    assert_eq!(settings.model, Model::Gpt53);
    assert_eq!(settings.reasoning_effort, None);
    Ok(())
}

#[test]
fn resolve_run_agent_settings_cli_overrides_task_agent_and_config() -> anyhow::Result<()> {
    let resolved = resolved_with_agent_defaults(
        Some(Runner::Opencode),
        Some(Model::Gpt53),
        Some(ReasoningEffort::Low),
    );

    let mut task = base_task();
    task.agent = Some(TaskAgent {
        runner: Some(Runner::Opencode),
        model: Some(Model::Gpt53),
        model_effort: ModelEffort::Low,
        phases: None,
        iterations: None,
        followup_reasoning_effort: None,
        runner_cli: None,
        phase_overrides: None,
    });

    let overrides = AgentOverrides {
        profile: None,
        runner: Some(Runner::Codex),
        model: Some(Model::Gpt53Codex),
        reasoning_effort: Some(ReasoningEffort::High),
        runner_cli: RunnerCliOptionsPatch::default(),
        phases: None,
        repoprompt_plan_required: None,
        repoprompt_tool_injection: None,
        git_revert_mode: None,
        git_publish_mode: None,
        include_draft: None,
        notify_on_complete: None,
        notify_on_fail: None,
        notify_on_loop_complete: None,
        notify_sound: None,
        lfs_check: None,
        no_progress: None,
        phase_overrides: None,
    };

    let settings = crate::commands::run::resolve_run_agent_settings(&resolved, &task, &overrides)?;
    assert_eq!(settings.runner, Runner::Codex);
    assert_eq!(settings.model, Model::Gpt53Codex);
    assert_eq!(settings.reasoning_effort, Some(ReasoningEffort::High));
    Ok(())
}

#[test]
fn resolve_run_agent_settings_defaults_to_glm47_for_opencode_runner() -> anyhow::Result<()> {
    let resolved = resolved_with_agent_defaults(
        Some(Runner::Codex),
        Some(Model::Gpt53Codex),
        Some(ReasoningEffort::Medium),
    );

    let task = base_task();

    let overrides = AgentOverrides {
        profile: None,
        runner: Some(Runner::Opencode),
        model: None,
        reasoning_effort: None,
        runner_cli: RunnerCliOptionsPatch::default(),
        phases: None,
        repoprompt_plan_required: None,
        repoprompt_tool_injection: None,
        git_revert_mode: None,
        git_publish_mode: None,
        include_draft: None,
        notify_on_complete: None,
        notify_on_fail: None,
        notify_on_loop_complete: None,
        notify_sound: None,
        lfs_check: None,
        no_progress: None,
        phase_overrides: None,
    };

    let settings = crate::commands::run::resolve_run_agent_settings(&resolved, &task, &overrides)?;
    assert_eq!(settings.runner, Runner::Opencode);
    assert_eq!(settings.model, Model::Glm47);
    assert_eq!(settings.reasoning_effort, None);
    Ok(())
}

#[test]
fn resolve_run_agent_settings_defaults_to_gemini_flash_for_gemini_runner() -> anyhow::Result<()> {
    let resolved = resolved_with_agent_defaults(
        Some(Runner::Codex),
        Some(Model::Gpt53Codex),
        Some(ReasoningEffort::Medium),
    );

    let task = base_task();

    let overrides = AgentOverrides {
        profile: None,
        runner: Some(Runner::Gemini),
        model: None,
        reasoning_effort: None,
        runner_cli: RunnerCliOptionsPatch::default(),
        phases: None,
        repoprompt_plan_required: None,
        repoprompt_tool_injection: None,
        git_revert_mode: None,
        git_publish_mode: None,
        include_draft: None,
        notify_on_complete: None,
        notify_on_fail: None,
        notify_on_loop_complete: None,
        notify_sound: None,
        lfs_check: None,
        no_progress: None,
        phase_overrides: None,
    };

    let settings = crate::commands::run::resolve_run_agent_settings(&resolved, &task, &overrides)?;
    assert_eq!(settings.runner, Runner::Gemini);
    assert_eq!(settings.model.as_str(), "gemini-3-flash-preview");
    assert_eq!(settings.reasoning_effort, None);
    Ok(())
}

#[test]
fn resolve_run_agent_settings_effort_defaults_to_medium_for_codex_when_unspecified()
-> anyhow::Result<()> {
    let resolved = resolved_with_agent_defaults(Some(Runner::Codex), Some(Model::Gpt53Codex), None);

    let task = base_task();
    let overrides = AgentOverrides::default();

    let settings = crate::commands::run::resolve_run_agent_settings(&resolved, &task, &overrides)?;
    assert_eq!(settings.runner, Runner::Codex);
    assert_eq!(settings.model, Model::Gpt53Codex);
    assert_eq!(settings.reasoning_effort, Some(ReasoningEffort::Medium));
    Ok(())
}

#[test]
fn resolve_run_agent_settings_model_effort_default_uses_config() -> anyhow::Result<()> {
    let resolved = resolved_with_agent_defaults(
        Some(Runner::Codex),
        Some(Model::Gpt53Codex),
        Some(ReasoningEffort::High),
    );

    let mut task = base_task();
    task.agent = Some(TaskAgent {
        runner: Some(Runner::Codex),
        model: Some(Model::Gpt53Codex),
        model_effort: ModelEffort::Default,
        phases: None,
        iterations: None,
        followup_reasoning_effort: None,
        runner_cli: None,
        phase_overrides: None,
    });

    let overrides = AgentOverrides::default();
    let settings = crate::commands::run::resolve_run_agent_settings(&resolved, &task, &overrides)?;
    assert_eq!(settings.reasoning_effort, Some(ReasoningEffort::High));
    Ok(())
}

#[test]
fn resolve_run_agent_settings_model_effort_overrides_config_for_codex() -> anyhow::Result<()> {
    let resolved = resolved_with_agent_defaults(
        Some(Runner::Codex),
        Some(Model::Gpt53Codex),
        Some(ReasoningEffort::Low),
    );

    let mut task = base_task();
    task.agent = Some(TaskAgent {
        runner: Some(Runner::Codex),
        model: Some(Model::Gpt53Codex),
        model_effort: ModelEffort::XHigh,
        phases: None,
        iterations: None,
        followup_reasoning_effort: None,
        runner_cli: None,
        phase_overrides: None,
    });

    let overrides = AgentOverrides::default();
    let settings = crate::commands::run::resolve_run_agent_settings(&resolved, &task, &overrides)?;
    assert_eq!(settings.reasoning_effort, Some(ReasoningEffort::XHigh));
    Ok(())
}

#[test]
fn resolve_run_agent_settings_effort_is_ignored_for_opencode() -> anyhow::Result<()> {
    let resolved = resolved_with_agent_defaults(
        Some(Runner::Opencode),
        Some(Model::Gpt53),
        Some(ReasoningEffort::Low),
    );

    let mut task = base_task();
    task.agent = Some(TaskAgent {
        runner: Some(Runner::Opencode),
        model: Some(Model::Gpt53),
        model_effort: ModelEffort::High,
        phases: None,
        iterations: None,
        followup_reasoning_effort: None,
        runner_cli: None,
        phase_overrides: None,
    });
    let overrides = AgentOverrides {
        profile: None,
        runner: Some(Runner::Opencode),
        model: Some(Model::Gpt53),
        reasoning_effort: Some(ReasoningEffort::High),
        runner_cli: RunnerCliOptionsPatch::default(),
        phases: None,
        repoprompt_plan_required: None,
        repoprompt_tool_injection: None,
        git_revert_mode: None,
        git_publish_mode: None,
        include_draft: None,
        notify_on_complete: None,
        notify_on_fail: None,
        notify_on_loop_complete: None,
        notify_sound: None,
        lfs_check: None,
        no_progress: None,
        phase_overrides: None,
    };

    let settings = crate::commands::run::resolve_run_agent_settings(&resolved, &task, &overrides)?;
    assert_eq!(settings.runner, Runner::Opencode);
    assert_eq!(settings.model, Model::Gpt53);
    assert_eq!(settings.reasoning_effort, None);
    Ok(())
}

#[test]
fn resolve_iteration_settings_defaults_to_one() -> anyhow::Result<()> {
    let resolved = resolved_with_agent_defaults(None, None, None);
    let task = base_task();

    let settings = resolve_iteration_settings(&task, &resolved.config.agent)?;
    assert_eq!(settings.count, 1);
    assert_eq!(settings.followup_reasoning_effort, None);
    Ok(())
}

#[test]
fn resolve_iteration_settings_prefers_task_over_config() -> anyhow::Result<()> {
    let mut resolved = resolved_with_agent_defaults(None, None, None);
    resolved.config.agent.iterations = Some(3);
    resolved.config.agent.followup_reasoning_effort = Some(ReasoningEffort::Low);

    let mut task = base_task();
    task.agent = Some(TaskAgent {
        runner: None,
        model: None,
        model_effort: ModelEffort::Default,
        phases: None,
        iterations: Some(2),
        followup_reasoning_effort: Some(ReasoningEffort::High),
        runner_cli: None,
        phase_overrides: None,
    });

    let settings = resolve_iteration_settings(&task, &resolved.config.agent)?;
    assert_eq!(settings.count, 2);
    assert_eq!(
        settings.followup_reasoning_effort,
        Some(ReasoningEffort::High)
    );
    Ok(())
}

#[test]
fn apply_followup_reasoning_effort_overrides_codex_only() {
    let base = runner::AgentSettings {
        runner: Runner::Codex,
        model: Model::Gpt53Codex,
        reasoning_effort: Some(ReasoningEffort::Medium),
        runner_cli: runner::ResolvedRunnerCliOptions::default(),
    };
    let updated = apply_followup_reasoning_effort(&base, Some(ReasoningEffort::High), true);
    assert_eq!(updated.reasoning_effort, Some(ReasoningEffort::High));

    let base_non_codex = runner::AgentSettings {
        runner: Runner::Opencode,
        model: Model::Glm47,
        reasoning_effort: None,
        runner_cli: runner::ResolvedRunnerCliOptions::default(),
    };
    let updated_non_codex =
        apply_followup_reasoning_effort(&base_non_codex, Some(ReasoningEffort::High), true);
    assert_eq!(updated_non_codex.reasoning_effort, None);
}

#[test]
fn apply_followup_reasoning_effort_warns_for_non_codex() {
    let base_non_codex = runner::AgentSettings {
        runner: Runner::Opencode,
        model: Model::Glm47,
        reasoning_effort: None,
        runner_cli: runner::ResolvedRunnerCliOptions::default(),
    };

    let (state, _) = take_logs();
    let _ = apply_followup_reasoning_effort(&base_non_codex, Some(ReasoningEffort::High), true);
    let (_, logs) = take_logs();

    if state == LoggerState::TestLogger {
        assert!(
            logs.iter()
                .any(|entry| entry.contains("Follow-up reasoning_effort configured")),
            "expected warning log, got {logs:?}"
        );
    }
}

#[test]
fn task_context_block_includes_id_and_title() -> anyhow::Result<()> {
    let mut t = base_task();
    t.id = "RQ-0001".to_string();
    t.title = "Hello world".to_string();
    let rendered = task_context_for_prompt(&t)?;
    assert!(rendered.contains("RQ-0001"));
    assert!(rendered.contains("Hello world"));
    assert!(rendered.contains("Raw task JSON"));
    Ok(())
}