ito-core 0.1.33

Core functionality and business logic for Ito
Documentation
//! End-to-end smoke tests for the worktree ensure + init flow.

use std::fs;
use std::path::Path;
use std::process::Command;

/// Create a minimal git repository with a single commit.
fn init_git_repo(path: &Path) {
    fs::create_dir_all(path).unwrap();
    run_git(path, &["init", "--initial-branch=main"]);
    run_git(path, &["config", "user.email", "test@example.com"]);
    run_git(path, &["config", "user.name", "Test"]);
    fs::write(path.join("README.md"), "# Test").unwrap();
    run_git(path, &["add", "."]);
    run_git(path, &["commit", "-m", "initial"]);
}

fn integrate_change(path: &Path, change_id: &str) {
    let change = path.join(".ito/changes").join(change_id);
    fs::create_dir_all(change.join("specs/worktree")).unwrap();
    fs::write(change.join(".ito.yaml"), "schema: spec-driven\n").unwrap();
    fs::write(
        change.join("proposal.md"),
        "# Proposal\n\nCreate a guarded implementation worktree.\n",
    )
    .unwrap();
    fs::write(
        change.join("design.md"),
        "# Design\n\nUse the captured authority commit as the worktree base.\n",
    )
    .unwrap();
    fs::write(
        change.join("tasks.md"),
        "## Wave 1\n- **Depends On**: None\n\n### Task 1.1: Create worktree\n- **Dependencies**: None\n- **Updated At**: 2026-07-13\n- **Status**: [ ] pending\n",
    )
    .unwrap();
    fs::write(
        change.join("specs/worktree/spec.md"),
        "## ADDED Requirements\n\n### Requirement: Guarded worktree\nIto SHALL create worktrees from accepted proposal history.\n\n#### Scenario: Accepted proposal\n- **WHEN** a worktree is created\n- **THEN** it uses the authority commit\n",
    )
    .unwrap();
    let baseline = path.join(".ito/specs/worktree/spec.md");
    fs::create_dir_all(baseline.parent().unwrap()).unwrap();
    fs::write(
        baseline,
        "# Worktree\n\n## Requirements\n\n### Requirement: Existing baseline\nIto SHALL preserve main specifications in implementation worktrees.\n",
    )
    .unwrap();
    run_git(path, &["add", ".ito"]);
    run_git(path, &["commit", "-m", "integrate reviewed proposal"]);
}

