scv-tools 0.4.9

Workspace-scoped filesystem, process, skill, and agent tools for SCV
Documentation
//! Unit tests for `src/delegate/review/prompts.rs`.

use super::*;

fn finding(id: &str, severity: Severity) -> Finding {
    Finding {
        id: id.into(),
        severity,
        title: "Sleep instead of a lock".into(),
        detail: Some("cart.rs:88 sleeps before commit".into()),
        location: Some("shop/src/cart.rs:88".into()),
    }
}

#[test]
fn the_builder_notice_names_the_rounds_and_the_landing_mode() {
    let none = builder_notice(LandMode::NoLanding, 3);
    assert!(none.contains("up to 3 rounds"), "{none}");
    assert!(none.contains("```scv-landing\n"), "{none}");
    assert!(none.ends_with("message anyone in this job."), "{none}");
    let after = builder_notice(LandMode::AfterApproval, 5);
    assert!(after.contains("name its base and head commits"), "{after}");
    let before = builder_notice(LandMode::BeforeReview, 3);
    assert!(
        before.contains("authorizes landing before review"),
        "{before}"
    );
}

#[test]
fn the_reviewer_is_never_told_the_round_limit() {
    let open = [finding("1.1", Severity::Blocking)];
    for land in [
        LandMode::NoLanding,
        LandMode::AfterApproval,
        LandMode::BeforeReview,
    ] {
        let prompt = reviewer_prompt(&ReviewerBrief {
            land,
            prompt: "Fix the flaky checkout test.",
            focus: Some("no sleeps"),
            reply: "Replaced the sleep with a lock.",
            open: &open,
            landed: None,
        });
        // It may hear that rounds exist, never how many or which this is.
        for told in ["3 rounds", " of 3", "round 1", "round 2", "rounds left"] {
            assert!(!prompt.contains(told), "{told}: {prompt}");
        }
        assert!(prompt.contains("1.1 [blocking] \"Sleep instead of a lock\""));
        assert!(prompt.contains("Focus from the user: no sleeps"));
        assert!(prompt.contains("```scv-verdict\n"));
        assert!(
            prompt
                .contains("Commits the brief names as landed before this job are not a violation.")
        );
        assert!(prompt.contains("untrusted"));
    }
}

#[test]
fn the_reviewer_checks_a_reported_landing_and_names_what_it_approves() {
    let commits = ["4f2a9c1".to_owned()];
    let prompt = reviewer_prompt(&ReviewerBrief {
        land: LandMode::BeforeReview,
        prompt: "Hotfix it.",
        focus: None,
        reply: "Landed.",
        open: &[],
        landed: Some(("origin/main", &commits)),
    });
    assert!(prompt.contains("landed 4f2a9c1 on origin/main"), "{prompt}");
    assert!(!prompt.contains("settle every open finding"));
    let after = reviewer_prompt(&ReviewerBrief {
        land: LandMode::AfterApproval,
        prompt: "Fix it.",
        focus: None,
        reply: "Done.",
        open: &[],
        landed: None,
    });
    assert!(after.contains("\"approved\""), "{after}");
}

