apm-core 0.1.12

Core library for APM — a git-native project manager for parallel AI coding agents.
Documentation
use apm_core::{config::Config, git_util, ticket::Ticket, verify::verify_tickets};
use std::collections::HashSet;
use std::process::Command;
use tempfile::TempDir;

fn git(dir: &std::path::Path, args: &[&str]) {
    Command::new("git")
        .args(args)
        .current_dir(dir)
        .env("GIT_AUTHOR_NAME", "test")
        .env("GIT_AUTHOR_EMAIL", "test@test.com")
        .env("GIT_COMMITTER_NAME", "test")
        .env("GIT_COMMITTER_EMAIL", "test@test.com")
        .status()
        .unwrap();
}

fn setup() -> TempDir {
    let dir = tempfile::tempdir().unwrap();
    let p = dir.path();

    git(p, &["init", "-q", "-b", "main"]);
    git(p, &["config", "user.email", "test@test.com"]);
    git(p, &["config", "user.name", "test"]);

    std::fs::write(
        p.join("apm.toml"),
        r#"[project]
name = "test"

[tickets]
dir = "tickets"

[worktrees]
dir = "worktrees"

[[workflow.states]]
id = "in_design"
label = "In Design"

[[workflow.states]]
id = "in_progress"
label = "In Progress"

[[workflow.states]]
id = "specd"
label = "Specd"
"#,
    )
    .unwrap();

    git(p, &["add", "apm.toml"]);
    git(p, &["-c", "commit.gpgsign=false", "commit", "-m", "init"]);

    dir
}

fn make_ticket(root: &std::path::Path, id: &str, state: &str, branch: Option<&str>) -> Ticket {
    let branch_line = match branch {
        Some(b) => format!("branch = \"{b}\"\n"),
        None => String::new(),
    };
    let raw = format!(
        "+++\nid = \"{id}\"\ntitle = \"Test ticket\"\nstate = \"{state}\"\n{branch_line}+++\n\n## Spec\n\n## History\n"
    );
    let path = root.join("tickets").join(format!("{id}-test.md"));
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(&path, &raw).unwrap();
    Ticket::parse(&path, &raw).unwrap()
}

#[test]
fn worktree_missing_in_design() {
    let dir = setup();
    let root = dir.path();
    let config = Config::load(root).unwrap();
    let ticket = make_ticket(root, "abcd1234", "in_design", Some("ticket/abcd1234-test"));

    let issues = verify_tickets(root, &config, &[ticket], &HashSet::new());

    let main_root = git_util::main_worktree_root(root).unwrap_or_else(|| root.to_path_buf());
    let wt_path = main_root.join("worktrees").join("ticket-abcd1234-test");
    let expected = format!(
        "#abcd1234 [in_design]: worktree at {} is missing",
        wt_path.display()
    );
    assert!(
        issues.iter().any(|i| i == &expected),
        "expected worktree missing issue; got: {issues:?}"
    );
}

#[test]
fn worktree_present_no_issue() {
    let dir = setup();
    let root = dir.path();
    let config = Config::load(root).unwrap();
    let ticket = make_ticket(root, "abcd1234", "in_design", Some("ticket/abcd1234-test"));

    std::fs::create_dir_all(root.join("worktrees").join("ticket-abcd1234-test")).unwrap();

    let issues = verify_tickets(root, &config, &[ticket], &HashSet::new());
    assert!(
        !issues.iter().any(|i| i.contains("worktree")),
        "unexpected worktree issue; got: {issues:?}"
    );
}

#[test]
fn worktree_check_skipped_for_other_states() {
    let dir = setup();
    let root = dir.path();
    let config = Config::load(root).unwrap();
    let ticket = make_ticket(root, "abcd1234", "specd", Some("ticket/abcd1234-test"));

    let issues = verify_tickets(root, &config, &[ticket], &HashSet::new());
    assert!(
        !issues.iter().any(|i| i.contains("worktree")),
        "unexpected worktree issue for specd state; got: {issues:?}"
    );
}