fn run_git(cwd: &Path, args: &[&str]) {
    let output = Command::new("git")
        .args(args)
        .current_dir(cwd)
        .env_remove("GIT_DIR")
        .env_remove("GIT_WORK_TREE")
        .output()
        .unwrap_or_else(|e| panic!("Failed to run git {:?}: {e}", args));
    assert!(
        output.status.success(),
        "git {:?} failed: {}",
        args,
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
#[cfg(feature = "coordination-branch")]
fn ensure_worktree_creates_and_initializes_with_include_files() {
    use ito_config::types::{
        CoordinationStorage, ItoConfig, WorktreeInitConfig, WorktreeLayoutConfig, WorktreeStrategy,
    };
    use ito_core::repo_paths::{GitRepoKind, ResolvedEnv, ResolvedWorktreePaths, WorktreeFeature};
    use ito_core::worktree_ensure::ensure_worktree;

    let tmp = tempfile::tempdir().unwrap();
    let project_root = tmp.path().join("repo");
    init_git_repo(&project_root);
    integrate_change(&project_root, "test-change");

    // Create an .env file in the project root (not committed — it's a local-only file).
    fs::write(project_root.join(".env"), "SECRET=test123").unwrap();

    let worktrees_root = tmp.path().join("ito-worktrees");
    let coordination_root = tmp.path().join("coordination");
    let coordination_ito = coordination_root.join(".ito");
    for dir in ["changes", "specs", "modules", "workflows", "audit"] {
        fs::create_dir_all(coordination_ito.join(dir)).unwrap();
    }

    let mut config = ItoConfig::default();
    config.changes.proposal.integration_mode =
        ito_config::types::ProposalIntegrationMode::DirectMerge;
    config.worktrees.enabled = true;
    config.worktrees.strategy = WorktreeStrategy::CheckoutSiblings;
    config.changes.coordination_branch.storage = CoordinationStorage::Worktree;
    config.changes.coordination_branch.worktree_path =
        Some(coordination_root.to_string_lossy().to_string());
    config.worktrees.layout = WorktreeLayoutConfig {
        base_dir: None,
        dir_name: "ito-worktrees".to_string(),
    };
    config.worktrees.init = WorktreeInitConfig {
        include: vec![".env".to_string()],
        setup: None,
    };

    let env = ResolvedEnv {
        worktree_root: project_root.clone(),
        project_root: project_root.clone(),
        ito_root: project_root.join(".ito"),
        git_repo_kind: GitRepoKind::NonBare,
    };

    let paths = ResolvedWorktreePaths {
        feature: WorktreeFeature::Enabled,
        strategy: WorktreeStrategy::CheckoutSiblings,
        worktrees_root: Some(worktrees_root.clone()),
        main_worktree_root: Some(project_root.clone()),
    };

    // First call: creates the worktree.
    let result = ensure_worktree("test-change", &config, &env, &paths, &project_root);
    let wt_path = result.unwrap();

    assert_eq!(wt_path, worktrees_root.join("test-change"));
    assert!(wt_path.is_dir());

    // Verify include file was copied.
    let env_file = wt_path.join(".env");
    assert!(env_file.exists(), ".env should be copied to worktree");
    assert_eq!(fs::read_to_string(&env_file).unwrap(), "SECRET=test123");

    if cfg!(unix) {
        assert!(
            wt_path.join(".ito/changes/test-change/.ito.yaml").is_file(),
            "accepted proposal contents must remain in the Git worktree"
        );
        assert!(
            wt_path.join(".ito/specs/worktree/spec.md").is_file(),
            "main specifications must remain in the Git worktree"
        );
        for dir in ["modules", "workflows", "audit"] {
            let path = wt_path.join(".ito").join(dir);
            let target = fs::read_link(&path).unwrap_or_else(|err| {
                panic!("{dir} should be a coordination symlink: {err}");
            });
            assert_eq!(target, coordination_ito.join(dir));
        }
    }

    // Second call: idempotent — returns the same path without error.
    let result2 = ensure_worktree("test-change", &config, &env, &paths, &project_root);
    assert_eq!(result2.unwrap(), wt_path);
}

#[test]
#[cfg(all(unix, feature = "coordination-branch"))]
fn ensure_worktree_repairs_missing_coordination_links_in_existing_worktree() {
    use ito_config::types::{
        CoordinationStorage, ItoConfig, WorktreeInitConfig, WorktreeLayoutConfig, WorktreeStrategy,
    };
    use ito_core::repo_paths::{GitRepoKind, ResolvedEnv, ResolvedWorktreePaths, WorktreeFeature};
    use ito_core::worktree_ensure::ensure_worktree;

    let tmp = tempfile::tempdir().unwrap();
    let project_root = tmp.path().join("repo");
    init_git_repo(&project_root);
    integrate_change(&project_root, "repair-test");

    let worktrees_root = tmp.path().join("ito-worktrees");
    let coordination_root = tmp.path().join("coordination");
    let coordination_ito = coordination_root.join(".ito");
    for dir in ["changes", "specs", "modules", "workflows", "audit"] {
        fs::create_dir_all(coordination_ito.join(dir)).unwrap();
    }

    let mut config = ItoConfig::default();
    config.changes.proposal.integration_mode =
        ito_config::types::ProposalIntegrationMode::DirectMerge;
    config.worktrees.enabled = true;
    config.worktrees.strategy = WorktreeStrategy::CheckoutSiblings;
    config.changes.coordination_branch.storage = CoordinationStorage::Worktree;
    config.changes.coordination_branch.worktree_path =
        Some(coordination_root.to_string_lossy().to_string());
    config.worktrees.layout = WorktreeLayoutConfig {
        base_dir: None,
        dir_name: "ito-worktrees".to_string(),
    };
    config.worktrees.init = WorktreeInitConfig {
        include: vec![],
        setup: None,
    };

    let env = ResolvedEnv {
        worktree_root: project_root.clone(),
        project_root: project_root.clone(),
        ito_root: project_root.join(".ito"),
        git_repo_kind: GitRepoKind::NonBare,
    };

    let paths = ResolvedWorktreePaths {
        feature: WorktreeFeature::Enabled,
        strategy: WorktreeStrategy::CheckoutSiblings,
        worktrees_root: Some(worktrees_root.clone()),
        main_worktree_root: Some(project_root.clone()),
    };

    let wt_path = ensure_worktree("repair-test", &config, &env, &paths, &project_root).unwrap();
    std::fs::remove_file(wt_path.join(".ito/modules")).expect("remove broken symlink");

    let repaired = ensure_worktree("repair-test", &config, &env, &paths, &project_root).unwrap();
    assert_eq!(repaired, wt_path);

    let target =
        std::fs::read_link(wt_path.join(".ito/modules")).expect("modules symlink restored");
    assert_eq!(target, coordination_ito.join("modules"));
}

#[test]
fn ensure_worktree_disabled_returns_cwd() {
    use ito_config::types::ItoConfig;
    use ito_core::repo_paths::{GitRepoKind, ResolvedEnv, ResolvedWorktreePaths, WorktreeFeature};
    use ito_core::worktree_ensure::ensure_worktree;

    let tmp = tempfile::tempdir().unwrap();
    let cwd = tmp.path();

    let config = ItoConfig::default();
    let env = ResolvedEnv {
        worktree_root: cwd.to_path_buf(),
        project_root: cwd.to_path_buf(),
        ito_root: cwd.join(".ito"),
        git_repo_kind: GitRepoKind::NonBare,
    };
    let paths = ResolvedWorktreePaths {
        feature: WorktreeFeature::Disabled,
        strategy: ito_config::types::WorktreeStrategy::CheckoutSubdir,
        worktrees_root: None,
        main_worktree_root: None,
    };

    let result = ensure_worktree("any-change", &config, &env, &paths, cwd);
    assert_eq!(result.unwrap(), cwd.to_path_buf());
}

#[test]
fn ensure_worktree_with_setup_script() {
    use ito_config::types::{
        CoordinationStorage, ItoConfig, WorktreeInitConfig, WorktreeLayoutConfig,
        WorktreeSetupConfig, WorktreeStrategy,
    };
    use ito_core::repo_paths::{GitRepoKind, ResolvedEnv, ResolvedWorktreePaths, WorktreeFeature};
    use ito_core::worktree_ensure::ensure_worktree;

    let tmp = tempfile::tempdir().unwrap();
    let project_root = tmp.path().join("repo");
    init_git_repo(&project_root);
    integrate_change(&project_root, "setup-test");

    let worktrees_root = tmp.path().join("ito-worktrees");

    let mut config = ItoConfig::default();
    config.changes.proposal.integration_mode =
        ito_config::types::ProposalIntegrationMode::DirectMerge;
    config.worktrees.enabled = true;
    config.worktrees.strategy = WorktreeStrategy::CheckoutSiblings;
    config.changes.coordination_branch.storage = CoordinationStorage::Embedded;
    config.worktrees.layout = WorktreeLayoutConfig {
        base_dir: None,
        dir_name: "ito-worktrees".to_string(),
    };
    // Setup command creates a sentinel file to prove it ran.
    config.worktrees.init = WorktreeInitConfig {
        include: vec![],
        setup: Some(WorktreeSetupConfig::Single(
            "touch .setup-complete".to_string(),
        )),
    };

    let env = ResolvedEnv {
        worktree_root: project_root.clone(),
        project_root: project_root.clone(),
        ito_root: project_root.join(".ito"),
        git_repo_kind: GitRepoKind::NonBare,
    };

    let paths = ResolvedWorktreePaths {
        feature: WorktreeFeature::Enabled,
        strategy: WorktreeStrategy::CheckoutSiblings,
        worktrees_root: Some(worktrees_root.clone()),
        main_worktree_root: Some(project_root.clone()),
    };

    let result = ensure_worktree("setup-test", &config, &env, &paths, &project_root);
    let wt_path = result.unwrap();

    // Verify the setup command ran by checking for the sentinel file.
    assert!(
        wt_path.join(".setup-complete").exists(),
        "Setup command should have created .setup-complete in the worktree"
    );
}