cueloop 0.5.0

A Rust CLI for managing AI agent loops with a structured JSON task queue
Documentation
use super::*;

#[test]
fn plan_task_decomposition_rejects_stray_mutations() -> Result<()> {
    let (_temp, mut resolved) = test_resolved()?;
    git_test::init_repo(&resolved.repo_root)?;
    std::fs::write(resolved.repo_root.join("README.md"), "# decompose test\n")?;
    queue::save_queue(&resolved.queue_path, &QueueFile::default())?;
    queue::save_queue(&resolved.done_path, &QueueFile::default())?;
    git_test::commit_all(&resolved.repo_root, "init")?;

    let readme_path = resolved.repo_root.join("README.md");
    let runner_script = format!(
        r#"#!/bin/sh
set -e
cat >/dev/null
printf '\nstray edit\n' >> "{readme_path}"
echo '{{"type":"item.completed","item":{{"type":"agent_message","text":"{{\"tree\":{{\"title\":\"Ship auth\",\"children\":[]}}}}"}}}}'
"#,
        readme_path = readme_path.display(),
    );
    let runner_dir = TempDir::new()?;
    let runner_path = create_fake_runner(runner_dir.path(), "codex", &runner_script)?;
    resolved.config.agent.codex_bin = Some(runner_path.to_string_lossy().to_string());
    resolved.config.agent.git_revert_mode = Some(crate::contracts::GitRevertMode::Enabled);

    let opts = TaskDecomposeOptions {
        source: TaskDecomposeSourceInput::Inline("Ship auth".to_string()),
        attach_to_task_id: None,
        max_depth: 3,
        max_children: 5,
        max_nodes: 10,
        status: TaskStatus::Draft,
        parent_status: TaskStatus::Draft,
        leaf_status: TaskStatus::Draft,
        child_policy: DecompositionChildPolicy::Fail,
        with_dependencies: false,
        runner_override: Some(crate::contracts::Runner::Codex),
        model_override: Some(crate::contracts::Model::Gpt53Codex),
        reasoning_effort_override: None,
        runner_cli_overrides: crate::contracts::RunnerCliOptionsPatch::default(),
        repoprompt_tool_injection: false,
        stream_planner_output: false,
        force: false,
    };

    let err = super::super::plan_task_decomposition(&resolved, &opts)
        .expect_err("planner should fail on stray mutation");
    let message = format!("{err:#}");
    assert!(message.contains("Queue-only mutation boundary violated."));
    assert!(message.contains("README.md"));
    assert_eq!(std::fs::read_to_string(&readme_path)?, "# decompose test\n");
    Ok(())
}