malvin 0.1.4

Non-interactive CLI agent, via Cursor ACP
Documentation
use crate::artifacts::RunArtifacts;
use crate::orchestrator::{
    WorkflowError, clear_review_file, prefer_primary_errors_over_timing, prompt_md_stem,
    should_run_learn_check, workflow_context,
};
use crate::prompts::PromptStore;
use crate::review_sync::{is_lgtm, sync_review_file};

fn tmp_review_paths() -> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf) {
    let t = tempfile::tempdir().unwrap();
    let workspace = t.path().join("review.md");
    let artifact = t.path().join("run").join("review.md");
    std::fs::create_dir_all(artifact.parent().unwrap()).unwrap();
    (t, workspace, artifact)
}

#[test]
fn prompt_md_stem_strips_suffix_without_panicking_on_short_names() {
    assert_eq!(prompt_md_stem("implement.md"), "implement");
    assert_eq!(prompt_md_stem("x.md"), "x");
    assert_eq!(prompt_md_stem(""), "");
    assert_eq!(prompt_md_stem("ab"), "ab");
    assert_eq!(prompt_md_stem("readme.markdown"), "readme.markdown");
}

#[test]
fn legacy_slice_stem_diverges_from_prompt_md_stem() {
    fn legacy_stem(s: &str) -> &str {
        &s[..s.len().saturating_sub(3)]
    }
    assert_eq!(legacy_stem("review_1.md"), prompt_md_stem("review_1.md"));
    assert_eq!(legacy_stem("review_2.md"), prompt_md_stem("review_2.md"));
    assert_ne!(
        legacy_stem("readme.markdown"),
        prompt_md_stem("readme.markdown")
    );
    assert_ne!(legacy_stem("review_1.MD"), prompt_md_stem("review_1.MD"));
}

#[test]
fn is_lgtm_reads_file() {
    let t = tempfile::tempdir().unwrap();
    let p = t.path().join("r.md");
    std::fs::write(&p, "LGTM\n").unwrap();
    assert!(is_lgtm(&p));
}

#[test]
fn sync_review_file_clears_artifact_when_workspace_empty_so_stale_lgtm_is_removed() {
    let (_t, workspace, artifact) = tmp_review_paths();
    std::fs::write(&workspace, "").unwrap();
    std::fs::write(&artifact, "LGTM\n").unwrap();
    sync_review_file(&workspace, &artifact).unwrap();
    assert_eq!(std::fs::read_to_string(&artifact).unwrap(), "");
}

#[test]
fn sync_review_file_clears_artifact_when_workspace_whitespace_only() {
    let (_t, workspace, artifact) = tmp_review_paths();
    std::fs::write(&workspace, "  \n\t\n").unwrap();
    std::fs::write(&artifact, "LGTM\n").unwrap();
    sync_review_file(&workspace, &artifact).unwrap();
    assert_eq!(std::fs::read_to_string(&artifact).unwrap(), "");
}

#[test]
fn prefer_primary_errors_prefers_workflow_over_timing_when_both_fail() {
    let r = prefer_primary_errors_over_timing(
        Err(WorkflowError("workflow".into())),
        Ok(()),
        Err(WorkflowError("timing".into())),
    );
    assert_eq!(r.err().unwrap().0, "workflow");
}

#[test]
fn prefer_primary_errors_surfaces_timing_when_workflow_and_end_succeed() {
    let r = prefer_primary_errors_over_timing(Ok(()), Ok(()), Err(WorkflowError("timing".into())));
    assert_eq!(r.err().unwrap().0, "timing");
}

#[test]
fn sync_review_file_copies_nonempty_workspace_to_artifact() {
    let (_t, workspace, artifact) = tmp_review_paths();
    std::fs::write(&workspace, "LGTM\n").unwrap();
    std::fs::write(&artifact, "old").unwrap();
    sync_review_file(&workspace, &artifact).unwrap();
    assert_eq!(std::fs::read_to_string(&artifact).unwrap().trim(), "LGTM");
}

#[test]
fn workflow_context_review_path_points_to_artifact() {
    let t = tempfile::tempdir().unwrap();
    let run_dir = t.path().join("_malvin").join("run123");
    std::fs::create_dir_all(&run_dir).unwrap();
    let plan_path = run_dir.join("plan.md");
    std::fs::write(&plan_path, "test plan").unwrap();

    let artifacts = RunArtifacts {
        run_dir,
        plan_path,
        work_dir: t.path().to_path_buf(),
    };
    let prompts = PromptStore::default_store();
    let ctx = workflow_context(&artifacts, &prompts, "code").expect("workflow_context");

    let review_path = ctx
        .get("review_path")
        .expect("review_path must be in context");

    assert!(
        review_path.contains("_malvin"),
        "review_path must point to artifact (./_malvin/.../review.md); got: {review_path}"
    );
    assert_eq!(
        review_path, "./_malvin/run123/review.md",
        "review_path should be the artifact path"
    );
    assert!(
        ctx.contains_key("quality_gates"),
        "quality_gates must be in context"
    );
}

