use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn get_twin_binary() -> PathBuf {
let binary_name = if cfg!(windows) { "twin.exe" } else { "twin" };
let current_exe = std::env::current_exe().expect("Failed to get current exe path");
let path = current_exe
.parent() .and_then(|p| p.parent()) .map(|p| p.join(binary_name))
.expect("Failed to construct twin binary path");
if !path.exists() {
eprintln!("Current exe: {current_exe:?}");
eprintln!("Looking for twin binary at: {path:?}");
eprintln!("Current dir: {:?}", std::env::current_dir().ok());
panic!("twin binary not found. Run 'cargo build' first.");
}
path
}
fn setup_git_repo() -> TempDir {
let dir = TempDir::new().unwrap();
Command::new("git")
.args(["init"])
.current_dir(dir.path())
.output()
.expect("git init failed");
Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(dir.path())
.output()
.unwrap();
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(dir.path())
.output()
.unwrap();
std::fs::write(dir.path().join("README.md"), "# Test").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(dir.path())
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "initial"])
.current_dir(dir.path())
.output()
.unwrap();
dir
}
#[test]
fn test_help_command() {
let twin = get_twin_binary();
let output = Command::new(&twin)
.arg("--help")
.output()
.expect("Failed to run twin --help");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("create"));
assert!(stdout.contains("list"));
assert!(stdout.contains("remove"));
}
#[test]
fn test_create_environment() {
let repo = setup_git_repo();
let twin = get_twin_binary();
let output = Command::new(&twin)
.args(["create", "test-env"])
.current_dir(repo.path())
.output()
.expect("Failed to run twin create");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
assert!(
stdout.contains("環境") || stdout.contains("Worktree") || output.status.success(),
"Environment creation should succeed or show appropriate message"
);
let worktree_output = Command::new("git")
.args(["worktree", "list"])
.current_dir(repo.path())
.output()
.unwrap();
let worktree_list = String::from_utf8_lossy(&worktree_output.stdout);
println!("Worktrees: {worktree_list}");
assert!(
worktree_list.contains("test-env") || worktree_list.contains("agent"),
"Worktree should be created"
);
}
#[test]
fn test_list_environments() {
let repo = setup_git_repo();
let twin = get_twin_binary();
Command::new(&twin)
.args(["create", "list-test"])
.current_dir(repo.path())
.output()
.unwrap();
let output = Command::new(&twin)
.args(["list"])
.current_dir(repo.path())
.output()
.expect("Failed to run twin list");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
println!("List output: {stdout}");
}
#[test]
fn test_remove_environment() {
let repo = setup_git_repo();
let twin = get_twin_binary();
Command::new(&twin)
.args(["create", "remove-test"])
.current_dir(repo.path())
.output()
.unwrap();
let output = Command::new(&twin)
.args(["remove", "remove-test", "--force"])
.current_dir(repo.path())
.output()
.expect("Failed to run twin remove");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Remove STDOUT: {stdout}");
println!("Remove STDERR: {stderr}");
assert!(
stdout.contains("削除") || output.status.success(),
"Remove should complete"
);
}
#[test]
fn test_config_with_symlinks() {
let repo = setup_git_repo();
let twin = get_twin_binary();
let config_content = r#"
[[files]]
path = ".env"
mapping_type = "symlink"
description = "Environment variables"
[[files]]
path = "config.json"
mapping_type = "copy"
description = "Configuration file"
"#;
std::fs::write(repo.path().join(".twin.toml"), config_content).unwrap();
std::fs::write(repo.path().join(".env"), "TEST=true").unwrap();
std::fs::write(repo.path().join("config.json"), r#"{"test": true}"#).unwrap();
let output = Command::new(&twin)
.args(["create", "config-test", "--config", ".twin.toml"])
.current_dir(repo.path())
.output()
.expect("Failed to create with config");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Config create STDOUT: {stdout}");
println!("Config create STDERR: {stderr}");
assert!(
output.status.success() || stdout.contains("環境"),
"Should create environment with config"
);
}
#[test]
fn test_partial_config_file() {
let repo = setup_git_repo();
let twin = get_twin_binary();
let config_content = r#"
[[files]]
path = "test.txt"
"#;
std::fs::write(repo.path().join("minimal.toml"), config_content).unwrap();
std::fs::write(repo.path().join("test.txt"), "test content").unwrap();
let output = Command::new(&twin)
.args(["create", "minimal-test", "--config", "minimal.toml"])
.current_dir(repo.path())
.output()
.expect("Failed to create with minimal config");
assert!(
output.status.success(),
"Should create environment with minimal config"
);
let hooks_content = r#"
[hooks]
post_create = [{command = "echo 'Created!'"}]
"#;
std::fs::write(repo.path().join("hooks.toml"), hooks_content).unwrap();
let output = Command::new(&twin)
.args(["create", "hooks-test", "--config", "hooks.toml"])
.current_dir(repo.path())
.output()
.expect("Failed to create with hooks config");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success() || stderr.contains("Created!"),
"Should create environment with hooks config"
);
}
#[test]
fn test_hook_execution() {
let repo = setup_git_repo();
let twin = get_twin_binary();
let config_content = r#"
[hooks]
pre_create = [
{command = "echo Pre-create hook running"}
]
post_create = [
{command = "echo Post-create hook completed"}
]
[[files]]
path = "dummy.txt"
"#;
std::fs::write(repo.path().join("hook-test.toml"), config_content).unwrap();
std::fs::write(repo.path().join("dummy.txt"), "dummy").unwrap();
let output = Command::new(&twin)
.args(["create", "hook-env", "--config", "hook-test.toml"])
.env("TWIN_VERBOSE", "1") .current_dir(repo.path())
.output()
.expect("Failed to create with hooks");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Hook test STDOUT: {stdout}");
println!("Hook test STDERR: {stderr}");
assert!(
output.status.success() || stdout.contains("hook") || stderr.contains("hook"),
"Hooks should be executed or environment created successfully"
);
}
#[test]
fn test_json_output_format() {
let repo = setup_git_repo();
let twin = get_twin_binary();
let output = Command::new(&twin)
.args(["list", "--format", "json"])
.current_dir(repo.path())
.output()
.expect("Failed to list with JSON format");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
println!("JSON output: {stdout}");
let trimmed = stdout.trim();
assert!(
trimmed.is_empty() || trimmed.starts_with('[') || trimmed.starts_with('{'),
"Should output JSON format or be empty"
);
}
#[test]
fn test_verbose_logging() {
let repo = setup_git_repo();
let twin = get_twin_binary();
let output = Command::new(&twin)
.args(["create", "verbose-test"])
.env("TWIN_VERBOSE", "1")
.current_dir(repo.path())
.output()
.expect("Failed to run with verbose");
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("🔧") || stderr.contains("実行中") {
println!("Verbose logging is working");
} else {
println!("Verbose logging may not be fully implemented");
}
}
#[test]
fn test_branch_naming() {
let repo = setup_git_repo();
let twin = get_twin_binary();
Command::new(&twin)
.args(["create", "branch-test"])
.current_dir(repo.path())
.output()
.unwrap();
let output = Command::new("git")
.args(["branch", "-a"])
.current_dir(repo.path())
.output()
.unwrap();
let branches = String::from_utf8_lossy(&output.stdout);
println!("Branches: {branches}");
assert!(
branches.contains("branch-test") || branches.contains("agent"),
"Branch should be created with appropriate name"
);
}
#[test]
fn test_custom_branch_name() {
let repo = setup_git_repo();
let twin = get_twin_binary();
let output = Command::new(&twin)
.args(["create", "-b", "feature/my-branch", "../custom"])
.current_dir(repo.path())
.output()
.expect("Failed to create with custom branch");
let stdout = String::from_utf8_lossy(&output.stdout);
println!("Custom branch output: {stdout}");
let branch_output = Command::new("git")
.args(["branch", "-a"])
.current_dir(repo.path())
.output()
.unwrap();
let branches = String::from_utf8_lossy(&branch_output.stdout);
assert!(
branches.contains("feature/my-branch"),
"Custom branch should be created"
);
}