ito-core 0.1.33

Core functionality and business logic for Ito
Documentation
use ito_core::tasks::{
    TaskStatus, add_task, complete_task, get_next_task, init_tasks, shelve_task, start_task,
    unshelve_task,
};
mod support;
use support::write;

fn enhanced_tasks_fixture(change_id: &str) -> String {
    format!(
        "# Tasks for: {change_id}\n\n## Wave 1\n\n- **Depends On**: None\n\n### Task 1.1: First\n- **Files**: `a.rs`\n- **Dependencies**: None\n- **Action**:\n  - Do the thing\n- **Verify**: `cargo test -p ito-core`\n- **Done When**: Done\n- **Updated At**: 2026-02-01\n- **Status**: [ ] pending\n\n### Task 1.2: Second\n- **Files**: `b.rs`\n- **Dependencies**: Task 1.1\n- **Action**:\n  - Do the second thing\n- **Verify**: `cargo test -p ito-core`\n- **Done When**: Done\n- **Updated At**: 2026-02-01\n- **Status**: [ ] pending\n"
    )
}

fn write_schema_with_tracks(project_root: &std::path::Path, schema_name: &str, tracks: &str) {
    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join(schema_name)
            .join("schema.yaml"),
        &format!("name: {schema_name}\nversion: 1\nartifacts: []\napply:\n  tracks: {tracks}\n"),
    );
}

fn write_tracking_validation(project_root: &std::path::Path, schema_name: &str, validator: &str) {
    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join(schema_name)
            .join("validation.yaml"),
        &format!(
            "version: 1\ntracking:\n  source: apply_tracks\n  required: true\n  validate_as: {validator}\n"
        ),
    );
}

#[test]
fn init_tasks_creates_file_when_missing() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";

    let (path, existed) = init_tasks(&ito, change_id).expect("init should succeed");
    assert!(!existed);
    assert!(path.exists());

    let contents = std::fs::read_to_string(path).expect("read tasks.md");
    assert!(contents.contains(change_id));
    assert!(contents.contains("# Tasks for:"));
}

#[test]
fn tasks_api_operates_on_schema_apply_tracks_file() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    write_schema_with_tracks(project_root, "tracks", "todo.md");
    write_tracking_validation(project_root, "tracks", "ito.tasks-tracking.v1");

    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: tracks\n",
    );

    let todo_path = ito.join("changes").join(change_id).join("todo.md");
    write(&todo_path, &enhanced_tasks_fixture(change_id));

    let started = start_task(&ito, change_id, "1.1").expect("start 1.1");
    assert_eq!(started.status, TaskStatus::InProgress);
    let updated = std::fs::read_to_string(&todo_path).expect("read todo.md");
    assert!(updated.contains("- **Status**: [>] in-progress"));
    assert!(
        !ito.join("changes")
            .join(change_id)
            .join("tasks.md")
            .exists()
    );

    let completed = complete_task(&ito, change_id, "1.1", None).expect("complete 1.1");
    assert_eq!(completed.status, TaskStatus::Complete);
    let updated = std::fs::read_to_string(&todo_path).expect("read todo.md");
    assert!(updated.contains("- **Status**: [x] complete"));
}

#[test]
fn tasks_api_rejects_non_tasks_tracking_validator_for_schema_tracking() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    write_schema_with_tracks(project_root, "weird", "progress.md");
    write_tracking_validation(project_root, "weird", "ito.delta-specs.v1");

    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: weird\n",
    );

    write(
        &ito.join("changes").join(change_id).join("progress.md"),
        "- [ ] 1: Not actually supported\n",
    );

    let err = start_task(&ito, change_id, "1").expect_err("should reject non tasks-tracking");
    assert!(err.to_string().contains("ito.delta-specs.v1"));
    assert!(err.to_string().contains("not supported"));
}

