Files
owlen/crates/platform/plugins/tests/plugin_hooks_integration.rs
vikingowl 10c8e2baae feat(v2): complete multi-LLM providers, TUI redesign, and advanced agent features
Multi-LLM Provider Support:
- Add llm-core crate with LlmProvider trait abstraction
- Implement Anthropic Claude API client with streaming
- Implement OpenAI API client with streaming
- Add token counting with SimpleTokenCounter and ClaudeTokenCounter
- Add retry logic with exponential backoff and jitter

Borderless TUI Redesign:
- Rewrite theme system with terminal capability detection (Full/Unicode256/Basic)
- Add provider tabs component with keybind switching [1]/[2]/[3]
- Implement vim-modal input (Normal/Insert/Visual/Command modes)
- Redesign chat panel with timestamps and streaming indicators
- Add multi-provider status bar with cost tracking
- Add Nerd Font icons with graceful ASCII fallbacks
- Add syntax highlighting (syntect) and markdown rendering (pulldown-cmark)

Advanced Agent Features:
- Add system prompt builder with configurable components
- Enhance subagent orchestration with parallel execution
- Add git integration module for safe command detection
- Add streaming tool results via channels
- Expand tool set: AskUserQuestion, TodoWrite, LS, MultiEdit, BashOutput, KillShell
- Add WebSearch with provider abstraction

Plugin System Enhancement:
- Add full agent definition parsing from YAML frontmatter
- Add skill system with progressive disclosure
- Wire plugin hooks into HookManager

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 17:24:14 +01:00

176 lines
5.5 KiB
Rust

// End-to-end integration test for plugin hooks
use color_eyre::eyre::Result;
use plugins::PluginManager;
use std::fs;
use tempfile::TempDir;
fn create_test_plugin_with_hooks(plugin_dir: &std::path::Path) -> Result<()> {
fs::create_dir_all(plugin_dir)?;
// Create plugin manifest
let manifest = serde_json::json!({
"name": "test-hook-plugin",
"version": "1.0.0",
"description": "Test plugin with hooks",
"commands": [],
"agents": [],
"skills": [],
"hooks": {},
"mcp_servers": []
});
fs::write(
plugin_dir.join("plugin.json"),
serde_json::to_string_pretty(&manifest)?,
)?;
// Create hooks directory and hooks.json
let hooks_dir = plugin_dir.join("hooks");
fs::create_dir_all(&hooks_dir)?;
let hooks_config = serde_json::json!({
"description": "Validate edit and write operations",
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validate.py",
"timeout": 5000
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo 'Bash hook' && exit 0"
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "echo 'Post-tool hook' && exit 0"
}
]
}
]
}
});
fs::write(
hooks_dir.join("hooks.json"),
serde_json::to_string_pretty(&hooks_config)?,
)?;
Ok(())
}
#[test]
fn test_load_plugin_hooks_config() -> Result<()> {
let temp_dir = TempDir::new()?;
let plugin_dir = temp_dir.path().join("test-plugin");
create_test_plugin_with_hooks(&plugin_dir)?;
// Load all plugins
let mut plugin_manager = PluginManager::with_dirs(vec![temp_dir.path().to_path_buf()]);
plugin_manager.load_all()?;
assert_eq!(plugin_manager.plugins().len(), 1);
let plugin = &plugin_manager.plugins()[0];
// Load hooks config
let hooks_config = plugin.load_hooks_config()?;
assert!(hooks_config.is_some());
let config = hooks_config.unwrap();
assert_eq!(config.description, Some("Validate edit and write operations".to_string()));
assert!(config.hooks.contains_key("PreToolUse"));
assert!(config.hooks.contains_key("PostToolUse"));
// Check PreToolUse hooks
let pre_tool_hooks = &config.hooks["PreToolUse"];
assert_eq!(pre_tool_hooks.len(), 2);
// First matcher: Edit|Write
assert_eq!(pre_tool_hooks[0].matcher, Some("Edit|Write".to_string()));
assert_eq!(pre_tool_hooks[0].hooks.len(), 1);
assert_eq!(pre_tool_hooks[0].hooks[0].hook_type, "command");
assert!(pre_tool_hooks[0].hooks[0].command.as_ref().unwrap().contains("validate.py"));
// Second matcher: Bash
assert_eq!(pre_tool_hooks[1].matcher, Some("Bash".to_string()));
assert_eq!(pre_tool_hooks[1].hooks.len(), 1);
Ok(())
}
#[test]
fn test_plugin_hooks_substitution() -> Result<()> {
let temp_dir = TempDir::new()?;
let plugin_dir = temp_dir.path().join("test-plugin");
create_test_plugin_with_hooks(&plugin_dir)?;
// Load all plugins
let mut plugin_manager = PluginManager::with_dirs(vec![temp_dir.path().to_path_buf()]);
plugin_manager.load_all()?;
assert_eq!(plugin_manager.plugins().len(), 1);
let plugin = &plugin_manager.plugins()[0];
// Load hooks config and register
let hooks_config = plugin.load_hooks_config()?.unwrap();
let hooks_to_register = plugin.register_hooks_with_manager(&hooks_config);
// Check that ${CLAUDE_PLUGIN_ROOT} was substituted
let edit_write_hook = hooks_to_register.iter()
.find(|(event, _, pattern, _)| {
event == "PreToolUse" && pattern.as_ref().map(|p| p.contains("Edit")).unwrap_or(false)
})
.unwrap();
// The command should have the plugin path substituted
assert!(edit_write_hook.1.contains(&plugin_dir.to_string_lossy().to_string()));
assert!(edit_write_hook.1.contains("validate.py"));
assert!(!edit_write_hook.1.contains("${CLAUDE_PLUGIN_ROOT}"));
Ok(())
}
#[test]
fn test_multiple_plugins_with_hooks() -> Result<()> {
let temp_dir = TempDir::new()?;
// Create two plugins with hooks
let plugin1_dir = temp_dir.path().join("plugin1");
create_test_plugin_with_hooks(&plugin1_dir)?;
let plugin2_dir = temp_dir.path().join("plugin2");
create_test_plugin_with_hooks(&plugin2_dir)?;
// Load all plugins
let mut plugin_manager = PluginManager::with_dirs(vec![temp_dir.path().to_path_buf()]);
plugin_manager.load_all()?;
assert_eq!(plugin_manager.plugins().len(), 2);
// Collect all hooks from all plugins
let mut total_hooks = 0;
for plugin in plugin_manager.plugins() {
if let Ok(Some(hooks_config)) = plugin.load_hooks_config() {
let hooks = plugin.register_hooks_with_manager(&hooks_config);
total_hooks += hooks.len();
}
}
// Each plugin has 3 hooks (2 PreToolUse + 1 PostToolUse)
assert_eq!(total_hooks, 6);
Ok(())
}