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>
This commit is contained in:
175
crates/platform/plugins/tests/plugin_hooks_integration.rs
Normal file
175
crates/platform/plugins/tests/plugin_hooks_integration.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
// 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user