#[test]
fn an_empty_open_list_omits_prior_and_a_listed_one_is_settled_by_id() {
    let empty = reviewer_prompt(&ReviewerBrief {
        land: LandMode::NoLanding,
        prompt: "Fix the flaky checkout test.",
        focus: None,
        reply: "Replaced the sleep with a lock.",
        open: &[],
        landed: None,
    });
    assert!(
        empty.contains("There are no open findings, so omit \"prior\" or set it to []."),
        "{empty}"
    );
    assert!(
        empty.contains("Do not invent finding ids such as \"1\" or \"1.1\"."),
        "{empty}"
    );
    assert!(
        empty.contains("A \"prior\" entry is valid only for an id listed under open findings."),
        "{empty}"
    );
    assert!(!empty.contains("settle every open finding"), "{empty}");
    assert!(empty.contains("\"prior\":[]"), "{empty}");
    assert!(!empty.contains("<open finding ID>"), "{empty}");
    assert!(
        empty.contains(
            "\"prior\" is required only when open findings were listed, one entry per listed id"
        ),
        "{empty}"
    );
    assert!(
        empty.contains("otherwise omit \"prior\" or set it to []"),
        "{empty}"
    );

    let open = [
        finding("1.1", Severity::Blocking),
        finding("1.2", Severity::Minor),
    ];
    let later = reviewer_prompt(&ReviewerBrief {
        land: LandMode::NoLanding,
        prompt: "Fix the flaky checkout test.",
        focus: None,
        reply: "Replaced the sleep with a lock.",
        open: &open,
        landed: None,
    });
    assert!(
        later.contains("First settle every open finding below by its ID in \"prior\""),
        "{later}"
    );
    assert!(later.contains("- 1.1 [blocking]"), "{later}");
    assert!(later.contains("- 1.2 [minor]"), "{later}");
    assert!(
        later.contains(
            "\"prior\" is required only when open findings were listed, one entry per listed id"
        ),
        "{later}"
    );
    assert!(!later.contains("There are no open findings"), "{later}");
    assert!(!later.contains("<open finding ID>"), "{later}");
}

#[test]
fn the_repair_prompt_keeps_the_parser_error_and_forbids_an_invented_prior_id() {
    let clause = "If the error is that \"prior\" names an id that is not an open finding, omit \
                  \"prior\" or send [] unless the review listed open findings, and never invent \
                  an id.";
    let missing = repair_prompt("no scv-verdict block");
    assert!(missing.starts_with(
        "Your reply had no valid scv-verdict block (no scv-verdict block). Reply with only that \
         block,"
    ));
    assert!(missing.contains(clause), "{missing}");
    let named = repair_prompt("prior names \"1.1\", which is not an open finding");
    assert!(
        named.contains("(prior names \"1.1\", which is not an open finding)"),
        "{named}"
    );
    assert!(named.contains(clause), "{named}");
}

#[test]
fn quoted_text_is_bounded() {
    let long = "x".repeat(20 * 1024);
    let prompt = reviewer_prompt(&ReviewerBrief {
        land: LandMode::NoLanding,
        prompt: &long,
        focus: None,
        reply: &long,
        open: &[],
        landed: None,
    });
    assert!(prompt.len() < 20 * 1024, "{}", prompt.len());
    assert_eq!(prompt.matches("[cut at 8 KiB]").count(), 2);
}

/// Round 2's brief for `findings`, after a round-1 `report`.
fn fix<'a>(
    land: LandMode,
    report: &'a str,
    findings: &'a [Finding],
    landed: Option<(&'a str, &'a [String])>,
) -> FixBrief<'a> {
    FixBrief {
        round: 2,
        rounds: 3,
        land,
        prompt: "Fix the flaky checkout test.",
        report,
        findings,
        landed,
    }
}

#[test]
fn the_fix_prompt_lists_findings_and_what_already_landed() {
    let findings = [
        finding("1.1", Severity::Blocking),
        finding("1.2", Severity::Minor),
    ];
    let commits = ["4f2a9c1".to_owned()];
    let prompt = fix_prompt(&fix(
        LandMode::BeforeReview,
        "Committed on fix/race.",
        &findings,
        Some(("origin/main", &commits)),
    ));
    assert!(prompt.starts_with("[SCV review, round 2 of 3]"), "{prompt}");
    assert!(prompt.contains("in a new conversation"));
    assert!(prompt.contains("already landed on origin/main (4f2a9c1)"));
    assert!(prompt.contains("Land the fix as before"));
    assert!(prompt.contains("- 1.1 [blocking]"));
    assert!(prompt.contains("- 1.2 [minor]"));
    // A new conversation gets the task and where the work stands.
    assert!(prompt.contains("The task, as it was first given:\nFix the flaky checkout test."));
    assert!(prompt.contains("up to 3 rounds"));
    assert!(prompt.contains("Committed on fix/race."));
    let held = fix_prompt(&fix(
        LandMode::NoLanding,
        "",
        &findings,
        Some(("origin/main", &commits)),
    ));
    assert!(held.contains("Do not land the fix now."), "{held}");
}