#[test]
fn start_and_complete_task_enforced_by_dependencies_for_enhanced_format() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let tasks_path = ito.join("changes").join(change_id).join("tasks.md");
    write(&tasks_path, &enhanced_tasks_fixture(change_id));

    let err = start_task(&ito, change_id, "1.2").expect_err("1.2 should be blocked");
    let msg = err.to_string();
    assert!(msg.contains("blocked"));
    assert!(msg.contains("1.1"));

    let first = start_task(&ito, change_id, "1.1").expect("start 1.1");
    assert_eq!(first.status, TaskStatus::InProgress);

    let first = complete_task(&ito, change_id, "1.1", None).expect("complete 1.1");
    assert_eq!(first.status, TaskStatus::Complete);

    let second = start_task(&ito, change_id, "1.2").expect("start 1.2");
    assert_eq!(second.status, TaskStatus::InProgress);
}

#[test]
fn shelve_and_unshelve_task_round_trip_for_enhanced_format() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let tasks_path = ito.join("changes").join(change_id).join("tasks.md");

    // Use a single-task file so shelving cannot create an invalid dependency
    // graph (pending task depending on a shelved task).
    write(
        &tasks_path,
        &format!(
            "# Tasks for: {change_id}\n\n## Wave 1\n\n- **Depends On**: None\n\n### Task 1.1: First\n\n- **Files**: `a.rs`\n- **Dependencies**: None\n- **Action**:\n  - Do the thing\n- **Verify**: `cargo test -p ito-core`\n- **Done When**: Done\n- **Updated At**: 2026-02-01\n- **Status**: [ ] pending\n"
        ),
    );

    let shelved = shelve_task(&ito, change_id, "1.1", Some("no longer needed".to_string()))
        .expect("shelve should succeed");
    assert_eq!(shelved.status, TaskStatus::Shelved);

    let unshelved = unshelve_task(&ito, change_id, "1.1").expect("unshelve should succeed");
    assert_eq!(unshelved.status, TaskStatus::Pending);
}

#[test]
fn get_next_task_returns_first_ready_task_for_enhanced_format() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let tasks_path = ito.join("changes").join(change_id).join("tasks.md");
    write(&tasks_path, &enhanced_tasks_fixture(change_id));

    let next = get_next_task(&ito, change_id).expect("get_next_task");
    let next = next.expect("should have a next task");
    assert_eq!(next.id, "1.1");
}

#[test]
fn add_task_appends_new_task_with_next_id() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let tasks_path = ito.join("changes").join(change_id).join("tasks.md");
    write(
        &tasks_path,
        &format!(
            "# Tasks for: {change_id}\n\n## Wave 1\n- **Depends On**: None\n\n### Task 1.1: First\n- **Files**: `a.rs`\n- **Dependencies**: None\n- **Action**:\n  - A\n- **Verify**: `true`\n- **Done When**: ok\n- **Updated At**: 2026-02-01\n- **Status**: [x] complete\n\n## Checkpoints\n"
        ),
    );

    let item = add_task(&ito, change_id, "Second", Some(1)).expect("add_task should succeed");
    assert_eq!(item.id, "1.2");
    assert_eq!(item.status, TaskStatus::Pending);

    let contents = std::fs::read_to_string(&tasks_path).expect("read tasks.md");
    assert!(contents.contains("### Task 1.2: Second"));
}

#[test]
fn init_tasks_returns_true_when_file_already_exists() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";

    let (_, existed1) = init_tasks(&ito, change_id).expect("first init should succeed");
    assert!(!existed1);

    let (_, existed2) = init_tasks(&ito, change_id).expect("second init should succeed");
    assert!(existed2);
}

#[test]
fn get_next_task_returns_none_when_all_tasks_complete() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let tasks_path = ito.join("changes").join(change_id).join("tasks.md");
    write(
        &tasks_path,
        &format!(
            "# Tasks for: {change_id}\n\n## Wave 1\n- **Depends On**: None\n\n### Task 1.1: First\n- **Dependencies**: None\n- **Updated At**: 2026-02-01\n- **Status**: [x] complete\n"
        ),
    );

    let next = get_next_task(&ito, change_id).expect("get_next_task");
    assert!(next.is_none());
}