#[test]
fn workflow_context_includes_malvin_command() {
    let t = tempfile::tempdir().unwrap();
    let run_dir = t.path().join("_malvin").join("run123");
    std::fs::create_dir_all(&run_dir).unwrap();
    let plan_path = run_dir.join("plan.md");
    std::fs::write(&plan_path, "test plan").unwrap();
    let artifacts = RunArtifacts {
        run_dir,
        plan_path,
        work_dir: t.path().to_path_buf(),
    };
    let prompts = PromptStore::default_store();
    let ctx = workflow_context(&artifacts, &prompts, "tidy").expect("workflow_context");
    assert_eq!(ctx.get("malvin_command").map(String::as_str), Some("tidy"));
}

#[test]
fn kiss_stringify_orchestrator_helpers() {
    let _ = stringify!(crate::orchestrator::insert_artifact_paths);
    let _ = stringify!(crate::orchestrator::insert_formatted);
    let _ = stringify!(crate::orchestrator::prompt_md_stem);
    let _ = stringify!(crate::orchestrator::format_prompt_path);
    let _ = stringify!(crate::orchestrator::clear_review_file);
    let _ = stringify!(crate::orchestrator::check_abort);
    let _ = stringify!(crate::orchestrator::review_loop_helpers::run_reviewer_pair_for_attempt);
    let _ = stringify!(crate::review_sync::sync_review_file_for_attempt);
    let _ = stringify!(crate::orchestrator::review_loop_helpers::run_concerns_and_check_abort_impl);
}

#[test]
fn should_run_learn_check_zero_threshold_always_runs() {
    assert!(
        should_run_learn_check(0, 0),
        "0 threshold, 0 elapsed => run"
    );
    assert!(
        should_run_learn_check(0, 1),
        "0 threshold, any elapsed => run"
    );
    assert!(
        should_run_learn_check(0, 300_000),
        "0 threshold, 5 min => run"
    );
}

#[test]
fn should_run_learn_check_below_threshold_skips() {
    assert!(
        !should_run_learn_check(300_000, 0),
        "5 min threshold, 0 elapsed => skip"
    );
    assert!(
        !should_run_learn_check(300_000, 299_999),
        "5 min threshold, just under => skip"
    );
}

#[test]
fn should_run_learn_check_at_or_above_threshold_runs() {
    assert!(
        should_run_learn_check(300_000, 300_000),
        "5 min threshold, exactly 5 min => run"
    );
    assert!(
        should_run_learn_check(300_000, 300_001),
        "5 min threshold, just over => run"
    );
    assert!(
        should_run_learn_check(300_000, 600_000),
        "5 min threshold, 10 min => run"
    );
}

#[test]
fn clear_review_file_removes_existing_lgtm_content() {
    let t = tempfile::tempdir().unwrap();
    let review_path = t.path().join("review.md");
    std::fs::write(&review_path, "LGTM\n").unwrap();
    assert!(is_lgtm(&review_path), "precondition: file contains LGTM");
    clear_review_file(&review_path).unwrap();
    assert!(
        !review_path.exists(),
        "clear_review_file should remove file"
    );
    assert!(!is_lgtm(&review_path), "is_lgtm returns false after clear");
}

#[test]
fn clear_review_file_succeeds_on_nonexistent_file() {
    let t = tempfile::tempdir().unwrap();
    let review_path = t.path().join("does_not_exist.md");
    clear_review_file(&review_path).unwrap();
    assert!(!review_path.exists());
}

#[test]
fn clear_review_file_returns_error_on_permission_denied() {
    use std::os::unix::fs::PermissionsExt;
    let t = tempfile::tempdir().unwrap();
    let protected_dir = t.path().join("protected");
    std::fs::create_dir(&protected_dir).unwrap();
    let review_path = protected_dir.join("review.md");
    std::fs::write(&review_path, "LGTM\n").unwrap();
    std::fs::set_permissions(&protected_dir, std::fs::Permissions::from_mode(0o000)).unwrap();
    let result = clear_review_file(&review_path);
    std::fs::set_permissions(&protected_dir, std::fs::Permissions::from_mode(0o755)).unwrap();
    assert!(
        result.is_err(),
        "clear_review_file should return error on permission denied"
    );
}

#[test]
fn stringify_orchestrator_run_methods() {
    let _ = stringify!(crate::orchestrator::Orchestrator::run_with_pre_summary_gap);
    let _ = stringify!(crate::orchestrator::Orchestrator::run);
}