#[test]
fn the_fix_prompt_quotes_only_a_bounded_last_report() {
    let findings = [finding("1.1", Severity::Blocking)];
    let report = format!("{}LAST LINE", "x".repeat(100 * 1024));
    let prompt = fix_prompt(&fix(LandMode::NoLanding, &report, &findings, None));
    assert!(prompt.len() < 16 * 1024, "{}", prompt.len());
    assert!(prompt.contains("[cut at 8 KiB]"));
    assert!(!prompt.contains("LAST LINE"));
}

#[test]
fn the_confirmation_names_the_approved_range_and_the_ref() {
    let prompt = confirmation_prompt(
        ("1b2c3d4", "7e8f9a0"),
        1,
        r#"{"status":"landed","ref":"origin/main","commits":["4f2a9c1"]}"#,
        "origin/main",
    );
    assert!(
        prompt.contains("You approved 1b2c3d4..7e8f9a0 in round 1"),
        "{prompt}"
    );
    assert!(prompt.contains("```scv-landing-check\n"));
    assert!(prompt.contains("Do not edit, commit, land, revert, push"));
    assert!(repair_prompt("no scv-verdict block").contains("(no scv-verdict block)"));
}

/// `count` findings from round `round`, each with a long detail.
fn many(round: u32, count: usize, title: &str, detail: usize) -> Vec<Finding> {
    (1..=count)
        .map(|n| Finding {
            id: format!("{round}.{n}"),
            severity: Severity::Blocking,
            title: format!("{title} {n}"),
            detail: Some("d".repeat(detail)),
            location: Some(format!("src/lib.rs:{n}")),
        })
        .collect()
}

fn listed(text: &str, findings: &[Finding]) -> Vec<String> {
    findings
        .iter()
        .filter(|finding| !text.contains(&format!("- {} [blocking] \"", finding.id)))
        .map(|finding| finding.id.clone())
        .collect()
}

#[test]
fn every_open_finding_reaches_the_next_reviewer_and_the_builder() {
    // Two verdicts of ten blocking findings with long details: the case an
    // earlier whole-list cut lost 2.4 to 2.10 in.
    let mut open = many(1, 10, "Defect", 1300);
    open.extend(many(2, 10, "Defect", 1300));
    let prompt = reviewer_prompt(&ReviewerBrief {
        land: LandMode::NoLanding,
        prompt: "Fix the project.",
        focus: None,
        reply: "I fixed the findings.",
        open: &open,
        landed: None,
    });
    assert!(
        listed(&prompt, &open).is_empty(),
        "{:?}",
        listed(&prompt, &open)
    );
    let fix = fix_prompt(&FixBrief {
        round: 3,
        rounds: 5,
        land: LandMode::NoLanding,
        prompt: "Fix the project.",
        report: "I fixed the findings.",
        findings: &open,
        landed: None,
    });
    assert_eq!(listed(&fix, &open), [] as [String; 0]);
    // Far more than fits: details shortened, every line kept.
    let open: Vec<Finding> = (1..=6)
        .flat_map(|round| many(round, 20, "Defect", 2000))
        .collect();
    let list = finding_list(&open, FINDINGS_BYTES);
    assert!(list.len() <= FINDINGS_BYTES, "{}", list.len());
    assert!(list.starts_with("(Details are shortened to fit"));
    assert_eq!(listed(&list, &open), [] as [String; 0]);
    assert!(list.contains("(src/lib.rs:20)"));
    // So many that only shortened lines fit.
    let title = "é".repeat(200);
    let open: Vec<Finding> = (1..=20)
        .flat_map(|round| many(round, 20, &title, 2000))
        .collect();
    let list = finding_list(&open, FINDINGS_BYTES);
    assert!(list.len() <= FINDINGS_BYTES, "{}", list.len());
    assert!(list.starts_with("(Too many findings to quote in full"));
    assert_eq!(listed(&list, &open), [] as [String; 0]);
    assert!(!list.contains("dddd"));
}