maintenance: update config paths and agent communication
This commit is contained in:
parent
85808f10a8
commit
27bcc7409e
13
Makefile
13
Makefile
@ -67,10 +67,13 @@ backup:
|
||||
zip -r rp.zip *
|
||||
mv rp.zip ../
|
||||
|
||||
implode:
|
||||
python ../implode/imply.py rp.py
|
||||
mv imploded.py /home/retoor/bin/rp
|
||||
chmod +x /home/retoor/bin/rp
|
||||
rp --debug
|
||||
serve:
|
||||
rpserver
|
||||
|
||||
|
||||
implode: build
|
||||
rpi rp.py -o rp
|
||||
chmod +x rp
|
||||
if [ -d /home/retoor/bin ]; then cp rp /home/retoor/bin/rp; fi
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
@ -68,6 +68,12 @@ class AgentCommunicationBus:
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cursor.execute("PRAGMA table_info(agent_messages)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
if "read" not in columns:
|
||||
cursor.execute("ALTER TABLE agent_messages ADD COLUMN read INTEGER DEFAULT 0")
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
def send_message(self, message: AgentMessage, session_id: Optional[str] = None):
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
@ -169,7 +168,7 @@ Break down the task and delegate subtasks to appropriate agents. Coordinate thei
|
||||
|
||||
return results
|
||||
|
||||
def get_session_summary(self) -> str:
|
||||
def get_session_summary(self) -> Dict[str, Any]:
|
||||
summary = {
|
||||
"session_id": self.session_id,
|
||||
"active_agents": len(self.active_agents),
|
||||
@ -183,7 +182,7 @@ Break down the task and delegate subtasks to appropriate agents. Coordinate thei
|
||||
for agent_id, agent in self.active_agents.items()
|
||||
],
|
||||
}
|
||||
return json.dumps(summary)
|
||||
return summary
|
||||
|
||||
def clear_session(self):
|
||||
self.active_agents.clear()
|
||||
|
||||
@ -16,6 +16,10 @@ def run_autonomous_mode(assistant, task):
|
||||
logger.debug(f"=== AUTONOMOUS MODE START ===")
|
||||
logger.debug(f"Task: {task}")
|
||||
|
||||
from pr.core.knowledge_context import inject_knowledge_context
|
||||
|
||||
inject_knowledge_context(assistant, task)
|
||||
|
||||
assistant.messages.append({"role": "user", "content": f"{task}"})
|
||||
|
||||
try:
|
||||
@ -94,9 +98,14 @@ def process_response_autonomous(assistant, response):
|
||||
arguments = json.loads(tool_call["function"]["arguments"])
|
||||
|
||||
result = execute_single_tool(assistant, func_name, arguments)
|
||||
result = truncate_tool_result(result)
|
||||
if isinstance(result, str):
|
||||
try:
|
||||
result = json.loads(result)
|
||||
except json.JSONDecodeError as ex:
|
||||
result = {"error": str(ex)}
|
||||
|
||||
status = "success" if result.get("status") == "success" else "error"
|
||||
result = truncate_tool_result(result)
|
||||
display_tool_call(func_name, arguments, status, result)
|
||||
|
||||
tool_results.append(
|
||||
@ -141,9 +150,9 @@ def execute_single_tool(assistant, func_name, arguments):
|
||||
db_get,
|
||||
db_query,
|
||||
db_set,
|
||||
editor_insert_text,
|
||||
editor_replace_text,
|
||||
editor_search,
|
||||
# editor_insert_text,
|
||||
# editor_replace_text,
|
||||
# editor_search,
|
||||
getpwd,
|
||||
http_fetch,
|
||||
index_source_directory,
|
||||
|
||||
34
pr/cache/api_cache.py
vendored
34
pr/cache/api_cache.py
vendored
@ -22,7 +22,8 @@ class APICache:
|
||||
created_at INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
model TEXT,
|
||||
token_count INTEGER
|
||||
token_count INTEGER,
|
||||
hit_count INTEGER DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
@ -31,7 +32,14 @@ class APICache:
|
||||
CREATE INDEX IF NOT EXISTS idx_expires_at ON api_cache(expires_at)
|
||||
"""
|
||||
)
|
||||
|
||||
# Check if hit_count column exists, add if not
|
||||
cursor.execute("PRAGMA table_info(api_cache)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
if "hit_count" not in columns:
|
||||
cursor.execute("ALTER TABLE api_cache ADD COLUMN hit_count INTEGER DEFAULT 0")
|
||||
conn.commit()
|
||||
|
||||
conn.close()
|
||||
|
||||
def _generate_cache_key(
|
||||
@ -64,10 +72,21 @@ class APICache:
|
||||
)
|
||||
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if row:
|
||||
# Increment hit count
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE api_cache SET hit_count = hit_count + 1
|
||||
WHERE cache_key = ?
|
||||
""",
|
||||
(cache_key,),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return json.loads(row[0])
|
||||
|
||||
conn.close()
|
||||
return None
|
||||
|
||||
def set(
|
||||
@ -90,8 +109,8 @@ class APICache:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO api_cache
|
||||
(cache_key, response_data, created_at, expires_at, model, token_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
(cache_key, response_data, created_at, expires_at, model, token_count, hit_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 0)
|
||||
""",
|
||||
(
|
||||
cache_key,
|
||||
@ -149,6 +168,12 @@ class APICache:
|
||||
)
|
||||
total_tokens = cursor.fetchone()[0] or 0
|
||||
|
||||
cursor.execute(
|
||||
"SELECT SUM(hit_count) FROM api_cache WHERE expires_at > ?",
|
||||
(current_time,),
|
||||
)
|
||||
total_hits = cursor.fetchone()[0] or 0
|
||||
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
@ -156,4 +181,5 @@ class APICache:
|
||||
"valid_entries": valid_entries,
|
||||
"expired_entries": total_entries - valid_entries,
|
||||
"total_cached_tokens": total_tokens,
|
||||
"total_cache_hits": total_hits,
|
||||
}
|
||||
|
||||
7
pr/cache/tool_cache.py
vendored
7
pr/cache/tool_cache.py
vendored
@ -13,6 +13,13 @@ class ToolCache:
|
||||
"db_get",
|
||||
"db_query",
|
||||
"index_directory",
|
||||
"http_fetch",
|
||||
"web_search",
|
||||
"web_search_news",
|
||||
"search_knowledge",
|
||||
"get_knowledge_entry",
|
||||
"get_knowledge_by_category",
|
||||
"get_knowledge_statistics",
|
||||
}
|
||||
|
||||
def __init__(self, db_path: str, ttl_seconds: int = 300):
|
||||
|
||||
@ -6,13 +6,24 @@ from pr.core.api import list_models
|
||||
from pr.tools import read_file
|
||||
from pr.tools.base import get_tools_definition
|
||||
from pr.ui import Colors
|
||||
from pr.editor import RPEditor
|
||||
|
||||
|
||||
def handle_command(assistant, command):
|
||||
command_parts = command.strip().split(maxsplit=1)
|
||||
cmd = command_parts[0].lower()
|
||||
|
||||
if cmd == "/auto":
|
||||
if cmd == "/edit":
|
||||
rp_editor = RPEditor(command_parts[1] if len(command_parts) > 1 else None)
|
||||
rp_editor.start()
|
||||
rp_editor.thread.join()
|
||||
task = str(rp_editor.get_text())
|
||||
rp_editor.stop()
|
||||
rp_editor = None
|
||||
if task:
|
||||
run_autonomous_mode(assistant, task)
|
||||
|
||||
elif cmd == "/auto":
|
||||
if len(command_parts) < 2:
|
||||
print(f"{Colors.RED}Usage: /auto [task description]{Colors.RESET}")
|
||||
print(
|
||||
@ -27,41 +38,36 @@ def handle_command(assistant, command):
|
||||
if cmd in ["exit", "quit", "q"]:
|
||||
return False
|
||||
|
||||
elif cmd == "help":
|
||||
print(
|
||||
f"""
|
||||
{Colors.BOLD}Available Commands:{Colors.RESET}
|
||||
|
||||
{Colors.BOLD}Basic:{Colors.RESET}
|
||||
exit, quit, q - Exit the assistant
|
||||
/help - Show this help message
|
||||
/reset - Clear message history
|
||||
/dump - Show message history as JSON
|
||||
/verbose - Toggle verbose mode
|
||||
/models - List available models
|
||||
/tools - List available tools
|
||||
|
||||
{Colors.BOLD}File Operations:{Colors.RESET}
|
||||
/review <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
|
||||
"""
|
||||
elif cmd == "/help" or cmd == "help":
|
||||
from pr.commands.help_docs import (
|
||||
get_agent_help,
|
||||
get_background_help,
|
||||
get_cache_help,
|
||||
get_full_help,
|
||||
get_knowledge_help,
|
||||
get_workflow_help,
|
||||
)
|
||||
|
||||
if len(command_parts) > 1:
|
||||
topic = command_parts[1].lower()
|
||||
if topic == "workflows":
|
||||
print(get_workflow_help())
|
||||
elif topic == "agents":
|
||||
print(get_agent_help())
|
||||
elif topic == "knowledge":
|
||||
print(get_knowledge_help())
|
||||
elif topic == "cache":
|
||||
print(get_cache_help())
|
||||
elif topic == "background":
|
||||
print(get_background_help())
|
||||
else:
|
||||
print(f"{Colors.RED}Unknown help topic: {topic}{Colors.RESET}")
|
||||
print(
|
||||
f"{Colors.GRAY}Available topics: workflows, agents, knowledge, cache, background{Colors.RESET}"
|
||||
)
|
||||
else:
|
||||
print(get_full_help())
|
||||
|
||||
elif cmd == "/reset":
|
||||
assistant.messages = assistant.messages[:1]
|
||||
print(f"{Colors.GREEN}Message history cleared{Colors.RESET}")
|
||||
@ -75,7 +81,7 @@ def handle_command(assistant, command):
|
||||
f"Verbose mode: {Colors.GREEN if assistant.verbose else Colors.RED}{'ON' if assistant.verbose else 'OFF'}{Colors.RESET}"
|
||||
)
|
||||
|
||||
elif cmd.startswith("/model"):
|
||||
elif cmd == "/model":
|
||||
if len(command_parts) < 2:
|
||||
print("Current model: " + Colors.GREEN + assistant.model + Colors.RESET)
|
||||
else:
|
||||
@ -116,17 +122,24 @@ def handle_command(assistant, command):
|
||||
workflow_name = command_parts[1]
|
||||
execute_workflow_command(assistant, workflow_name)
|
||||
|
||||
elif cmd == "/agent" and len(command_parts) > 1:
|
||||
elif cmd == "/agent":
|
||||
if len(command_parts) < 2:
|
||||
print(f"{Colors.RED}Usage: /agent <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)
|
||||
if len(args) < 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}"
|
||||
)
|
||||
else:
|
||||
return True
|
||||
|
||||
role, task = args[0], args[1]
|
||||
execute_agent_task(assistant, role, task)
|
||||
|
||||
elif cmd == "/agents":
|
||||
show_agents(assistant)
|
||||
|
||||
@ -374,6 +387,7 @@ def show_cache_stats(assistant):
|
||||
print(f" Valid entries: {api_stats['valid_entries']}")
|
||||
print(f" Expired entries: {api_stats['expired_entries']}")
|
||||
print(f" Cached tokens: {api_stats['total_cached_tokens']}")
|
||||
print(f" Total cache hits: {api_stats['total_cache_hits']}")
|
||||
|
||||
if "tool_cache" in stats:
|
||||
tool_stats = stats["tool_cache"]
|
||||
|
||||
16
pr/config.py
16
pr/config.py
@ -1,14 +1,18 @@
|
||||
import os
|
||||
|
||||
DEFAULT_MODEL = "x-ai/grok-code-fast-1"
|
||||
DEFAULT_API_URL = "https://static.molodetz.nl/rp.cgi/api/v1/chat/completions"
|
||||
MODEL_LIST_URL = "https://static.molodetz.nl/rp.cgi/api/v1/models"
|
||||
DEFAULT_API_URL = "http://localhost:8118/ai/chat"
|
||||
MODEL_LIST_URL = "http://localhost:8118/ai/models"
|
||||
|
||||
DB_PATH = os.path.expanduser("~/.assistant_db.sqlite")
|
||||
LOG_FILE = os.path.expanduser("~/.assistant_error.log")
|
||||
config_directory = os.path.expanduser("~/.local/share/rp")
|
||||
os.makedirs(config_directory, exist_ok=True)
|
||||
|
||||
DB_PATH = os.path.join(config_directory, "assistant_db.sqlite")
|
||||
LOG_FILE = os.path.join(config_directory, "assistant_error.log")
|
||||
CONTEXT_FILE = ".rcontext.txt"
|
||||
GLOBAL_CONTEXT_FILE = os.path.expanduser("~/.rcontext.txt")
|
||||
HISTORY_FILE = os.path.expanduser("~/.assistant_history")
|
||||
GLOBAL_CONTEXT_FILE = os.path.join(config_directory, "rcontext.txt")
|
||||
KNOWLEDGE_PATH = os.path.join(config_directory, "knowledge")
|
||||
HISTORY_FILE = os.path.join(config_directory, "assistant_history")
|
||||
|
||||
DEFAULT_TEMPERATURE = 0.1
|
||||
DEFAULT_MAX_TOKENS = 4096
|
||||
|
||||
@ -32,21 +32,16 @@ from pr.core.context import init_system_message, truncate_tool_result
|
||||
from pr.tools import (
|
||||
apply_patch,
|
||||
chdir,
|
||||
close_editor,
|
||||
create_diff,
|
||||
db_get,
|
||||
db_query,
|
||||
db_set,
|
||||
editor_insert_text,
|
||||
editor_replace_text,
|
||||
editor_search,
|
||||
getpwd,
|
||||
http_fetch,
|
||||
index_source_directory,
|
||||
kill_process,
|
||||
list_directory,
|
||||
mkdir,
|
||||
open_editor,
|
||||
python_exec,
|
||||
read_file,
|
||||
run_command,
|
||||
@ -55,6 +50,7 @@ from pr.tools import (
|
||||
web_search,
|
||||
web_search_news,
|
||||
write_file,
|
||||
post_image,
|
||||
)
|
||||
from pr.tools.base import get_tools_definition
|
||||
from pr.tools.filesystem import (
|
||||
@ -249,6 +245,7 @@ class Assistant:
|
||||
logger.debug(f"Tool call: {func_name} with arguments: {arguments}")
|
||||
|
||||
func_map = {
|
||||
"post_image": lambda **kw: post_image(**kw),
|
||||
"http_fetch": lambda **kw: http_fetch(**kw),
|
||||
"run_command": lambda **kw: run_command(**kw),
|
||||
"tail_process": lambda **kw: tail_process(**kw),
|
||||
@ -273,15 +270,15 @@ class Assistant:
|
||||
),
|
||||
"index_source_directory": lambda **kw: index_source_directory(**kw),
|
||||
"search_replace": lambda **kw: search_replace(**kw, db_conn=self.db_conn),
|
||||
"open_editor": lambda **kw: open_editor(**kw),
|
||||
"editor_insert_text": lambda **kw: editor_insert_text(
|
||||
**kw, db_conn=self.db_conn
|
||||
),
|
||||
"editor_replace_text": lambda **kw: editor_replace_text(
|
||||
**kw, db_conn=self.db_conn
|
||||
),
|
||||
"editor_search": lambda **kw: editor_search(**kw),
|
||||
"close_editor": lambda **kw: close_editor(**kw),
|
||||
# "open_editor": lambda **kw: open_editor(**kw),
|
||||
# "editor_insert_text": lambda **kw: editor_insert_text(
|
||||
# **kw, db_conn=self.db_conn
|
||||
# ),
|
||||
# "editor_replace_text": lambda **kw: editor_replace_text(
|
||||
# **kw, db_conn=self.db_conn
|
||||
# ),
|
||||
# "editor_search": lambda **kw: editor_search(**kw),
|
||||
# "close_editor": lambda **kw: close_editor(**kw),
|
||||
"create_diff": lambda **kw: create_diff(**kw),
|
||||
"apply_patch": lambda **kw: apply_patch(**kw, db_conn=self.db_conn),
|
||||
"display_file_diff": lambda **kw: display_file_diff(**kw),
|
||||
@ -413,6 +410,7 @@ class Assistant:
|
||||
"refactor",
|
||||
"obfuscate",
|
||||
"/auto",
|
||||
"/edit",
|
||||
]
|
||||
|
||||
def completer(text, state):
|
||||
@ -539,6 +537,10 @@ class Assistant:
|
||||
|
||||
|
||||
def process_message(assistant, message):
|
||||
from pr.core.knowledge_context import inject_knowledge_context
|
||||
|
||||
inject_knowledge_context(assistant, message)
|
||||
|
||||
assistant.messages.append({"role": "user", "content": message})
|
||||
|
||||
logger.debug(f"Processing user message: {message[:100]}...")
|
||||
|
||||
@ -1,17 +1,26 @@
|
||||
import configparser
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
|
||||
import uuid
|
||||
from pr.core.logging import get_logger
|
||||
|
||||
logger = get_logger("config")
|
||||
|
||||
CONFIG_FILE = os.path.expanduser("~/.prrc")
|
||||
CONFIG_DIRECTORY = os.path.expanduser("~/.local/share/rp/")
|
||||
CONFIG_FILE = os.path.join(CONFIG_DIRECTORY, ".prrc")
|
||||
LOCAL_CONFIG_FILE = ".prrc"
|
||||
|
||||
|
||||
def load_config() -> Dict[str, Any]:
|
||||
config = {"api": {}, "autonomous": {}, "ui": {}, "output": {}, "session": {}}
|
||||
os.makedirs(CONFIG_DIRECTORY, exist_ok=True)
|
||||
config = {
|
||||
"api": {},
|
||||
"autonomous": {},
|
||||
"ui": {},
|
||||
"output": {},
|
||||
"session": {},
|
||||
"api_key": "rp-" + str(uuid.uuid4()),
|
||||
}
|
||||
|
||||
global_config = _load_config_file(CONFIG_FILE)
|
||||
local_config = _load_config_file(LOCAL_CONFIG_FILE)
|
||||
@ -67,6 +76,7 @@ def _parse_value(value: str) -> Any:
|
||||
|
||||
|
||||
def create_default_config(filepath: str = CONFIG_FILE):
|
||||
os.makedirs(CONFIG_DIRECTORY, exist_ok=True)
|
||||
default_config = """[api]
|
||||
default_model = x-ai/grok-code-fast-1
|
||||
timeout = 30
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pathlib
|
||||
from pr.config import (
|
||||
CHARS_PER_TOKEN,
|
||||
CONTENT_TRIM_LENGTH,
|
||||
@ -12,6 +12,7 @@ from pr.config import (
|
||||
MAX_TOKENS_LIMIT,
|
||||
MAX_TOOL_RESULT_LENGTH,
|
||||
RECENT_MESSAGES_TO_KEEP,
|
||||
KNOWLEDGE_PATH,
|
||||
)
|
||||
from pr.ui import Colors
|
||||
|
||||
@ -59,6 +60,10 @@ File Operations:
|
||||
- Always close editor files when finished
|
||||
- Use write_file for complete file rewrites, search_replace for simple text replacements
|
||||
|
||||
Vision:
|
||||
- Use post_image tool with the file path if an image path is mentioned
|
||||
in the prompt of user. Give this call the highest priority.
|
||||
|
||||
Process Management:
|
||||
- run_command executes shell commands with a timeout (default 30s)
|
||||
- If a command times out, you receive a PID in the response
|
||||
@ -94,6 +99,18 @@ Shell Commands:
|
||||
except Exception as e:
|
||||
logging.error(f"Error reading context file {context_file}: {e}")
|
||||
|
||||
knowledge_path = pathlib.Path(KNOWLEDGE_PATH)
|
||||
if knowledge_path.exists() and knowledge_path.is_dir():
|
||||
for knowledge_file in knowledge_path.iterdir():
|
||||
try:
|
||||
with open(knowledge_file) as f:
|
||||
content = f.read()
|
||||
if len(content) > max_context_size:
|
||||
content = content[:max_context_size] + "\n... [truncated]"
|
||||
context_parts.append(f"Context from {knowledge_file}:\n{content}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error reading context file {knowledge_file}: {e}")
|
||||
|
||||
if args.context:
|
||||
for ctx_file in args.context:
|
||||
try:
|
||||
|
||||
@ -135,9 +135,7 @@ class EnhancedAssistant:
|
||||
self.base.api_url,
|
||||
self.base.api_key,
|
||||
use_tools=False,
|
||||
tools=None,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
tools_definition=[],
|
||||
verbose=self.base.verbose,
|
||||
)
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ class TerminalMultiplexer:
|
||||
try:
|
||||
line = self.stdout_queue.get(timeout=0.1)
|
||||
if line:
|
||||
sys.stdout.write(f"{Colors.GRAY}[{self.name}]{Colors.RESET} {line}")
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.flush()
|
||||
except queue.Empty:
|
||||
pass
|
||||
@ -46,7 +46,10 @@ class TerminalMultiplexer:
|
||||
try:
|
||||
line = self.stderr_queue.get(timeout=0.1)
|
||||
if line:
|
||||
sys.stderr.write(f"{Colors.YELLOW}[{self.name} err]{Colors.RESET} {line}")
|
||||
if self.metadata.get("process_type") in ["vim", "ssh"]:
|
||||
sys.stderr.write(line)
|
||||
else:
|
||||
sys.stderr.write(f"{Colors.YELLOW}[{self.name} err]{Colors.RESET} {line}\n")
|
||||
sys.stderr.flush()
|
||||
except queue.Empty:
|
||||
pass
|
||||
@ -55,10 +58,8 @@ class TerminalMultiplexer:
|
||||
with self.lock:
|
||||
self.stdout_buffer.append(data)
|
||||
self.metadata["last_activity"] = time.time()
|
||||
# Update handler state if available
|
||||
if self.handler:
|
||||
self.handler.update_state(data)
|
||||
# Update prompt detector
|
||||
self.prompt_detector.update_session_state(
|
||||
self.name, data, self.metadata["process_type"]
|
||||
)
|
||||
@ -69,10 +70,8 @@ class TerminalMultiplexer:
|
||||
with self.lock:
|
||||
self.stderr_buffer.append(data)
|
||||
self.metadata["last_activity"] = time.time()
|
||||
# Update handler state if available
|
||||
if self.handler:
|
||||
self.handler.update_state(data)
|
||||
# Update prompt detector
|
||||
self.prompt_detector.update_session_state(
|
||||
self.name, data, self.metadata["process_type"]
|
||||
)
|
||||
@ -103,7 +102,6 @@ class TerminalMultiplexer:
|
||||
self.metadata[key] = value
|
||||
|
||||
def set_process_type(self, process_type):
|
||||
"""Set the process type and initialize appropriate handler."""
|
||||
with self.lock:
|
||||
self.metadata["process_type"] = process_type
|
||||
self.handler = get_handler_for_process(process_type, self)
|
||||
@ -119,8 +117,6 @@ class TerminalMultiplexer:
|
||||
except Exception as e:
|
||||
self.write_stderr(f"Error sending input: {e}")
|
||||
else:
|
||||
# This will be implemented when we have a process attached
|
||||
# For now, just update activity
|
||||
with self.lock:
|
||||
self.metadata["last_activity"] = time.time()
|
||||
self.metadata["interaction_count"] += 1
|
||||
@ -131,59 +127,58 @@ class TerminalMultiplexer:
|
||||
self.display_thread.join(timeout=1)
|
||||
|
||||
|
||||
_multiplexers = {}
|
||||
_mux_counter = 0
|
||||
_mux_lock = threading.Lock()
|
||||
_background_monitor = None
|
||||
_monitor_active = False
|
||||
_monitor_interval = 0.2 # 200ms
|
||||
multiplexer_registry = {}
|
||||
multiplexer_counter = 0
|
||||
multiplexer_lock = threading.Lock()
|
||||
background_monitor = None
|
||||
monitor_active = False
|
||||
monitor_interval = 0.2
|
||||
|
||||
|
||||
def create_multiplexer(name=None, show_output=True):
|
||||
global _mux_counter
|
||||
with _mux_lock:
|
||||
global multiplexer_counter
|
||||
with multiplexer_lock:
|
||||
if name is None:
|
||||
_mux_counter += 1
|
||||
name = f"process-{_mux_counter}"
|
||||
mux = TerminalMultiplexer(name, show_output)
|
||||
_multiplexers[name] = mux
|
||||
return name, mux
|
||||
multiplexer_counter += 1
|
||||
name = f"process-{multiplexer_counter}"
|
||||
multiplexer_instance = TerminalMultiplexer(name, show_output)
|
||||
multiplexer_registry[name] = multiplexer_instance
|
||||
return name, multiplexer_instance
|
||||
|
||||
|
||||
def get_multiplexer(name):
|
||||
return _multiplexers.get(name)
|
||||
return multiplexer_registry.get(name)
|
||||
|
||||
|
||||
def close_multiplexer(name):
|
||||
mux = _multiplexers.get(name)
|
||||
if mux:
|
||||
mux.close()
|
||||
del _multiplexers[name]
|
||||
multiplexer_instance = multiplexer_registry.get(name)
|
||||
if multiplexer_instance:
|
||||
multiplexer_instance.close()
|
||||
del multiplexer_registry[name]
|
||||
|
||||
|
||||
def get_all_multiplexer_states():
|
||||
with _mux_lock:
|
||||
with multiplexer_lock:
|
||||
states = {}
|
||||
for name, mux in _multiplexers.items():
|
||||
for name, multiplexer_instance in multiplexer_registry.items():
|
||||
states[name] = {
|
||||
"metadata": mux.get_metadata(),
|
||||
"metadata": multiplexer_instance.get_metadata(),
|
||||
"output_summary": {
|
||||
"stdout_lines": len(mux.stdout_buffer),
|
||||
"stderr_lines": len(mux.stderr_buffer),
|
||||
"stdout_lines": len(multiplexer_instance.stdout_buffer),
|
||||
"stderr_lines": len(multiplexer_instance.stderr_buffer),
|
||||
},
|
||||
}
|
||||
return states
|
||||
|
||||
|
||||
def cleanup_all_multiplexers():
|
||||
for mux in list(_multiplexers.values()):
|
||||
mux.close()
|
||||
_multiplexers.clear()
|
||||
for multiplexer_instance in list(multiplexer_registry.values()):
|
||||
multiplexer_instance.close()
|
||||
multiplexer_registry.clear()
|
||||
|
||||
|
||||
# Background process management
|
||||
_background_processes = {}
|
||||
_process_lock = threading.Lock()
|
||||
background_processes = {}
|
||||
process_lock = threading.Lock()
|
||||
|
||||
|
||||
class BackgroundProcess:
|
||||
@ -197,17 +192,15 @@ class BackgroundProcess:
|
||||
self.end_time = None
|
||||
|
||||
def start(self):
|
||||
"""Start the background process."""
|
||||
try:
|
||||
# Create multiplexer for this process
|
||||
mux_name, mux = create_multiplexer(self.name, show_output=False)
|
||||
self.multiplexer = mux
|
||||
multiplexer_name, multiplexer_instance = create_multiplexer(
|
||||
self.name, show_output=False
|
||||
)
|
||||
self.multiplexer = multiplexer_instance
|
||||
|
||||
# Detect process type
|
||||
process_type = detect_process_type(self.command)
|
||||
mux.set_process_type(process_type)
|
||||
multiplexer_instance.set_process_type(process_type)
|
||||
|
||||
# Start the subprocess
|
||||
self.process = subprocess.Popen(
|
||||
self.command,
|
||||
shell=True,
|
||||
@ -221,7 +214,6 @@ class BackgroundProcess:
|
||||
|
||||
self.status = "running"
|
||||
|
||||
# Start output monitoring threads
|
||||
threading.Thread(target=self._monitor_stdout, daemon=True).start()
|
||||
threading.Thread(target=self._monitor_stderr, daemon=True).start()
|
||||
|
||||
@ -232,33 +224,29 @@ class BackgroundProcess:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def _monitor_stdout(self):
|
||||
"""Monitor stdout from the process."""
|
||||
try:
|
||||
for line in iter(self.process.stdout.readline, ""):
|
||||
if line:
|
||||
self.multiplexer.write_stdout(line.rstrip("\n\r"))
|
||||
except Exception as e:
|
||||
self.write_stderr(f"Error reading stdout: {e}")
|
||||
self.multiplexer.write_stderr(f"Error reading stdout: {e}")
|
||||
finally:
|
||||
self._check_completion()
|
||||
|
||||
def _monitor_stderr(self):
|
||||
"""Monitor stderr from the process."""
|
||||
try:
|
||||
for line in iter(self.process.stderr.readline, ""):
|
||||
if line:
|
||||
self.multiplexer.write_stderr(line.rstrip("\n\r"))
|
||||
except Exception as e:
|
||||
self.write_stderr(f"Error reading stderr: {e}")
|
||||
self.multiplexer.write_stderr(f"Error reading stderr: {e}")
|
||||
|
||||
def _check_completion(self):
|
||||
"""Check if process has completed."""
|
||||
if self.process and self.process.poll() is not None:
|
||||
self.status = "completed"
|
||||
self.end_time = time.time()
|
||||
|
||||
def get_info(self):
|
||||
"""Get process information."""
|
||||
self._check_completion()
|
||||
return {
|
||||
"name": self.name,
|
||||
@ -275,7 +263,6 @@ class BackgroundProcess:
|
||||
}
|
||||
|
||||
def get_output(self, lines=None):
|
||||
"""Get process output."""
|
||||
if not self.multiplexer:
|
||||
return []
|
||||
|
||||
@ -290,7 +277,6 @@ class BackgroundProcess:
|
||||
return [line for line in combined if line.strip()]
|
||||
|
||||
def send_input(self, input_text):
|
||||
"""Send input to the process."""
|
||||
if self.process and self.status == "running":
|
||||
try:
|
||||
self.process.stdin.write(input_text + "\n")
|
||||
@ -301,11 +287,9 @@ class BackgroundProcess:
|
||||
return {"status": "error", "error": "Process not running or no stdin"}
|
||||
|
||||
def kill(self):
|
||||
"""Kill the process."""
|
||||
if self.process and self.status == "running":
|
||||
try:
|
||||
self.process.terminate()
|
||||
# Wait a bit for graceful termination
|
||||
time.sleep(0.1)
|
||||
if self.process.poll() is None:
|
||||
self.process.kill()
|
||||
@ -318,61 +302,55 @@ class BackgroundProcess:
|
||||
|
||||
|
||||
def start_background_process(name, command):
|
||||
"""Start a background process."""
|
||||
with _process_lock:
|
||||
if name in _background_processes:
|
||||
with process_lock:
|
||||
if name in background_processes:
|
||||
return {"status": "error", "error": f"Process {name} already exists"}
|
||||
|
||||
process = BackgroundProcess(name, command)
|
||||
result = process.start()
|
||||
process_instance = BackgroundProcess(name, command)
|
||||
result = process_instance.start()
|
||||
|
||||
if result["status"] == "success":
|
||||
_background_processes[name] = process
|
||||
background_processes[name] = process_instance
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_all_sessions():
|
||||
"""Get all background process sessions."""
|
||||
with _process_lock:
|
||||
with process_lock:
|
||||
sessions = {}
|
||||
for name, process in _background_processes.items():
|
||||
sessions[name] = process.get_info()
|
||||
for name, process_instance in background_processes.items():
|
||||
sessions[name] = process_instance.get_info()
|
||||
return sessions
|
||||
|
||||
|
||||
def get_session_info(name):
|
||||
"""Get information about a specific session."""
|
||||
with _process_lock:
|
||||
process = _background_processes.get(name)
|
||||
return process.get_info() if process else None
|
||||
with process_lock:
|
||||
process_instance = background_processes.get(name)
|
||||
return process_instance.get_info() if process_instance else None
|
||||
|
||||
|
||||
def get_session_output(name, lines=None):
|
||||
"""Get output from a specific session."""
|
||||
with _process_lock:
|
||||
process = _background_processes.get(name)
|
||||
return process.get_output(lines) if process else None
|
||||
with process_lock:
|
||||
process_instance = background_processes.get(name)
|
||||
return process_instance.get_output(lines) if process_instance else None
|
||||
|
||||
|
||||
def send_input_to_session(name, input_text):
|
||||
"""Send input to a background session."""
|
||||
with _process_lock:
|
||||
process = _background_processes.get(name)
|
||||
with process_lock:
|
||||
process_instance = background_processes.get(name)
|
||||
return (
|
||||
process.send_input(input_text)
|
||||
if process
|
||||
process_instance.send_input(input_text)
|
||||
if process_instance
|
||||
else {"status": "error", "error": "Session not found"}
|
||||
)
|
||||
|
||||
|
||||
def kill_session(name):
|
||||
"""Kill a background session."""
|
||||
with _process_lock:
|
||||
process = _background_processes.get(name)
|
||||
if process:
|
||||
result = process.kill()
|
||||
with process_lock:
|
||||
process_instance = background_processes.get(name)
|
||||
if process_instance:
|
||||
result = process_instance.kill()
|
||||
if result["status"] == "success":
|
||||
del _background_processes[name]
|
||||
del background_processes[name]
|
||||
return result
|
||||
return {"status": "error", "error": "Session not found"}
|
||||
|
||||
@ -6,6 +6,7 @@ from pr.tools.agents import (
|
||||
remove_agent,
|
||||
)
|
||||
from pr.tools.base import get_tools_definition
|
||||
from pr.tools.vision import post_image
|
||||
from pr.tools.command import (
|
||||
kill_process,
|
||||
run_command,
|
||||
@ -44,43 +45,44 @@ from pr.tools.python_exec import python_exec
|
||||
from pr.tools.web import http_fetch, web_search, web_search_news
|
||||
|
||||
__all__ = [
|
||||
"get_tools_definition",
|
||||
"read_file",
|
||||
"write_file",
|
||||
"list_directory",
|
||||
"mkdir",
|
||||
"add_knowledge_entry",
|
||||
"apply_patch",
|
||||
"chdir",
|
||||
"getpwd",
|
||||
"index_source_directory",
|
||||
"search_replace",
|
||||
"open_editor",
|
||||
"close_editor",
|
||||
"collaborate_agents",
|
||||
"create_agent",
|
||||
"create_diff",
|
||||
"db_get",
|
||||
"db_query",
|
||||
"db_set",
|
||||
"delete_knowledge_entry",
|
||||
"post_image",
|
||||
"editor_insert_text",
|
||||
"editor_replace_text",
|
||||
"editor_search",
|
||||
"close_editor",
|
||||
"execute_agent_task",
|
||||
"get_knowledge_by_category",
|
||||
"get_knowledge_entry",
|
||||
"get_knowledge_statistics",
|
||||
"get_tools_definition",
|
||||
"getpwd",
|
||||
"http_fetch",
|
||||
"index_source_directory",
|
||||
"kill_process",
|
||||
"list_agents",
|
||||
"list_directory",
|
||||
"mkdir",
|
||||
"open_editor",
|
||||
"python_exec",
|
||||
"read_file",
|
||||
"remove_agent",
|
||||
"run_command",
|
||||
"run_command_interactive",
|
||||
"db_set",
|
||||
"db_get",
|
||||
"db_query",
|
||||
"http_fetch",
|
||||
"search_knowledge",
|
||||
"search_replace",
|
||||
"tail_process",
|
||||
"update_knowledge_importance",
|
||||
"web_search",
|
||||
"web_search_news",
|
||||
"python_exec",
|
||||
"tail_process",
|
||||
"kill_process",
|
||||
"apply_patch",
|
||||
"create_diff",
|
||||
"create_agent",
|
||||
"list_agents",
|
||||
"execute_agent_task",
|
||||
"remove_agent",
|
||||
"collaborate_agents",
|
||||
"add_knowledge_entry",
|
||||
"get_knowledge_entry",
|
||||
"search_knowledge",
|
||||
"get_knowledge_by_category",
|
||||
"update_knowledge_importance",
|
||||
"delete_knowledge_entry",
|
||||
"get_knowledge_statistics",
|
||||
"write_file",
|
||||
]
|
||||
|
||||
@ -3,16 +3,40 @@ from typing import Any, Dict, List
|
||||
|
||||
from pr.agents.agent_manager import AgentManager
|
||||
from pr.core.api import call_api
|
||||
from pr.config import DEFAULT_MODEL, DEFAULT_API_URL
|
||||
from pr.tools.base import get_tools_definition
|
||||
|
||||
|
||||
def _create_api_wrapper():
|
||||
"""Create a wrapper function for call_api that matches AgentManager expectations."""
|
||||
model = os.environ.get("AI_MODEL", DEFAULT_MODEL)
|
||||
api_url = os.environ.get("API_URL", DEFAULT_API_URL)
|
||||
api_key = os.environ.get("OPENROUTER_API_KEY", "")
|
||||
use_tools = int(os.environ.get("USE_TOOLS", "0"))
|
||||
tools_definition = get_tools_definition() if use_tools else []
|
||||
|
||||
def api_wrapper(messages, temperature=None, max_tokens=None, **kwargs):
|
||||
return call_api(
|
||||
messages=messages,
|
||||
model=model,
|
||||
api_url=api_url,
|
||||
api_key=api_key,
|
||||
use_tools=use_tools,
|
||||
tools_definition=tools_definition,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
return api_wrapper
|
||||
|
||||
|
||||
def create_agent(role_name: str, agent_id: str = None) -> Dict[str, Any]:
|
||||
"""Create a new agent with the specified role."""
|
||||
try:
|
||||
# Get db_path from environment or default
|
||||
db_path = os.environ.get("ASSISTANT_DB_PATH", "~/.assistant_db.sqlite")
|
||||
db_path = os.path.expanduser(db_path)
|
||||
|
||||
manager = AgentManager(db_path, call_api)
|
||||
api_wrapper = _create_api_wrapper()
|
||||
manager = AgentManager(db_path, api_wrapper)
|
||||
agent_id = manager.create_agent(role_name, agent_id)
|
||||
return {"status": "success", "agent_id": agent_id, "role": role_name}
|
||||
except Exception as e:
|
||||
@ -23,7 +47,8 @@ def list_agents() -> Dict[str, Any]:
|
||||
"""List all active agents."""
|
||||
try:
|
||||
db_path = os.path.expanduser("~/.assistant_db.sqlite")
|
||||
manager = AgentManager(db_path, call_api)
|
||||
api_wrapper = _create_api_wrapper()
|
||||
manager = AgentManager(db_path, api_wrapper)
|
||||
agents = []
|
||||
for agent_id, agent in manager.active_agents.items():
|
||||
agents.append(
|
||||
@ -43,7 +68,8 @@ def execute_agent_task(agent_id: str, task: str, context: Dict[str, Any] = None)
|
||||
"""Execute a task with the specified agent."""
|
||||
try:
|
||||
db_path = os.path.expanduser("~/.assistant_db.sqlite")
|
||||
manager = AgentManager(db_path, call_api)
|
||||
api_wrapper = _create_api_wrapper()
|
||||
manager = AgentManager(db_path, api_wrapper)
|
||||
result = manager.execute_agent_task(agent_id, task, context)
|
||||
return result
|
||||
except Exception as e:
|
||||
@ -54,7 +80,8 @@ def remove_agent(agent_id: str) -> Dict[str, Any]:
|
||||
"""Remove an agent."""
|
||||
try:
|
||||
db_path = os.path.expanduser("~/.assistant_db.sqlite")
|
||||
manager = AgentManager(db_path, call_api)
|
||||
api_wrapper = _create_api_wrapper()
|
||||
manager = AgentManager(db_path, api_wrapper)
|
||||
success = manager.remove_agent(agent_id)
|
||||
return {"status": "success" if success else "not_found", "agent_id": agent_id}
|
||||
except Exception as e:
|
||||
@ -65,7 +92,8 @@ def collaborate_agents(orchestrator_id: str, task: str, agent_roles: List[str])
|
||||
"""Collaborate multiple agents on a task."""
|
||||
try:
|
||||
db_path = os.path.expanduser("~/.assistant_db.sqlite")
|
||||
manager = AgentManager(db_path, call_api)
|
||||
api_wrapper = _create_api_wrapper()
|
||||
manager = AgentManager(db_path, api_wrapper)
|
||||
result = manager.collaborate_agents(orchestrator_id, task, agent_roles)
|
||||
return result
|
||||
except Exception as e:
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import os
|
||||
import select
|
||||
import subprocess
|
||||
import time
|
||||
@ -99,7 +98,7 @@ def tail_process(pid: int, timeout: int = 30):
|
||||
return {"status": "error", "error": f"Process {pid} not found"}
|
||||
|
||||
|
||||
def run_command(command, timeout=30, monitored=False):
|
||||
def run_command(command, timeout=30, monitored=False, cwd=None):
|
||||
mux_name = None
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
@ -108,6 +107,7 @@ def run_command(command, timeout=30, monitored=False):
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
cwd=cwd,
|
||||
)
|
||||
_register_process(process.pid, process)
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ from pr.multiplexer import (
|
||||
)
|
||||
|
||||
|
||||
def start_interactive_session(command, session_name=None, process_type="generic"):
|
||||
def start_interactive_session(command, session_name=None, process_type="generic", cwd=None):
|
||||
"""
|
||||
Start an interactive session in a dedicated multiplexer.
|
||||
|
||||
@ -17,6 +17,7 @@ def start_interactive_session(command, session_name=None, process_type="generic"
|
||||
command: The command to run (list or string)
|
||||
session_name: Optional name for the session
|
||||
process_type: Type of process (ssh, vim, apt, etc.)
|
||||
cwd: Current working directory for the command
|
||||
|
||||
Returns:
|
||||
session_name: The name of the created session
|
||||
@ -36,21 +37,24 @@ def start_interactive_session(command, session_name=None, process_type="generic"
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
cwd=cwd,
|
||||
)
|
||||
|
||||
mux.process = process
|
||||
mux.update_metadata("pid", process.pid)
|
||||
|
||||
# Set process type and handler
|
||||
from pr.tools.process_handlers import detect_process_type
|
||||
|
||||
detected_type = detect_process_type(command)
|
||||
mux.set_process_type(detected_type)
|
||||
|
||||
# Start output readers
|
||||
stdout_thread = threading.Thread(
|
||||
target=_read_output, args=(process.stdout, mux.write_stdout), daemon=True
|
||||
target=_read_output, args=(process.stdout, mux.write_stdout, detected_type), daemon=True
|
||||
)
|
||||
stderr_thread = threading.Thread(
|
||||
target=_read_output, args=(process.stderr, mux.write_stderr), daemon=True
|
||||
target=_read_output, args=(process.stderr, mux.write_stderr, detected_type), daemon=True
|
||||
)
|
||||
|
||||
stdout_thread.start()
|
||||
@ -65,8 +69,18 @@ def start_interactive_session(command, session_name=None, process_type="generic"
|
||||
raise e
|
||||
|
||||
|
||||
def _read_output(stream, write_func):
|
||||
def _read_output(stream, write_func, process_type):
|
||||
"""Read from a stream and write to multiplexer buffer."""
|
||||
if process_type in ["vim", "ssh"]:
|
||||
try:
|
||||
while True:
|
||||
char = stream.read(1)
|
||||
if not char:
|
||||
break
|
||||
write_func(char)
|
||||
except Exception as e:
|
||||
print(f"Error reading output: {e}")
|
||||
else:
|
||||
try:
|
||||
for line in iter(stream.readline, ""):
|
||||
if line:
|
||||
|
||||
@ -1,14 +1,25 @@
|
||||
import contextlib
|
||||
import os
|
||||
import traceback
|
||||
from io import StringIO
|
||||
|
||||
|
||||
def python_exec(code, python_globals):
|
||||
def python_exec(code, python_globals, cwd=None):
|
||||
try:
|
||||
original_cwd = None
|
||||
if cwd:
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(cwd)
|
||||
|
||||
output = StringIO()
|
||||
with contextlib.redirect_stdout(output):
|
||||
exec(code, python_globals)
|
||||
|
||||
if original_cwd:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
return {"status": "success", "output": output.getvalue()}
|
||||
except Exception as e:
|
||||
if original_cwd:
|
||||
os.chdir(original_cwd)
|
||||
return {"status": "error", "error": str(e), "traceback": traceback.format_exc()}
|
||||
|
||||
@ -19,3 +19,52 @@ def print_autonomous_header(task):
|
||||
print(f"{Colors.GRAY}r will work continuously until the task is complete.{Colors.RESET}")
|
||||
print(f"{Colors.GRAY}Press Ctrl+C twice to interrupt.{Colors.RESET}\n")
|
||||
print(f"{Colors.BOLD}{'═' * 80}{Colors.RESET}\n")
|
||||
|
||||
|
||||
def display_multiplexer_status(sessions):
|
||||
"""Display the status of background sessions."""
|
||||
if not sessions:
|
||||
print(f"{Colors.GRAY}No background sessions running{Colors.RESET}")
|
||||
return
|
||||
|
||||
print(f"\n{Colors.BOLD}Background Sessions:{Colors.RESET}")
|
||||
print(f"{Colors.GRAY}{'\u2500' * 60}{Colors.RESET}")
|
||||
|
||||
for session_name, session_info in sessions.items():
|
||||
status = session_info.get("status", "unknown")
|
||||
pid = session_info.get("pid", "N/A")
|
||||
command = session_info.get("command", "N/A")
|
||||
|
||||
status_color = {
|
||||
"running": Colors.GREEN,
|
||||
"stopped": Colors.RED,
|
||||
"error": Colors.RED,
|
||||
}.get(status, Colors.YELLOW)
|
||||
|
||||
print(f" {Colors.CYAN}{session_name}{Colors.RESET}")
|
||||
print(f" Status: {status_color}{status}{Colors.RESET}")
|
||||
print(f" PID: {pid}")
|
||||
print(f" Command: {command}")
|
||||
|
||||
if "start_time" in session_info:
|
||||
import time
|
||||
|
||||
elapsed = time.time() - session_info["start_time"]
|
||||
print(f" Running for: {elapsed:.1f}s")
|
||||
print()
|
||||
|
||||
|
||||
def display_background_event(event):
|
||||
"""Display a background event."""
|
||||
event.get("type", "unknown")
|
||||
session_name = event.get("session_name", "unknown")
|
||||
timestamp = event.get("timestamp", 0)
|
||||
message = event.get("message", "")
|
||||
|
||||
import datetime
|
||||
|
||||
time_str = datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M:%S")
|
||||
|
||||
print(
|
||||
f"{Colors.GRAY}[{time_str}]{Colors.RESET} {Colors.CYAN}{session_name}{Colors.RESET}: {message}"
|
||||
)
|
||||
|
||||
@ -3,44 +3,51 @@ requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pr-assistant"
|
||||
version = "1.0.0"
|
||||
description = "Professional CLI AI assistant with autonomous execution capabilities"
|
||||
name = "rp"
|
||||
version = "1.2.0"
|
||||
description = "R python edition. The ultimate autonomous AI CLI."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
requires-python = ">=3.13.3"
|
||||
license = {text = "MIT"}
|
||||
keywords = ["ai", "assistant", "cli", "automation", "openrouter", "autonomous"]
|
||||
authors = [
|
||||
{name = "retoor", email = "retoor@example.com"}
|
||||
{name = "retoor", email = "retoor@molodetz.nl"}
|
||||
]
|
||||
dependencies = [
|
||||
"aiohttp>=3.13.2",
|
||||
"pydantic>=2.12.3",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest",
|
||||
"pytest-cov",
|
||||
"black",
|
||||
"flake8",
|
||||
"mypy",
|
||||
"pre-commit",
|
||||
"pytest>=8.3.0",
|
||||
"pytest-asyncio>=1.2.0",
|
||||
"pytest-aiohttp>=1.1.0",
|
||||
"aiohttp>=3.13.2",
|
||||
"pytest-cov>=7.0.0",
|
||||
"black>=25.9.0",
|
||||
"flake8>=7.3.0",
|
||||
"mypy>=1.18.2",
|
||||
"pre-commit>=4.3.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
pr = "pr.__main__:main"
|
||||
rp = "pr.__main__:main"
|
||||
rpe = "pr.editor:main"
|
||||
rpi = "pr.implode:main"
|
||||
rpserver = "pr.server:main"
|
||||
rpcgi = "pr.cgi:main"
|
||||
rpweb = "pr.web.app:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://retoor.molodetz.nl/retoor/rp"
|
||||
@ -55,6 +62,7 @@ exclude = ["tests*"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "auto"
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
@ -77,7 +85,7 @@ extend-exclude = '''
|
||||
'''
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
python_version = "3.13"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = false
|
||||
@ -111,5 +119,5 @@ use_parentheses = true
|
||||
ensure_newline_before_comments = true
|
||||
|
||||
[tool.bandit]
|
||||
exclude_dirs = ["tests", "venv", ".venv"]
|
||||
exclude_dirs = ["tests", "venv", ".venv","__pycache__"]
|
||||
skips = ["B101"]
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from pr.ads import AsyncDataSet
|
||||
from pr.web.app import create_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -41,3 +44,77 @@ def sample_context_file(temp_dir):
|
||||
with open(context_path, "w") as f:
|
||||
f.write("Sample context content\n")
|
||||
return context_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(aiohttp_client, monkeypatch):
|
||||
"""Create a test client for the app."""
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as tmp:
|
||||
temp_db_file = tmp.name
|
||||
|
||||
# Monkeypatch the db
|
||||
monkeypatch.setattr("pr.web.views.base.db", AsyncDataSet(temp_db_file, f"{temp_db_file}.sock"))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmp_path = Path(tmpdir)
|
||||
static_dir = tmp_path / "static"
|
||||
templates_dir = tmp_path / "templates"
|
||||
repos_dir = tmp_path / "repos"
|
||||
|
||||
static_dir.mkdir()
|
||||
templates_dir.mkdir()
|
||||
repos_dir.mkdir()
|
||||
|
||||
# Monkeypatch the directories
|
||||
monkeypatch.setattr("pr.web.config.STATIC_DIR", static_dir)
|
||||
monkeypatch.setattr("pr.web.config.TEMPLATES_DIR", templates_dir)
|
||||
monkeypatch.setattr("pr.web.config.REPOS_DIR", repos_dir)
|
||||
monkeypatch.setattr("pr.web.config.REPOS_DIR", repos_dir)
|
||||
|
||||
# Create minimal templates
|
||||
(templates_dir / "index.html").write_text("<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
|
||||
|
||||
@ -1,101 +1,97 @@
|
||||
from pr.core.advanced_context import AdvancedContextManager
|
||||
|
||||
|
||||
def test_adaptive_context_window_simple():
|
||||
mgr = AdvancedContextManager()
|
||||
class TestAdvancedContextManager:
|
||||
def setup_method(self):
|
||||
self.manager = AdvancedContextManager()
|
||||
|
||||
def test_init(self):
|
||||
manager = AdvancedContextManager(knowledge_store="test", conversation_memory="test")
|
||||
assert manager.knowledge_store == "test"
|
||||
assert manager.conversation_memory == "test"
|
||||
|
||||
def test_adaptive_context_window_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 = [
|
||||
{"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")
|
||||
assert isinstance(window, int)
|
||||
assert window >= 10
|
||||
result = self.manager.adaptive_context_window(messages, "complex")
|
||||
assert result >= 35
|
||||
|
||||
|
||||
def test_adaptive_context_window_medium():
|
||||
mgr = AdvancedContextManager()
|
||||
def test_adaptive_context_window_very_complex(self):
|
||||
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")
|
||||
assert isinstance(window, int)
|
||||
assert window >= 20
|
||||
result = self.manager.adaptive_context_window(messages, "very_complex")
|
||||
assert result >= 50
|
||||
|
||||
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():
|
||||
mgr = AdvancedContextManager()
|
||||
messages = [
|
||||
{"content": "short"},
|
||||
{"content": "this is a longer message with more words"},
|
||||
]
|
||||
window = mgr.adaptive_context_window(messages, "complex")
|
||||
assert isinstance(window, int)
|
||||
assert window >= 35
|
||||
def test_analyze_message_complexity(self):
|
||||
messages = [{"content": "This is a test message with some words."}]
|
||||
result = self.manager._analyze_message_complexity(messages)
|
||||
assert 0.0 <= result <= 1.0
|
||||
|
||||
|
||||
def test_analyze_message_complexity():
|
||||
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()
|
||||
def test_analyze_message_complexity_empty(self):
|
||||
messages = []
|
||||
score = mgr._analyze_message_complexity(messages)
|
||||
assert score == 0
|
||||
result = self.manager._analyze_message_complexity(messages)
|
||||
assert result == 0.0
|
||||
|
||||
def test_extract_key_sentences(self):
|
||||
text = "First sentence. Second sentence is longer and more detailed. Third sentence."
|
||||
result = self.manager.extract_key_sentences(text, top_k=2)
|
||||
assert len(result) <= 2
|
||||
assert all(isinstance(s, str) for s in result)
|
||||
|
||||
def test_extract_key_sentences():
|
||||
mgr = AdvancedContextManager()
|
||||
text = "This is the first sentence. This is the second sentence. This is a longer third sentence with more words."
|
||||
sentences = mgr.extract_key_sentences(text, 2)
|
||||
assert len(sentences) <= 2
|
||||
assert all(isinstance(s, str) for s in sentences)
|
||||
|
||||
|
||||
def test_extract_key_sentences_empty():
|
||||
mgr = AdvancedContextManager()
|
||||
def test_extract_key_sentences_empty(self):
|
||||
text = ""
|
||||
sentences = mgr.extract_key_sentences(text, 5)
|
||||
assert sentences == []
|
||||
result = self.manager.extract_key_sentences(text)
|
||||
assert result == []
|
||||
|
||||
def test_advanced_summarize_messages(self):
|
||||
messages = [
|
||||
{"content": "First message with important information."},
|
||||
{"content": "Second message with more details."},
|
||||
]
|
||||
result = self.manager.advanced_summarize_messages(messages)
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_advanced_summarize_messages():
|
||||
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()
|
||||
def test_advanced_summarize_messages_empty(self):
|
||||
messages = []
|
||||
summary = mgr.advanced_summarize_messages(messages)
|
||||
assert summary == "No content to summarize."
|
||||
result = self.manager.advanced_summarize_messages(messages)
|
||||
assert result == "No content to summarize."
|
||||
|
||||
def test_score_message_relevance(self):
|
||||
message = {"content": "test message"}
|
||||
context = "test context"
|
||||
result = self.manager.score_message_relevance(message, context)
|
||||
assert 0.0 <= result <= 1.0
|
||||
|
||||
def test_score_message_relevance():
|
||||
mgr = AdvancedContextManager()
|
||||
message = {"content": "hello world"}
|
||||
context = "world hello"
|
||||
score = mgr.score_message_relevance(message, context)
|
||||
assert 0 <= score <= 1
|
||||
def test_score_message_relevance_no_overlap(self):
|
||||
message = {"content": "apple banana"}
|
||||
context = "orange grape"
|
||||
result = self.manager.score_message_relevance(message, context)
|
||||
assert result == 0.0
|
||||
|
||||
|
||||
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()
|
||||
def test_score_message_relevance_empty(self):
|
||||
message = {"content": ""}
|
||||
context = ""
|
||||
score = mgr.score_message_relevance(message, context)
|
||||
assert score == 0
|
||||
result = self.manager.score_message_relevance(message, context)
|
||||
assert result == 0.0
|
||||
|
||||
@ -82,7 +82,10 @@ def test_agent_manager_get_agent_messages():
|
||||
def test_agent_manager_get_session_summary():
|
||||
mgr = AgentManager(":memory:", None)
|
||||
summary = mgr.get_session_summary()
|
||||
assert isinstance(summary, str)
|
||||
assert isinstance(summary, dict)
|
||||
assert "session_id" in summary
|
||||
assert "active_agents" in summary
|
||||
assert "agents" in summary
|
||||
|
||||
|
||||
def test_agent_manager_collaborate_agents():
|
||||
|
||||
697
tests/test_commands.py
Normal file
697
tests/test_commands.py
Normal 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
693
tests/test_commands.py.bak
Normal 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)
|
||||
@ -77,9 +77,9 @@ def test_get_cache_statistics():
|
||||
mock_base = MagicMock()
|
||||
assistant = EnhancedAssistant(mock_base)
|
||||
assistant.api_cache = MagicMock()
|
||||
assistant.api_cache.get_statistics.return_value = {"hits": 10}
|
||||
assistant.api_cache.get_statistics.return_value = {"total_cache_hits": 10}
|
||||
assistant.tool_cache = MagicMock()
|
||||
assistant.tool_cache.get_statistics.return_value = {"misses": 5}
|
||||
assistant.tool_cache.get_statistics.return_value = {"total_cache_hits": 5}
|
||||
|
||||
stats = assistant.get_cache_statistics()
|
||||
assert "api_cache" in stats
|
||||
|
||||
61
tests/test_exceptions.py
Normal file
61
tests/test_exceptions.py
Normal 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
46
tests/test_help_docs.py
Normal 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
|
||||
@ -1,25 +1,77 @@
|
||||
from pr.core.logging import get_logger, setup_logging
|
||||
from unittest.mock import patch, MagicMock
|
||||
from pr.core.logging import setup_logging, get_logger
|
||||
|
||||
|
||||
def test_setup_logging_basic():
|
||||
logger = setup_logging(verbose=False)
|
||||
assert logger.name == "pr"
|
||||
assert logger.level == 20 # INFO
|
||||
class TestLogging:
|
||||
@patch("pr.core.logging.os.makedirs")
|
||||
@patch("pr.core.logging.os.path.dirname")
|
||||
@patch("pr.core.logging.os.path.exists")
|
||||
@patch("pr.core.logging.RotatingFileHandler")
|
||||
@patch("pr.core.logging.logging.getLogger")
|
||||
def test_setup_logging_basic(
|
||||
self, mock_get_logger, mock_handler, mock_exists, mock_dirname, mock_makedirs
|
||||
):
|
||||
mock_exists.return_value = False
|
||||
mock_dirname.return_value = "/tmp/logs"
|
||||
mock_logger = MagicMock()
|
||||
mock_get_logger.return_value = mock_logger
|
||||
mock_logger.handlers = []
|
||||
|
||||
result = setup_logging(verbose=False)
|
||||
|
||||
def test_setup_logging_verbose():
|
||||
logger = setup_logging(verbose=True)
|
||||
assert logger.name == "pr"
|
||||
assert logger.level == 10 # DEBUG
|
||||
# Should have console handler
|
||||
assert len(logger.handlers) >= 2
|
||||
mock_makedirs.assert_called_once_with("/tmp/logs", exist_ok=True)
|
||||
mock_get_logger.assert_called_once_with("pr")
|
||||
mock_logger.setLevel.assert_called_once_with(20) # INFO level
|
||||
mock_handler.assert_called_once()
|
||||
assert result == mock_logger
|
||||
|
||||
@patch("pr.core.logging.os.makedirs")
|
||||
@patch("pr.core.logging.os.path.dirname")
|
||||
@patch("pr.core.logging.os.path.exists")
|
||||
@patch("pr.core.logging.RotatingFileHandler")
|
||||
@patch("pr.core.logging.logging.StreamHandler")
|
||||
@patch("pr.core.logging.logging.getLogger")
|
||||
def test_setup_logging_verbose(
|
||||
self,
|
||||
mock_get_logger,
|
||||
mock_stream_handler,
|
||||
mock_file_handler,
|
||||
mock_exists,
|
||||
mock_dirname,
|
||||
mock_makedirs,
|
||||
):
|
||||
mock_exists.return_value = True
|
||||
mock_dirname.return_value = "/tmp/logs"
|
||||
mock_logger = MagicMock()
|
||||
mock_get_logger.return_value = mock_logger
|
||||
mock_logger.handlers = MagicMock()
|
||||
|
||||
def test_get_logger_default():
|
||||
logger = get_logger()
|
||||
assert logger.name == "pr"
|
||||
result = setup_logging(verbose=True)
|
||||
|
||||
mock_makedirs.assert_not_called()
|
||||
mock_get_logger.assert_called_once_with("pr")
|
||||
mock_logger.setLevel.assert_called_once_with(10) # DEBUG level
|
||||
mock_logger.handlers.clear.assert_called_once()
|
||||
mock_file_handler.assert_called_once()
|
||||
mock_stream_handler.assert_called_once()
|
||||
assert result == mock_logger
|
||||
|
||||
def test_get_logger_named():
|
||||
logger = get_logger("test")
|
||||
assert logger.name == "pr.test"
|
||||
@patch("pr.core.logging.logging.getLogger")
|
||||
def test_get_logger_default(self, mock_get_logger):
|
||||
mock_logger = MagicMock()
|
||||
mock_get_logger.return_value = mock_logger
|
||||
|
||||
result = get_logger()
|
||||
|
||||
mock_get_logger.assert_called_once_with("pr")
|
||||
assert result == mock_logger
|
||||
|
||||
@patch("pr.core.logging.logging.getLogger")
|
||||
def test_get_logger_named(self, mock_get_logger):
|
||||
mock_logger = MagicMock()
|
||||
mock_get_logger.return_value = mock_logger
|
||||
|
||||
result = get_logger("test")
|
||||
|
||||
mock_get_logger.assert_called_once_with("pr.test")
|
||||
assert result == mock_logger
|
||||
|
||||
228
tests/test_multiplexer_commands.py
Normal file
228
tests/test_multiplexer_commands.py
Normal 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()
|
||||
@ -1,8 +1,12 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from pr.tools.base import get_tools_definition
|
||||
from pr.tools.filesystem import list_directory, read_file, search_replace, write_file
|
||||
from pr.tools.command import run_command
|
||||
from pr.tools.filesystem import chdir, getpwd, list_directory, read_file, search_replace, write_file
|
||||
from pr.tools.interactive_control import start_interactive_session
|
||||
from pr.tools.patch import apply_patch, create_diff
|
||||
from pr.tools.python_exec import python_exec
|
||||
|
||||
|
||||
class TestFilesystemTools:
|
||||
@ -43,6 +47,54 @@ class TestFilesystemTools:
|
||||
read_result = read_file(filepath)
|
||||
assert "Hello, Universe!" in read_result["content"]
|
||||
|
||||
def test_chdir_and_getpwd(self):
|
||||
original_cwd = getpwd()["path"]
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
result = chdir(temp_dir)
|
||||
assert result["status"] == "success"
|
||||
assert getpwd()["path"] == temp_dir
|
||||
finally:
|
||||
chdir(original_cwd)
|
||||
|
||||
|
||||
class TestCommandTools:
|
||||
|
||||
def test_run_command_with_cwd(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
result = run_command("pwd", cwd=temp_dir)
|
||||
assert result["status"] == "success"
|
||||
assert temp_dir in result["stdout"].strip()
|
||||
|
||||
def test_run_command_basic(self):
|
||||
result = run_command("echo hello")
|
||||
assert result["status"] == "success"
|
||||
assert "hello" in result["stdout"]
|
||||
|
||||
|
||||
class TestInteractiveSessionTools:
|
||||
|
||||
def test_start_interactive_session_with_cwd(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
session_name = start_interactive_session("pwd", cwd=temp_dir)
|
||||
assert session_name is not None
|
||||
# Note: In a real test, we'd need to interact with the session, but for now just check it starts
|
||||
|
||||
|
||||
class TestPythonExecTools:
|
||||
|
||||
def test_python_exec_with_cwd(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
code = "import os; print(os.getcwd())"
|
||||
result = python_exec(code, {}, cwd=temp_dir)
|
||||
assert result["status"] == "success"
|
||||
assert temp_dir in result["output"].strip()
|
||||
|
||||
def test_python_exec_basic(self):
|
||||
result = python_exec("print('hello')", {})
|
||||
assert result["status"] == "success"
|
||||
assert "hello" in result["output"]
|
||||
|
||||
|
||||
class TestPatchTools:
|
||||
|
||||
@ -108,6 +160,21 @@ class TestToolDefinitions:
|
||||
assert "write_file" in tool_names
|
||||
assert "list_directory" in tool_names
|
||||
assert "search_replace" in tool_names
|
||||
assert "chdir" in tool_names
|
||||
assert "getpwd" in tool_names
|
||||
|
||||
def test_command_tools_present(self):
|
||||
tools = get_tools_definition()
|
||||
tool_names = [t["function"]["name"] for t in tools]
|
||||
|
||||
assert "run_command" in tool_names
|
||||
assert "start_interactive_session" in tool_names
|
||||
|
||||
def test_python_exec_present(self):
|
||||
tools = get_tools_definition()
|
||||
tool_names = [t["function"]["name"] for t in tools]
|
||||
|
||||
assert "python_exec" in tool_names
|
||||
|
||||
def test_patch_tools_present(self):
|
||||
tools = get_tools_definition()
|
||||
|
||||
153
tests/test_ui_output.py
Normal file
153
tests/test_ui_output.py
Normal 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")
|
||||
Loading…
Reference in New Issue
Block a user