ralph-agent-loop 0.4.0

A Rust CLI for managing AI agent loops with a structured JSON task queue
Documentation
//! Prompt flow integration tests.

use ralph::contracts::Config;
use ralph::promptflow::{self, PromptPolicy};
use ralph::prompts;
use tempfile::TempDir;

#[test]
fn build_phase1_prompt_contains_required_elements() {
    let base = "BASE_PROMPT";
    let task_id = "RQ-1234";
    let config = Config::default();
    let policy = PromptPolicy {
        repoprompt_plan_required: true,
        repoprompt_tool_injection: true,
    };
    let repo_root = TempDir::new().unwrap();
    let template = prompts::load_worker_phase1_prompt(repo_root.path()).unwrap();

    let prompt = promptflow::build_phase1_prompt(
        &template,
        base,
        "",
        promptflow::PHASE1_TASK_REFRESH_REQUIRED_INSTRUCTION,
        task_id,
        2,
        &policy,
        &config,
    )
    .unwrap();

    assert!(prompt.contains("PLANNING MODE - PHASE 1 OF 2"));
    assert!(prompt.contains("TASK REFRESH STEP"));
    assert!(prompt.contains(prompts::REPOPROMPT_REQUIRED_INSTRUCTION));
    assert!(prompt.contains(prompts::REPOPROMPT_CONTEXT_BUILDER_PLANNING_INSTRUCTION));
    assert!(prompt.contains("PLAN ONLY"));
    assert!(prompt.contains(".ralph/cache/plans/RQ-1234.md"));
    assert!(prompt.contains(base));
    assert!(!prompt.contains("IMPLEMENTATION COMPLETION CHECKLIST"));
}

#[test]
fn build_phase1_prompt_omits_rp_if_disabled() {
    let base = "BASE_PROMPT";
    let task_id = "RQ-1234";
    let config = Config::default();
    let policy = PromptPolicy {
        repoprompt_plan_required: false,
        repoprompt_tool_injection: false,
    };
    let repo_root = TempDir::new().unwrap();
    let template = prompts::load_worker_phase1_prompt(repo_root.path()).unwrap();

    let prompt = promptflow::build_phase1_prompt(
        &template,
        base,
        "",
        promptflow::PHASE1_TASK_REFRESH_REQUIRED_INSTRUCTION,
        task_id,
        2,
        &policy,
        &config,
    )
    .unwrap();

    assert!(!prompt.contains(prompts::REPOPROMPT_REQUIRED_INSTRUCTION));
    assert!(!prompt.contains(prompts::REPOPROMPT_CONTEXT_BUILDER_PLANNING_INSTRUCTION));
}

#[test]
fn build_phase2_prompt_contains_required_elements() {
    let plan = "My Plan";
    let checklist = "## IMPLEMENTATION COMPLETION CHECKLIST\n- done";
    let config = Config::default();
    let policy = PromptPolicy {
        repoprompt_plan_required: false,
        repoprompt_tool_injection: true,
    };
    let repo_root = TempDir::new().unwrap();
    let template = prompts::load_worker_phase2_prompt(repo_root.path()).unwrap();

    let prompt = promptflow::build_phase2_prompt(
        &template,
        "BASE_PROMPT",
        plan,
        checklist,
        "",
        "",
        "RQ-1234",
        2,
        &policy,
        &config,
    )
    .unwrap();

    assert!(prompt.contains("IMPLEMENTATION MODE - PHASE 2 OF 2"));
    assert!(prompt.contains("CURRENT TASK: RQ-1234"));
    assert!(prompt.contains(prompts::REPOPROMPT_REQUIRED_INSTRUCTION));
    assert!(prompt.contains(checklist));
    assert!(prompt.contains("APPROVED PLAN"));
    assert!(prompt.contains(plan));
    assert!(prompt.contains("BASE_PROMPT"));
}

#[test]
fn build_single_phase_prompt_contains_required_elements() {
    let base = "BASE_PROMPT";
    let checklist = "## IMPLEMENTATION COMPLETION CHECKLIST\n- done";
    let task_id = "RQ-1234";
    let config = Config::default();
    let policy = PromptPolicy {
        repoprompt_plan_required: false,
        repoprompt_tool_injection: true,
    };
    let repo_root = TempDir::new().unwrap();
    let template = prompts::load_worker_single_phase_prompt(repo_root.path()).unwrap();

    let prompt = promptflow::build_single_phase_prompt(
        &template, base, checklist, "", "", task_id, &policy, &config,
    )
    .unwrap();

    assert!(prompt.contains("REPOPROMPT TOOLING (WHEN CONNECTED)"));
    assert!(!prompt.contains(prompts::REPOPROMPT_CONTEXT_BUILDER_PLANNING_INSTRUCTION));
    assert!(prompt.contains(checklist));
    assert!(prompt.contains("single-pass execution mode"));
    assert!(prompt.contains(base));
}