#[test]
fn start_task_rejects_starting_shelved_task_directly() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let tasks_path = ito.join("changes").join(change_id).join("tasks.md");
    write(
        &tasks_path,
        &format!(
            "# Tasks for: {change_id}\n\n## Wave 1\n- **Depends On**: None\n\n### Task 1.1: First\n- **Dependencies**: None\n- **Updated At**: 2026-02-01\n- **Status**: [-] shelved\n"
        ),
    );

    let err = start_task(&ito, change_id, "1.1").expect_err("should fail for shelved task");
    assert!(err.to_string().contains("shelved"));
}

#[test]
fn complete_task_accepts_note_parameter() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let tasks_path = ito.join("changes").join(change_id).join("tasks.md");
    write(
        &tasks_path,
        &format!(
            "# Tasks for: {change_id}\n\n## Wave 1\n- **Depends On**: None\n\n### Task 1.1: First\n- **Dependencies**: None\n- **Updated At**: 2026-02-01\n- **Status**: [>] in-progress\n"
        ),
    );

    let result = complete_task(&ito, change_id, "1.1", Some("Done early".to_string()))
        .expect("complete with note should succeed");
    assert_eq!(result.status, TaskStatus::Complete);
}

#[test]
fn shelve_task_accepts_reason_parameter() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let tasks_path = ito.join("changes").join(change_id).join("tasks.md");
    write(
        &tasks_path,
        &format!(
            "# Tasks for: {change_id}\n\n## Wave 1\n- **Depends On**: None\n\n### Task 1.1: First\n- **Dependencies**: None\n- **Updated At**: 2026-02-01\n- **Status**: [ ] pending\n"
        ),
    );

    let result = shelve_task(&ito, change_id, "1.1", Some("No longer needed".to_string()))
        .expect("shelve with reason should succeed");
    assert_eq!(result.status, TaskStatus::Shelved);
}

#[test]
fn shelve_task_rejects_shelving_complete_task() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let tasks_path = ito.join("changes").join(change_id).join("tasks.md");
    write(
        &tasks_path,
        &format!(
            "# Tasks for: {change_id}\n\n## Wave 1\n- **Depends On**: None\n\n### Task 1.1: First\n- **Dependencies**: None\n- **Updated At**: 2026-02-01\n- **Status**: [x] complete\n"
        ),
    );

    let err = shelve_task(&ito, change_id, "1.1", None).expect_err("should fail for complete task");
    assert!(err.to_string().contains("already complete"));
}

#[test]
fn add_task_creates_wave_if_not_exists() {
    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let tasks_path = ito.join("changes").join(change_id).join("tasks.md");
    write(
        &tasks_path,
        &format!(
            "# Tasks for: {change_id}\n\n## Wave 1\n- **Depends On**: None\n\n### Task 1.1: First\n- **Dependencies**: None\n- **Updated At**: 2026-02-01\n- **Status**: [x] complete\n\n## Checkpoints\n"
        ),
    );

    let item = add_task(&ito, change_id, "New wave task", Some(2))
        .expect("add_task to new wave should succeed");
    assert_eq!(item.id, "2.1");
    assert_eq!(item.wave, Some(2));

    let contents = std::fs::read_to_string(&tasks_path).expect("read tasks.md");
    assert!(contents.contains("## Wave 2"));
    assert!(contents.contains("### Task 2.1: New wave task"));
}

#[test]
fn list_ready_tasks_across_changes_handles_empty_repo() {
    use ito_core::change_repository::FsChangeRepository;
    use ito_core::tasks::list_ready_tasks_across_changes;

    let td = tempfile::tempdir().expect("tempdir should succeed");
    let ito = td.path().join(".ito");
    std::fs::create_dir_all(&ito).expect("create ito dir");

    let change_repo = FsChangeRepository::new(&ito);
    let ready = list_ready_tasks_across_changes(&change_repo, &ito)
        .expect("list should succeed on empty repo");

    assert!(ready.is_empty());
}