#![allow(clippy::unwrap_used, clippy::expect_used, clippy::missing_const_for_fn)]
use std::process::Command;
fn tokf() -> Command {
Command::new(env!("CARGO_BIN_EXE_tokf"))
}
#[test]
fn hook_install_creates_files() {
let dir = tempfile::TempDir::new().unwrap();
let output = tokf()
.args(["hook", "install"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"hook install failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let hook_script = dir.path().join(".tokf/hooks/pre-tool-use.sh");
assert!(hook_script.exists(), "hook script should exist");
let content = std::fs::read_to_string(&hook_script).unwrap();
assert!(content.starts_with("#!/bin/sh\n"));
assert!(content.contains("hook handle"));
let settings = dir.path().join(".claude/settings.json");
assert!(settings.exists(), "settings.json should exist");
let settings_content = std::fs::read_to_string(&settings).unwrap();
let value: serde_json::Value = serde_json::from_str(&settings_content).unwrap();
assert!(value["hooks"]["PreToolUse"].is_array());
let arr = value["hooks"]["PreToolUse"].as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["matcher"], "Bash");
}
#[test]
fn hook_install_idempotent() {
let dir = tempfile::TempDir::new().unwrap();
for _ in 0..2 {
let output = tokf()
.args(["hook", "install"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(output.status.success());
}
let settings = dir.path().join(".claude/settings.json");
let content = std::fs::read_to_string(&settings).unwrap();
let value: serde_json::Value = serde_json::from_str(&content).unwrap();
let arr = value["hooks"]["PreToolUse"].as_array().unwrap();
assert_eq!(
arr.len(),
1,
"should have exactly one entry after double install"
);
}
#[test]
fn hook_install_preserves_existing_settings() {
let dir = tempfile::TempDir::new().unwrap();
let claude_dir = dir.path().join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
std::fs::write(
claude_dir.join("settings.json"),
r#"{"permissions": {"allow": ["Read"]}, "hooks": {"PostToolUse": []}}"#,
)
.unwrap();
let output = tokf()
.args(["hook", "install"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(output.status.success());
let content = std::fs::read_to_string(claude_dir.join("settings.json")).unwrap();
let value: serde_json::Value = serde_json::from_str(&content).unwrap();
assert!(value["permissions"]["allow"].is_array());
assert!(value["hooks"]["PostToolUse"].is_array());
assert!(value["hooks"]["PreToolUse"].is_array());
}
#[test]
fn hook_install_shows_info_on_stderr() {
let dir = tempfile::TempDir::new().unwrap();
let output = tokf()
.args(["hook", "install"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("hook installed"),
"expected install confirmation, got: {stderr}"
);
assert!(
stderr.contains("script:"),
"expected script path, got: {stderr}"
);
assert!(
stderr.contains("settings:"),
"expected settings path, got: {stderr}"
);
}
#[test]
fn hook_install_opencode_creates_plugin_file() {
let dir = tempfile::TempDir::new().unwrap();
let output = tokf()
.args(["hook", "install", "--tool", "opencode"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"hook install --tool opencode failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let plugin_file = dir.path().join(".opencode/plugins/tokf.ts");
assert!(
plugin_file.exists(),
".opencode/plugins/tokf.ts should exist after install"
);
}
#[test]
fn hook_install_opencode_is_idempotent() {
let dir = tempfile::TempDir::new().unwrap();
for _ in 0..2 {
let output = tokf()
.args(["hook", "install", "--tool", "opencode"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"hook install --tool opencode failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let plugin_dir = dir.path().join(".opencode/plugins");
let entries: Vec<_> = std::fs::read_dir(&plugin_dir).unwrap().collect();
assert_eq!(
entries.len(),
1,
"should have exactly one plugin file after double install"
);
}
#[test]
fn hook_install_opencode_embeds_tokf_path() {
let dir = tempfile::TempDir::new().unwrap();
let output = tokf()
.args(["hook", "install", "--tool", "opencode"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"hook install --tool opencode failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let plugin_file = dir.path().join(".opencode/plugins/tokf.ts");
let content = std::fs::read_to_string(&plugin_file).unwrap();
assert!(
!content.contains("{{TOKF_BIN}}"),
"plugin file must not contain raw placeholder, got: {content}"
);
}
#[test]
fn hook_install_custom_path_embeds_in_shim() {
let dir = tempfile::TempDir::new().unwrap();
let output = tokf()
.args(["hook", "install", "--path", "/custom/bin/tokf"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"hook install --path failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let hook_script = dir.path().join(".tokf/hooks/pre-tool-use.sh");
let content = std::fs::read_to_string(&hook_script).unwrap();
assert!(
content.contains("'/custom/bin/tokf'"),
"expected custom path in hook shim, got: {content}"
);
}
#[test]
fn hook_install_custom_path_opencode() {
let dir = tempfile::TempDir::new().unwrap();
let output = tokf()
.args([
"hook",
"install",
"--tool",
"opencode",
"--path",
"/custom/bin/tokf",
])
.current_dir(dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"hook install --tool opencode --path failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let plugin_file = dir.path().join(".opencode/plugins/tokf.ts");
let content = std::fs::read_to_string(&plugin_file).unwrap();
assert!(
content.contains(r#"const TOKF_BIN = "/custom/bin/tokf";"#),
"expected custom path in plugin, got: {content}"
);
}
#[test]
fn hook_install_codex_creates_skill_file() {
let dir = tempfile::TempDir::new().unwrap();
let output = tokf()
.args(["hook", "install", "--tool", "codex"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"hook install --tool codex failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let skill_file = dir.path().join(".agents/skills/tokf-run/SKILL.md");
assert!(
skill_file.exists(),
".agents/skills/tokf-run/SKILL.md should exist after install"
);
}
#[test]
fn hook_install_codex_is_idempotent() {
let dir = tempfile::TempDir::new().unwrap();
for _ in 0..2 {
let output = tokf()
.args(["hook", "install", "--tool", "codex"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(
output.status.success(),
"hook install --tool codex failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let skill_dir = dir.path().join(".agents/skills/tokf-run");
let entries: Vec<_> = std::fs::read_dir(&skill_dir).unwrap().collect();
assert_eq!(
entries.len(),
1,
"should have exactly one skill file after double install"
);
}
#[test]
fn hook_install_codex_has_frontmatter() {
let dir = tempfile::TempDir::new().unwrap();
let output = tokf()
.args(["hook", "install", "--tool", "codex"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(output.status.success());
let skill_file = dir.path().join(".agents/skills/tokf-run/SKILL.md");
let content = std::fs::read_to_string(&skill_file).unwrap();
assert!(
content.starts_with("---\n"),
"SKILL.md should start with YAML frontmatter"
);
assert!(
content.contains("name: tokf-run"),
"SKILL.md should contain name: tokf-run"
);
}
#[test]
fn hook_install_codex_shows_info_on_stderr() {
let dir = tempfile::TempDir::new().unwrap();
let output = tokf()
.args(["hook", "install", "--tool", "codex"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Codex skill installed"),
"expected install confirmation, got: {stderr}"
);
assert!(
stderr.contains("SKILL.md"),
"expected skill file path in output, got: {stderr}"
);
}