maintenance: update config paths and agent communication

This commit is contained in:
retoor 2025-11-05 15:34:23 +01:00
parent 85808f10a8
commit 27bcc7409e
32 changed files with 2598 additions and 340 deletions

View File

@ -67,10 +67,13 @@ backup:
zip -r rp.zip * zip -r rp.zip *
mv rp.zip ../ mv rp.zip ../
implode: serve:
python ../implode/imply.py rp.py rpserver
mv imploded.py /home/retoor/bin/rp
chmod +x /home/retoor/bin/rp
rp --debug 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 .DEFAULT_GOAL := help

View File

@ -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() self.conn.commit()
def send_message(self, message: AgentMessage, session_id: Optional[str] = None): def send_message(self, message: AgentMessage, session_id: Optional[str] = None):

View File

@ -1,4 +1,3 @@
import json
import time import time
import uuid import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -169,7 +168,7 @@ Break down the task and delegate subtasks to appropriate agents. Coordinate thei
return results return results
def get_session_summary(self) -> str: def get_session_summary(self) -> Dict[str, Any]:
summary = { summary = {
"session_id": self.session_id, "session_id": self.session_id,
"active_agents": len(self.active_agents), "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() for agent_id, agent in self.active_agents.items()
], ],
} }
return json.dumps(summary) return summary
def clear_session(self): def clear_session(self):
self.active_agents.clear() self.active_agents.clear()

View File