#[test]
fn plan_cache_roundtrip() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();
    let task_id = "RQ-9999";
    let plan = "Cached Plan";

    promptflow::write_plan_cache(root, task_id, plan).unwrap();
    let loaded = promptflow::read_plan_cache(root, task_id).unwrap();

    assert_eq!(loaded, plan);
}

#[test]
fn read_plan_cache_fails_when_missing() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();
    let err = promptflow::read_plan_cache(root, "RQ-0000").unwrap_err();
    assert!(err.to_string().contains("Plan cache not found"));
}

#[test]
fn read_plan_cache_fails_when_empty() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();
    let task_id = "RQ-0002";
    promptflow::write_plan_cache(root, task_id, "   ").unwrap();
    let err = promptflow::read_plan_cache(root, task_id).unwrap_err();
    assert!(err.to_string().contains("Plan cache is empty"));
}

#[test]
fn build_phase2_handoff_prompt_contains_required_elements() {
    let plan = "My Plan";
    let checklist = "## PHASE 2 HANDOFF CHECKLIST (3-PHASE WORKFLOW)\n- done";
    let config = Config::default();
    let policy = PromptPolicy {
        repoprompt_plan_required: false,
        repoprompt_tool_injection: false,
    };
    let repo_root = TempDir::new().unwrap();
    let template = prompts::load_worker_phase2_handoff_prompt(repo_root.path()).unwrap();

    let prompt = promptflow::build_phase2_handoff_prompt(
        &template,
        "BASE_PROMPT",
        plan,
        checklist,
        "",
        "",
        "RQ-1234",
        3,
        &policy,
        &config,
    )
    .unwrap();

    assert!(prompt.contains("IMPLEMENTATION MODE - PHASE 2 OF 3"));
    assert!(prompt.contains("CURRENT TASK: RQ-1234"));
    assert!(prompt.contains(checklist));
    assert!(prompt.contains("resolve follow-ups, inconsistencies, missing tests"));
    assert!(prompt.contains("concrete remediation steps"));
    assert!(prompt.contains("APPROVED PLAN"));
    assert!(prompt.contains(plan));
    assert!(prompt.contains("BASE_PROMPT"));
}

#[test]
fn build_phase3_prompt_contains_required_elements() {
    let base = "BASE_PROMPT";
    let review = "CODE REVIEW BODY";
    let phase2_final = "PHASE 2 FINAL";
    let config = Config::default();
    let policy = PromptPolicy {
        repoprompt_plan_required: false,
        repoprompt_tool_injection: true,
    };
    let repo_root = TempDir::new().unwrap();
    let template = prompts::load_worker_phase3_prompt(repo_root.path()).unwrap();

    let prompt = promptflow::build_phase3_prompt(
        &template,
        base,
        review,
        phase2_final,
        "RQ-0001",
        "CHECKLIST",
        "",
        "",
        prompts::PHASE3_COMPLETION_GUIDANCE_FINAL,
        3,
        &policy,
        &config,
    )
    .unwrap();

    assert!(prompt.contains("CODE REVIEW MODE - PHASE 3 OF 3"));
    assert!(prompt.contains("CURRENT TASK: RQ-0001"));
    assert!(prompt.contains(prompts::REPOPROMPT_REQUIRED_INSTRUCTION));
    assert!(prompt.contains("PRE-FLIGHT OVERRIDE"));
    assert!(prompt.contains(review));
    assert!(prompt.contains(phase2_final));
    assert!(prompt.contains("CHECKLIST"));
    assert!(prompt.contains(base));
    assert!(prompt.contains("Leave it unchanged until terminal task bookkeeping is complete."));
    assert!(prompt.contains("PREFERRED: investigate and resolve any risks"));
}

#[test]
fn iteration_checklist_requires_closing_flagged_issues() {
    let config = Config::default();
    let repo_root = TempDir::new().unwrap();
    let template = prompts::load_iteration_checklist(repo_root.path()).unwrap();
    let rendered = prompts::render_iteration_checklist(&template, "RQ-0002", &config).unwrap();

    assert!(rendered.contains("PREFERRED: investigate and resolve suspicious leads"));
}

#[test]
fn completion_checklist_requires_closing_flagged_issues() {
    let config = Config::default();
    let repo_root = TempDir::new().unwrap();
    let template = prompts::load_completion_checklist(repo_root.path()).unwrap();
    let rendered =
        prompts::render_completion_checklist(&template, "RQ-0003", &config, false).unwrap();

    assert!(rendered.contains("PREFERRED: investigate and resolve any risks"));
    assert!(rendered.contains("Run mode for this session: `normal`"));
}

#[test]
fn phase2_handoff_checklist_discourages_deferrals() {
    let config = Config::default();
    let repo_root = TempDir::new().unwrap();
    let template = prompts::load_phase2_handoff_checklist(repo_root.path()).unwrap();
    let rendered = prompts::render_phase2_handoff_checklist(&template, &config).unwrap();

    assert!(rendered.contains("PREFERRED: resolve follow-ups"));
    assert!(rendered.contains("If you are truly blocked"));
}