From 27bcc7409e6e92feeeac58cabec4360cf5d17472 Mon Sep 17 00:00:00 2001 From: retoor Date: Wed, 5 Nov 2025 15:34:23 +0100 Subject: [PATCH] maintenance: update config paths and agent communication --- Makefile | 13 +- pr/agents/agent_communication.py | 6 + pr/agents/agent_manager.py | 5 +- pr/autonomous/mode.py | 17 +- pr/cache/api_cache.py | 36 +- pr/cache/tool_cache.py | 7 + pr/commands/handlers.py | 92 ++-- pr/config.py | 16 +- pr/core/assistant.py | 30 +- pr/core/config_loader.py | 16 +- pr/core/context.py | 19 +- pr/core/enhanced_assistant.py | 4 +- pr/multiplexer.py | 148 +++--- pr/tools/__init__.py | 64 +-- pr/tools/agents.py | 40 +- pr/tools/command.py | 4 +- pr/tools/interactive_control.py | 34 +- pr/tools/python_exec.py | 13 +- pr/ui/display.py | 49 ++ pyproject.toml | 44 +- tests/conftest.py | 77 ++++ tests/test_advanced_context.py | 162 ++++--- tests/test_agents.py | 5 +- tests/test_commands.py | 697 +++++++++++++++++++++++++++++ tests/test_commands.py.bak | 693 ++++++++++++++++++++++++++++ tests/test_enhanced_assistant.py | 4 +- tests/test_exceptions.py | 61 +++ tests/test_help_docs.py | 46 ++ tests/test_logging.py | 86 +++- tests/test_multiplexer_commands.py | 228 ++++++++++ tests/test_tools.py | 69 ++- tests/test_ui_output.py | 153 +++++++ 32 files changed, 2598 insertions(+), 340 deletions(-) create mode 100644 tests/test_commands.py create mode 100644 tests/test_commands.py.bak create mode 100644 tests/test_exceptions.py create mode 100644 tests/test_help_docs.py create mode 100644 tests/test_multiplexer_commands.py create mode 100644 tests/test_ui_output.py diff --git a/Makefile b/Makefile index a0e3cba..9f72562 100644 --- a/Makefile +++ b/Makefile @@ -67,10 +67,13 @@ backup: zip -r rp.zip * mv rp.zip ../ -implode: - python ../implode/imply.py rp.py - mv imploded.py /home/retoor/bin/rp - chmod +x /home/retoor/bin/rp - rp --debug +serve: + rpserver + + +implode: build + rpi rp.py -o rp + chmod +x rp + if [ -d /home/retoor/bin ]; then cp rp /home/retoor/bin/rp; fi .DEFAULT_GOAL := help diff --git a/pr/agents/agent_communication.py b/pr/agents/agent_communication.py index 0875bd0..daa18d7 100644 --- a/pr/agents/agent_communication.py +++ b/pr/agents/agent_communication.py @@ -68,6 +68,12 @@ class AgentCommunicationBus: ) """ ) + + cursor.execute("PRAGMA table_info(agent_messages)") + columns = [row[1] for row in cursor.fetchall()] + if "read" not in columns: + cursor.execute("ALTER TABLE agent_messages ADD COLUMN read INTEGER DEFAULT 0") + self.conn.commit() def send_message(self, message: AgentMessage, session_id: Optional[str] = None): diff --git a/pr/agents/agent_manager.py b/pr/agents/agent_manager.py index 12ec1df..441ece3 100644 --- a/pr/agents/agent_manager.py +++ b/pr/agents/agent_manager.py @@ -1,4 +1,3 @@ -import json import time import uuid from dataclasses import dataclass, field @@ -169,7 +168,7 @@ Break down the task and delegate subtasks to appropriate agents. Coordinate thei return results - def get_session_summary(self) -> str: + def get_session_summary(self) -> Dict[str, Any]: summary = { "session_id": self.session_id, "active_agents": len(self.active_agents), @@ -183,7 +182,7 @@ Break down the task and delegate subtasks to appropriate agents. Coordinate thei for agent_id, agent in self.active_agents.items() ], } - return json.dumps(summary) + return summary def clear_session(self): self.active_agents.clear() diff --git a/pr/autonomous/mode.py b/pr/autonomous/mode.py index f05571b..a02910a 100644 --- a/pr/autonomous/mode.py +++ b/pr/autonomous/mode.py @@ -16,6 +16,10 @@ def run_autonomous_mode(assistant, task): logger.debug(f"=== AUTONOMOUS MODE START ===") logger.debug(f"Task: {task}") + from pr.core.knowledge_context import inject_knowledge_context + + inject_knowledge_context(assistant, task) + assistant.messages.append({"role": "user", "content": f"{task}"}) try: @@ -94,9 +98,14 @@ def process_response_autonomous(assistant, response): arguments = json.loads(tool_call["function"]["arguments"]) result = execute_single_tool(assistant, func_name, arguments) - result = truncate_tool_result(result) + if isinstance(result, str): + try: + result = json.loads(result) + except json.JSONDecodeError as ex: + result = {"error": str(ex)} status = "success" if result.get("status") == "success" else "error" + result = truncate_tool_result(result) display_tool_call(func_name, arguments, status, result) tool_results.append( @@ -141,9 +150,9 @@ def execute_single_tool(assistant, func_name, arguments): db_get, db_query, db_set, - editor_insert_text, - editor_replace_text, - editor_search, + # editor_insert_text, + # editor_replace_text, + # editor_search, getpwd, http_fetch, index_source_directory, diff --git a/pr/cache/api_cache.py b/pr/cache/api_cache.py index 6dc6548..37cca9d 100644 --- a/pr/cache/api_cache.py +++ b/pr/cache/api_cache.py @@ -22,7 +22,8 @@ class APICache: created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, model TEXT, - token_count INTEGER + token_count INTEGER, + hit_count INTEGER DEFAULT 0 ) """ ) @@ -31,7 +32,14 @@ class APICache: CREATE INDEX IF NOT EXISTS idx_expires_at ON api_cache(expires_at) """ ) - conn.commit() + + # Check if hit_count column exists, add if not + cursor.execute("PRAGMA table_info(api_cache)") + columns = [row[1] for row in cursor.fetchall()] + if "hit_count" not in columns: + cursor.execute("ALTER TABLE api_cache ADD COLUMN hit_count INTEGER DEFAULT 0") + conn.commit() + conn.close() def _generate_cache_key( @@ -64,10 +72,21 @@ class APICache: ) row = cursor.fetchone() - conn.close() if row: + # Increment hit count + cursor.execute( + """ + UPDATE api_cache SET hit_count = hit_count + 1 + WHERE cache_key = ? + """, + (cache_key,), + ) + conn.commit() + conn.close() return json.loads(row[0]) + + conn.close() return None def set( @@ -90,8 +109,8 @@ class APICache: cursor.execute( """ INSERT OR REPLACE INTO api_cache - (cache_key, response_data, created_at, expires_at, model, token_count) - VALUES (?, ?, ?, ?, ?, ?) + (cache_key, response_data, created_at, expires_at, model, token_count, hit_count) + VALUES (?, ?, ?, ?, ?, ?, 0) """, ( cache_key, @@ -149,6 +168,12 @@ class APICache: ) total_tokens = cursor.fetchone()[0] or 0 + cursor.execute( + "SELECT SUM(hit_count) FROM api_cache WHERE expires_at > ?", + (current_time,), + ) + total_hits = cursor.fetchone()[0] or 0 + conn.close() return { @@ -156,4 +181,5 @@ class APICache: "valid_entries": valid_entries, "expired_entries": total_entries - valid_entries, "total_cached_tokens": total_tokens, + "total_cache_hits": total_hits, } diff --git a/pr/cache/tool_cache.py b/pr/cache/tool_cache.py index 383982d..b22b048 100644 --- a/pr/cache/tool_cache.py +++ b/pr/cache/tool_cache.py @@ -13,6 +13,13 @@ class ToolCache: "db_get", "db_query", "index_directory", + "http_fetch", + "web_search", + "web_search_news", + "search_knowledge", + "get_knowledge_entry", + "get_knowledge_by_category", + "get_knowledge_statistics", } def __init__(self, db_path: str, ttl_seconds: int = 300): diff --git a/pr/commands/handlers.py b/pr/commands/handlers.py index 4968684..0fd9d55 100644 --- a/pr/commands/handlers.py +++ b/pr/commands/handlers.py @@ -6,13 +6,24 @@ from pr.core.api import list_models from pr.tools import read_file from pr.tools.base import get_tools_definition from pr.ui import Colors +from pr.editor import RPEditor def handle_command(assistant, command): command_parts = command.strip().split(maxsplit=1) cmd = command_parts[0].lower() - if cmd == "/auto": + if cmd == "/edit": + rp_editor = RPEditor(command_parts[1] if len(command_parts) > 1 else None) + rp_editor.start() + rp_editor.thread.join() + task = str(rp_editor.get_text()) + rp_editor.stop() + rp_editor = None + if task: + run_autonomous_mode(assistant, task) + + elif cmd == "/auto": if len(command_parts) < 2: print(f"{Colors.RED}Usage: /auto [task description]{Colors.RESET}") print( @@ -27,41 +38,36 @@ def handle_command(assistant, command): if cmd in ["exit", "quit", "q"]: return False - elif cmd == "help": - print( - f""" -{Colors.BOLD}Available Commands:{Colors.RESET} - -{Colors.BOLD}Basic:{Colors.RESET} - exit, quit, q - Exit the assistant - /help - Show this help message - /reset - Clear message history - /dump - Show message history as JSON - /verbose - Toggle verbose mode - /models - List available models - /tools - List available tools - -{Colors.BOLD}File Operations:{Colors.RESET} - /review - Review a file - /refactor - Refactor code in a file - /obfuscate - Obfuscate code in a file - -{Colors.BOLD}Advanced Features:{Colors.RESET} - {Colors.CYAN}/auto {Colors.RESET} - Enter autonomous mode - {Colors.CYAN}/workflow {Colors.RESET} - Execute a workflow - {Colors.CYAN}/workflows{Colors.RESET} - List all workflows - {Colors.CYAN}/agent {Colors.RESET} - Create specialized agent and assign task - {Colors.CYAN}/agents{Colors.RESET} - Show active agents - {Colors.CYAN}/collaborate {Colors.RESET} - Use multiple agents to collaborate - {Colors.CYAN}/knowledge {Colors.RESET} - Search knowledge base - {Colors.CYAN}/remember {Colors.RESET} - Store information in knowledge base - {Colors.CYAN}/history{Colors.RESET} - Show conversation history - {Colors.CYAN}/cache{Colors.RESET} - Show cache statistics - {Colors.CYAN}/cache clear{Colors.RESET} - Clear all caches - {Colors.CYAN}/stats{Colors.RESET} - Show system statistics - """ + elif cmd == "/help" or cmd == "help": + from pr.commands.help_docs import ( + get_agent_help, + get_background_help, + get_cache_help, + get_full_help, + get_knowledge_help, + get_workflow_help, ) + if len(command_parts) > 1: + topic = command_parts[1].lower() + if topic == "workflows": + print(get_workflow_help()) + elif topic == "agents": + print(get_agent_help()) + elif topic == "knowledge": + print(get_knowledge_help()) + elif topic == "cache": + print(get_cache_help()) + elif topic == "background": + print(get_background_help()) + else: + print(f"{Colors.RED}Unknown help topic: {topic}{Colors.RESET}") + print( + f"{Colors.GRAY}Available topics: workflows, agents, knowledge, cache, background{Colors.RESET}" + ) + else: + print(get_full_help()) + elif cmd == "/reset": assistant.messages = assistant.messages[:1] print(f"{Colors.GREEN}Message history cleared{Colors.RESET}") @@ -75,7 +81,7 @@ def handle_command(assistant, command): f"Verbose mode: {Colors.GREEN if assistant.verbose else Colors.RED}{'ON' if assistant.verbose else 'OFF'}{Colors.RESET}" ) - elif cmd.startswith("/model"): + elif cmd == "/model": if len(command_parts) < 2: print("Current model: " + Colors.GREEN + assistant.model + Colors.RESET) else: @@ -116,17 +122,24 @@ def handle_command(assistant, command): workflow_name = command_parts[1] execute_workflow_command(assistant, workflow_name) - elif cmd == "/agent" and len(command_parts) > 1: + elif cmd == "/agent": + if len(command_parts) < 2: + print(f"{Colors.RED}Usage: /agent {Colors.RESET}") + print( + f"{Colors.GRAY}Available roles: coding, research, data_analysis, planning, testing, documentation{Colors.RESET}" + ) + return True + args = command_parts[1].split(maxsplit=1) if len(args) < 2: print(f"{Colors.RED}Usage: /agent {Colors.RESET}") print( f"{Colors.GRAY}Available roles: coding, research, data_analysis, planning, testing, documentation{Colors.RESET}" ) - else: - role, task = args[0], args[1] - execute_agent_task(assistant, role, task) + return True + role, task = args[0], args[1] + execute_agent_task(assistant, role, task) elif cmd == "/agents": show_agents(assistant) @@ -374,6 +387,7 @@ def show_cache_stats(assistant): print(f" Valid entries: {api_stats['valid_entries']}") print(f" Expired entries: {api_stats['expired_entries']}") print(f" Cached tokens: {api_stats['total_cached_tokens']}") + print(f" Total cache hits: {api_stats['total_cache_hits']}") if "tool_cache" in stats: tool_stats = stats["tool_cache"] diff --git a/pr/config.py b/pr/config.py index 3e950b9..5006ec0 100644 --- a/pr/config.py +++ b/pr/config.py @@ -1,14 +1,18 @@ import os DEFAULT_MODEL = "x-ai/grok-code-fast-1" -DEFAULT_API_URL = "https://static.molodetz.nl/rp.cgi/api/v1/chat/completions" -MODEL_LIST_URL = "https://static.molodetz.nl/rp.cgi/api/v1/models" +DEFAULT_API_URL = "http://localhost:8118/ai/chat" +MODEL_LIST_URL = "http://localhost:8118/ai/models" -DB_PATH = os.path.expanduser("~/.assistant_db.sqlite") -LOG_FILE = os.path.expanduser("~/.assistant_error.log") +config_directory = os.path.expanduser("~/.local/share/rp") +os.makedirs(config_directory, exist_ok=True) + +DB_PATH = os.path.join(config_directory, "assistant_db.sqlite") +LOG_FILE = os.path.join(config_directory, "assistant_error.log") CONTEXT_FILE = ".rcontext.txt" -GLOBAL_CONTEXT_FILE = os.path.expanduser("~/.rcontext.txt") -HISTORY_FILE = os.path.expanduser("~/.assistant_history") +GLOBAL_CONTEXT_FILE = os.path.join(config_directory, "rcontext.txt") +KNOWLEDGE_PATH = os.path.join(config_directory, "knowledge") +HISTORY_FILE = os.path.join(config_directory, "assistant_history") DEFAULT_TEMPERATURE = 0.1 DEFAULT_MAX_TOKENS = 4096 diff --git a/pr/core/assistant.py b/pr/core/assistant.py index b962c7d..10e7b1d 100644 --- a/pr/core/assistant.py +++ b/pr/core/assistant.py @@ -32,21 +32,16 @@ from pr.core.context import init_system_message, truncate_tool_result from pr.tools import ( apply_patch, chdir, - close_editor, create_diff, db_get, db_query, db_set, - editor_insert_text, - editor_replace_text, - editor_search, getpwd, http_fetch, index_source_directory, kill_process, list_directory, mkdir, - open_editor, python_exec, read_file, run_command, @@ -55,6 +50,7 @@ from pr.tools import ( web_search, web_search_news, write_file, + post_image, ) from pr.tools.base import get_tools_definition from pr.tools.filesystem import ( @@ -249,6 +245,7 @@ class Assistant: logger.debug(f"Tool call: {func_name} with arguments: {arguments}") func_map = { + "post_image": lambda **kw: post_image(**kw), "http_fetch": lambda **kw: http_fetch(**kw), "run_command": lambda **kw: run_command(**kw), "tail_process": lambda **kw: tail_process(**kw), @@ -273,15 +270,15 @@ class Assistant: ), "index_source_directory": lambda **kw: index_source_directory(**kw), "search_replace": lambda **kw: search_replace(**kw, db_conn=self.db_conn), - "open_editor": lambda **kw: open_editor(**kw), - "editor_insert_text": lambda **kw: editor_insert_text( - **kw, db_conn=self.db_conn - ), - "editor_replace_text": lambda **kw: editor_replace_text( - **kw, db_conn=self.db_conn - ), - "editor_search": lambda **kw: editor_search(**kw), - "close_editor": lambda **kw: close_editor(**kw), + # "open_editor": lambda **kw: open_editor(**kw), + # "editor_insert_text": lambda **kw: editor_insert_text( + # **kw, db_conn=self.db_conn + # ), + # "editor_replace_text": lambda **kw: editor_replace_text( + # **kw, db_conn=self.db_conn + # ), + # "editor_search": lambda **kw: editor_search(**kw), + # "close_editor": lambda **kw: close_editor(**kw), "create_diff": lambda **kw: create_diff(**kw), "apply_patch": lambda **kw: apply_patch(**kw, db_conn=self.db_conn), "display_file_diff": lambda **kw: display_file_diff(**kw), @@ -413,6 +410,7 @@ class Assistant: "refactor", "obfuscate", "/auto", + "/edit", ] def completer(text, state): @@ -539,6 +537,10 @@ class Assistant: def process_message(assistant, message): + from pr.core.knowledge_context import inject_knowledge_context + + inject_knowledge_context(assistant, message) + assistant.messages.append({"role": "user", "content": message}) logger.debug(f"Processing user message: {message[:100]}...") diff --git a/pr/core/config_loader.py b/pr/core/config_loader.py index 790ba56..b47780c 100644 --- a/pr/core/config_loader.py +++ b/pr/core/config_loader.py @@ -1,17 +1,26 @@ import configparser import os from typing import Any, Dict - +import uuid from pr.core.logging import get_logger logger = get_logger("config") -CONFIG_FILE = os.path.expanduser("~/.prrc") +CONFIG_DIRECTORY = os.path.expanduser("~/.local/share/rp/") +CONFIG_FILE = os.path.join(CONFIG_DIRECTORY, ".prrc") LOCAL_CONFIG_FILE = ".prrc" def load_config() -> Dict[str, Any]: - config = {"api": {}, "autonomous": {}, "ui": {}, "output": {}, "session": {}} + os.makedirs(CONFIG_DIRECTORY, exist_ok=True) + config = { + "api": {}, + "autonomous": {}, + "ui": {}, + "output": {}, + "session": {}, + "api_key": "rp-" + str(uuid.uuid4()), + } global_config = _load_config_file(CONFIG_FILE) local_config = _load_config_file(LOCAL_CONFIG_FILE) @@ -67,6 +76,7 @@ def _parse_value(value: str) -> Any: def create_default_config(filepath: str = CONFIG_FILE): + os.makedirs(CONFIG_DIRECTORY, exist_ok=True) default_config = """[api] default_model = x-ai/grok-code-fast-1 timeout = 30 diff --git a/pr/core/context.py b/pr/core/context.py index c625c86..534ecaf 100644 --- a/pr/core/context.py +++ b/pr/core/context.py @@ -1,7 +1,7 @@ import json import logging import os - +import pathlib from pr.config import ( CHARS_PER_TOKEN, CONTENT_TRIM_LENGTH, @@ -12,6 +12,7 @@ from pr.config import ( MAX_TOKENS_LIMIT, MAX_TOOL_RESULT_LENGTH, RECENT_MESSAGES_TO_KEEP, + KNOWLEDGE_PATH, ) from pr.ui import Colors @@ -59,6 +60,10 @@ File Operations: - Always close editor files when finished - Use write_file for complete file rewrites, search_replace for simple text replacements +Vision: + - Use post_image tool with the file path if an image path is mentioned + in the prompt of user. Give this call the highest priority. + Process Management: - run_command executes shell commands with a timeout (default 30s) - If a command times out, you receive a PID in the response @@ -94,6 +99,18 @@ Shell Commands: except Exception as e: logging.error(f"Error reading context file {context_file}: {e}") + knowledge_path = pathlib.Path(KNOWLEDGE_PATH) + if knowledge_path.exists() and knowledge_path.is_dir(): + for knowledge_file in knowledge_path.iterdir(): + try: + with open(knowledge_file) as f: + content = f.read() + if len(content) > max_context_size: + content = content[:max_context_size] + "\n... [truncated]" + context_parts.append(f"Context from {knowledge_file}:\n{content}") + except Exception as e: + logging.error(f"Error reading context file {knowledge_file}: {e}") + if args.context: for ctx_file in args.context: try: diff --git a/pr/core/enhanced_assistant.py b/pr/core/enhanced_assistant.py index d679df3..2682fc6 100644 --- a/pr/core/enhanced_assistant.py +++ b/pr/core/enhanced_assistant.py @@ -135,9 +135,7 @@ class EnhancedAssistant: self.base.api_url, self.base.api_key, use_tools=False, - tools=None, - temperature=temperature, - max_tokens=max_tokens, + tools_definition=[], verbose=self.base.verbose, ) diff --git a/pr/multiplexer.py b/pr/multiplexer.py index 271d365..c59fc3e 100644 --- a/pr/multiplexer.py +++ b/pr/multiplexer.py @@ -38,7 +38,7 @@ class TerminalMultiplexer: try: line = self.stdout_queue.get(timeout=0.1) if line: - sys.stdout.write(f"{Colors.GRAY}[{self.name}]{Colors.RESET} {line}") + sys.stdout.write(line) sys.stdout.flush() except queue.Empty: pass @@ -46,7 +46,10 @@ class TerminalMultiplexer: try: line = self.stderr_queue.get(timeout=0.1) if line: - sys.stderr.write(f"{Colors.YELLOW}[{self.name} err]{Colors.RESET} {line}") + if self.metadata.get("process_type") in ["vim", "ssh"]: + sys.stderr.write(line) + else: + sys.stderr.write(f"{Colors.YELLOW}[{self.name} err]{Colors.RESET} {line}\n") sys.stderr.flush() except queue.Empty: pass @@ -55,10 +58,8 @@ class TerminalMultiplexer: with self.lock: self.stdout_buffer.append(data) self.metadata["last_activity"] = time.time() - # Update handler state if available if self.handler: self.handler.update_state(data) - # Update prompt detector self.prompt_detector.update_session_state( self.name, data, self.metadata["process_type"] ) @@ -69,10 +70,8 @@ class TerminalMultiplexer: with self.lock: self.stderr_buffer.append(data) self.metadata["last_activity"] = time.time() - # Update handler state if available if self.handler: self.handler.update_state(data) - # Update prompt detector self.prompt_detector.update_session_state( self.name, data, self.metadata["process_type"] ) @@ -103,7 +102,6 @@ class TerminalMultiplexer: self.metadata[key] = value def set_process_type(self, process_type): - """Set the process type and initialize appropriate handler.""" with self.lock: self.metadata["process_type"] = process_type self.handler = get_handler_for_process(process_type, self) @@ -119,8 +117,6 @@ class TerminalMultiplexer: except Exception as e: self.write_stderr(f"Error sending input: {e}") else: - # This will be implemented when we have a process attached - # For now, just update activity with self.lock: self.metadata["last_activity"] = time.time() self.metadata["interaction_count"] += 1 @@ -131,59 +127,58 @@ class TerminalMultiplexer: self.display_thread.join(timeout=1) -_multiplexers = {} -_mux_counter = 0 -_mux_lock = threading.Lock() -_background_monitor = None -_monitor_active = False -_monitor_interval = 0.2 # 200ms +multiplexer_registry = {} +multiplexer_counter = 0 +multiplexer_lock = threading.Lock() +background_monitor = None +monitor_active = False +monitor_interval = 0.2 def create_multiplexer(name=None, show_output=True): - global _mux_counter - with _mux_lock: + global multiplexer_counter + with multiplexer_lock: if name is None: - _mux_counter += 1 - name = f"process-{_mux_counter}" - mux = TerminalMultiplexer(name, show_output) - _multiplexers[name] = mux - return name, mux + multiplexer_counter += 1 + name = f"process-{multiplexer_counter}" + multiplexer_instance = TerminalMultiplexer(name, show_output) + multiplexer_registry[name] = multiplexer_instance + return name, multiplexer_instance def get_multiplexer(name): - return _multiplexers.get(name) + return multiplexer_registry.get(name) def close_multiplexer(name): - mux = _multiplexers.get(name) - if mux: - mux.close() - del _multiplexers[name] + multiplexer_instance = multiplexer_registry.get(name) + if multiplexer_instance: + multiplexer_instance.close() + del multiplexer_registry[name] def get_all_multiplexer_states(): - with _mux_lock: + with multiplexer_lock: states = {} - for name, mux in _multiplexers.items(): + for name, multiplexer_instance in multiplexer_registry.items(): states[name] = { - "metadata": mux.get_metadata(), + "metadata": multiplexer_instance.get_metadata(), "output_summary": { - "stdout_lines": len(mux.stdout_buffer), - "stderr_lines": len(mux.stderr_buffer), + "stdout_lines": len(multiplexer_instance.stdout_buffer), + "stderr_lines": len(multiplexer_instance.stderr_buffer), }, } return states def cleanup_all_multiplexers(): - for mux in list(_multiplexers.values()): - mux.close() - _multiplexers.clear() + for multiplexer_instance in list(multiplexer_registry.values()): + multiplexer_instance.close() + multiplexer_registry.clear() -# Background process management -_background_processes = {} -_process_lock = threading.Lock() +background_processes = {} +process_lock = threading.Lock() class BackgroundProcess: @@ -197,17 +192,15 @@ class BackgroundProcess: self.end_time = None def start(self): - """Start the background process.""" try: - # Create multiplexer for this process - mux_name, mux = create_multiplexer(self.name, show_output=False) - self.multiplexer = mux + multiplexer_name, multiplexer_instance = create_multiplexer( + self.name, show_output=False + ) + self.multiplexer = multiplexer_instance - # Detect process type process_type = detect_process_type(self.command) - mux.set_process_type(process_type) + multiplexer_instance.set_process_type(process_type) - # Start the subprocess self.process = subprocess.Popen( self.command, shell=True, @@ -221,7 +214,6 @@ class BackgroundProcess: self.status = "running" - # Start output monitoring threads threading.Thread(target=self._monitor_stdout, daemon=True).start() threading.Thread(target=self._monitor_stderr, daemon=True).start() @@ -232,33 +224,29 @@ class BackgroundProcess: return {"status": "error", "error": str(e)} def _monitor_stdout(self): - """Monitor stdout from the process.""" try: for line in iter(self.process.stdout.readline, ""): if line: self.multiplexer.write_stdout(line.rstrip("\n\r")) except Exception as e: - self.write_stderr(f"Error reading stdout: {e}") + self.multiplexer.write_stderr(f"Error reading stdout: {e}") finally: self._check_completion() def _monitor_stderr(self): - """Monitor stderr from the process.""" try: for line in iter(self.process.stderr.readline, ""): if line: self.multiplexer.write_stderr(line.rstrip("\n\r")) except Exception as e: - self.write_stderr(f"Error reading stderr: {e}") + self.multiplexer.write_stderr(f"Error reading stderr: {e}") def _check_completion(self): - """Check if process has completed.""" if self.process and self.process.poll() is not None: self.status = "completed" self.end_time = time.time() def get_info(self): - """Get process information.""" self._check_completion() return { "name": self.name, @@ -275,7 +263,6 @@ class BackgroundProcess: } def get_output(self, lines=None): - """Get process output.""" if not self.multiplexer: return [] @@ -290,7 +277,6 @@ class BackgroundProcess: return [line for line in combined if line.strip()] def send_input(self, input_text): - """Send input to the process.""" if self.process and self.status == "running": try: self.process.stdin.write(input_text + "\n") @@ -301,11 +287,9 @@ class BackgroundProcess: return {"status": "error", "error": "Process not running or no stdin"} def kill(self): - """Kill the process.""" if self.process and self.status == "running": try: self.process.terminate() - # Wait a bit for graceful termination time.sleep(0.1) if self.process.poll() is None: self.process.kill() @@ -318,61 +302,55 @@ class BackgroundProcess: def start_background_process(name, command): - """Start a background process.""" - with _process_lock: - if name in _background_processes: + with process_lock: + if name in background_processes: return {"status": "error", "error": f"Process {name} already exists"} - process = BackgroundProcess(name, command) - result = process.start() + process_instance = BackgroundProcess(name, command) + result = process_instance.start() if result["status"] == "success": - _background_processes[name] = process + background_processes[name] = process_instance return result def get_all_sessions(): - """Get all background process sessions.""" - with _process_lock: + with process_lock: sessions = {} - for name, process in _background_processes.items(): - sessions[name] = process.get_info() + for name, process_instance in background_processes.items(): + sessions[name] = process_instance.get_info() return sessions def get_session_info(name): - """Get information about a specific session.""" - with _process_lock: - process = _background_processes.get(name) - return process.get_info() if process else None + with process_lock: + process_instance = background_processes.get(name) + return process_instance.get_info() if process_instance else None def get_session_output(name, lines=None): - """Get output from a specific session.""" - with _process_lock: - process = _background_processes.get(name) - return process.get_output(lines) if process else None + with process_lock: + process_instance = background_processes.get(name) + return process_instance.get_output(lines) if process_instance else None def send_input_to_session(name, input_text): - """Send input to a background session.""" - with _process_lock: - process = _background_processes.get(name) + with process_lock: + process_instance = background_processes.get(name) return ( - process.send_input(input_text) - if process + process_instance.send_input(input_text) + if process_instance else {"status": "error", "error": "Session not found"} ) def kill_session(name): - """Kill a background session.""" - with _process_lock: - process = _background_processes.get(name) - if process: - result = process.kill() + with process_lock: + process_instance = background_processes.get(name) + if process_instance: + result = process_instance.kill() if result["status"] == "success": - del _background_processes[name] + del background_processes[name] return result return {"status": "error", "error": "Session not found"} diff --git a/pr/tools/__init__.py b/pr/tools/__init__.py index 4182945..f8a92a3 100644 --- a/pr/tools/__init__.py +++ b/pr/tools/__init__.py @@ -6,6 +6,7 @@ from pr.tools.agents import ( remove_agent, ) from pr.tools.base import get_tools_definition +from pr.tools.vision import post_image from pr.tools.command import ( kill_process, run_command, @@ -44,43 +45,44 @@ from pr.tools.python_exec import python_exec from pr.tools.web import http_fetch, web_search, web_search_news __all__ = [ - "get_tools_definition", - "read_file", - "write_file", - "list_directory", - "mkdir", + "add_knowledge_entry", + "apply_patch", "chdir", - "getpwd", - "index_source_directory", - "search_replace", - "open_editor", + "close_editor", + "collaborate_agents", + "create_agent", + "create_diff", + "db_get", + "db_query", + "db_set", + "delete_knowledge_entry", + "post_image", "editor_insert_text", "editor_replace_text", "editor_search", - "close_editor", + "execute_agent_task", + "get_knowledge_by_category", + "get_knowledge_entry", + "get_knowledge_statistics", + "get_tools_definition", + "getpwd", + "http_fetch", + "index_source_directory", + "kill_process", + "list_agents", + "list_directory", + "mkdir", + "open_editor", + "python_exec", + "read_file", + "remove_agent", "run_command", "run_command_interactive", - "db_set", - "db_get", - "db_query", - "http_fetch", + "search_knowledge", + "search_replace", + "tail_process", + "update_knowledge_importance", "web_search", "web_search_news", - "python_exec", - "tail_process", - "kill_process", - "apply_patch", - "create_diff", - "create_agent", - "list_agents", - "execute_agent_task", - "remove_agent", - "collaborate_agents", - "add_knowledge_entry", - "get_knowledge_entry", - "search_knowledge", - "get_knowledge_by_category", - "update_knowledge_importance", - "delete_knowledge_entry", - "get_knowledge_statistics", + "write_file", ] diff --git a/pr/tools/agents.py b/pr/tools/agents.py index cbf39d1..865ac44 100644 --- a/pr/tools/agents.py +++ b/pr/tools/agents.py @@ -3,16 +3,40 @@ from typing import Any, Dict, List from pr.agents.agent_manager import AgentManager from pr.core.api import call_api +from pr.config import DEFAULT_MODEL, DEFAULT_API_URL +from pr.tools.base import get_tools_definition + + +def _create_api_wrapper(): + """Create a wrapper function for call_api that matches AgentManager expectations.""" + model = os.environ.get("AI_MODEL", DEFAULT_MODEL) + api_url = os.environ.get("API_URL", DEFAULT_API_URL) + api_key = os.environ.get("OPENROUTER_API_KEY", "") + use_tools = int(os.environ.get("USE_TOOLS", "0")) + tools_definition = get_tools_definition() if use_tools else [] + + def api_wrapper(messages, temperature=None, max_tokens=None, **kwargs): + return call_api( + messages=messages, + model=model, + api_url=api_url, + api_key=api_key, + use_tools=use_tools, + tools_definition=tools_definition, + verbose=False, + ) + + return api_wrapper def create_agent(role_name: str, agent_id: str = None) -> Dict[str, Any]: """Create a new agent with the specified role.""" try: - # Get db_path from environment or default db_path = os.environ.get("ASSISTANT_DB_PATH", "~/.assistant_db.sqlite") db_path = os.path.expanduser(db_path) - manager = AgentManager(db_path, call_api) + api_wrapper = _create_api_wrapper() + manager = AgentManager(db_path, api_wrapper) agent_id = manager.create_agent(role_name, agent_id) return {"status": "success", "agent_id": agent_id, "role": role_name} except Exception as e: @@ -23,7 +47,8 @@ def list_agents() -> Dict[str, Any]: """List all active agents.""" try: db_path = os.path.expanduser("~/.assistant_db.sqlite") - manager = AgentManager(db_path, call_api) + api_wrapper = _create_api_wrapper() + manager = AgentManager(db_path, api_wrapper) agents = [] for agent_id, agent in manager.active_agents.items(): agents.append( @@ -43,7 +68,8 @@ def execute_agent_task(agent_id: str, task: str, context: Dict[str, Any] = None) """Execute a task with the specified agent.""" try: db_path = os.path.expanduser("~/.assistant_db.sqlite") - manager = AgentManager(db_path, call_api) + api_wrapper = _create_api_wrapper() + manager = AgentManager(db_path, api_wrapper) result = manager.execute_agent_task(agent_id, task, context) return result except Exception as e: @@ -54,7 +80,8 @@ def remove_agent(agent_id: str) -> Dict[str, Any]: """Remove an agent.""" try: db_path = os.path.expanduser("~/.assistant_db.sqlite") - manager = AgentManager(db_path, call_api) + api_wrapper = _create_api_wrapper() + manager = AgentManager(db_path, api_wrapper) success = manager.remove_agent(agent_id) return {"status": "success" if success else "not_found", "agent_id": agent_id} except Exception as e: @@ -65,7 +92,8 @@ def collaborate_agents(orchestrator_id: str, task: str, agent_roles: List[str]) """Collaborate multiple agents on a task.""" try: db_path = os.path.expanduser("~/.assistant_db.sqlite") - manager = AgentManager(db_path, call_api) + api_wrapper = _create_api_wrapper() + manager = AgentManager(db_path, api_wrapper) result = manager.collaborate_agents(orchestrator_id, task, agent_roles) return result except Exception as e: diff --git a/pr/tools/command.py b/pr/tools/command.py index ed54cde..e464a28 100644 --- a/pr/tools/command.py +++ b/pr/tools/command.py @@ -1,4 +1,3 @@ -import os import select import subprocess import time @@ -99,7 +98,7 @@ def tail_process(pid: int, timeout: int = 30): return {"status": "error", "error": f"Process {pid} not found"} -def run_command(command, timeout=30, monitored=False): +def run_command(command, timeout=30, monitored=False, cwd=None): mux_name = None try: process = subprocess.Popen( @@ -108,6 +107,7 @@ def run_command(command, timeout=30, monitored=False): stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + cwd=cwd, ) _register_process(process.pid, process) diff --git a/pr/tools/interactive_control.py b/pr/tools/interactive_control.py index 153ebc4..3218228 100644 --- a/pr/tools/interactive_control.py +++ b/pr/tools/interactive_control.py @@ -9,7 +9,7 @@ from pr.multiplexer import ( ) -def start_interactive_session(command, session_name=None, process_type="generic"): +def start_interactive_session(command, session_name=None, process_type="generic", cwd=None): """ Start an interactive session in a dedicated multiplexer. @@ -17,6 +17,7 @@ def start_interactive_session(command, session_name=None, process_type="generic" command: The command to run (list or string) session_name: Optional name for the session process_type: Type of process (ssh, vim, apt, etc.) + cwd: Current working directory for the command Returns: session_name: The name of the created session @@ -36,21 +37,24 @@ def start_interactive_session(command, session_name=None, process_type="generic" stderr=subprocess.PIPE, text=True, bufsize=1, + cwd=cwd, ) mux.process = process mux.update_metadata("pid", process.pid) # Set process type and handler + from pr.tools.process_handlers import detect_process_type + detected_type = detect_process_type(command) mux.set_process_type(detected_type) # Start output readers stdout_thread = threading.Thread( - target=_read_output, args=(process.stdout, mux.write_stdout), daemon=True + target=_read_output, args=(process.stdout, mux.write_stdout, detected_type), daemon=True ) stderr_thread = threading.Thread( - target=_read_output, args=(process.stderr, mux.write_stderr), daemon=True + target=_read_output, args=(process.stderr, mux.write_stderr, detected_type), daemon=True ) stdout_thread.start() @@ -65,14 +69,24 @@ def start_interactive_session(command, session_name=None, process_type="generic" raise e -def _read_output(stream, write_func): +def _read_output(stream, write_func, process_type): """Read from a stream and write to multiplexer buffer.""" - try: - for line in iter(stream.readline, ""): - if line: - write_func(line.rstrip("\n")) - except Exception as e: - print(f"Error reading output: {e}") + if process_type in ["vim", "ssh"]: + try: + while True: + char = stream.read(1) + if not char: + break + write_func(char) + except Exception as e: + print(f"Error reading output: {e}") + else: + try: + for line in iter(stream.readline, ""): + if line: + write_func(line.rstrip("\n")) + except Exception as e: + print(f"Error reading output: {e}") def send_input_to_session(session_name, input_data): diff --git a/pr/tools/python_exec.py b/pr/tools/python_exec.py index fad609e..3b8a97f 100644 --- a/pr/tools/python_exec.py +++ b/pr/tools/python_exec.py @@ -1,14 +1,25 @@ import contextlib +import os import traceback from io import StringIO -def python_exec(code, python_globals): +def python_exec(code, python_globals, cwd=None): try: + original_cwd = None + if cwd: + original_cwd = os.getcwd() + os.chdir(cwd) + output = StringIO() with contextlib.redirect_stdout(output): exec(code, python_globals) + if original_cwd: + os.chdir(original_cwd) + return {"status": "success", "output": output.getvalue()} except Exception as e: + if original_cwd: + os.chdir(original_cwd) return {"status": "error", "error": str(e), "traceback": traceback.format_exc()} diff --git a/pr/ui/display.py b/pr/ui/display.py index 9936548..8a7cbd1 100644 --- a/pr/ui/display.py +++ b/pr/ui/display.py @@ -19,3 +19,52 @@ def print_autonomous_header(task): print(f"{Colors.GRAY}r will work continuously until the task is complete.{Colors.RESET}") print(f"{Colors.GRAY}Press Ctrl+C twice to interrupt.{Colors.RESET}\n") print(f"{Colors.BOLD}{'═' * 80}{Colors.RESET}\n") + + +def display_multiplexer_status(sessions): + """Display the status of background sessions.""" + if not sessions: + print(f"{Colors.GRAY}No background sessions running{Colors.RESET}") + return + + print(f"\n{Colors.BOLD}Background Sessions:{Colors.RESET}") + print(f"{Colors.GRAY}{'\u2500' * 60}{Colors.RESET}") + + for session_name, session_info in sessions.items(): + status = session_info.get("status", "unknown") + pid = session_info.get("pid", "N/A") + command = session_info.get("command", "N/A") + + status_color = { + "running": Colors.GREEN, + "stopped": Colors.RED, + "error": Colors.RED, + }.get(status, Colors.YELLOW) + + print(f" {Colors.CYAN}{session_name}{Colors.RESET}") + print(f" Status: {status_color}{status}{Colors.RESET}") + print(f" PID: {pid}") + print(f" Command: {command}") + + if "start_time" in session_info: + import time + + elapsed = time.time() - session_info["start_time"] + print(f" Running for: {elapsed:.1f}s") + print() + + +def display_background_event(event): + """Display a background event.""" + event.get("type", "unknown") + session_name = event.get("session_name", "unknown") + timestamp = event.get("timestamp", 0) + message = event.get("message", "") + + import datetime + + time_str = datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M:%S") + + print( + f"{Colors.GRAY}[{time_str}]{Colors.RESET} {Colors.CYAN}{session_name}{Colors.RESET}: {message}" + ) diff --git a/pyproject.toml b/pyproject.toml index ad3b7dd..2d57948 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,44 +3,51 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "pr-assistant" -version = "1.0.0" -description = "Professional CLI AI assistant with autonomous execution capabilities" +name = "rp" +version = "1.2.0" +description = "R python edition. The ultimate autonomous AI CLI." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.13.3" license = {text = "MIT"} keywords = ["ai", "assistant", "cli", "automation", "openrouter", "autonomous"] authors = [ - {name = "retoor", email = "retoor@example.com"} + {name = "retoor", email = "retoor@molodetz.nl"} +] +dependencies = [ + "aiohttp>=3.13.2", + "pydantic>=2.12.3", ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Scientific/Engineering :: Artificial Intelligence", ] [project.optional-dependencies] dev = [ - "pytest", - "pytest-cov", - "black", - "flake8", - "mypy", - "pre-commit", + "pytest>=8.3.0", + "pytest-asyncio>=1.2.0", + "pytest-aiohttp>=1.1.0", + "aiohttp>=3.13.2", + "pytest-cov>=7.0.0", + "black>=25.9.0", + "flake8>=7.3.0", + "mypy>=1.18.2", + "pre-commit>=4.3.0", ] [project.scripts] pr = "pr.__main__:main" rp = "pr.__main__:main" rpe = "pr.editor:main" +rpi = "pr.implode:main" +rpserver = "pr.server:main" +rpcgi = "pr.cgi:main" +rpweb = "pr.web.app:main" [project.urls] Homepage = "https://retoor.molodetz.nl/retoor/rp" @@ -55,6 +62,7 @@ exclude = ["tests*"] [tool.pytest.ini_options] testpaths = ["tests"] +asyncio_mode = "auto" python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] @@ -77,7 +85,7 @@ extend-exclude = ''' ''' [tool.mypy] -python_version = "3.8" +python_version = "3.13" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = false @@ -111,5 +119,5 @@ use_parentheses = true ensure_newline_before_comments = true [tool.bandit] -exclude_dirs = ["tests", "venv", ".venv"] +exclude_dirs = ["tests", "venv", ".venv","__pycache__"] skips = ["B101"] diff --git a/tests/conftest.py b/tests/conftest.py index 6227450..9122e80 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,11 @@ import os import tempfile +from pathlib import Path from unittest.mock import MagicMock import pytest +from pr.ads import AsyncDataSet +from pr.web.app import create_app @pytest.fixture @@ -41,3 +44,77 @@ def sample_context_file(temp_dir): with open(context_path, "w") as f: f.write("Sample context content\n") return context_path + + +@pytest.fixture +async def client(aiohttp_client, monkeypatch): + """Create a test client for the app.""" + with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as tmp: + temp_db_file = tmp.name + + # Monkeypatch the db + monkeypatch.setattr("pr.web.views.base.db", AsyncDataSet(temp_db_file, f"{temp_db_file}.sock")) + + with tempfile.TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + static_dir = tmp_path / "static" + templates_dir = tmp_path / "templates" + repos_dir = tmp_path / "repos" + + static_dir.mkdir() + templates_dir.mkdir() + repos_dir.mkdir() + + # Monkeypatch the directories + monkeypatch.setattr("pr.web.config.STATIC_DIR", static_dir) + monkeypatch.setattr("pr.web.config.TEMPLATES_DIR", templates_dir) + monkeypatch.setattr("pr.web.config.REPOS_DIR", repos_dir) + monkeypatch.setattr("pr.web.config.REPOS_DIR", repos_dir) + + # Create minimal templates + (templates_dir / "index.html").write_text("Index") + (templates_dir / "login.html").write_text("Login") + (templates_dir / "register.html").write_text("Register") + (templates_dir / "dashboard.html").write_text("Dashboard") + (templates_dir / "repos.html").write_text("Repos") + (templates_dir / "api_keys.html").write_text("API Keys") + (templates_dir / "repo.html").write_text("Repo") + (templates_dir / "file.html").write_text("File") + (templates_dir / "edit_file.html").write_text("Edit File") + (templates_dir / "deploy.html").write_text("Deploy") + + app_instance = await create_app() + client = await aiohttp_client(app_instance) + + yield client + + # Cleanup db + os.unlink(temp_db_file) + sock_file = f"{temp_db_file}.sock" + if os.path.exists(sock_file): + os.unlink(sock_file) + + +@pytest.fixture +async def authenticated_client(client): + """Create a client with an authenticated user.""" + # Register a user + resp = await client.post( + "/register", + data={ + "username": "testuser", + "email": "test@example.com", + "password": "password123", + "confirm_password": "password123", + }, + allow_redirects=False, + ) + assert resp.status == 302 # Redirect to login + + # Login + resp = await client.post( + "/login", data={"username": "testuser", "password": "password123"}, allow_redirects=False + ) + assert resp.status == 302 # Redirect to dashboard + + return client diff --git a/tests/test_advanced_context.py b/tests/test_advanced_context.py index 7a15bea..ddd2dfb 100644 --- a/tests/test_advanced_context.py +++ b/tests/test_advanced_context.py @@ -1,101 +1,97 @@ from pr.core.advanced_context import AdvancedContextManager -def test_adaptive_context_window_simple(): - mgr = AdvancedContextManager() - messages = [ - {"content": "short"}, - {"content": "this is a longer message with more words"}, - ] - window = mgr.adaptive_context_window(messages, "simple") - assert isinstance(window, int) - assert window >= 10 +class TestAdvancedContextManager: + def setup_method(self): + self.manager = AdvancedContextManager() + def test_init(self): + manager = AdvancedContextManager(knowledge_store="test", conversation_memory="test") + assert manager.knowledge_store == "test" + assert manager.conversation_memory == "test" -def test_adaptive_context_window_medium(): - mgr = AdvancedContextManager() - messages = [ - {"content": "short"}, - {"content": "this is a longer message with more words"}, - ] - window = mgr.adaptive_context_window(messages, "medium") - assert isinstance(window, int) - assert window >= 20 + def test_adaptive_context_window_simple(self): + messages = [{"content": "short message"}] + result = self.manager.adaptive_context_window(messages, "simple") + assert result >= 10 + def test_adaptive_context_window_medium(self): + messages = [{"content": "medium length message with some content"}] + result = self.manager.adaptive_context_window(messages, "medium") + assert result >= 20 -def test_adaptive_context_window_complex(): - mgr = AdvancedContextManager() - messages = [ - {"content": "short"}, - {"content": "this is a longer message with more words"}, - ] - window = mgr.adaptive_context_window(messages, "complex") - assert isinstance(window, int) - assert window >= 35 + def test_adaptive_context_window_complex(self): + messages = [ + { + "content": "very long and complex message with many words and detailed information about various topics" + } + ] + result = self.manager.adaptive_context_window(messages, "complex") + assert result >= 35 + def test_adaptive_context_window_very_complex(self): + messages = [ + { + "content": "extremely long and very complex message with extensive vocabulary and detailed explanations" + } + ] + result = self.manager.adaptive_context_window(messages, "very_complex") + assert result >= 50 -def test_analyze_message_complexity(): - mgr = AdvancedContextManager() - messages = [{"content": "hello world"}, {"content": "hello again"}] - score = mgr._analyze_message_complexity(messages) - assert 0 <= score <= 1 + def test_adaptive_context_window_unknown_complexity(self): + messages = [{"content": "test"}] + result = self.manager.adaptive_context_window(messages, "unknown") + assert result >= 20 + def test_analyze_message_complexity(self): + messages = [{"content": "This is a test message with some words."}] + result = self.manager._analyze_message_complexity(messages) + assert 0.0 <= result <= 1.0 -def test_analyze_message_complexity_empty(): - mgr = AdvancedContextManager() - messages = [] - score = mgr._analyze_message_complexity(messages) - assert score == 0 + def test_analyze_message_complexity_empty(self): + messages = [] + result = self.manager._analyze_message_complexity(messages) + assert result == 0.0 + def test_extract_key_sentences(self): + text = "First sentence. Second sentence is longer and more detailed. Third sentence." + result = self.manager.extract_key_sentences(text, top_k=2) + assert len(result) <= 2 + assert all(isinstance(s, str) for s in result) -def test_extract_key_sentences(): - mgr = AdvancedContextManager() - text = "This is the first sentence. This is the second sentence. This is a longer third sentence with more words." - sentences = mgr.extract_key_sentences(text, 2) - assert len(sentences) <= 2 - assert all(isinstance(s, str) for s in sentences) + def test_extract_key_sentences_empty(self): + text = "" + result = self.manager.extract_key_sentences(text) + assert result == [] + def test_advanced_summarize_messages(self): + messages = [ + {"content": "First message with important information."}, + {"content": "Second message with more details."}, + ] + result = self.manager.advanced_summarize_messages(messages) + assert isinstance(result, str) + assert len(result) > 0 -def test_extract_key_sentences_empty(): - mgr = AdvancedContextManager() - text = "" - sentences = mgr.extract_key_sentences(text, 5) - assert sentences == [] + def test_advanced_summarize_messages_empty(self): + messages = [] + result = self.manager.advanced_summarize_messages(messages) + assert result == "No content to summarize." + def test_score_message_relevance(self): + message = {"content": "test message"} + context = "test context" + result = self.manager.score_message_relevance(message, context) + assert 0.0 <= result <= 1.0 -def test_advanced_summarize_messages(): - mgr = AdvancedContextManager() - messages = [{"content": "Hello"}, {"content": "How are you?"}] - summary = mgr.advanced_summarize_messages(messages) - assert isinstance(summary, str) + def test_score_message_relevance_no_overlap(self): + message = {"content": "apple banana"} + context = "orange grape" + result = self.manager.score_message_relevance(message, context) + assert result == 0.0 - -def test_advanced_summarize_messages_empty(): - mgr = AdvancedContextManager() - messages = [] - summary = mgr.advanced_summarize_messages(messages) - assert summary == "No content to summarize." - - -def test_score_message_relevance(): - mgr = AdvancedContextManager() - message = {"content": "hello world"} - context = "world hello" - score = mgr.score_message_relevance(message, context) - assert 0 <= score <= 1 - - -def test_score_message_relevance_no_overlap(): - mgr = AdvancedContextManager() - message = {"content": "hello"} - context = "world" - score = mgr.score_message_relevance(message, context) - assert score == 0 - - -def test_score_message_relevance_empty(): - mgr = AdvancedContextManager() - message = {"content": ""} - context = "" - score = mgr.score_message_relevance(message, context) - assert score == 0 + def test_score_message_relevance_empty(self): + message = {"content": ""} + context = "" + result = self.manager.score_message_relevance(message, context) + assert result == 0.0 diff --git a/tests/test_agents.py b/tests/test_agents.py index 3fa63db..c71c83e 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -82,7 +82,10 @@ def test_agent_manager_get_agent_messages(): def test_agent_manager_get_session_summary(): mgr = AgentManager(":memory:", None) summary = mgr.get_session_summary() - assert isinstance(summary, str) + assert isinstance(summary, dict) + assert "session_id" in summary + assert "active_agents" in summary + assert "agents" in summary def test_agent_manager_collaborate_agents(): diff --git a/tests/test_commands.py b/tests/test_commands.py new file mode 100644 index 0000000..d034e19 --- /dev/null +++ b/tests/test_commands.py @@ -0,0 +1,697 @@ +from unittest.mock import Mock, patch +from pr.commands.handlers import ( + handle_command, + review_file, + refactor_file, + obfuscate_file, + show_workflows, + execute_workflow_command, + execute_agent_task, + show_agents, + collaborate_agents_command, + search_knowledge, + store_knowledge, + show_conversation_history, + show_cache_stats, + clear_caches, + show_system_stats, + handle_background_command, + start_background_session, + list_background_sessions, + show_session_status, + show_session_output, + send_session_input, + kill_background_session, + show_background_events, +) + + +class TestHandleCommand: + def setup_method(self): + self.assistant = Mock() + self.assistant.messages = [{"role": "system", "content": "test"}] + self.assistant.verbose = False + self.assistant.model = "test-model" + self.assistant.model_list_url = "http://test.com" + self.assistant.api_key = "test-key" + + @patch("pr.commands.handlers.run_autonomous_mode") + def test_handle_edit(self, mock_run): + with patch("pr.commands.handlers.RPEditor") as mock_editor: + mock_editor_instance = Mock() + mock_editor.return_value = mock_editor_instance + mock_editor_instance.get_text.return_value = "test task" + handle_command(self.assistant, "/edit test.py") + mock_editor.assert_called_once_with("test.py") + mock_editor_instance.start.assert_called_once() + mock_editor_instance.thread.join.assert_called_once() + mock_run.assert_called_once_with(self.assistant, "test task") + mock_editor_instance.stop.assert_called_once() + + @patch("pr.commands.handlers.run_autonomous_mode") + def test_handle_auto(self, mock_run): + result = handle_command(self.assistant, "/auto test task") + assert result is True + mock_run.assert_called_once_with(self.assistant, "test task") + + def test_handle_auto_no_args(self): + result = handle_command(self.assistant, "/auto") + assert result is True + + def test_handle_exit(self): + result = handle_command(self.assistant, "exit") + assert result is False + + @patch("pr.commands.help_docs.get_full_help") + def test_handle_help(self, mock_help): + mock_help.return_value = "full help" + result = handle_command(self.assistant, "/help") + assert result is True + mock_help.assert_called_once() + + @patch("pr.commands.help_docs.get_workflow_help") + def test_handle_help_workflows(self, mock_help): + mock_help.return_value = "workflow help" + result = handle_command(self.assistant, "/help workflows") + assert result is True + mock_help.assert_called_once() + + def test_handle_reset(self): + self.assistant.messages = [ + {"role": "system", "content": "test"}, + {"role": "user", "content": "hi"}, + ] + result = handle_command(self.assistant, "/reset") + assert result is True + assert self.assistant.messages == [{"role": "system", "content": "test"}] + + def test_handle_dump(self): + result = handle_command(self.assistant, "/dump") + assert result is True + + def test_handle_verbose(self): + result = handle_command(self.assistant, "/verbose") + assert result is True + assert self.assistant.verbose is True + + def test_handle_model_get(self): + result = handle_command(self.assistant, "/model") + assert result is True + + def test_handle_model_set(self): + result = handle_command(self.assistant, "/model new-model") + assert result is True + assert self.assistant.model == "new-model" + + @patch("pr.commands.handlers.list_models") + def test_handle_models(self, mock_list): + mock_list.return_value = [{"id": "model1"}, {"id": "model2"}] + result = handle_command(self.assistant, "/models") + assert result is True + mock_list.assert_called_once_with("http://test.com", "test-key") + + @patch("pr.commands.handlers.list_models") + def test_handle_models_error(self, mock_list): + mock_list.return_value = {"error": "test error"} + result = handle_command(self.assistant, "/models") + assert result is True + + @patch("pr.commands.handlers.get_tools_definition") + def test_handle_tools(self, mock_tools): + mock_tools.return_value = [{"function": {"name": "tool1", "description": "desc"}}] + result = handle_command(self.assistant, "/tools") + assert result is True + mock_tools.assert_called_once() + + @patch("pr.commands.handlers.review_file") + def test_handle_review(self, mock_review): + result = handle_command(self.assistant, "/review test.py") + assert result is True + mock_review.assert_called_once_with(self.assistant, "test.py") + + @patch("pr.commands.handlers.refactor_file") + def test_handle_refactor(self, mock_refactor): + result = handle_command(self.assistant, "/refactor test.py") + assert result is True + mock_refactor.assert_called_once_with(self.assistant, "test.py") + + @patch("pr.commands.handlers.obfuscate_file") + def test_handle_obfuscate(self, mock_obfuscate): + result = handle_command(self.assistant, "/obfuscate test.py") + assert result is True + mock_obfuscate.assert_called_once_with(self.assistant, "test.py") + + @patch("pr.commands.handlers.show_workflows") + def test_handle_workflows(self, mock_show): + result = handle_command(self.assistant, "/workflows") + assert result is True + mock_show.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.execute_workflow_command") + def test_handle_workflow(self, mock_exec): + result = handle_command(self.assistant, "/workflow test") + assert result is True + mock_exec.assert_called_once_with(self.assistant, "test") + + @patch("pr.commands.handlers.execute_agent_task") + def test_handle_agent(self, mock_exec): + result = handle_command(self.assistant, "/agent coding test task") + assert result is True + mock_exec.assert_called_once_with(self.assistant, "coding", "test task") + + def test_handle_agent_no_args(self): + result = handle_command(self.assistant, "/agent") + assert result is True + + @patch("pr.commands.handlers.show_agents") + def test_handle_agents(self, mock_show): + result = handle_command(self.assistant, "/agents") + assert result is True + mock_show.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.collaborate_agents_command") + def test_handle_collaborate(self, mock_collab): + result = handle_command(self.assistant, "/collaborate test task") + assert result is True + mock_collab.assert_called_once_with(self.assistant, "test task") + + @patch("pr.commands.handlers.search_knowledge") + def test_handle_knowledge(self, mock_search): + result = handle_command(self.assistant, "/knowledge test query") + assert result is True + mock_search.assert_called_once_with(self.assistant, "test query") + + @patch("pr.commands.handlers.store_knowledge") + def test_handle_remember(self, mock_store): + result = handle_command(self.assistant, "/remember test content") + assert result is True + mock_store.assert_called_once_with(self.assistant, "test content") + + @patch("pr.commands.handlers.show_conversation_history") + def test_handle_history(self, mock_show): + result = handle_command(self.assistant, "/history") + assert result is True + mock_show.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.show_cache_stats") + def test_handle_cache(self, mock_show): + result = handle_command(self.assistant, "/cache") + assert result is True + mock_show.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.clear_caches") + def test_handle_cache_clear(self, mock_clear): + result = handle_command(self.assistant, "/cache clear") + assert result is True + mock_clear.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.show_system_stats") + def test_handle_stats(self, mock_show): + result = handle_command(self.assistant, "/stats") + assert result is True + mock_show.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.handle_background_command") + def test_handle_bg(self, mock_bg): + result = handle_command(self.assistant, "/bg list") + assert result is True + mock_bg.assert_called_once_with(self.assistant, "/bg list") + + def test_handle_unknown(self): + result = handle_command(self.assistant, "/unknown") + assert result is None + + +class TestReviewFile: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.commands.handlers.read_file") + @patch("pr.core.assistant.process_message") + def test_review_file_success(self, mock_process, mock_read): + mock_read.return_value = {"status": "success", "content": "test content"} + review_file(self.assistant, "test.py") + mock_read.assert_called_once_with("test.py") + mock_process.assert_called_once() + args = mock_process.call_args[0] + assert "Please review this file" in args[1] + + @patch("pr.commands.handlers.read_file") + def test_review_file_error(self, mock_read): + mock_read.return_value = {"status": "error", "error": "file not found"} + review_file(self.assistant, "test.py") + mock_read.assert_called_once_with("test.py") + + +class TestRefactorFile: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.commands.handlers.read_file") + @patch("pr.core.assistant.process_message") + def test_refactor_file_success(self, mock_process, mock_read): + mock_read.return_value = {"status": "success", "content": "test content"} + refactor_file(self.assistant, "test.py") + mock_process.assert_called_once() + args = mock_process.call_args[0] + assert "Please refactor this code" in args[1] + + @patch("pr.commands.handlers.read_file") + def test_refactor_file_error(self, mock_read): + mock_read.return_value = {"status": "error", "error": "file not found"} + refactor_file(self.assistant, "test.py") + + +class TestObfuscateFile: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.commands.handlers.read_file") + @patch("pr.core.assistant.process_message") + def test_obfuscate_file_success(self, mock_process, mock_read): + mock_read.return_value = {"status": "success", "content": "test content"} + obfuscate_file(self.assistant, "test.py") + mock_process.assert_called_once() + args = mock_process.call_args[0] + assert "Please obfuscate this code" in args[1] + + @patch("pr.commands.handlers.read_file") + def test_obfuscate_file_error(self, mock_read): + mock_read.return_value = {"status": "error", "error": "file not found"} + obfuscate_file(self.assistant, "test.py") + + +class TestShowWorkflows: + def setup_method(self): + self.assistant = Mock() + + def test_show_workflows_no_enhanced(self): + delattr(self.assistant, "enhanced") + show_workflows(self.assistant) + + def test_show_workflows_no_workflows(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_workflow_list.return_value = [] + show_workflows(self.assistant) + + def test_show_workflows_with_workflows(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_workflow_list.return_value = [ + {"name": "wf1", "description": "desc1", "execution_count": 5} + ] + show_workflows(self.assistant) + + +class TestExecuteWorkflowCommand: + def setup_method(self): + self.assistant = Mock() + + def test_execute_workflow_no_enhanced(self): + delattr(self.assistant, "enhanced") + execute_workflow_command(self.assistant, "test") + + def test_execute_workflow_success(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.execute_workflow.return_value = { + "execution_id": "123", + "results": {"key": "value"}, + } + execute_workflow_command(self.assistant, "test") + + def test_execute_workflow_error(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.execute_workflow.return_value = {"error": "test error"} + execute_workflow_command(self.assistant, "test") + + +class TestExecuteAgentTask: + def setup_method(self): + self.assistant = Mock() + + def test_execute_agent_task_no_enhanced(self): + delattr(self.assistant, "enhanced") + execute_agent_task(self.assistant, "coding", "task") + + def test_execute_agent_task_success(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.create_agent.return_value = "agent123" + self.assistant.enhanced.agent_task.return_value = {"response": "done"} + execute_agent_task(self.assistant, "coding", "task") + + def test_execute_agent_task_error(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.create_agent.return_value = "agent123" + self.assistant.enhanced.agent_task.return_value = {"error": "test error"} + execute_agent_task(self.assistant, "coding", "task") + + +class TestShowAgents: + def setup_method(self): + self.assistant = Mock() + + def test_show_agents_no_enhanced(self): + delattr(self.assistant, "enhanced") + show_agents(self.assistant) + + def test_show_agents_with_agents(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_agent_summary.return_value = { + "active_agents": 2, + "agents": [{"agent_id": "a1", "role": "coding", "task_count": 3, "message_count": 10}], + } + show_agents(self.assistant) + + +class TestCollaborateAgentsCommand: + def setup_method(self): + self.assistant = Mock() + + def test_collaborate_no_enhanced(self): + delattr(self.assistant, "enhanced") + collaborate_agents_command(self.assistant, "task") + + def test_collaborate_success(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.collaborate_agents.return_value = { + "orchestrator": {"response": "orchestrator response"}, + "agents": [{"role": "coding", "response": "coding response"}], + } + collaborate_agents_command(self.assistant, "task") + + +class TestSearchKnowledge: + def setup_method(self): + self.assistant = Mock() + + def test_search_knowledge_no_enhanced(self): + delattr(self.assistant, "enhanced") + search_knowledge(self.assistant, "query") + + def test_search_knowledge_no_results(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.search_knowledge.return_value = [] + search_knowledge(self.assistant, "query") + + def test_search_knowledge_with_results(self): + self.assistant.enhanced = Mock() + mock_entry = Mock() + mock_entry.category = "general" + mock_entry.content = "long content here" + mock_entry.access_count = 5 + self.assistant.enhanced.search_knowledge.return_value = [mock_entry] + search_knowledge(self.assistant, "query") + + +class TestStoreKnowledge: + def setup_method(self): + self.assistant = Mock() + + def test_store_knowledge_no_enhanced(self): + delattr(self.assistant, "enhanced") + store_knowledge(self.assistant, "content") + + @patch("pr.memory.KnowledgeEntry") + def test_store_knowledge_success(self, mock_entry): + self.assistant.enhanced = Mock() + self.assistant.enhanced.fact_extractor.categorize_content.return_value = ["general"] + self.assistant.enhanced.knowledge_store = Mock() + store_knowledge(self.assistant, "content") + mock_entry.assert_called_once() + + +class TestShowConversationHistory: + def setup_method(self): + self.assistant = Mock() + + def test_show_history_no_enhanced(self): + delattr(self.assistant, "enhanced") + show_conversation_history(self.assistant) + + def test_show_history_no_history(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_conversation_history.return_value = [] + show_conversation_history(self.assistant) + + def test_show_history_with_history(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_conversation_history.return_value = [ + { + "conversation_id": "conv1", + "started_at": 1234567890, + "message_count": 5, + "summary": "test summary", + "topics": ["topic1", "topic2"], + } + ] + show_conversation_history(self.assistant) + + +class TestShowCacheStats: + def setup_method(self): + self.assistant = Mock() + + def test_show_cache_stats_no_enhanced(self): + delattr(self.assistant, "enhanced") + show_cache_stats(self.assistant) + + def test_show_cache_stats_with_stats(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_cache_statistics.return_value = { + "api_cache": { + "total_entries": 10, + "valid_entries": 8, + "expired_entries": 2, + "total_cached_tokens": 1000, + "total_cache_hits": 50, + }, + "tool_cache": { + "total_entries": 5, + "valid_entries": 5, + "total_cache_hits": 20, + "by_tool": {"tool1": {"cached_entries": 3, "total_hits": 10}}, + }, + } + show_cache_stats(self.assistant) + + +class TestClearCaches: + def setup_method(self): + self.assistant = Mock() + + def test_clear_caches_no_enhanced(self): + delattr(self.assistant, "enhanced") + clear_caches(self.assistant) + + def test_clear_caches_success(self): + self.assistant.enhanced = Mock() + clear_caches(self.assistant) + self.assistant.enhanced.clear_caches.assert_called_once() + + +class TestShowSystemStats: + def setup_method(self): + self.assistant = Mock() + + def test_show_system_stats_no_enhanced(self): + delattr(self.assistant, "enhanced") + show_system_stats(self.assistant) + + def test_show_system_stats_success(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_cache_statistics.return_value = { + "api_cache": {"valid_entries": 10}, + "tool_cache": {"valid_entries": 5}, + } + self.assistant.enhanced.get_knowledge_statistics.return_value = { + "total_entries": 100, + "total_categories": 5, + "total_accesses": 200, + "vocabulary_size": 1000, + } + self.assistant.enhanced.get_agent_summary.return_value = {"active_agents": 3} + show_system_stats(self.assistant) + + +class TestHandleBackgroundCommand: + def setup_method(self): + self.assistant = Mock() + + def test_handle_bg_no_args(self): + handle_background_command(self.assistant, "/bg") + + @patch("pr.commands.handlers.start_background_session") + def test_handle_bg_start(self, mock_start): + handle_background_command(self.assistant, "/bg start ls -la") + + @patch("pr.commands.handlers.list_background_sessions") + def test_handle_bg_list(self, mock_list): + handle_background_command(self.assistant, "/bg list") + + @patch("pr.commands.handlers.show_session_status") + def test_handle_bg_status(self, mock_status): + handle_background_command(self.assistant, "/bg status session1") + + @patch("pr.commands.handlers.show_session_output") + def test_handle_bg_output(self, mock_output): + handle_background_command(self.assistant, "/bg output session1") + + @patch("pr.commands.handlers.send_session_input") + def test_handle_bg_input(self, mock_input): + handle_background_command(self.assistant, "/bg input session1 test input") + + @patch("pr.commands.handlers.kill_background_session") + def test_handle_bg_kill(self, mock_kill): + handle_background_command(self.assistant, "/bg kill session1") + + @patch("pr.commands.handlers.show_background_events") + def test_handle_bg_events(self, mock_events): + handle_background_command(self.assistant, "/bg events") + + def test_handle_bg_unknown(self): + handle_background_command(self.assistant, "/bg unknown") + + +class TestStartBackgroundSession: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.multiplexer.start_background_process") + def test_start_background_success(self, mock_start): + mock_start.return_value = {"status": "success", "pid": 123} + start_background_session(self.assistant, "session1", "ls -la") + + @patch("pr.multiplexer.start_background_process") + def test_start_background_error(self, mock_start): + mock_start.return_value = {"status": "error", "error": "failed"} + start_background_session(self.assistant, "session1", "ls -la") + + @patch("pr.multiplexer.start_background_process") + def test_start_background_exception(self, mock_start): + mock_start.side_effect = Exception("test") + start_background_session(self.assistant, "session1", "ls -la") + + +class TestListBackgroundSessions: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.multiplexer.get_all_sessions") + @patch("pr.ui.display.display_multiplexer_status") + def test_list_sessions_success(self, mock_display, mock_get): + mock_get.return_value = {} + list_background_sessions(self.assistant) + + @patch("pr.multiplexer.get_all_sessions") + def test_list_sessions_exception(self, mock_get): + mock_get.side_effect = Exception("test") + list_background_sessions(self.assistant) + + +class TestShowSessionStatus: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.multiplexer.get_session_info") + def test_show_status_found(self, mock_get): + mock_get.return_value = { + "status": "running", + "pid": 123, + "command": "ls", + "start_time": 1234567890.0, + } + show_session_status(self.assistant, "session1") + + @patch("pr.multiplexer.get_session_info") + def test_show_status_not_found(self, mock_get): + mock_get.return_value = None + show_session_status(self.assistant, "session1") + + @patch("pr.multiplexer.get_session_info") + def test_show_status_exception(self, mock_get): + mock_get.side_effect = Exception("test") + show_session_status(self.assistant, "session1") + + +class TestShowSessionOutput: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.multiplexer.get_session_output") + def test_show_output_success(self, mock_get): + mock_get.return_value = ["line1", "line2"] + show_session_output(self.assistant, "session1") + + @patch("pr.multiplexer.get_session_output") + def test_show_output_no_output(self, mock_get): + mock_get.return_value = None + show_session_output(self.assistant, "session1") + + @patch("pr.multiplexer.get_session_output") + def test_show_output_exception(self, mock_get): + mock_get.side_effect = Exception("test") + show_session_output(self.assistant, "session1") + + +class TestSendSessionInput: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.multiplexer.send_input_to_session") + def test_send_input_success(self, mock_send): + mock_send.return_value = {"status": "success"} + send_session_input(self.assistant, "session1", "input") + + @patch("pr.multiplexer.send_input_to_session") + def test_send_input_error(self, mock_send): + mock_send.return_value = {"status": "error", "error": "failed"} + send_session_input(self.assistant, "session1", "input") + + @patch("pr.multiplexer.send_input_to_session") + def test_send_input_exception(self, mock_send): + mock_send.side_effect = Exception("test") + send_session_input(self.assistant, "session1", "input") + + +class TestKillBackgroundSession: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.multiplexer.kill_session") + def test_kill_success(self, mock_kill): + mock_kill.return_value = {"status": "success"} + kill_background_session(self.assistant, "session1") + + @patch("pr.multiplexer.kill_session") + def test_kill_error(self, mock_kill): + mock_kill.return_value = {"status": "error", "error": "failed"} + kill_background_session(self.assistant, "session1") + + @patch("pr.multiplexer.kill_session") + def test_kill_exception(self, mock_kill): + mock_kill.side_effect = Exception("test") + kill_background_session(self.assistant, "session1") + + +class TestShowBackgroundEvents: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.core.background_monitor.get_global_monitor") + def test_show_events_success(self, mock_get): + mock_monitor = Mock() + mock_monitor.get_events.return_value = [{"event": "test"}] + mock_get.return_value = mock_monitor + with patch("pr.ui.display.display_background_event"): + show_background_events(self.assistant) + + @patch("pr.core.background_monitor.get_global_monitor") + def test_show_events_no_events(self, mock_get): + mock_monitor = Mock() + mock_monitor.get_events.return_value = [] + mock_get.return_value = mock_monitor + show_background_events(self.assistant) + + @patch("pr.core.background_monitor.get_global_monitor") + def test_show_events_exception(self, mock_get): + mock_get.side_effect = Exception("test") + show_background_events(self.assistant) diff --git a/tests/test_commands.py.bak b/tests/test_commands.py.bak new file mode 100644 index 0000000..97afbbf --- /dev/null +++ b/tests/test_commands.py.bak @@ -0,0 +1,693 @@ +from unittest.mock import Mock, patch +from pr.commands.handlers import ( + handle_command, + review_file, + refactor_file, + obfuscate_file, + show_workflows, + execute_workflow_command, + execute_agent_task, + show_agents, + collaborate_agents_command, + search_knowledge, + store_knowledge, + show_conversation_history, + show_cache_stats, + clear_caches, + show_system_stats, + handle_background_command, + start_background_session, + list_background_sessions, + show_session_status, + show_session_output, + send_session_input, + kill_background_session, + show_background_events, +) + + +class TestHandleCommand: + def setup_method(self): + self.assistant = Mock() + self.assistant.messages = [{"role": "system", "content": "test"}] + self.assistant.verbose = False + self.assistant.model = "test-model" + self.assistant.model_list_url = "http://test.com" + self.assistant.api_key = "test-key" + + @patch("pr.commands.handlers.run_autonomous_mode") + def test_handle_edit(self, mock_run): + with patch("pr.commands.handlers.RPEditor") as mock_editor: + mock_editor_instance = Mock() + mock_editor.return_value = mock_editor_instance + mock_editor_instance.get_text.return_value = "test task" + handle_command(self.assistant, "/edit test.py") + mock_editor.assert_called_once_with("test.py") + mock_editor_instance.start.assert_called_once() + mock_editor_instance.thread.join.assert_called_once() + mock_run.assert_called_once_with(self.assistant, "test task") + mock_editor_instance.stop.assert_called_once() + + @patch("pr.commands.handlers.run_autonomous_mode") + def test_handle_auto(self, mock_run): + result = handle_command(self.assistant, "/auto test task") + assert result is True + mock_run.assert_called_once_with(self.assistant, "test task") + + def test_handle_auto_no_args(self): + result = handle_command(self.assistant, "/auto") + assert result is True + + def test_handle_exit(self): + result = handle_command(self.assistant, "exit") + assert result is False + + @patch("pr.commands.help_docs.get_full_help") + def test_handle_help(self, mock_help): + mock_help.return_value = "full help" + result = handle_command(self.assistant, "/help") + assert result is True + mock_help.assert_called_once() + + @patch("pr.commands.help_docs.get_workflow_help") + def test_handle_help_workflows(self, mock_help): + mock_help.return_value = "workflow help" + result = handle_command(self.assistant, "/help workflows") + assert result is True + mock_help.assert_called_once() + + def test_handle_reset(self): + self.assistant.messages = [ + {"role": "system", "content": "test"}, + {"role": "user", "content": "hi"}, + ] + result = handle_command(self.assistant, "/reset") + assert result is True + assert self.assistant.messages == [{"role": "system", "content": "test"}] + + def test_handle_dump(self): + result = handle_command(self.assistant, "/dump") + assert result is True + + def test_handle_verbose(self): + result = handle_command(self.assistant, "/verbose") + assert result is True + assert self.assistant.verbose is True + + def test_handle_model_get(self): + result = handle_command(self.assistant, "/model") + assert result is True + + def test_handle_model_set(self): + result = handle_command(self.assistant, "/model new-model") + assert result is True + assert self.assistant.model == "new-model" + + @patch("pr.core.api.list_models") + @patch("pr.core.api.list_models") + def test_handle_models(self, mock_list): + mock_list.return_value = [{"id": "model1"}, {"id": "model2"}] + with patch('pr.commands.handlers.list_models', mock_list): + result = handle_command(self.assistant, "/models") + assert result is True + mock_list.assert_called_once_with("http://test.com", "test-key")ef test_handle_models_error(self, mock_list): + mock_list.return_value = {"error": "test error"} + result = handle_command(self.assistant, "/models") + assert result is True + + @patch("pr.tools.base.get_tools_definition") + @patch("pr.tools.base.get_tools_definition") + def test_handle_tools(self, mock_tools): + mock_tools.return_value = [{"function": {"name": "tool1", "description": "desc"}}] + with patch('pr.commands.handlers.get_tools_definition', mock_tools): + result = handle_command(self.assistant, "/tools") + assert result is True + mock_tools.assert_called_once()ef test_handle_review(self, mock_review): + result = handle_command(self.assistant, "/review test.py") + assert result is True + mock_review.assert_called_once_with(self.assistant, "test.py") + + @patch("pr.commands.handlers.refactor_file") + def test_handle_refactor(self, mock_refactor): + result = handle_command(self.assistant, "/refactor test.py") + assert result is True + mock_refactor.assert_called_once_with(self.assistant, "test.py") + + @patch("pr.commands.handlers.obfuscate_file") + def test_handle_obfuscate(self, mock_obfuscate): + result = handle_command(self.assistant, "/obfuscate test.py") + assert result is True + mock_obfuscate.assert_called_once_with(self.assistant, "test.py") + + @patch("pr.commands.handlers.show_workflows") + def test_handle_workflows(self, mock_show): + result = handle_command(self.assistant, "/workflows") + assert result is True + mock_show.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.execute_workflow_command") + def test_handle_workflow(self, mock_exec): + result = handle_command(self.assistant, "/workflow test") + assert result is True + mock_exec.assert_called_once_with(self.assistant, "test") + + @patch("pr.commands.handlers.execute_agent_task") + def test_handle_agent(self, mock_exec): + result = handle_command(self.assistant, "/agent coding test task") + assert result is True + mock_exec.assert_called_once_with(self.assistant, "coding", "test task") + + def test_handle_agent_no_args(self): + result = handle_command(self.assistant, "/agent") + assert result is None assert result is True + + @patch("pr.commands.handlers.show_agents") + def test_handle_agents(self, mock_show): + result = handle_command(self.assistant, "/agents") + assert result is True + mock_show.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.collaborate_agents_command") + def test_handle_collaborate(self, mock_collab): + result = handle_command(self.assistant, "/collaborate test task") + assert result is True + mock_collab.assert_called_once_with(self.assistant, "test task") + + @patch("pr.commands.handlers.search_knowledge") + def test_handle_knowledge(self, mock_search): + result = handle_command(self.assistant, "/knowledge test query") + assert result is True + mock_search.assert_called_once_with(self.assistant, "test query") + + @patch("pr.commands.handlers.store_knowledge") + def test_handle_remember(self, mock_store): + result = handle_command(self.assistant, "/remember test content") + assert result is True + mock_store.assert_called_once_with(self.assistant, "test content") + + @patch("pr.commands.handlers.show_conversation_history") + def test_handle_history(self, mock_show): + result = handle_command(self.assistant, "/history") + assert result is True + mock_show.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.show_cache_stats") + def test_handle_cache(self, mock_show): + result = handle_command(self.assistant, "/cache") + assert result is True + mock_show.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.clear_caches") + def test_handle_cache_clear(self, mock_clear): + result = handle_command(self.assistant, "/cache clear") + assert result is True + mock_clear.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.show_system_stats") + def test_handle_stats(self, mock_show): + result = handle_command(self.assistant, "/stats") + assert result is True + mock_show.assert_called_once_with(self.assistant) + + @patch("pr.commands.handlers.handle_background_command") + def test_handle_bg(self, mock_bg): + result = handle_command(self.assistant, "/bg list") + assert result is True + mock_bg.assert_called_once_with(self.assistant, "/bg list") + + def test_handle_unknown(self): + result = handle_command(self.assistant, "/unknown") + assert result is None + + +class TestReviewFile: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.tools.read_file") + @patch("pr.core.assistant.process_message") + def test_review_file_success(self, mock_process, mock_read): + mock_read.return_value = {"status": "success", "content": "test content"} + review_file(self.assistant, "test.py") + mock_read.assert_called_once_with("test.py") + mock_process.assert_called_once() + args = mock_process.call_args[0] + assert "Please review this file" in args[1] + + @patch("pr.tools.read_file")ef test_review_file_error(self, mock_read): + mock_read.return_value = {"status": "error", "error": "file not found"} + review_file(self.assistant, "test.py") + mock_read.assert_called_once_with("test.py") + + +class TestRefactorFile: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.tools.read_file") + @patch("pr.core.assistant.process_message") + def test_refactor_file_success(self, mock_process, mock_read): + mock_read.return_value = {"status": "success", "content": "test content"} + refactor_file(self.assistant, "test.py") + mock_process.assert_called_once() + args = mock_process.call_args[0] + assert "Please refactor this code" in args[1] + + @patch("pr.commands.handlers.read_file") + @patch("pr.tools.read_file") mock_read.return_value = {"status": "error", "error": "file not found"} + refactor_file(self.assistant, "test.py") + + +class TestObfuscateFile: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.tools.read_file") + @patch("pr.core.assistant.process_message") + def test_obfuscate_file_success(self, mock_process, mock_read): + mock_read.return_value = {"status": "success", "content": "test content"} + obfuscate_file(self.assistant, "test.py") + mock_process.assert_called_once() + args = mock_process.call_args[0] + assert "Please obfuscate this code" in args[1] + + @patch("pr.commands.handlers.read_file") + def test_obfuscate_file_error(self, mock_read): + mock_read.return_value = {"status": "error", "error": "file not found"} + obfuscate_file(self.assistant, "test.py") + + +class TestShowWorkflows: + def setup_method(self): + self.assistant = Mock() + + def test_show_workflows_no_enhanced(self): + delattr(self.assistant, "enhanced") + show_workflows(self.assistant) + + def test_show_workflows_no_workflows(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_workflow_list.return_value = [] + show_workflows(self.assistant) + + def test_show_workflows_with_workflows(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_workflow_list.return_value = [ + {"name": "wf1", "description": "desc1", "execution_count": 5} + ] + show_workflows(self.assistant) + + +class TestExecuteWorkflowCommand: + def setup_method(self): + self.assistant = Mock() + + def test_execute_workflow_no_enhanced(self): + delattr(self.assistant, "enhanced") + execute_workflow_command(self.assistant, "test") + + def test_execute_workflow_success(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.execute_workflow.return_value = { + "execution_id": "123", + "results": {"key": "value"}, + } + execute_workflow_command(self.assistant, "test") + + def test_execute_workflow_error(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.execute_workflow.return_value = {"error": "test error"} + execute_workflow_command(self.assistant, "test") + + +class TestExecuteAgentTask: + def setup_method(self): + self.assistant = Mock() + + def test_execute_agent_task_no_enhanced(self): + delattr(self.assistant, "enhanced") + execute_agent_task(self.assistant, "coding", "task") + + def test_execute_agent_task_success(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.create_agent.return_value = "agent123" + self.assistant.enhanced.agent_task.return_value = {"response": "done"} + execute_agent_task(self.assistant, "coding", "task") + + def test_execute_agent_task_error(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.create_agent.return_value = "agent123" + self.assistant.enhanced.agent_task.return_value = {"error": "test error"} + execute_agent_task(self.assistant, "coding", "task") + + +class TestShowAgents: + def setup_method(self): + self.assistant = Mock() + + def test_show_agents_no_enhanced(self): + delattr(self.assistant, "enhanced") + show_agents(self.assistant) + + def test_show_agents_with_agents(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_agent_summary.return_value = { + "active_agents": 2, + "agents": [{"agent_id": "a1", "role": "coding", "task_count": 3, "message_count": 10}], + } + show_agents(self.assistant) + + +class TestCollaborateAgentsCommand: + def setup_method(self): + self.assistant = Mock() + + def test_collaborate_no_enhanced(self): + delattr(self.assistant, "enhanced") + collaborate_agents_command(self.assistant, "task") + + def test_collaborate_success(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.collaborate_agents.return_value = { + "orchestrator": {"response": "orchestrator response"}, + "agents": [{"role": "coding", "response": "coding response"}], + } + collaborate_agents_command(self.assistant, "task") + + +class TestSearchKnowledge: + def setup_method(self): + self.assistant = Mock() + + def test_search_knowledge_no_enhanced(self): + delattr(self.assistant, "enhanced") + search_knowledge(self.assistant, "query") + + def test_search_knowledge_no_results(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.search_knowledge.return_value = [] + search_knowledge(self.assistant, "query") + + def test_search_knowledge_with_results(self): + self.assistant.enhanced = Mock() + mock_entry = Mock() + mock_entry.category = "general" + mock_entry.content = "long content here" + mock_entry.access_count = 5 + self.assistant.enhanced.search_knowledge.return_value = [mock_entry] + search_knowledge(self.assistant, "query") + + +class TestStoreKnowledge: + def setup_method(self): + self.assistant = Mock() + + def test_store_knowledge_no_enhanced(self): + delattr(self.assistant, "enhanced") + store_knowledge(self.assistant, "content") + + @patch("pr.memory.KnowledgeEntry") + def test_store_knowledge_success(self, mock_entry): + self.assistant.enhanced = Mock() + self.assistant.enhanced.fact_extractor.categorize_content.return_value = ["general"] + self.assistant.enhanced.knowledge_store = Mock() + store_knowledge(self.assistant, "content") + mock_entry.assert_called_once() + + +class TestShowConversationHistory: + def setup_method(self): + self.assistant = Mock() + + def test_show_history_no_enhanced(self): + delattr(self.assistant, "enhanced") + show_conversation_history(self.assistant) + + def test_show_history_no_history(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_conversation_history.return_value = [] + show_conversation_history(self.assistant) + + def test_show_history_with_history(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_conversation_history.return_value = [ + { + "conversation_id": "conv1", + "started_at": 1234567890, + "message_count": 5, + "summary": "test summary", + "topics": ["topic1", "topic2"], + } + ] + show_conversation_history(self.assistant) + + +class TestShowCacheStats: + def setup_method(self): + self.assistant = Mock() + + def test_show_cache_stats_no_enhanced(self): + delattr(self.assistant, "enhanced") + show_cache_stats(self.assistant) + + def test_show_cache_stats_with_stats(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_cache_statistics.return_value = { + "api_cache": { + "total_entries": 10, + "valid_entries": 8, + "expired_entries": 2, + "total_cached_tokens": 1000, + "total_cache_hits": 50, + }, + "tool_cache": { + "total_entries": 5, + "valid_entries": 5, + "total_cache_hits": 20, + "by_tool": {"tool1": {"cached_entries": 3, "total_hits": 10}}, + }, + } + show_cache_stats(self.assistant) + + +class TestClearCaches: + def setup_method(self): + self.assistant = Mock() + + def test_clear_caches_no_enhanced(self): + delattr(self.assistant, "enhanced") + clear_caches(self.assistant) + + def test_clear_caches_success(self): + self.assistant.enhanced = Mock() + clear_caches(self.assistant) + self.assistant.enhanced.clear_caches.assert_called_once() + + +class TestShowSystemStats: + def setup_method(self): + self.assistant = Mock() + + def test_show_system_stats_no_enhanced(self): + delattr(self.assistant, "enhanced") + show_system_stats(self.assistant) + + def test_show_system_stats_success(self): + self.assistant.enhanced = Mock() + self.assistant.enhanced.get_cache_statistics.return_value = { + "api_cache": {"valid_entries": 10}, + "tool_cache": {"valid_entries": 5}, + } + self.assistant.enhanced.get_knowledge_statistics.return_value = { + "total_entries": 100, + "total_categories": 5, + "total_accesses": 200, + "vocabulary_size": 1000, + } + self.assistant.enhanced.get_agent_summary.return_value = {"active_agents": 3} + show_system_stats(self.assistant) + + +class TestHandleBackgroundCommand: + def setup_method(self): + self.assistant = Mock() + + def test_handle_bg_no_args(self): + handle_background_command(self.assistant, "/bg") + + @patch("pr.commands.handlers.start_background_session") + def test_handle_bg_start(self, mock_start): + handle_background_command(self.assistant, "/bg start ls -la") + + @patch("pr.commands.handlers.list_background_sessions") + def test_handle_bg_list(self, mock_list): + handle_background_command(self.assistant, "/bg list") + + @patch("pr.commands.handlers.show_session_status") + def test_handle_bg_status(self, mock_status): + handle_background_command(self.assistant, "/bg status session1") + + @patch("pr.commands.handlers.show_session_output") + def test_handle_bg_output(self, mock_output): + handle_background_command(self.assistant, "/bg output session1") + + @patch("pr.commands.handlers.send_session_input") + def test_handle_bg_input(self, mock_input): + handle_background_command(self.assistant, "/bg input session1 test input") + + @patch("pr.commands.handlers.kill_background_session") + def test_handle_bg_kill(self, mock_kill): + handle_background_command(self.assistant, "/bg kill session1") + + @patch("pr.commands.handlers.show_background_events") + def test_handle_bg_events(self, mock_events): + handle_background_command(self.assistant, "/bg events") + + def test_handle_bg_unknown(self): + handle_background_command(self.assistant, "/bg unknown") + + +class TestStartBackgroundSession: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.commands.handlers.start_background_process") + def test_start_background_success(self, mock_start): + mock_start.return_value = {"status": "success", "pid": 123} + start_background_session(self.assistant, "session1", "ls -la") + + @patch("pr.commands.handlers.start_background_process") + def test_start_background_error(self, mock_start): + mock_start.return_value = {"status": "error", "error": "failed"} + start_background_session(self.assistant, "session1", "ls -la") + + @patch("pr.commands.handlers.start_background_process") + def test_start_background_exception(self, mock_start): + mock_start.side_effect = Exception("test") + start_background_session(self.assistant, "session1", "ls -la") + + +class TestListBackgroundSessions: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.commands.handlers.get_all_sessions") + @patch("pr.commands.handlers.display_multiplexer_status") + def test_list_sessions_success(self, mock_display, mock_get): + mock_get.return_value = {} + list_background_sessions(self.assistant) + + @patch("pr.commands.handlers.get_all_sessions") + def test_list_sessions_exception(self, mock_get): + mock_get.side_effect = Exception("test") + list_background_sessions(self.assistant) + + +class TestShowSessionStatus: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.commands.handlers.get_session_info") + def test_show_status_found(self, mock_get): + mock_get.return_value = { + "status": "running", + "pid": 123, + "command": "ls", + "start_time": 1234567890.0, + } + show_session_status(self.assistant, "session1") + + @patch("pr.commands.handlers.get_session_info") + def test_show_status_not_found(self, mock_get): + mock_get.return_value = None + show_session_status(self.assistant, "session1") + + @patch("pr.commands.handlers.get_session_info") + def test_show_status_exception(self, mock_get): + mock_get.side_effect = Exception("test") + show_session_status(self.assistant, "session1") + + +class TestShowSessionOutput: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.commands.handlers.get_session_output") + def test_show_output_success(self, mock_get): + mock_get.return_value = ["line1", "line2"] + show_session_output(self.assistant, "session1") + + @patch("pr.commands.handlers.get_session_output") + def test_show_output_no_output(self, mock_get): + mock_get.return_value = None + show_session_output(self.assistant, "session1") + + @patch("pr.commands.handlers.get_session_output") + def test_show_output_exception(self, mock_get): + mock_get.side_effect = Exception("test") + show_session_output(self.assistant, "session1") + + +class TestSendSessionInput: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.commands.handlers.send_input_to_session") + def test_send_input_success(self, mock_send): + mock_send.return_value = {"status": "success"} + send_session_input(self.assistant, "session1", "input") + + @patch("pr.commands.handlers.send_input_to_session") + def test_send_input_error(self, mock_send): + mock_send.return_value = {"status": "error", "error": "failed"} + send_session_input(self.assistant, "session1", "input") + + @patch("pr.commands.handlers.send_input_to_session") + def test_send_input_exception(self, mock_send): + mock_send.side_effect = Exception("test") + send_session_input(self.assistant, "session1", "input") + + +class TestKillBackgroundSession: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.commands.handlers.kill_session") + def test_kill_success(self, mock_kill): + mock_kill.return_value = {"status": "success"} + kill_background_session(self.assistant, "session1") + + @patch("pr.commands.handlers.kill_session") + def test_kill_error(self, mock_kill): + mock_kill.return_value = {"status": "error", "error": "failed"} + kill_background_session(self.assistant, "session1") + + @patch("pr.commands.handlers.kill_session") + def test_kill_exception(self, mock_kill): + mock_kill.side_effect = Exception("test") + kill_background_session(self.assistant, "session1") + + +class TestShowBackgroundEvents: + def setup_method(self): + self.assistant = Mock() + + @patch("pr.commands.handlers.get_global_monitor") + def test_show_events_success(self, mock_get): + mock_monitor = Mock() + mock_monitor.get_pending_events.return_value = [{"event": "test"}] + mock_get.return_value = mock_monitor + with patch("pr.commands.handlers.display_background_event"): + show_background_events(self.assistant) + + @patch("pr.commands.handlers.get_global_monitor") + def test_show_events_no_events(self, mock_get): + mock_monitor = Mock() + mock_monitor.get_pending_events.return_value = [] + mock_get.return_value = mock_monitor + show_background_events(self.assistant) + + @patch("pr.commands.handlers.get_global_monitor") + def test_show_events_exception(self, mock_get): + mock_get.side_effect = Exception("test") + show_background_events(self.assistant) \ No newline at end of file diff --git a/tests/test_enhanced_assistant.py b/tests/test_enhanced_assistant.py index 29f1d5d..64e3466 100644 --- a/tests/test_enhanced_assistant.py +++ b/tests/test_enhanced_assistant.py @@ -77,9 +77,9 @@ def test_get_cache_statistics(): mock_base = MagicMock() assistant = EnhancedAssistant(mock_base) assistant.api_cache = MagicMock() - assistant.api_cache.get_statistics.return_value = {"hits": 10} + assistant.api_cache.get_statistics.return_value = {"total_cache_hits": 10} assistant.tool_cache = MagicMock() - assistant.tool_cache.get_statistics.return_value = {"misses": 5} + assistant.tool_cache.get_statistics.return_value = {"total_cache_hits": 5} stats = assistant.get_cache_statistics() assert "api_cache" in stats diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000..024e120 --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,61 @@ +import pytest +from pr.core.exceptions import ( + PRException, + APIException, + APIConnectionError, + APITimeoutError, + APIResponseError, + ConfigurationError, + ToolExecutionError, + FileSystemError, + SessionError, + ContextError, + ValidationError, +) + + +class TestExceptions: + def test_pre_exception(self): + with pytest.raises(PRException): + raise PRException("test") + + def test_api_exception(self): + with pytest.raises(APIException): + raise APIException("test") + + def test_api_connection_error(self): + with pytest.raises(APIConnectionError): + raise APIConnectionError("test") + + def test_api_timeout_error(self): + with pytest.raises(APITimeoutError): + raise APITimeoutError("test") + + def test_api_response_error(self): + with pytest.raises(APIResponseError): + raise APIResponseError("test") + + def test_configuration_error(self): + with pytest.raises(ConfigurationError): + raise ConfigurationError("test") + + def test_tool_execution_error(self): + error = ToolExecutionError("test_tool", "test message") + assert error.tool_name == "test_tool" + assert str(error) == "Error executing tool 'test_tool': test message" + + def test_file_system_error(self): + with pytest.raises(FileSystemError): + raise FileSystemError("test") + + def test_session_error(self): + with pytest.raises(SessionError): + raise SessionError("test") + + def test_context_error(self): + with pytest.raises(ContextError): + raise ContextError("test") + + def test_validation_error(self): + with pytest.raises(ValidationError): + raise ValidationError("test") diff --git a/tests/test_help_docs.py b/tests/test_help_docs.py new file mode 100644 index 0000000..f0e9e67 --- /dev/null +++ b/tests/test_help_docs.py @@ -0,0 +1,46 @@ +from pr.commands.help_docs import ( + get_workflow_help, + get_agent_help, + get_knowledge_help, + get_cache_help, + get_background_help, + get_full_help, +) + + +class TestHelpDocs: + def test_get_workflow_help(self): + result = get_workflow_help() + assert isinstance(result, str) + assert "WORKFLOWS" in result + assert "AUTOMATED TASK EXECUTION" in result + + def test_get_agent_help(self): + result = get_agent_help() + assert isinstance(result, str) + assert "AGENTS" in result + assert "SPECIALIZED AI ASSISTANTS" in result + + def test_get_knowledge_help(self): + result = get_knowledge_help() + assert isinstance(result, str) + assert "KNOWLEDGE BASE" in result + assert "PERSISTENT INFORMATION STORAGE" in result + + def test_get_cache_help(self): + result = get_cache_help() + assert isinstance(result, str) + assert "CACHING SYSTEM" in result + assert "PERFORMANCE OPTIMIZATION" in result + + def test_get_background_help(self): + result = get_background_help() + assert isinstance(result, str) + assert "BACKGROUND SESSIONS" in result + assert "CONCURRENT TASK EXECUTION" in result + + def test_get_full_help(self): + result = get_full_help() + assert isinstance(result, str) + assert "R - PROFESSIONAL AI ASSISTANT" in result + assert "BASIC COMMANDS" in result diff --git a/tests/test_logging.py b/tests/test_logging.py index 5336aea..b83ef02 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,25 +1,77 @@ -from pr.core.logging import get_logger, setup_logging +from unittest.mock import patch, MagicMock +from pr.core.logging import setup_logging, get_logger -def test_setup_logging_basic(): - logger = setup_logging(verbose=False) - assert logger.name == "pr" - assert logger.level == 20 # INFO +class TestLogging: + @patch("pr.core.logging.os.makedirs") + @patch("pr.core.logging.os.path.dirname") + @patch("pr.core.logging.os.path.exists") + @patch("pr.core.logging.RotatingFileHandler") + @patch("pr.core.logging.logging.getLogger") + def test_setup_logging_basic( + self, mock_get_logger, mock_handler, mock_exists, mock_dirname, mock_makedirs + ): + mock_exists.return_value = False + mock_dirname.return_value = "/tmp/logs" + mock_logger = MagicMock() + mock_get_logger.return_value = mock_logger + mock_logger.handlers = [] + result = setup_logging(verbose=False) -def test_setup_logging_verbose(): - logger = setup_logging(verbose=True) - assert logger.name == "pr" - assert logger.level == 10 # DEBUG - # Should have console handler - assert len(logger.handlers) >= 2 + mock_makedirs.assert_called_once_with("/tmp/logs", exist_ok=True) + mock_get_logger.assert_called_once_with("pr") + mock_logger.setLevel.assert_called_once_with(20) # INFO level + mock_handler.assert_called_once() + assert result == mock_logger + @patch("pr.core.logging.os.makedirs") + @patch("pr.core.logging.os.path.dirname") + @patch("pr.core.logging.os.path.exists") + @patch("pr.core.logging.RotatingFileHandler") + @patch("pr.core.logging.logging.StreamHandler") + @patch("pr.core.logging.logging.getLogger") + def test_setup_logging_verbose( + self, + mock_get_logger, + mock_stream_handler, + mock_file_handler, + mock_exists, + mock_dirname, + mock_makedirs, + ): + mock_exists.return_value = True + mock_dirname.return_value = "/tmp/logs" + mock_logger = MagicMock() + mock_get_logger.return_value = mock_logger + mock_logger.handlers = MagicMock() -def test_get_logger_default(): - logger = get_logger() - assert logger.name == "pr" + result = setup_logging(verbose=True) + mock_makedirs.assert_not_called() + mock_get_logger.assert_called_once_with("pr") + mock_logger.setLevel.assert_called_once_with(10) # DEBUG level + mock_logger.handlers.clear.assert_called_once() + mock_file_handler.assert_called_once() + mock_stream_handler.assert_called_once() + assert result == mock_logger -def test_get_logger_named(): - logger = get_logger("test") - assert logger.name == "pr.test" + @patch("pr.core.logging.logging.getLogger") + def test_get_logger_default(self, mock_get_logger): + mock_logger = MagicMock() + mock_get_logger.return_value = mock_logger + + result = get_logger() + + mock_get_logger.assert_called_once_with("pr") + assert result == mock_logger + + @patch("pr.core.logging.logging.getLogger") + def test_get_logger_named(self, mock_get_logger): + mock_logger = MagicMock() + mock_get_logger.return_value = mock_logger + + result = get_logger("test") + + mock_get_logger.assert_called_once_with("pr.test") + assert result == mock_logger diff --git a/tests/test_multiplexer_commands.py b/tests/test_multiplexer_commands.py new file mode 100644 index 0000000..9861e6a --- /dev/null +++ b/tests/test_multiplexer_commands.py @@ -0,0 +1,228 @@ +from unittest.mock import Mock, patch +from pr.commands.multiplexer_commands import ( + show_sessions, + attach_session, + detach_session, + kill_session, + send_command, + show_session_log, + show_session_status, + list_waiting_sessions, +) + + +class TestShowSessions: + @patch("pr.commands.multiplexer_commands.list_active_sessions") + @patch("pr.commands.multiplexer_commands.get_session_status") + def test_show_sessions_no_sessions(self, mock_status, mock_list): + mock_list.return_value = {} + show_sessions() + mock_list.assert_called_once() + + @patch("pr.commands.multiplexer_commands.list_active_sessions") + @patch("pr.commands.multiplexer_commands.get_session_status") + def test_show_sessions_with_sessions(self, mock_status, mock_list): + mock_list.return_value = { + "session1": { + "metadata": { + "process_type": "test", + "start_time": 123.0, + "interaction_count": 5, + "state": "running", + }, + "output_summary": {"stdout_lines": 10, "stderr_lines": 2}, + } + } + mock_status.return_value = {"is_active": True, "pid": 123} + show_sessions() + mock_list.assert_called_once() + mock_status.assert_called_once_with("session1") + + +class TestAttachSession: + def test_attach_session_no_args(self): + attach_session([]) + + @patch("pr.commands.multiplexer_commands.get_session_status") + def test_attach_session_not_found(self, mock_status): + mock_status.return_value = None + attach_session(["session1"]) + + @patch("pr.commands.multiplexer_commands.get_session_status") + @patch("pr.commands.multiplexer_commands.read_session_output") + def test_attach_session_success(self, mock_read, mock_status): + mock_status.return_value = {"is_active": True, "metadata": {"process_type": "test"}} + mock_read.return_value = {"stdout": "line1\nline2", "stderr": ""} + attach_session(["session1"]) + + @patch("pr.commands.multiplexer_commands.get_session_status") + @patch("pr.commands.multiplexer_commands.read_session_output") + def test_attach_session_with_stderr(self, mock_read, mock_status): + mock_status.return_value = {"is_active": False, "metadata": {"process_type": "test"}} + mock_read.return_value = {"stdout": "", "stderr": "error1\nerror2"} + attach_session(["session1"]) + + @patch("pr.commands.multiplexer_commands.get_session_status") + @patch("pr.commands.multiplexer_commands.read_session_output") + def test_attach_session_read_error(self, mock_read, mock_status): + mock_status.return_value = {"is_active": True, "metadata": {"process_type": "test"}} + mock_read.side_effect = Exception("test error") + attach_session(["session1"]) + + +class TestDetachSession: + def test_detach_session_no_args(self): + detach_session([]) + + @patch("pr.commands.multiplexer_commands.get_multiplexer") + def test_detach_session_not_found(self, mock_get): + mock_get.return_value = None + detach_session(["session1"]) + + @patch("pr.commands.multiplexer_commands.get_multiplexer") + def test_detach_session_success(self, mock_get): + mock_mux = Mock() + mock_get.return_value = mock_mux + detach_session(["session1"]) + assert mock_mux.show_output is False + + +class TestKillSession: + def test_kill_session_no_args(self): + kill_session([]) + + @patch("pr.commands.multiplexer_commands.close_interactive_session") + def test_kill_session_success(self, mock_close): + kill_session(["session1"]) + mock_close.assert_called_once_with("session1") + + @patch("pr.commands.multiplexer_commands.close_interactive_session") + def test_kill_session_error(self, mock_close): + mock_close.side_effect = Exception("test error") + kill_session(["session1"]) + + +class TestSendCommand: + def test_send_command_no_args(self): + send_command([]) + + def test_send_command_insufficient_args(self): + send_command(["session1"]) + + @patch("pr.commands.multiplexer_commands.send_input_to_session") + def test_send_command_success(self, mock_send): + send_command(["session1", "ls", "-la"]) + mock_send.assert_called_once_with("session1", "ls -la") + + @patch("pr.commands.multiplexer_commands.send_input_to_session") + def test_send_command_error(self, mock_send): + mock_send.side_effect = Exception("test error") + send_command(["session1", "ls"]) + + +class TestShowSessionLog: + def test_show_session_log_no_args(self): + show_session_log([]) + + @patch("pr.commands.multiplexer_commands.read_session_output") + def test_show_session_log_success(self, mock_read): + mock_read.return_value = {"stdout": "stdout content", "stderr": "stderr content"} + show_session_log(["session1"]) + + @patch("pr.commands.multiplexer_commands.read_session_output") + def test_show_session_log_no_stderr(self, mock_read): + mock_read.return_value = {"stdout": "stdout content", "stderr": ""} + show_session_log(["session1"]) + + @patch("pr.commands.multiplexer_commands.read_session_output") + def test_show_session_log_error(self, mock_read): + mock_read.side_effect = Exception("test error") + show_session_log(["session1"]) + + +class TestShowSessionStatus: + def test_show_session_status_no_args(self): + show_session_status([]) + + @patch("pr.commands.multiplexer_commands.get_session_status") + def test_show_session_status_not_found(self, mock_status): + mock_status.return_value = None + show_session_status(["session1"]) + + @patch("pr.commands.multiplexer_commands.get_session_status") + @patch("pr.commands.multiplexer_commands.get_global_detector") + def test_show_session_status_success(self, mock_detector, mock_status): + mock_status.return_value = { + "is_active": True, + "pid": 123, + "metadata": { + "process_type": "test", + "start_time": 123.0, + "last_activity": 456.0, + "interaction_count": 5, + "state": "running", + }, + "output_summary": {"stdout_lines": 10, "stderr_lines": 2}, + } + mock_detector_instance = Mock() + mock_detector_instance.get_session_info.return_value = { + "current_state": "waiting", + "is_waiting": True, + } + mock_detector.return_value = mock_detector_instance + show_session_status(["session1"]) + + @patch("pr.commands.multiplexer_commands.get_session_status") + @patch("pr.commands.multiplexer_commands.get_global_detector") + def test_show_session_status_no_detector_info(self, mock_detector, mock_status): + mock_status.return_value = { + "is_active": False, + "metadata": { + "process_type": "test", + "start_time": 123.0, + "last_activity": 456.0, + "interaction_count": 5, + "state": "running", + }, + "output_summary": {"stdout_lines": 10, "stderr_lines": 2}, + } + mock_detector_instance = Mock() + mock_detector_instance.get_session_info.return_value = None + mock_detector.return_value = mock_detector_instance + show_session_status(["session1"]) + + +class TestListWaitingSessions: + @patch("pr.commands.multiplexer_commands.list_active_sessions") + @patch("pr.commands.multiplexer_commands.get_global_detector") + def test_list_waiting_sessions_no_sessions(self, mock_detector, mock_list): + mock_list.return_value = {} + mock_detector_instance = Mock() + mock_detector_instance.is_waiting_for_input.return_value = False + mock_detector.return_value = mock_detector_instance + list_waiting_sessions() + + @patch("pr.commands.multiplexer_commands.list_active_sessions") + @patch("pr.commands.multiplexer_commands.get_session_status") + @patch("pr.commands.multiplexer_commands.get_global_detector") + def test_list_waiting_sessions_with_waiting(self, mock_detector, mock_status, mock_list): + mock_list.return_value = ["session1"] + mock_detector_instance = Mock() + mock_detector_instance.is_waiting_for_input.return_value = True + mock_detector_instance.get_session_info.return_value = { + "current_state": "waiting", + "is_waiting": True, + } + mock_detector_instance.get_response_suggestions.return_value = ["yes", "no", "quit"] + mock_detector.return_value = mock_detector_instance + mock_status.return_value = {"metadata": {"process_type": "test"}} + list_waiting_sessions() + + @patch("pr.commands.multiplexer_commands.list_active_sessions") + @patch("pr.commands.multiplexer_commands.get_global_detector") + def test_list_waiting_sessions_no_waiting(self, mock_detector, mock_list): + mock_list.return_value = ["session1"] + mock_detector_instance = Mock() + mock_detector_instance.is_waiting_for_input.return_value = False + mock_detector.return_value = mock_detector_instance + list_waiting_sessions() diff --git a/tests/test_tools.py b/tests/test_tools.py index 5854c4b..e6e1790 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1,8 +1,12 @@ import os +import tempfile from pr.tools.base import get_tools_definition -from pr.tools.filesystem import list_directory, read_file, search_replace, write_file +from pr.tools.command import run_command +from pr.tools.filesystem import chdir, getpwd, list_directory, read_file, search_replace, write_file +from pr.tools.interactive_control import start_interactive_session from pr.tools.patch import apply_patch, create_diff +from pr.tools.python_exec import python_exec class TestFilesystemTools: @@ -43,6 +47,54 @@ class TestFilesystemTools: read_result = read_file(filepath) assert "Hello, Universe!" in read_result["content"] + def test_chdir_and_getpwd(self): + original_cwd = getpwd()["path"] + try: + with tempfile.TemporaryDirectory() as temp_dir: + result = chdir(temp_dir) + assert result["status"] == "success" + assert getpwd()["path"] == temp_dir + finally: + chdir(original_cwd) + + +class TestCommandTools: + + def test_run_command_with_cwd(self): + with tempfile.TemporaryDirectory() as temp_dir: + result = run_command("pwd", cwd=temp_dir) + assert result["status"] == "success" + assert temp_dir in result["stdout"].strip() + + def test_run_command_basic(self): + result = run_command("echo hello") + assert result["status"] == "success" + assert "hello" in result["stdout"] + + +class TestInteractiveSessionTools: + + def test_start_interactive_session_with_cwd(self): + with tempfile.TemporaryDirectory() as temp_dir: + session_name = start_interactive_session("pwd", cwd=temp_dir) + assert session_name is not None + # Note: In a real test, we'd need to interact with the session, but for now just check it starts + + +class TestPythonExecTools: + + def test_python_exec_with_cwd(self): + with tempfile.TemporaryDirectory() as temp_dir: + code = "import os; print(os.getcwd())" + result = python_exec(code, {}, cwd=temp_dir) + assert result["status"] == "success" + assert temp_dir in result["output"].strip() + + def test_python_exec_basic(self): + result = python_exec("print('hello')", {}) + assert result["status"] == "success" + assert "hello" in result["output"] + class TestPatchTools: @@ -108,6 +160,21 @@ class TestToolDefinitions: assert "write_file" in tool_names assert "list_directory" in tool_names assert "search_replace" in tool_names + assert "chdir" in tool_names + assert "getpwd" in tool_names + + def test_command_tools_present(self): + tools = get_tools_definition() + tool_names = [t["function"]["name"] for t in tools] + + assert "run_command" in tool_names + assert "start_interactive_session" in tool_names + + def test_python_exec_present(self): + tools = get_tools_definition() + tool_names = [t["function"]["name"] for t in tools] + + assert "python_exec" in tool_names def test_patch_tools_present(self): tools = get_tools_definition() diff --git a/tests/test_ui_output.py b/tests/test_ui_output.py new file mode 100644 index 0000000..79ae9a2 --- /dev/null +++ b/tests/test_ui_output.py @@ -0,0 +1,153 @@ +import json +from unittest.mock import patch +from pr.ui.output import OutputFormatter + + +class TestOutputFormatter: + def test_init(self): + formatter = OutputFormatter() + assert formatter.format_type == "text" + assert formatter.quiet is False + + formatter = OutputFormatter("json", True) + assert formatter.format_type == "json" + assert formatter.quiet is True + + @patch("builtins.print") + def test_output_text_quiet(self, mock_print): + formatter = OutputFormatter(quiet=True) + formatter.output("test", "response") + mock_print.assert_not_called() + + @patch("builtins.print") + def test_output_text_error_quiet(self, mock_print): + formatter = OutputFormatter(quiet=True) + formatter.output("test", "error") + mock_print.assert_called() + + @patch("builtins.print") + def test_output_text_result_quiet(self, mock_print): + formatter = OutputFormatter(quiet=True) + formatter.output("test", "result") + mock_print.assert_called() + + @patch("builtins.print") + def test_output_json(self, mock_print): + formatter = OutputFormatter("json") + formatter.output("test data", "response") + args = mock_print.call_args[0][0] + data = json.loads(args) + assert data["type"] == "response" + assert data["data"] == "test data" + assert "timestamp" in data + + @patch("builtins.print") + def test_output_structured_dict(self, mock_print): + formatter = OutputFormatter("structured") + formatter.output({"key": "value", "key2": "value2"}, "response") + assert mock_print.call_count == 2 + calls = [call[0][0] for call in mock_print.call_args_list] + assert "key: value" in calls + assert "key2: value2" in calls + + @patch("builtins.print") + def test_output_structured_list(self, mock_print): + formatter = OutputFormatter("structured") + formatter.output(["item1", "item2"], "response") + assert mock_print.call_count == 2 + calls = [call[0][0] for call in mock_print.call_args_list] + assert "- item1" in calls + assert "- item2" in calls + + @patch("builtins.print") + def test_output_structured_other(self, mock_print): + formatter = OutputFormatter("structured") + formatter.output("plain text", "response") + mock_print.assert_called_once_with("plain text") + + @patch("builtins.print") + def test_output_text_dict(self, mock_print): + formatter = OutputFormatter("text") + formatter.output({"key": "value"}, "response") + args = mock_print.call_args[0][0] + data = json.loads(args) + assert data == {"key": "value"} + + @patch("builtins.print") + def test_output_text_list(self, mock_print): + formatter = OutputFormatter("text") + formatter.output(["item1"], "response") + args = mock_print.call_args[0][0] + data = json.loads(args) + assert data == ["item1"] + + @patch("builtins.print") + def test_output_text_other(self, mock_print): + formatter = OutputFormatter("text") + formatter.output("plain text", "response") + mock_print.assert_called_once_with("plain text") + + @patch("builtins.print") + def test_error_json(self, mock_print): + formatter = OutputFormatter("json") + formatter.error("test error") + args = mock_print.call_args[0][0] + data = json.loads(args) + assert data["type"] == "error" + assert data["data"]["error"] == "test error" + + @patch("sys.stderr") + def test_error_text(self, mock_stderr): + formatter = OutputFormatter("text") + formatter.error("test error") + mock_stderr.write.assert_called() + calls = mock_stderr.write.call_args_list + assert any("Error: test error" in call[0][0] for call in calls) + + @patch("builtins.print") + def test_success_json(self, mock_print): + formatter = OutputFormatter("json") + formatter.success("test success") + args = mock_print.call_args[0][0] + data = json.loads(args) + assert data["type"] == "success" + assert data["data"]["success"] == "test success" + + @patch("builtins.print") + def test_success_text(self, mock_print): + formatter = OutputFormatter("text") + formatter.success("test success") + mock_print.assert_called_once_with("test success") + + @patch("builtins.print") + def test_success_quiet(self, mock_print): + formatter = OutputFormatter("text", quiet=True) + formatter.success("test success") + mock_print.assert_not_called() + + @patch("builtins.print") + def test_info_json(self, mock_print): + formatter = OutputFormatter("json") + formatter.info("test info") + args = mock_print.call_args[0][0] + data = json.loads(args) + assert data["type"] == "info" + assert data["data"]["info"] == "test info" + + @patch("builtins.print") + def test_info_text(self, mock_print): + formatter = OutputFormatter("text") + formatter.info("test info") + mock_print.assert_called_once_with("test info") + + @patch("builtins.print") + def test_info_quiet(self, mock_print): + formatter = OutputFormatter("text", quiet=True) + formatter.info("test info") + mock_print.assert_not_called() + + @patch("builtins.print") + def test_result(self, mock_print): + formatter = OutputFormatter("text") + formatter.result("test result") + mock_print.assert_called_once_with("test result")