@ -16,6 +16,10 @@ def run_autonomous_mode(assistant, task):
logger.debug(f"=== AUTONOMOUS MODE START ===") logger.debug(f"=== AUTONOMOUS MODE START ===")
logger.debug(f"Task: {task}") 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}"}) assistant.messages.append({"role": "user", "content": f"{task}"})
try: try:
@ -94,9 +98,14 @@ def process_response_autonomous(assistant, response):
arguments = json.loads(tool_call["function"]["arguments"]) arguments = json.loads(tool_call["function"]["arguments"])
result = execute_single_tool(assistant, func_name, 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" status = "success" if result.get("status") == "success" else "error"
result = truncate_tool_result(result)
display_tool_call(func_name, arguments, status, result) display_tool_call(func_name, arguments, status, result)
tool_results.append( tool_results.append(
@ -141,9 +150,9 @@ def execute_single_tool(assistant, func_name, arguments):
db_get, db_get,
db_query, db_query,
db_set, db_set,
editor_insert_text, # editor_insert_text,
editor_replace_text, # editor_replace_text,
editor_search, # editor_search,
getpwd, getpwd,
http_fetch, http_fetch,
index_source_directory, index_source_directory,

34
pr/cache/api_cache.py vendored
View File

@ -22,7 +22,8 @@ class APICache:
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL, expires_at INTEGER NOT NULL,
model TEXT, 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) CREATE INDEX IF NOT EXISTS idx_expires_at ON api_cache(expires_at)
""" """
) )
# 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.commit()
conn.close() conn.close()
def _generate_cache_key( def _generate_cache_key(
@ -64,10 +72,21 @@ class APICache:
) )
row = cursor.fetchone() row = cursor.fetchone()
conn.close()
if row: 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]) return json.loads(row[0])
conn.close()
return None return None
def set( def set(
@ -90,8 +109,8 @@ class APICache:
cursor.execute( cursor.execute(
""" """
INSERT OR REPLACE INTO api_cache INSERT OR REPLACE INTO api_cache
(cache_key, response_data, created_at, expires_at, model, token_count) (cache_key, response_data, created_at, expires_at, model, token_count, hit_count)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, 0)
""", """,
( (
cache_key, cache_key,
@ -149,6 +168,12 @@ class APICache:
) )
total_tokens = cursor.fetchone()[0] or 0 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() conn.close()
return { return {
@ -156,4 +181,5 @@ class APICache:
"valid_entries": valid_entries, "valid_entries": valid_entries,
"expired_entries": total_entries - valid_entries, "expired_entries": total_entries - valid_entries,
"total_cached_tokens": total_tokens, "total_cached_tokens": total_tokens,
"total_cache_hits": total_hits,
} }

View File

@ -13,6 +13,13 @@ class ToolCache:
"db_get", "db_get",
"db_query", "db_query",
"index_directory", "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): def __init__(self, db_path: str, ttl_seconds: int = 300):

View File

@ -6,13 +6,24 @@ from pr.core.api import list_models
from pr.tools import read_file from pr.tools import read_file
from pr.tools.base import get_tools_definition from pr.tools.base import get_tools_definition
from pr.ui import Colors from pr.ui import Colors
from pr.editor import RPEditor
def handle_command(assistant, command): def handle_command(assistant, command):
command_parts = command.strip().split(maxsplit=1) command_parts = command.strip().split(maxsplit=1)
cmd = command_parts[0].lower() 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: if len(command_parts) < 2:
print(f"{Colors.RED}Usage: /auto [task description]{Colors.RESET}") print(f"{Colors.RED}Usage: /auto [task description]{Colors.RESET}")
print( print(
@ -27,41 +38,36 @@ def handle_command(assistant, command):
if cmd in ["exit", "quit", "q"]: if cmd in ["exit", "quit", "q"]:
return False return False
elif cmd == "help": elif cmd == "/help" or cmd == "help":
print( from pr.commands.help_docs import (
f""" get_agent_help,
{Colors.BOLD}Available Commands:{Colors.RESET} get_background_help,
get_cache_help,
{Colors.BOLD}Basic:{Colors.RESET} get_full_help,
exit, quit, q - Exit the assistant get_knowledge_help,
/help - Show this help message get_workflow_help,
/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 <file> - Review a file
/refactor <file> - Refactor code in a file
/obfuscate <file> - Obfuscate code in a file
{Colors.BOLD}Advanced Features:{Colors.RESET}
{Colors.CYAN}/auto <task>{Colors.RESET} - Enter autonomous mode
{Colors.CYAN}/workflow <name>{Colors.RESET} - Execute a workflow
{Colors.CYAN}/workflows{Colors.RESET} - List all workflows
{Colors.CYAN}/agent <role> <task>{Colors.RESET} - Create specialized agent and assign task
{Colors.CYAN}/agents{Colors.RESET} - Show active agents
{Colors.CYAN}/collaborate <task>{Colors.RESET} - Use multiple agents to collaborate
{Colors.CYAN}/knowledge <query>{Colors.RESET} - Search knowledge base
{Colors.CYAN}/remember <content>{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
"""
) )
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": elif cmd == "/reset":
assistant.messages = assistant.messages[:1] assistant.messages = assistant.messages[:1]
print(f"{Colors.GREEN}Message history cleared{Colors.RESET}") 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}" 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: if len(command_parts) < 2:
print("Current model: " + Colors.GREEN + assistant.model + Colors.RESET) print("Current model: " + Colors.GREEN + assistant.model + Colors.RESET)
else: else:
@ -116,17 +122,24 @@ def handle_command(assistant, command):
workflow_name = command_parts[1] workflow_name = command_parts[1]
execute_workflow_command(assistant, workflow_name) 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 <role> <task>{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) args = command_parts[1].split(maxsplit=1)
if len(args) < 2: if len(args) < 2:
print(f"{Colors.RED}Usage: /agent <role> <task>{Colors.RESET}") print(f"{Colors.RED}Usage: /agent <role> <task>{Colors.RESET}")
print( print(
f"{Colors.GRAY}Available roles: coding, research, data_analysis, planning, testing, documentation{Colors.RESET}" f"{Colors.GRAY}Available roles: coding, research, data_analysis, planning, testing, documentation{Colors.RESET}"
) )
else: return True
role, task = args[0], args[1] role, task = args[0], args[1]
execute_agent_task(assistant, role, task) execute_agent_task(assistant, role, task)
elif cmd == "/agents": elif cmd == "/agents":
show_agents(assistant) show_agents(assistant)
@ -374,6 +387,7 @@ def show_cache_stats(assistant):
print(f" Valid entries: {api_stats['valid_entries']}") print(f" Valid entries: {api_stats['valid_entries']}")
print(f" Expired entries: {api_stats['expired_entries']}") print(f" Expired entries: {api_stats['expired_entries']}")
print(f" Cached tokens: {api_stats['total_cached_tokens']}") print(f" Cached tokens: {api_stats['total_cached_tokens']}")
print(f" Total cache hits: {api_stats['total_cache_hits']}")
if "tool_cache" in stats: if "tool_cache" in stats:
tool_stats = stats["tool_cache"] tool_stats = stats["tool_cache"]

View File

@ -1,14 +1,18 @@
import os import os
DEFAULT_MODEL = "x-ai/grok-code-fast-1" DEFAULT_MODEL = "x-ai/grok-code-fast-1"
DEFAULT_API_URL = "https://static.molodetz.nl/rp.cgi/api/v1/chat/completions" DEFAULT_API_URL = "http://localhost:8118/ai/chat"
MODEL_LIST_URL = "https://static.molodetz.nl/rp.cgi/api/v1/models" MODEL_LIST_URL = "http://localhost:8118/ai/models"
DB_PATH = os.path.expanduser("~/.assistant_db.sqlite") config_directory = os.path.expanduser("~/.local/share/rp")
LOG_FILE = os.path.expanduser("~/.assistant_error.log") 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" CONTEXT_FILE = ".rcontext.txt"
GLOBAL_CONTEXT_FILE = os.path.expanduser("~/.rcontext.txt") GLOBAL_CONTEXT_FILE = os.path.join(config_directory, "rcontext.txt")
HISTORY_FILE = os.path.expanduser("~/.assistant_history") KNOWLEDGE_PATH = os.path.join(config_directory, "knowledge")
HISTORY_FILE = os.path.join(config_directory, "assistant_history")
DEFAULT_TEMPERATURE = 0.1 DEFAULT_TEMPERATURE = 0.1
DEFAULT_MAX_TOKENS = 4096 DEFAULT_MAX_TOKENS = 4096

View File

@ -32,21 +32,16 @@ from pr.core.context import init_system_message, truncate_tool_result
from pr.tools import ( from pr.tools import (
apply_patch, apply_patch,
chdir, chdir,
close_editor,
create_diff, create_diff,
db_get, db_get,
db_query, db_query,
db_set, db_set,
editor_insert_text,
editor_replace_text,
editor_search,
getpwd, getpwd,
http_fetch, http_fetch,
index_source_directory, index_source_directory,
kill_process, kill_process,
list_directory, list_directory,
mkdir, mkdir,
open_editor,
python_exec, python_exec,
read_file, read_file,
run_command, run_command,
@ -55,6 +50,7 @@ from pr.tools import (
web_search, web_search,
web_search_news, web_search_news,
write_file, write_file,
post_image,
) )
from pr.tools.base import get_tools_definition from pr.tools.base import get_tools_definition
from pr.tools.filesystem import ( from pr.tools.filesystem import (
@ -249,6 +245,7 @@ class Assistant:
logger.debug(f"Tool call: {func_name} with arguments: {arguments}") logger.debug(f"Tool call: {func_name} with arguments: {arguments}")
func_map = { func_map = {
"post_image": lambda **kw: post_image(**kw),
"http_fetch": lambda **kw: http_fetch(**kw), "http_fetch": lambda **kw: http_fetch(**kw),
"run_command": lambda **kw: run_command(**kw), "run_command": lambda **kw: run_command(**kw),
"tail_process": lambda **kw: tail_process(**kw), "tail_process": lambda **kw: tail_process(**kw),
@ -273,15 +270,15 @@ class Assistant:
), ),
"index_source_directory": lambda **kw: index_source_directory(**kw), "index_source_directory": lambda **kw: index_source_directory(**kw),
"search_replace": lambda **kw: search_replace(**kw, db_conn=self.db_conn), "search_replace": lambda **kw: search_replace(**kw, db_conn=self.db_conn),
"open_editor": lambda **kw: open_editor(**kw), # "open_editor": lambda **kw: open_editor(**kw),
"editor_insert_text": lambda **kw: editor_insert_text( # "editor_insert_text": lambda **kw: editor_insert_text(
**kw, db_conn=self.db_conn # **kw, db_conn=self.db_conn
), # ),
"editor_replace_text": lambda **kw: editor_replace_text( # "editor_replace_text": lambda **kw: editor_replace_text(
**kw, db_conn=self.db_conn # **kw, db_conn=self.db_conn
), # ),
"editor_search": lambda **kw: editor_search(**kw), # "editor_search": lambda **kw: editor_search(**kw),
"close_editor": lambda **kw: close_editor(**kw), # "close_editor": lambda **kw: close_editor(**kw),
"create_diff": lambda **kw: create_diff(**kw), "create_diff": lambda **kw: create_diff(**kw),
"apply_patch": lambda **kw: apply_patch(**kw, db_conn=self.db_conn), "apply_patch": lambda **kw: apply_patch(**kw, db_conn=self.db_conn),
"display_file_diff": lambda **kw: display_file_diff(**kw), "display_file_diff": lambda **kw: display_file_diff(**kw),
@ -413,6 +410,7 @@ class Assistant:
"refactor", "refactor",
"obfuscate", "obfuscate",
"/auto", "/auto",
"/edit",
] ]
def completer(text, state): def completer(text, state):
@ -539,6 +537,10 @@ class Assistant:
def process_message(assistant, message): 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}) assistant.messages.append({"role": "user", "content": message})
logger.debug(f"Processing user message: {message[:100]}...") logger.debug(f"Processing user message: {message[:100]}...")

View File

@ -1,17 +1,26 @@
import configparser import configparser
import os import os
from typing import Any, Dict from typing import Any, Dict
import uuid
from pr.core.logging import get_logger from pr.core.logging import get_logger
logger = get_logger("config") 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" LOCAL_CONFIG_FILE = ".prrc"
def load_config() -> Dict[str, Any]: 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) global_config = _load_config_file(CONFIG_FILE)
local_config = _load_config_file(LOCAL_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): def create_default_config(filepath: str = CONFIG_FILE):
os.makedirs(CONFIG_DIRECTORY, exist_ok=True)
default_config = """[api] default_config = """[api]
default_model = x-ai/grok-code-fast-1 default_model = x-ai/grok-code-fast-1
timeout = 30 timeout = 30

View File

@ -1,7 +1,7 @@
import json import json
import logging import logging
import os import os
import pathlib
from pr.config import ( from pr.config import (
CHARS_PER_TOKEN, CHARS_PER_TOKEN,
CONTENT_TRIM_LENGTH, CONTENT_TRIM_LENGTH,
@ -12,6 +12,7 @@ from pr.config import (
MAX_TOKENS_LIMIT, MAX_TOKENS_LIMIT,
MAX_TOOL_RESULT_LENGTH, MAX_TOOL_RESULT_LENGTH,
RECENT_MESSAGES_TO_KEEP, RECENT_MESSAGES_TO_KEEP,
KNOWLEDGE_PATH,
) )
from pr.ui import Colors from pr.ui import Colors
@ -59,6 +60,10 @@ File Operations:
- Always close editor files when finished - Always close editor files when finished
- Use write_file for complete file rewrites, search_replace for simple text replacements - 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: Process Management:
- run_command executes shell commands with a timeout (default 30s) - run_command executes shell commands with a timeout (default 30s)
- If a command times out, you receive a PID in the response - If a command times out, you receive a PID in the response
@ -94,6 +99,18 @@ Shell Commands:
except Exception as e: except Exception as e:
logging.error(f"Error reading context file {context_file}: {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: if args.context:
for ctx_file in args.context: for ctx_file in args.context:
try: try:

View File

@ -135,9 +135,7 @@ class EnhancedAssistant:
self.base.api_url, self.base.api_url,
self.base.api_key, self.base.api_key,
use_tools=False, use_tools=False,
tools=None, tools_definition=[],
temperature=temperature,
max_tokens=max_tokens,
verbose=self.base.verbose, verbose=self.base.verbose,
) )

View File

@ -38,7 +38,7 @@ class TerminalMultiplexer:
try: try:
line = self.stdout_queue.get(timeout=0.1) line = self.stdout_queue.get(timeout=0.1)
if line: if line:
sys.stdout.write(f"{Colors.GRAY}[{self.name}]{Colors.RESET} {line}") sys.stdout.write(line)
sys.stdout.flush() sys.stdout.flush()
except queue.Empty: except queue.Empty:
pass pass
@ -46,7 +46,10 @@ class TerminalMultiplexer:
try: try:
line = self.stderr_queue.get(timeout=0.1) line = self.stderr_queue.get(timeout=0.1)
if line: 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() sys.stderr.flush()
except queue.Empty: except queue.Empty:
pass pass
@ -55,10 +58,8 @@ class TerminalMultiplexer:
with self.lock: with self.lock:
self.stdout_buffer.append(data) self.stdout_buffer.append(data)
self.metadata["last_activity"] = time.time() self.metadata["last_activity"] = time.time()
# Update handler state if available
if self.handler: if self.handler:
self.handler.update_state(data) self.handler.update_state(data)
# Update prompt detector
self.prompt_detector.update_session_state( self.prompt_detector.update_session_state(
self.name, data, self.metadata["process_type"] self.name, data, self.metadata["process_type"]
) )
@ -69,10 +70,8 @@ class TerminalMultiplexer:
with self.lock: with self.lock:
self.stderr_buffer.append(data) self.stderr_buffer.append(data)
self.metadata["last_activity"] = time.time() self.metadata["last_activity"] = time.time()
# Update handler state if available
if self.handler: if self.handler:
self.handler.update_state(data) self.handler.update_state(data)
# Update prompt detector
self.prompt_detector.update_session_state( self.prompt_detector.update_session_state(
self.name, data, self.metadata["process_type"] self.name, data, self.metadata["process_type"]
) )
@ -103,7 +102,6 @@ class TerminalMultiplexer:
self.metadata[key] = value self.metadata[key] = value
def set_process_type(self, process_type): def set_process_type(self, process_type):
"""Set the process type and initialize appropriate handler."""
with self.lock: with self.lock:
self.metadata["process_type"] = process_type self.metadata["process_type"] = process_type
self.handler = get_handler_for_process(process_type, self) self.handler = get_handler_for_process(process_type, self)
@ -119,8 +117,6 @@ class TerminalMultiplexer:
except Exception as e: except Exception as e:
self.write_stderr(f"Error sending input: {e}") self.write_stderr(f"Error sending input: {e}")
else: else:
# This will be implemented when we have a process attached
# For now, just update activity
with self.lock: with self.lock:
self.metadata["last_activity"] = time.time() self.metadata["last_activity"] = time.time()
self.metadata["interaction_count"] += 1 self.metadata["interaction_count"] += 1
@ -131,59 +127,58 @@ class TerminalMultiplexer:
self.display_thread.join(timeout=1) self.display_thread.join(timeout=1)
_multiplexers = {} multiplexer_registry = {}
_mux_counter = 0 multiplexer_counter = 0
_mux_lock = threading.Lock() multiplexer_lock = threading.Lock()
_background_monitor = None background_monitor = None
_monitor_active = False monitor_active = False
_monitor_interval = 0.2 # 200ms monitor_interval = 0.2
def create_multiplexer(name=None, show_output=True): def create_multiplexer(name=None, show_output=True):
global _mux_counter global multiplexer_counter
with _mux_lock: with multiplexer_lock:
if name is None: if name is None:
_mux_counter += 1 multiplexer_counter += 1
name = f"process-{_mux_counter}" name = f"process-{multiplexer_counter}"
mux = TerminalMultiplexer(name, show_output) multiplexer_instance = TerminalMultiplexer(name, show_output)
_multiplexers[name] = mux multiplexer_registry[name] = multiplexer_instance
return name, mux return name, multiplexer_instance
def get_multiplexer(name): def get_multiplexer(name):
return _multiplexers.get(name) return multiplexer_registry.get(name)
def close_multiplexer(name): def close_multiplexer(name):
mux = _multiplexers.get(name) multiplexer_instance = multiplexer_registry.get(name)
if mux: if multiplexer_instance:
mux.close() multiplexer_instance.close()
del _multiplexers[name] del multiplexer_registry[name]
def get_all_multiplexer_states(): def get_all_multiplexer_states():
with _mux_lock: with multiplexer_lock:
states = {} states = {}
for name, mux in _multiplexers.items(): for name, multiplexer_instance in multiplexer_registry.items():
states[name] = { states[name] = {
"metadata": mux.get_metadata(), "metadata": multiplexer_instance.get_metadata(),
"output_summary": { "output_summary": {
"stdout_lines": len(mux.stdout_buffer), "stdout_lines": len(multiplexer_instance.stdout_buffer),
"stderr_lines": len(mux.stderr_buffer), "stderr_lines": len(multiplexer_instance.stderr_buffer),
}, },
} }
return states return states
def cleanup_all_multiplexers(): def cleanup_all_multiplexers():
for mux in list(_multiplexers.values()): for multiplexer_instance in list(multiplexer_registry.values()):
mux.close() multiplexer_instance.close()
_multiplexers.clear() multiplexer_registry.clear()
# Background process management background_processes = {}
_background_processes = {} process_lock = threading.Lock()
_process_lock = threading.Lock()
class BackgroundProcess: class BackgroundProcess:
@ -197,17 +192,15 @@ class BackgroundProcess:
self.end_time = None self.end_time = None
def start(self): def start(self):
"""Start the background process."""
try: try:
# Create multiplexer for this process multiplexer_name, multiplexer_instance = create_multiplexer(
mux_name, mux = create_multiplexer(self.name, show_output=False) self.name, show_output=False
self.multiplexer = mux )
self.multiplexer = multiplexer_instance
# Detect process type
process_type = detect_process_type(self.command) 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.process = subprocess.Popen(
self.command, self.command,
shell=True, shell=True,
@ -221,7 +214,6 @@ class BackgroundProcess:
self.status = "running" self.status = "running"
# Start output monitoring threads
threading.Thread(target=self._monitor_stdout, daemon=True).start() threading.Thread(target=self._monitor_stdout, daemon=True).start()
threading.Thread(target=self._monitor_stderr, daemon=True).start() threading.Thread(target=self._monitor_stderr, daemon=True).start()
@ -232,33 +224,29 @@ class BackgroundProcess:
return {"status": "error", "error": str(e)} return {"status": "error", "error": str(e)}
def _monitor_stdout(self): def _monitor_stdout(self):
"""Monitor stdout from the process."""
try: try:
for line in iter(self.process.stdout.readline, ""): for line in iter(self.process.stdout.readline, ""):
if line: if line:
self.multiplexer.write_stdout(line.rstrip("\n\r")) self.multiplexer.write_stdout(line.rstrip("\n\r"))
except Exception as e: except Exception as e:
self.write_stderr(f"Error reading stdout: {e}") self.multiplexer.write_stderr(f"Error reading stdout: {e}")
finally: finally:
self._check_completion() self._check_completion()
def _monitor_stderr(self): def _monitor_stderr(self):
"""Monitor stderr from the process."""
try: try:
for line in iter(self.process.stderr.readline, ""): for line in iter(self.process.stderr.readline, ""):
if line: if line:
self.multiplexer.write_stderr(line.rstrip("\n\r")) self.multiplexer.write_stderr(line.rstrip("\n\r"))
except Exception as e: 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): def _check_completion(self):
"""Check if process has completed."""
if self.process and self.process.poll() is not None: if self.process and self.process.poll() is not None:
self.status = "completed" self.status = "completed"
self.end_time = time.time() self.end_time = time.time()
def get_info(self): def get_info(self):
"""Get process information."""
self._check_completion() self._check_completion()
return { return {
"name": self.name, "name": self.name,
@ -275,7 +263,6 @@ class BackgroundProcess:
} }
def get_output(self, lines=None): def get_output(self, lines=None):
"""Get process output."""
if not self.multiplexer: if not self.multiplexer:
return [] return []
@ -290,7 +277,6 @@ class BackgroundProcess:
return [line for line in combined if line.strip()] return [line for line in combined if line.strip()]
def send_input(self, input_text): def send_input(self, input_text):
"""Send input to the process."""
if self.process and self.status == "running": if self.process and self.status == "running":
try: try:
self.process.stdin.write(input_text + "\n") self.process.stdin.write(input_text + "\n")
@ -301,11 +287,9 @@ class BackgroundProcess:
return {"status": "error", "error": "Process not running or no stdin"} return {"status": "error", "error": "Process not running or no stdin"}
def kill(self): def kill(self):
"""Kill the process."""
if self.process and self.status == "running": if self.process and self.status == "running":
try: try:
self.process.terminate() self.process.terminate()
# Wait a bit for graceful termination
time.sleep(0.1) time.sleep(0.1)
if self.process.poll() is None: if self.process.poll() is None:
self.process.kill() self.process.kill()
@ -318,61 +302,55 @@ class BackgroundProcess:
def start_background_process(name, command): def start_background_process(name, command):
"""Start a background process.""" with process_lock:
with _process_lock: if name in background_processes:
if name in _background_processes:
return {"status": "error", "error": f"Process {name} already exists"} return {"status": "error", "error": f"Process {name} already exists"}
process = BackgroundProcess(name, command) process_instance = BackgroundProcess(name, command)
result = process.start() result = process_instance.start()
if result["status"] == "success": if result["status"] == "success":
_background_processes[name] = process background_processes[name] = process_instance
return result return result
def get_all_sessions(): def get_all_sessions():
"""Get all background process sessions.""" with process_lock:
with _process_lock:
sessions = {} sessions = {}
for name, process in _background_processes.items(): for name, process_instance in background_processes.items():
sessions[name] = process.get_info() sessions[name] = process_instance.get_info()
return sessions return sessions
def get_session_info(name): def get_session_info(name):
"""Get information about a specific session.""" with process_lock:
with _process_lock: process_instance = background_processes.get(name)
process = _background_processes.get(name) return process_instance.get_info() if process_instance else None
return process.get_info() if process else None
def get_session_output(name, lines=None): def get_session_output(name, lines=None):
"""Get output from a specific session.""" with process_lock:
with _process_lock: process_instance = background_processes.get(name)
process = _background_processes.get(name) return process_instance.get_output(lines) if process_instance else None
return process.get_output(lines) if process else None
def send_input_to_session(name, input_text): def send_input_to_session(name, input_text):
"""Send input to a background session.""" with process_lock:
with _process_lock: process_instance = background_processes.get(name)
process = _background_processes.get(name)
return ( return (
process.send_input(input_text) process_instance.send_input(input_text)
if process if process_instance
else {"status": "error", "error": "Session not found"} else {"status": "error", "error": "Session not found"}
) )
def kill_session(name): def kill_session(name):
"""Kill a background session.""" with process_lock:
with _process_lock: process_instance = background_processes.get(name)
process = _background_processes.get(name) if process_instance:
if process: result = process_instance.kill()
result = process.kill()
if result["status"] == "success": if result["status"] == "success":
del _background_processes[name] del background_processes[name]
return result return result
return {"status": "error", "error": "Session not found"} return {"status": "error", "error": "Session not found"}

View File

@ -6,6 +6,7 @@ from pr.tools.agents import (
remove_agent, remove_agent,
) )
from pr.tools.base import get_tools_definition from pr.tools.base import get_tools_definition
from pr.tools.vision import post_image
from pr.tools.command import ( from pr.tools.command import (
kill_process, kill_process,
run_command, 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 from pr.tools.web import http_fetch, web_search, web_search_news
__all__ = [ __all__ = [
"get_tools_definition", "add_knowledge_entry",
"read_file", "apply_patch",
"write_file",
"list_directory",
"mkdir",
"chdir", "chdir",
"getpwd", "close_editor",
"index_source_directory", "collaborate_agents",
"search_replace", "create_agent",
"open_editor", "create_diff",
"db_get",
"db_query",
"db_set",
"delete_knowledge_entry",
"post_image",
"editor_insert_text", "editor_insert_text",
"editor_replace_text", "editor_replace_text",
"editor_search", "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",
"run_command_interactive", "run_command_interactive",
"db_set", "search_knowledge",
"db_get", "search_replace",
"db_query", "tail_process",
"http_fetch", "update_knowledge_importance",
"web_search", "web_search",
"web_search_news", "web_search_news",
"python_exec", "write_file",
"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",
] ]

View File

@ -3,16 +3,40 @@ from typing import Any, Dict, List
from pr.agents.agent_manager import AgentManager from pr.agents.agent_manager import AgentManager
from pr.core.api import call_api 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]: def create_agent(role_name: str, agent_id: str = None) -> Dict[str, Any]:
"""Create a new agent with the specified role.""" """Create a new agent with the specified role."""
try: try:
# Get db_path from environment or default
db_path = os.environ.get("ASSISTANT_DB_PATH", "~/.assistant_db.sqlite") db_path = os.environ.get("ASSISTANT_DB_PATH", "~/.assistant_db.sqlite")
db_path = os.path.expanduser(db_path) 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) agent_id = manager.create_agent(role_name, agent_id)
return {"status": "success", "agent_id": agent_id, "role": role_name} return {"status": "success", "agent_id": agent_id, "role": role_name}
except Exception as e: except Exception as e:
@ -23,7 +47,8 @@ def list_agents() -> Dict[str, Any]:
"""List all active agents.""" """List all active agents."""
try: try:
db_path = os.path.expanduser("~/.assistant_db.sqlite") 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 = [] agents = []
for agent_id, agent in manager.active_agents.items(): for agent_id, agent in manager.active_agents.items():
agents.append( 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.""" """Execute a task with the specified agent."""
try: try:
db_path = os.path.expanduser("~/.assistant_db.sqlite") 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) result = manager.execute_agent_task(agent_id, task, context)
return result return result
except Exception as e: except Exception as e:
@ -54,7 +80,8 @@ def remove_agent(agent_id: str) -> Dict[str, Any]:
"""Remove an agent.""" """Remove an agent."""
try: try:
db_path = os.path.expanduser("~/.assistant_db.sqlite") 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) success = manager.remove_agent(agent_id)
return {"status": "success" if success else "not_found", "agent_id": agent_id} return {"status": "success" if success else "not_found", "agent_id": agent_id}
except Exception as e: 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.""" """Collaborate multiple agents on a task."""
try: try:
db_path = os.path.expanduser("~/.assistant_db.sqlite") 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) result = manager.collaborate_agents(orchestrator_id, task, agent_roles)
return result return result
except Exception as e: except Exception as e:

View File

@ -1,4 +1,3 @@
import os
import select import select
import subprocess import subprocess
import time import time
@ -99,7 +98,7 @@ def tail_process(pid: int, timeout: int = 30):
return {"status": "error", "error": f"Process {pid} not found"} 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 mux_name = None
try: try:
process = subprocess.Popen( process = subprocess.Popen(
@ -108,6 +107,7 @@ def run_command(command, timeout=30, monitored=False):
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, text=True,
cwd=cwd,
) )
_register_process(process.pid, process) _register_process(process.pid, process)

View File

@ -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. 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) command: The command to run (list or string)
session_name: Optional name for the session session_name: Optional name for the session
process_type: Type of process (ssh, vim, apt, etc.) process_type: Type of process (ssh, vim, apt, etc.)
cwd: Current working directory for the command
Returns: Returns:
session_name: The name of the created session 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, stderr=subprocess.PIPE,
text=True, text=True,
bufsize=1, bufsize=1,
cwd=cwd,
) )
mux.process = process mux.process = process
mux.update_metadata("pid", process.pid) mux.update_metadata("pid", process.pid)
# Set process type and handler # Set process type and handler
from pr.tools.process_handlers import detect_process_type
detected_type = detect_process_type(command) detected_type = detect_process_type(command)
mux.set_process_type(detected_type) mux.set_process_type(detected_type)
# Start output readers # Start output readers
stdout_thread = threading.Thread( 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( 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() stdout_thread.start()
@ -65,8 +69,18 @@ def start_interactive_session(command, session_name=None, process_type="generic"
raise e 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.""" """Read from a stream and write to multiplexer buffer."""
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: try:
for line in iter(stream.readline, ""): for line in iter(stream.readline, ""):
if line: if line:

View File

@ -1,14 +1,25 @@
import contextlib import contextlib
import os
import traceback import traceback
from io import StringIO from io import StringIO
def python_exec(code, python_globals): def python_exec(code, python_globals, cwd=None):
try: try:
original_cwd = None
if cwd:
original_cwd = os.getcwd()
os.chdir(cwd)
output = StringIO() output = StringIO()
with contextlib.redirect_stdout(output): with contextlib.redirect_stdout(output):
exec(code, python_globals) exec(code, python_globals)
if original_cwd:
os.chdir(original_cwd)
return {"status": "success", "output": output.getvalue()} return {"status": "success", "output": output.getvalue()}
except Exception as e: except Exception as e:
if original_cwd:
os.chdir(original_cwd)
return {"status": "error", "error": str(e), "traceback": traceback.format_exc()} return {"status": "error", "error": str(e), "traceback": traceback.format_exc()}

View File

@ -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}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.GRAY}Press Ctrl+C twice to interrupt.{Colors.RESET}\n")
print(f"{Colors.BOLD}{'' * 80}{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}"
)

View File

@ -3,44 +3,51 @@ requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "pr-assistant" name = "rp"
version = "1.0.0" version = "1.2.0"
description = "Professional CLI AI assistant with autonomous execution capabilities" description = "R python edition. The ultimate autonomous AI CLI."
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.13.3"
license = {text = "MIT"} license = {text = "MIT"}
keywords = ["ai", "assistant", "cli", "automation", "openrouter", "autonomous"] keywords = ["ai", "assistant", "cli", "automation", "openrouter", "autonomous"]
authors = [ authors = [
{name = "retoor", email = "retoor@example.com"} {name = "retoor", email = "retoor@molodetz.nl"}
]
dependencies = [
"aiohttp>=3.13.2",
"pydantic>=2.12.3",
] ]
classifiers = [ classifiers = [
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Scientific/Engineering :: Artificial Intelligence",
] ]
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"pytest", "pytest>=8.3.0",
"pytest-cov", "pytest-asyncio>=1.2.0",
"black", "pytest-aiohttp>=1.1.0",
"flake8", "aiohttp>=3.13.2",
"mypy", "pytest-cov>=7.0.0",
"pre-commit", "black>=25.9.0",
"flake8>=7.3.0",
"mypy>=1.18.2",
"pre-commit>=4.3.0",
] ]
[project.scripts] [project.scripts]
pr = "pr.__main__:main" pr = "pr.__main__:main"
rp = "pr.__main__:main" rp = "pr.__main__:main"
rpe = "pr.editor:main" rpe = "pr.editor:main"
rpi = "pr.implode:main"
rpserver = "pr.server:main"
rpcgi = "pr.cgi:main"
rpweb = "pr.web.app:main"
[project.urls] [project.urls]
Homepage = "https://retoor.molodetz.nl/retoor/rp" Homepage = "https://retoor.molodetz.nl/retoor/rp"
@ -55,6 +62,7 @@ exclude = ["tests*"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
asyncio_mode = "auto"
python_files = ["test_*.py"] python_files = ["test_*.py"]
python_classes = ["Test*"] python_classes = ["Test*"]
python_functions = ["test_*"] python_functions = ["test_*"]
@ -77,7 +85,7 @@ extend-exclude = '''
''' '''
[tool.mypy] [tool.mypy]
python_version = "3.8" python_version = "3.13"
warn_return_any = true warn_return_any = true
warn_unused_configs = true warn_unused_configs = true
disallow_untyped_defs = false disallow_untyped_defs = false
@ -111,5 +119,5 @@ use_parentheses = true
ensure_newline_before_comments = true ensure_newline_before_comments = true
[tool.bandit] [tool.bandit]
exclude_dirs = ["tests", "venv", ".venv"] exclude_dirs = ["tests", "venv", ".venv","__pycache__"]
skips = ["B101"] skips = ["B101"]

View File

@ -1,8 +1,11 @@
import os import os
import tempfile import tempfile
from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from pr.ads import AsyncDataSet
from pr.web.app import create_app
@pytest.fixture @pytest.fixture
@ -41,3 +44,77 @@ def sample_context_file(temp_dir):
with open(context_path, "w") as f: with open(context_path, "w") as f:
f.write("Sample context content\n") f.write("Sample context content\n")
return context_path 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("<html>Index</html>")
(templates_dir / "login.html").write_text("<html>Login</html>")
(templates_dir / "register.html").write_text("<html>Register</html>")
(templates_dir / "dashboard.html").write_text("<html>Dashboard</html>")
(templates_dir / "repos.html").write_text("<html>Repos</html>")
(templates_dir / "api_keys.html").write_text("<html>API Keys</html>")
(templates_dir / "repo.html").write_text("<html>Repo</html>")
(templates_dir / "file.html").write_text("<html>File</html>")
(templates_dir / "edit_file.html").write_text("<html>Edit File</html>")
(templates_dir / "deploy.html").write_text("<html>Deploy</html>")
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

View File

@ -1,101 +1,97 @@
from pr.core.advanced_context import AdvancedContextManager from pr.core.advanced_context import AdvancedContextManager
def test_adaptive_context_window_simple(): class TestAdvancedContextManager:
mgr = AdvancedContextManager() 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_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(self):
messages = [ messages = [
{"content": "short"}, {
{"content": "this is a longer message with more words"}, "content": "very long and complex message with many words and detailed information about various topics"
}
] ]
window = mgr.adaptive_context_window(messages, "simple") result = self.manager.adaptive_context_window(messages, "complex")
assert isinstance(window, int) assert result >= 35
assert window >= 10
def test_adaptive_context_window_very_complex(self):
def test_adaptive_context_window_medium():
mgr = AdvancedContextManager()
messages = [ messages = [
{"content": "short"}, {
{"content": "this is a longer message with more words"}, "content": "extremely long and very complex message with extensive vocabulary and detailed explanations"
}
] ]
window = mgr.adaptive_context_window(messages, "medium") result = self.manager.adaptive_context_window(messages, "very_complex")
assert isinstance(window, int) assert result >= 50
assert window >= 20
def test_adaptive_context_window_unknown_complexity(self):
messages = [{"content": "test"}]
result = self.manager.adaptive_context_window(messages, "unknown")
assert result >= 20
def test_adaptive_context_window_complex(): def test_analyze_message_complexity(self):
mgr = AdvancedContextManager() messages = [{"content": "This is a test message with some words."}]
messages = [ result = self.manager._analyze_message_complexity(messages)
{"content": "short"}, assert 0.0 <= result <= 1.0
{"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_analyze_message_complexity_empty(self):
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_analyze_message_complexity_empty():
mgr = AdvancedContextManager()
messages = [] messages = []
score = mgr._analyze_message_complexity(messages) result = self.manager._analyze_message_complexity(messages)
assert score == 0 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(): def test_extract_key_sentences_empty(self):
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():
mgr = AdvancedContextManager()
text = "" text = ""
sentences = mgr.extract_key_sentences(text, 5) result = self.manager.extract_key_sentences(text)
assert sentences == [] 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_advanced_summarize_messages(): def test_advanced_summarize_messages_empty(self):
mgr = AdvancedContextManager()
messages = [{"content": "Hello"}, {"content": "How are you?"}]
summary = mgr.advanced_summarize_messages(messages)
assert isinstance(summary, str)
def test_advanced_summarize_messages_empty():
mgr = AdvancedContextManager()
messages = [] messages = []
summary = mgr.advanced_summarize_messages(messages) result = self.manager.advanced_summarize_messages(messages)
assert summary == "No content to summarize." 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_score_message_relevance(): def test_score_message_relevance_no_overlap(self):
mgr = AdvancedContextManager() message = {"content": "apple banana"}
message = {"content": "hello world"} context = "orange grape"
context = "world hello" result = self.manager.score_message_relevance(message, context)
score = mgr.score_message_relevance(message, context) assert result == 0.0
assert 0 <= score <= 1
def test_score_message_relevance_empty(self):
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": ""} message = {"content": ""}
context = "" context = ""
score = mgr.score_message_relevance(message, context) result = self.manager.score_message_relevance(message, context)
assert score == 0 assert result == 0.0

View File

@ -82,7 +82,10 @@ def test_agent_manager_get_agent_messages():
def test_agent_manager_get_session_summary(): def test_agent_manager_get_session_summary():
mgr = AgentManager(":memory:", None) mgr = AgentManager(":memory:", None)
summary = mgr.get_session_summary() 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(): def test_agent_manager_collaborate_agents():

697
tests/test_commands.py Normal file
View File

@ -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)

693
tests/test_commands.py.bak Normal file
View File

@ -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)

View File

@ -77,9 +77,9 @@ def test_get_cache_statistics():
mock_base = MagicMock() mock_base = MagicMock()
assistant = EnhancedAssistant(mock_base) assistant = EnhancedAssistant(mock_base)
assistant.api_cache = MagicMock() 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 = 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() stats = assistant.get_cache_statistics()
assert "api_cache" in stats assert "api_cache" in stats

61
tests/test_exceptions.py Normal file
View File

@ -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")

46
tests/test_help_docs.py Normal file
View File

@ -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

View File

@ -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(): class TestLogging:
logger = setup_logging(verbose=False) @patch("pr.core.logging.os.makedirs")
assert logger.name == "pr" @patch("pr.core.logging.os.path.dirname")
assert logger.level == 20 # INFO @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(): mock_makedirs.assert_called_once_with("/tmp/logs", exist_ok=True)
logger = setup_logging(verbose=True) mock_get_logger.assert_called_once_with("pr")
assert logger.name == "pr" mock_logger.setLevel.assert_called_once_with(20) # INFO level
assert logger.level == 10 # DEBUG mock_handler.assert_called_once()
# Should have console handler assert result == mock_logger
assert len(logger.handlers) >= 2
@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(): result = setup_logging(verbose=True)
logger = get_logger()
assert logger.name == "pr"
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(): @patch("pr.core.logging.logging.getLogger")
logger = get_logger("test") def test_get_logger_default(self, mock_get_logger):
assert logger.name == "pr.test" 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

View File

@ -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()

View File

@ -1,8 +1,12 @@
import os import os
import tempfile
from pr.tools.base import get_tools_definition 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.patch import apply_patch, create_diff
from pr.tools.python_exec import python_exec
class TestFilesystemTools: class TestFilesystemTools:
@ -43,6 +47,54 @@ class TestFilesystemTools:
read_result = read_file(filepath) read_result = read_file(filepath)
assert "Hello, Universe!" in read_result["content"] 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: class TestPatchTools:
@ -108,6 +160,21 @@ class TestToolDefinitions:
assert "write_file" in tool_names assert "write_file" in tool_names
assert "list_directory" in tool_names assert "list_directory" in tool_names
assert "search_replace" 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): def test_patch_tools_present(self):
tools = get_tools_definition() tools = get_tools_definition()

153
tests/test_ui_output.py Normal file
View File

@ -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")