choreo-daemon 0.1.0

Agentic coding assistant — daemon, TUI, and bridges
use super::*;
use crate::tools::fs::{EditFileArgs, TextEditArgs, WriteFileArgs};

fn test_temp_path(prefix: &str) -> std::path::PathBuf {
    let unique = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("time")
        .as_nanos();
    std::env::temp_dir().join(format!("{prefix}-{unique}.txt"))
}

#[test]
fn write_file_tool_writes_new_file() {
    let path = test_temp_path("choreo-write-tool");

    execute_write_file_tool(
        &WriteFileArgs {
            path: path.display().to_string(),
            content: "hello from write tool\n".into(),
            overwrite: Some(true),
            create_parents: Some(true),
        },
        None,
    )
    .unwrap();

    assert_eq!(
        std::fs::read_to_string(&path).expect("read file"),
        "hello from write tool\n"
    );

    let _ = std::fs::remove_file(&path);
}

#[test]
fn write_file_tool_refuses_overwrite_when_disabled() {
    let path = test_temp_path("choreo-write-tool-existing");
    std::fs::write(&path, "original\n").expect("seed file");

    let result = execute_write_file_tool(
        &WriteFileArgs {
            path: path.display().to_string(),
            content: "replacement\n".into(),
            overwrite: Some(false),
            create_parents: Some(true),
        },
        None,
    );

    assert!(result.is_err(), "{}", result.unwrap_err());
    let err = result.unwrap_err().to_string();
    assert!(
        err.contains("refusing to overwrite existing file"),
        "{}",
        err
    );
    assert_eq!(
        std::fs::read_to_string(&path).expect("read file"),
        "original\n"
    );

    let _ = std::fs::remove_file(&path);
}

#[test]
fn write_file_tool_creates_parent_directories() {
    let dir = test_temp_path("choreo-write-tool-dir").with_extension("");
    let path = dir.join("nested/output.txt");

    execute_write_file_tool(
        &WriteFileArgs {
            path: path.display().to_string(),
            content: "nested hello\n".into(),
            overwrite: Some(true),
            create_parents: Some(true),
        },
        None,
    )
    .unwrap();

    assert_eq!(
        std::fs::read_to_string(&path).expect("read file"),
        "nested hello\n"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn edit_file_tool_replaces_single_unique_match() {
    let path = test_temp_path("choreo-edit-tool-single");
    std::fs::write(&path, "alpha\nbeta\ngamma\n").expect("seed file");

    let result = execute_edit_file_tool(
        &EditFileArgs {
            path: path.display().to_string(),
            edits: vec![TextEditArgs {
                old_text: "beta".into(),
                new_text: "delta".into(),
                replace_all: None,
            }],
            expected_sha256: None,
            dry_run: None,
        },
        None,
    );

    let content = result.unwrap_or_default();
    assert!(content.contains("edited file:"), "{}", content);
    assert_eq!(
        std::fs::read_to_string(&path).expect("read file"),
        "alpha\ndelta\ngamma\n"
    );

    let _ = std::fs::remove_file(&path);
}

#[test]
fn edit_file_tool_fails_when_old_text_is_missing() {
    let path = test_temp_path("choreo-edit-tool-missing");
    std::fs::write(&path, "hello\nworld\n").expect("seed file");

    let result = execute_edit_file_tool(
        &EditFileArgs {
            path: path.display().to_string(),
            edits: vec![TextEditArgs {
                old_text: "absent".into(),
                new_text: "present".into(),
                replace_all: None,
            }],
            expected_sha256: None,
            dry_run: None,
        },
        None,
    );

    assert!(result.is_err(), "{}", result.unwrap_err());
    let err = result.unwrap_err().to_string();
    assert!(err.contains("old_text not found"), "{}", err);
    assert_eq!(
        std::fs::read_to_string(&path).expect("read file"),
        "hello\nworld\n"
    );

    let _ = std::fs::remove_file(&path);
}

#[test]
fn edit_file_tool_fails_on_ambiguous_non_replace_all_edit() {
    let path = test_temp_path("choreo-edit-tool-ambiguous");
    std::fs::write(&path, "repeat\nrepeat\n").expect("seed file");

    let result = execute_edit_file_tool(
        &EditFileArgs {
            path: path.display().to_string(),
            edits: vec![TextEditArgs {
                old_text: "repeat".into(),
                new_text: "done".into(),
                replace_all: None,
            }],
            expected_sha256: None,
            dry_run: None,
        },
        None,
    );

    assert!(result.is_err(), "{}", result.unwrap_err());
    let err = result.unwrap_err().to_string();
    assert!(err.contains("matched 2 locations"), "{}", err);
    assert_eq!(
        std::fs::read_to_string(&path).expect("read file"),
        "repeat\nrepeat\n"
    );

    let _ = std::fs::remove_file(&path);
}

#[test]
fn edit_file_tool_supports_replace_all_and_dry_run() {
    let path = test_temp_path("choreo-edit-tool-replace-all");
    std::fs::write(&path, "foo\nfoo\n").expect("seed file");

    let result = execute_edit_file_tool(
        &EditFileArgs {
            path: path.display().to_string(),
            edits: vec![TextEditArgs {
                old_text: "foo".into(),
                new_text: "bar".into(),
                replace_all: Some(true),
            }],
            expected_sha256: None,
            dry_run: Some(true),
        },
        None,
    );

    let content = result.unwrap_or_default();
    assert!(content.contains("would edit file:"), "{}", content);
    assert!(content.contains("2 replacements"), "{}", content);
    assert_eq!(
        std::fs::read_to_string(&path).expect("read file"),
        "foo\nfoo\n"
    );

    let _ = std::fs::remove_file(&path);
}

#[test]
fn edit_file_tool_validates_expected_sha256() {
    let path = test_temp_path("choreo-edit-tool-sha");
    let original = "red\nblue\n";
    std::fs::write(&path, original).expect("seed file");
    let expected_sha256 = sha256_hex(original);

    let success = execute_edit_file_tool(
        &EditFileArgs {
            path: path.display().to_string(),
            edits: vec![TextEditArgs {
                old_text: "blue".into(),
                new_text: "green".into(),
                replace_all: None,
            }],
            expected_sha256: Some(expected_sha256.clone()),
            dry_run: None,
        },
        None,
    );
    assert!(success.is_ok(), "{}", success.unwrap_err());
    assert_eq!(
        std::fs::read_to_string(&path).expect("read file"),
        "red\ngreen\n"
    );

    let failure = execute_edit_file_tool(
        &EditFileArgs {
            path: path.display().to_string(),
            edits: vec![TextEditArgs {
                old_text: "green".into(),
                new_text: "purple".into(),
                replace_all: None,
            }],
            expected_sha256: Some(expected_sha256),
            dry_run: None,
        },
        None,
    );
    assert!(failure.is_err(), "{}", failure.unwrap_err());
    let err = failure.unwrap_err().to_string();
    assert!(err.contains("expected_sha256 mismatch"), "{}", err);
    assert_eq!(
        std::fs::read_to_string(&path).expect("read file"),
        "red\ngreen\n"
    );

    let _ = std::fs::remove_file(&path);
}