malvin 0.1.4

Non-interactive CLI agent, via Cursor ACP
Documentation
mod common;

#[cfg(unix)]
use common::{
    acp_mock_code_abort_after_implement_js,
    acp_mock_code_abort_result_after_check_plan_lgtm_js,
    acp_mock_code_check_plan_tampers_kissconfig_then_implement_verifies_restore_js,
    acp_mock_code_review_lgtm_to_artifact_js,
    acp_mock_code_review_lgtm_with_abort_js,
    assert_review_abort_behavior, only_run_dir, run_code_max_loops_zero_with_mock,
    run_code_max_loops_zero_with_mock_without_trust_plan, run_code_with_mock_js,
    run_code_with_mock_js_trust_plan, run_code_with_mock_js_trust_plan_in_workspace,
    CodeRunOpts, MAX_LOOPS_EXHAUSTED,
};

#[cfg_attr(unix, test)]
fn code_stops_when_implement_writes_abort_result() {
    let out = run_code_with_mock_js(
        &acp_mock_code_abort_after_implement_js(),
        &["--max-loops", "1"],
        true,
    );
    assert!(
        !out.status.success(),
        "expected ABORT failure path: {out:?}"
    );
    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(
        combined.contains("ABORT: stop now"),
        "expected implement ABORT to stop the workflow: {combined:?}"
    );
    assert!(
        !combined.contains(MAX_LOOPS_EXHAUSTED),
        "workflow should stop on ABORT before review exhaustion: {combined:?}"
    );
}

#[cfg_attr(unix, test)]
fn code_stops_when_check_plan_writes_abort_result_with_lgtm_review() {
    let out = run_code_with_mock_js_trust_plan(
        &acp_mock_code_abort_result_after_check_plan_lgtm_js(),
        &["--max-loops", "1"],
        &CodeRunOpts {
            no_tee: true,
            trust_plan: false,
        },
    );
    assert!(
        !out.status.success(),
        "expected ABORT failure path: {out:?}"
    );
    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(
        combined.contains("ABORT: after check plan"),
        "expected check_plan ABORT to stop the workflow: {combined:?}"
    );
    assert!(
        !combined.contains("implement_phase_ran"),
        "implement must not run after ABORT in result.md from check_plan: {combined:?}"
    );
}

#[cfg_attr(unix, test)]
fn check_plan_kissconfig_restore_happens_before_implement() {
    let out = run_code_with_mock_js_trust_plan(
        &acp_mock_code_check_plan_tampers_kissconfig_then_implement_verifies_restore_js(),
        &["--max-loops", "1"],
        &CodeRunOpts {
            no_tee: false,
            trust_plan: false,
        },
    );
    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(
        combined.contains("implement ok"),
        "expected implement to see restored kissconfig: {combined:?}"
    );
    assert!(
        !combined.contains("ABORT: kissconfig leaked into implement"),
        "check_plan kissconfig mutation must not leak into implement: {combined:?}"
    );
    assert!(
        out.status.success(),
        "expected successful exit when check_plan + implement restore path converges: {combined:?}"
    );
}

#[cfg_attr(unix, test)]
fn max_loops_zero_skips_review_attempts_and_fails() {
    let out = run_code_max_loops_zero_with_mock();
    assert!(
        !out.status.success(),
        "malvin code unexpectedly succeeded: {out:?}"
    );
    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(
        combined.contains(MAX_LOOPS_EXHAUSTED),
        "expected max_loops=0 review skip failure: {combined:?}"
    );
    assert!(
        combined.contains("Review-1 (attempt 1)"),
        "review-1 should run at least once when --max-loops=0: {combined:?}"
    );
}

#[cfg_attr(unix, test)]
fn max_loops_zero_skips_check_plan_attempt() {
    let out = run_code_max_loops_zero_with_mock_without_trust_plan();
    assert!(
        !out.status.success(),
        "malvin code unexpectedly succeeded: {out:?}"
    );
    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(
        combined.contains("CheckPlan"),
        "check_plan should run at least once when max_loops=0: {combined:?}"
    );
    assert!(
        !combined.contains("Review-1 (attempt 1)"),
        "review attempt must not run when --max-loops=0: {combined:?}"
    );
    assert!(
        combined.contains("check_plan: agent did not write review file after retries"),
        "expected check_plan missing-review failure path: {combined:?}"
    );
}

#[cfg_attr(unix, test)]
fn review_loop_accepts_lgtm_written_to_artifact_path() {
    let out = run_code_with_mock_js(
        &acp_mock_code_review_lgtm_to_artifact_js(),
        &["--max-loops", "1"],
        true,
    );
    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(
        !combined.contains(MAX_LOOPS_EXHAUSTED),
        "review loop should accept LGTM from artifact path: {combined:?}"
    );
    assert!(
        out.status.success(),
        "malvin code should succeed when reviewer writes LGTM to artifact: {combined:?}"
    );
}

#[cfg_attr(unix, test)]
fn code_stops_when_review_lgtm_also_writes_abort_result() {
    let out = run_code_with_mock_js(
        &acp_mock_code_review_lgtm_with_abort_js(),
        &["--max-loops", "1"],
        true,
    );
    assert_review_abort_behavior(
        &out,
        "ABORT: review lgtm abort test",
        "Review-2 (attempt 1)",
    );
}

#[cfg_attr(unix, test)]
fn skip_pre_checks_skips_initial_repo_gates_in_quality_log() {
    let js = acp_mock_code_review_lgtm_to_artifact_js();
    let opts = CodeRunOpts {
        no_tee: true,
        trust_plan: true,
    };
    let (out, _root, workspace) = run_code_with_mock_js_trust_plan_in_workspace(
        &js,
        &["--max-loops", "1", "--skip-pre-checks"],
        &opts,
    );
    assert!(
        out.status.success(),
        "malvin code should succeed: {:?}",
        String::from_utf8_lossy(&out.stderr)
    );
    let log = std::fs::read_to_string(only_run_dir(&workspace).join("quality_checks.log"))
        .expect("quality_checks.log");
    assert_eq!(
        log.matches("Running `kiss check`").count(),
        1,
        "expected one gate pass (pre-summary only): {log}"
    );

    let (out2, _root2, workspace2) =
        run_code_with_mock_js_trust_plan_in_workspace(&js, &["--max-loops", "1"], &opts);
    assert!(out2.status.success(), "baseline malvin code should succeed");
    let log2 = std::fs::read_to_string(only_run_dir(&workspace2).join("quality_checks.log"))
        .expect("quality_checks.log baseline");
    assert_eq!(
        log2.matches("Running `kiss check`").count(),
        2,
        "expected initial plus pre-summary gate passes: {log2}"
    );
}