cueloop 0.5.0

A Rust CLI for managing AI agent loops with a structured JSON task queue
Documentation
//! Integration test for README version marker error handling.
//!
//! Purpose:
//! - Integration test for README version marker error handling.
//!
//! Responsibilities:
//! - Provide focused implementation or regression coverage for this file's owning feature.
//!
//! Scope:
//! - Limited to this file's owning feature boundary.
//!
//! Verifies that malformed version markers fail fast with actionable errors.
//!
//! Usage:
//! - Used through the crate module tree or integration test harness.
//!
//! Invariants/Assumptions:
//! - Keep behavior aligned with CueLoop's canonical `cueloop` CLI, machine-contract, and queue semantics.

use anyhow::Result;
use cueloop::commands::init::{
    InitOptions, ReadmeVersionError, check_readme_current_from_root, extract_readme_version,
    run_init,
};
use cueloop::config::Resolved;
use cueloop::contracts::Config;
use std::fs;
use tempfile::TempDir;

fn resolved_for(dir: &TempDir) -> Resolved {
    let repo_root = dir.path().to_path_buf();
    let queue_path = repo_root.join(".cueloop/queue.jsonc");
    let done_path = repo_root.join(".cueloop/done.jsonc");
    let project_config_path = Some(repo_root.join(".cueloop/config.jsonc"));
    Resolved {
        config: Config::default(),
        repo_root,
        queue_path,
        done_path,
        id_prefix: "RQ".to_string(),
        id_width: 4,
        global_config_path: None,
        project_config_path,
    }
}

#[test]
fn extract_readme_version_fails_on_non_numeric_version() {
    let content = "<!-- CUELOOP_README_VERSION: abc -->\n# Test";
    let result = extract_readme_version(content);
    assert!(matches!(result, Err(ReadmeVersionError::ParseError { value }) if value == "abc"));
}

#[test]
fn extract_readme_version_fails_on_missing_end_delimiter() {
    let content = "<!-- CUELOOP_README_VERSION: 5\n# Test";
    let result = extract_readme_version(content);
    assert!(matches!(result, Err(ReadmeVersionError::InvalidFormat)));
}

#[test]
fn check_readme_current_propagates_malformed_marker_error() -> Result<()> {
    let dir = TempDir::new()?;
    let repo_root = dir.path();

    // Create the .cueloop directory and prompts that reference README
    fs::create_dir_all(repo_root.join(".cueloop/prompts"))?;
    fs::write(repo_root.join(".cueloop/config.jsonc"), r#"{"version":2}"#)?;
    let prompt_content = "This prompt references .cueloop/README.md for context";
    fs::write(repo_root.join(".cueloop/prompts/worker.md"), prompt_content)?;

    // Create a README with malformed version marker
    let malformed_readme = "<!-- CUELOOP_README_VERSION: not-a-number -->\n# Test README";
    fs::write(repo_root.join(".cueloop/README.md"), malformed_readme)?;

    // Check that the error is propagated
    let result = check_readme_current_from_root(repo_root);
    assert!(result.is_err());
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("malformed") || err_msg.contains("invalid"),
        "Error message should indicate malformed marker: {}",
        err_msg
    );

    Ok(())
}

#[test]
fn check_readme_current_handles_no_marker() -> Result<()> {
    let dir = TempDir::new()?;
    let repo_root = dir.path();

    // Create the .cueloop directory and prompts that reference README
    fs::create_dir_all(repo_root.join(".cueloop/prompts"))?;
    fs::write(repo_root.join(".cueloop/config.jsonc"), r#"{"version":2}"#)?;
    let prompt_content = "This prompt references .cueloop/README.md for context";
    fs::write(repo_root.join(".cueloop/prompts/worker.md"), prompt_content)?;

    // Create a README without any version marker.
    let unmarked_readme = "# Test README\nSome content";
    fs::write(repo_root.join(".cueloop/README.md"), unmarked_readme)?;

    // This should succeed and treat it as version 1
    let result = check_readme_current_from_root(repo_root)?;
    // Result will be Current(1) or Outdated depending on README_VERSION constant
    match result {
        cueloop::commands::init::ReadmeCheckResult::Current(v) => {
            assert_eq!(v, 1);
        }
        cueloop::commands::init::ReadmeCheckResult::Outdated {
            current_version, ..
        } => {
            assert_eq!(current_version, 1);
        }
        _ => panic!("Unexpected result for unmarked README: {:?}", result),
    }

    Ok(())
}

#[test]
fn init_fails_on_malformed_readme_version() -> Result<()> {
    let dir = TempDir::new()?;
    let resolved = resolved_for(&dir);

    // Set up existing files
    fs::create_dir_all(resolved.repo_root.join(".cueloop"))?;
    fs::create_dir_all(resolved.repo_root.join(".cueloop/prompts"))?;
    fs::write(&resolved.queue_path, r#"{"version":1,"tasks":[]}"#)?;
    fs::write(&resolved.done_path, r#"{"version":1,"tasks":[]}"#)?;
    fs::write(
        resolved.project_config_path.as_ref().unwrap(),
        r#"{"version":2}"#,
    )?;

    // Create prompt files that reference the README (so README check is triggered)
    let prompt_content = "This prompt references .cueloop/README.md for context";
    fs::write(
        resolved.repo_root.join(".cueloop/prompts/worker.md"),
        prompt_content,
    )?;

    // Create a README with malformed version marker
    let malformed_readme = "<!-- CUELOOP_README_VERSION: invalid-version -->\n# Test";
    fs::write(
        resolved.repo_root.join(".cueloop/README.md"),
        malformed_readme,
    )?;

    // Init should fail because README refresh validates malformed version markers.
    let result = run_init(
        &resolved,
        InitOptions {
            force: false,
            force_lock: false,
            interactive: false,
        },
    );

    assert!(
        result.is_err(),
        "Init should fail on malformed README version marker"
    );
    let err_msg = result.unwrap_err().to_string();
    assert!(
        err_msg.contains("malformed") || err_msg.contains("invalid") || err_msg.contains("version"),
        "Error should mention malformed version: {}",
        err_msg
    );

    Ok(())
}

#[test]
fn init_succeeds_on_readme_without_marker() -> Result<()> {
    let dir = TempDir::new()?;
    let resolved = resolved_for(&dir);

    // Set up existing files
    fs::create_dir_all(resolved.repo_root.join(".cueloop"))?;
    fs::create_dir_all(resolved.repo_root.join(".cueloop/prompts"))?;
    fs::write(&resolved.queue_path, r#"{"version":1,"tasks":[]}"#)?;
    fs::write(&resolved.done_path, r#"{"version":1,"tasks":[]}"#)?;
    fs::write(
        resolved.project_config_path.as_ref().unwrap(),
        r#"{"version":2}"#,
    )?;

    // Create prompt files that reference the README (so README check is triggered)
    let prompt_content = "This prompt references .cueloop/README.md for context";
    fs::write(
        resolved.repo_root.join(".cueloop/prompts/worker.md"),
        prompt_content,
    )?;

    // Create a README without version marker.
    let unmarked_readme = "# Test README\nSome content";
    fs::write(
        resolved.repo_root.join(".cueloop/README.md"),
        unmarked_readme,
    )?;

    // Init should succeed (treats as version 1)
    let result = run_init(
        &resolved,
        InitOptions {
            force: false,
            force_lock: false,
            interactive: false,
        },
    );

    assert!(
        result.is_ok(),
        "Init should succeed on README without marker"
    );
    Ok(())
}