use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
fn oo() -> Command {
Command::cargo_bin("oo").unwrap()
}
#[test]
fn test_echo_passthrough() {
oo().args(["echo", "hello"])
.assert()
.success()
.stdout("hello\n");
}
#[test]
fn test_multiword_echo() {
oo().args(["echo", "hello", "world"])
.assert()
.success()
.stdout("hello world\n");
}
#[test]
fn test_false_failure() {
oo().args(["false"])
.assert()
.failure()
.stdout(predicate::str::starts_with("\u{2717}")); }
#[test]
fn test_exit_code_preserved() {
oo().args(["sh", "-c", "exit 42"]).assert().code(42);
}
#[test]
fn test_version() {
oo().arg("version")
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn test_no_args_shows_help() {
oo().assert()
.success()
.stdout(predicate::str::contains("Usage"));
}
#[test]
fn test_large_output_gets_indicator() {
let dir = TempDir::new().unwrap();
Command::new("git")
.args(["init"])
.current_dir(dir.path())
.output()
.unwrap();
for i in 0..100 {
std::fs::write(dir.path().join("file.txt"), format!("content {}\n", i)).unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(dir.path())
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", &format!("commit {}", i)])
.env("GIT_AUTHOR_NAME", "Test")
.env("GIT_AUTHOR_EMAIL", "test@example.com")
.env("GIT_COMMITTER_NAME", "Test")
.env("GIT_COMMITTER_EMAIL", "test@example.com")
.current_dir(dir.path())
.output()
.unwrap();
}
oo().args(["git", "log"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::starts_with("\u{25CF}")); }
#[test]
fn test_stderr_included_in_failure() {
oo().args(["sh", "-c", "echo failure_msg >&2; exit 1"])
.assert()
.failure()
.stdout(predicate::str::contains("failure_msg"));
}
#[test]
fn test_forget_runs() {
oo().arg("forget")
.assert()
.success()
.stdout(predicate::str::contains("Cleared session data"));
}
#[test]
fn test_help_no_args_shows_usage() {
oo().arg("help")
.assert()
.success()
.stdout(predicate::str::contains("Usage:"));
}
#[test]
fn test_help_includes_help_cmd_in_usage() {
oo().assert()
.success()
.stdout(predicate::str::contains("oo help <cmd>"));
}
#[test]
fn test_help_empty_arg() {
Command::cargo_bin("oo")
.unwrap()
.args(&["help", ""])
.assert()
.failure();
}
#[test]
fn test_init_creates_hooks_json() {
let dir = TempDir::new().unwrap();
oo().arg("init").current_dir(dir.path()).assert().success();
let hooks_path = dir.path().join(".claude").join("hooks.json");
assert!(
hooks_path.exists(),
".claude/hooks.json must exist after oo init"
);
}
#[test]
fn test_init_hooks_json_is_valid_json() {
let dir = TempDir::new().unwrap();
oo().arg("init").current_dir(dir.path()).assert().success();
let content = std::fs::read_to_string(dir.path().join(".claude").join("hooks.json")).unwrap();
let parsed: serde_json::Value =
serde_json::from_str(&content).expect("hooks.json must be valid JSON");
assert!(parsed.get("hooks").is_some());
}
#[test]
fn test_init_prints_agents_snippet() {
let dir = TempDir::new().unwrap();
oo().arg("init")
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains(
"Prefix all shell commands with `oo`",
));
}
#[test]
fn test_init_second_run_does_not_overwrite() {
let dir = TempDir::new().unwrap();
oo().arg("init").current_dir(dir.path()).assert().success();
let hooks_path = dir.path().join(".claude").join("hooks.json");
std::fs::write(&hooks_path, r#"{"hooks":[],"sentinel":true}"#).unwrap();
oo().arg("init").current_dir(dir.path()).assert().success();
let after = std::fs::read_to_string(&hooks_path).unwrap();
assert!(
after.contains("\"sentinel\":true"),
"pre-existing hooks.json must not be overwritten on second oo init"
);
}
#[test]
fn test_init_second_run_succeeds_without_error() {
let dir = TempDir::new().unwrap();
oo().arg("init").current_dir(dir.path()).assert().success();
oo().arg("init").current_dir(dir.path()).assert().success();
}
#[test]
fn test_init_format_generic_exits_success() {
let dir = TempDir::new().unwrap();
oo().args(["init", "--format", "generic"])
.current_dir(dir.path())
.assert()
.success();
}
#[test]
fn test_init_format_generic_prints_agents_snippet() {
let dir = TempDir::new().unwrap();
oo().args(["init", "--format", "generic"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains(
"Prefix all shell commands with `oo`",
));
}
#[test]
fn test_init_format_generic_prints_setup_section() {
let dir = TempDir::new().unwrap();
oo().args(["init", "--format", "generic"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("## Setup"));
}
#[test]
fn test_init_format_generic_prints_shell_commands_instructions() {
let dir = TempDir::new().unwrap();
oo().args(["init", "--format", "generic"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("oo recall"))
.stdout(predicate::str::contains("oo help"))
.stdout(predicate::str::contains("oo learn"));
}
#[test]
fn test_init_format_generic_prints_alias_section() {
let dir = TempDir::new().unwrap();
oo().args(["init", "--format", "generic"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("alias o='oo'"));
}
#[test]
fn test_init_format_generic_does_not_create_hooks_json() {
let dir = TempDir::new().unwrap();
oo().args(["init", "--format", "generic"])
.current_dir(dir.path())
.assert()
.success();
let hooks_path = dir.path().join(".claude").join("hooks.json");
assert!(
!hooks_path.exists(),
"oo init --format generic must not create .claude/hooks.json"
);
}
#[test]
fn test_init_format_claude_creates_hooks_json() {
let dir = TempDir::new().unwrap();
oo().args(["init", "--format", "claude"])
.current_dir(dir.path())
.assert()
.success();
let hooks_path = dir.path().join(".claude").join("hooks.json");
assert!(
hooks_path.exists(),
".claude/hooks.json must exist after oo init --format claude"
);
}
#[test]
fn test_init_format_claude_prints_agents_snippet() {
let dir = TempDir::new().unwrap();
oo().args(["init", "--format", "claude"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains(
"Prefix all shell commands with `oo`",
));
}
#[test]
fn test_recall_no_args() {
oo().arg("recall")
.assert()
.failure()
.stderr(predicate::str::contains("recall requires a query"));
}
#[test]
fn test_recall_no_results() {
oo().args(["recall", "xyzzy_nonexistent_query_12345"])
.assert()
.success()
.stdout(predicate::str::contains("No results found"));
}
#[test]
fn test_learn_no_args() {
oo().arg("learn")
.assert()
.failure()
.stderr(predicate::str::contains("learn requires a command"));
}
#[test]
fn test_learn_no_api_key() {
oo().args(["learn", "echo", "hello"])
.env_remove("ANTHROPIC_API_KEY")
.env_remove("OPENAI_API_KEY")
.assert()
.success()
.stderr(predicate::str::contains("[learning pattern for"));
}
#[test]
fn test_run_command_not_found() {
oo().args(["nonexistent_binary_xyz_abc_123"])
.assert()
.failure();
}
#[test]
fn test_run_multiword_command() {
oo().args(["echo", "hello", "world"])
.assert()
.success()
.stdout(predicate::str::contains("hello world"));
}
#[test]
fn test_passthrough_git_version() {
oo().args(["git", "--version"])
.assert()
.success()
.stdout(predicate::str::starts_with("git version"));
}
#[test]
fn test_failure_stderr_output() {
oo().args(["ls", "/nonexistent_path_xyz_abc_12345"])
.assert()
.failure()
.stdout(predicate::str::starts_with("\u{2717}")); }
#[test]
fn test_dispatch_version() {
oo().arg("version").assert().success();
}
#[test]
fn test_dispatch_forget() {
oo().arg("forget")
.assert()
.success()
.stdout(predicate::str::contains("Cleared session data"));
}
#[test]
fn test_dispatch_run() {
oo().args(["echo", "hi"])
.assert()
.success()
.stdout(predicate::str::contains("hi"));
}
#[test]
fn test_dispatch_recall_query_joined() {
oo().args(["recall", "hello", "world"]).assert().success();
}
#[test]
fn test_dispatch_help() {
oo().arg("help")
.assert()
.success()
.stdout(predicate::str::contains("Usage"));
}
#[test]
fn test_dispatch_init() {
let dir = TempDir::new().unwrap();
oo().arg("init").current_dir(dir.path()).assert().success();
}
#[test]
fn test_patterns_no_learned_patterns() {
let dir = TempDir::new().unwrap();
oo().arg("patterns")
.env("HOME", dir.path())
.env("XDG_CONFIG_HOME", dir.path().join(".config"))
.assert()
.success()
.stdout(predicate::str::contains("no patterns yet"));
}
#[test]
fn test_patterns_with_learned_pattern() {
let dir = TempDir::new().unwrap();
#[cfg(target_os = "macos")]
let patterns_dir = dir
.path()
.join("Library")
.join("Application Support")
.join("oo")
.join("patterns");
#[cfg(not(target_os = "macos"))]
let patterns_dir = dir.path().join(".config").join("oo").join("patterns");
std::fs::create_dir_all(&patterns_dir).unwrap();
std::fs::write(
patterns_dir.join("pytest.toml"),
"command_match = \"^pytest\"\n[success]\npattern = '(?P<n>\\d+) passed'\nsummary = \"{n} passed\"\n",
)
.unwrap();
oo().arg("patterns")
.env("HOME", dir.path())
.env("XDG_CONFIG_HOME", dir.path().join(".config"))
.assert()
.success()
.stdout(predicate::str::contains("^pytest"));
}
#[test]
fn test_learn_provider_logged_to_stderr() {
oo().args(["learn", "echo", "hello"])
.env_remove("ANTHROPIC_API_KEY")
.env_remove("OPENAI_API_KEY")
.env_remove("CEREBRAS_API_KEY")
.assert()
.success()
.stderr(predicate::str::contains("anthropic"));
}
#[test]
fn test_version_shows_oo_prefix() {
oo().arg("version")
.assert()
.success()
.stdout(predicate::str::starts_with("oo "));
}
#[test]
fn test_version_shows_version_number() {
let version = env!("CARGO_PKG_VERSION");
oo().arg("version")
.assert()
.success()
.stdout(predicate::str::contains(version));
}
#[test]
#[ignore = "requires network access to cheat.sh"]
fn test_help_with_valid_command() {
oo().args(["help", "ls"])
.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn test_cargo_toml_has_shell_installer_enabled() {
let cargo_toml = include_str!("../Cargo.toml");
assert!(
cargo_toml.contains(r#"installers = ["shell"]"#),
"Cargo.toml must have shell installer enabled: installers = [\"shell\"]"
);
}
#[test]
fn test_cargo_toml_has_install_path_cargo_home() {
let cargo_toml = include_str!("../Cargo.toml");
assert!(
cargo_toml.contains(r#"install-path = "CARGO_HOME""#),
"Cargo.toml must have install-path = \"CARGO_HOME\""
);
}
#[test]
fn test_cargo_toml_has_cargo_dist_version() {
let cargo_toml = include_str!("../Cargo.toml");
assert!(
cargo_toml.contains("cargo-dist-version ="),
"Cargo.toml must specify cargo-dist-version for dist config"
);
}
#[test]
fn test_project_patterns_listed_when_present() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(".git")).unwrap();
let pat_dir = dir.path().join(".oo").join("patterns");
std::fs::create_dir_all(&pat_dir).unwrap();
std::fs::write(
pat_dir.join("mytest.toml"),
"command_match = \"^mytest\"\n[success]\npattern = '(?P<n>\\d+) ok'\nsummary = \"{n} ok\"\n",
)
.unwrap();
oo().arg("patterns")
.current_dir(dir.path())
.env("HOME", dir.path())
.env("XDG_CONFIG_HOME", dir.path().join(".config"))
.assert()
.success()
.stdout(predicate::str::contains("Project"))
.stdout(predicate::str::contains("^mytest"));
}
#[test]
fn test_project_patterns_override_builtins() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(".git")).unwrap();
let pat_dir = dir.path().join(".oo").join("patterns");
std::fs::create_dir_all(&pat_dir).unwrap();
std::fs::write(
pat_dir.join("echo.toml"),
"command_match = \"^echo\\\\b\"\n[success]\npattern = '(?s)(?P<all>.+)'\nsummary = \"proj-match\"\n",
)
.unwrap();
let big_arg = "x".repeat(5000);
oo().args(["echo", &big_arg])
.current_dir(dir.path())
.env("HOME", dir.path())
.env("XDG_CONFIG_HOME", dir.path().join(".config"))
.assert()
.success()
.stdout(predicate::str::contains("proj-match"));
}