ito-core 0.1.33

Core functionality and business logic for Ito
Documentation
use super::*;
use ito_domain::audit::event::{EventContext, SCHEMA_VERSION};

fn run_git(repo: &Path, args: &[&str]) {
    let output = std::process::Command::new("git")
        .args(args)
        .current_dir(repo)
        .env_remove("GIT_DIR")
        .env_remove("GIT_WORK_TREE")
        .output()
        .expect("git should run");
    assert!(
        output.status.success(),
        "git command failed: git {}\nstdout:\n{}\nstderr:\n{}",
        args.join(" "),
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn init_git_repo(repo: &Path) {
    run_git(repo, &["init"]);
    run_git(repo, &["config", "user.email", "test@example.com"]);
    run_git(repo, &["config", "user.name", "Test User"]);
    run_git(repo, &["config", "commit.gpgsign", "false"]);
    std::fs::write(repo.join("README.md"), "hi\n").expect("write readme");
    run_git(repo, &["add", "README.md"]);
    run_git(repo, &["commit", "-m", "initial"]);
}

fn test_event(entity_id: &str) -> AuditEvent {
    AuditEvent {
        v: SCHEMA_VERSION,
        ts: "2026-02-08T14:30:00.000Z".to_string(),
        entity: "task".to_string(),
        entity_id: entity_id.to_string(),
        scope: Some("test-change".to_string()),
        op: "create".to_string(),
        from: None,
        to: Some("pending".to_string()),
        actor: "cli".to_string(),
        by: "@test".to_string(),
        meta: None,
        count: 1,
        ctx: EventContext {
            session_id: "test-sid".to_string(),
            harness_session_id: None,
            branch: None,
            worktree: None,
            commit: None,
        },
    }
}

#[test]
fn internal_branch_location_keys_include_branch_identity() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let ito_path = tmp.path().join(".ito");

    let first = LocalAuditStore::new(
        &ito_path,
        "ito/internal/audit-one".to_string(),
        tmp.path().join("one.jsonl"),
    );
    let second = LocalAuditStore::new(
        &ito_path,
        "ito/internal/audit-two".to_string(),
        tmp.path().join("two.jsonl"),
    );

    assert_ne!(
        audit_storage_location_key(&first.location()),
        audit_storage_location_key(&second.location())
    );
}

#[test]
fn read_all_merges_and_replays_fallback_events_when_branch_recovers() {
    let tmp = tempfile::tempdir().expect("tempdir");
    init_git_repo(tmp.path());
    let ito_path = tmp.path().join(".ito");
    std::fs::create_dir_all(&ito_path).expect("create ito dir");

    let fallback_path = tmp.path().join("fallback-events.jsonl");
    let store = LocalAuditStore::new(
        &ito_path,
        "ito/internal/audit".to_string(),
        fallback_path.clone(),
    );

    let branch_event = test_event("1.1");
    store
        .append_to_branch(&branch_event)
        .expect("append branch event");

    let fallback_event = test_event("1.2");
    append_event_to_file(&fallback_path, &fallback_event).expect("append fallback event");

    let events = store.read_all();
    assert_eq!(events.len(), 2);
    assert_eq!(events[0].entity_id, "1.1");
    assert_eq!(events[1].entity_id, "1.2");

    let replayed = store.read_from_branch().expect("read branch after replay");
    let InternalBranchRead::Events(replayed) = replayed else {
        panic!("expected branch events after replay");
    };
    assert_eq!(replayed.len(), 2);
    assert!(!fallback_path.exists());
}

#[test]
fn legacy_worktree_log_is_removed_after_successful_migration() {
    let tmp = tempfile::tempdir().expect("tempdir");
    init_git_repo(tmp.path());
    let ito_path = tmp.path().join(".ito");
    std::fs::create_dir_all(&ito_path).expect("create ito dir");

    let fallback_path = tmp.path().join("fallback-events.jsonl");
    let store = LocalAuditStore::new(&ito_path, "ito/internal/audit".to_string(), fallback_path);
    let legacy_path = audit_log_path(&ito_path);
    append_event_to_file(&legacy_path, &test_event("1.1")).expect("write legacy event");

    let events = store.read_all();
    assert_eq!(events.len(), 1);
    assert!(!legacy_path.exists());

    let branch_events = store
        .read_from_branch()
        .expect("read migrated branch events");
    let InternalBranchRead::Events(branch_events) = branch_events else {
        panic!("expected migrated branch events");
    };
    assert_eq!(branch_events.len(), 1);
}