Based on claude-code-tools TmuxCLIController, this refactor: - Added DockerTmuxController class for robust tmux session management - Implements send_keys() with configurable delay_enter - Implements capture_pane() for output retrieval - Implements wait_for_prompt() for pattern-based completion detection - Implements wait_for_idle() for content-hash-based idle detection - Implements wait_for_shell_prompt() for shell prompt detection Also includes workflow improvements: - Pre-task git snapshot before agent execution - Post-task commit protocol in agent guidelines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
512 lines
16 KiB
Python
512 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Integration tests for Luzia orchestrator components
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import shutil
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
# Add lib to path
|
|
sys.path.insert(0, '/opt/server-agents/orchestrator/lib')
|
|
|
|
# Test results tracking
|
|
RESULTS = {'passed': 0, 'failed': 0, 'errors': []}
|
|
|
|
def test(name):
|
|
"""Decorator for test functions"""
|
|
def decorator(func):
|
|
def wrapper():
|
|
try:
|
|
func()
|
|
RESULTS['passed'] += 1
|
|
print(f" ✓ {name}")
|
|
return True
|
|
except AssertionError as e:
|
|
RESULTS['failed'] += 1
|
|
RESULTS['errors'].append(f"{name}: {e}")
|
|
print(f" ✗ {name}: {e}")
|
|
return False
|
|
except Exception as e:
|
|
RESULTS['failed'] += 1
|
|
RESULTS['errors'].append(f"{name}: {type(e).__name__}: {e}")
|
|
print(f" ✗ {name}: {type(e).__name__}: {e}")
|
|
return False
|
|
wrapper.__name__ = func.__name__
|
|
return wrapper
|
|
return decorator
|
|
|
|
|
|
# =============================================================================
|
|
# Chat Memory Lookup Tests
|
|
# =============================================================================
|
|
|
|
print("\n### Chat Memory Lookup Tests ###")
|
|
|
|
@test("ChatMemoryLookup imports")
|
|
def test_chat_memory_import():
|
|
from chat_memory_lookup import ChatMemoryLookup
|
|
assert ChatMemoryLookup is not None
|
|
|
|
@test("ChatMemoryLookup initializes")
|
|
def test_chat_memory_init():
|
|
from chat_memory_lookup import ChatMemoryLookup
|
|
lookup = ChatMemoryLookup(timeout_ms=150)
|
|
assert lookup.timeout_ms == 150
|
|
|
|
@test("ChatMemoryLookup.memory_statistics returns data")
|
|
def test_chat_memory_stats():
|
|
from chat_memory_lookup import ChatMemoryLookup
|
|
lookup = ChatMemoryLookup()
|
|
stats = lookup.memory_statistics()
|
|
assert 'available' in stats
|
|
assert stats['available'] == True
|
|
assert 'entities' in stats
|
|
assert stats['entities'] > 0
|
|
|
|
@test("ChatMemoryLookup.list_all_projects returns projects")
|
|
def test_chat_memory_projects():
|
|
from chat_memory_lookup import ChatMemoryLookup
|
|
lookup = ChatMemoryLookup()
|
|
result = lookup.list_all_projects()
|
|
assert 'projects' in result
|
|
assert 'count' in result
|
|
assert result['count'] > 0
|
|
assert len(result['projects']) > 0
|
|
|
|
@test("ChatMemoryLookup.search_entities works")
|
|
def test_chat_memory_search():
|
|
from chat_memory_lookup import ChatMemoryLookup
|
|
lookup = ChatMemoryLookup()
|
|
result = lookup.search_entities('admin', limit=5)
|
|
assert 'entities' in result
|
|
assert 'count' in result
|
|
|
|
test_chat_memory_import()
|
|
test_chat_memory_init()
|
|
test_chat_memory_stats()
|
|
test_chat_memory_projects()
|
|
test_chat_memory_search()
|
|
|
|
|
|
# =============================================================================
|
|
# Chat Intent Parser Tests
|
|
# =============================================================================
|
|
|
|
print("\n### Chat Intent Parser Tests ###")
|
|
|
|
@test("ChatIntentParser imports")
|
|
def test_intent_import():
|
|
from chat_intent_parser import ChatIntentParser
|
|
assert ChatIntentParser is not None
|
|
|
|
@test("ChatIntentParser.parse returns intent structure")
|
|
def test_intent_parse():
|
|
from chat_intent_parser import ChatIntentParser
|
|
parser = ChatIntentParser()
|
|
result = parser.parse("list projects")
|
|
assert 'intent' in result
|
|
assert 'keywords' in result
|
|
assert 'scope' in result
|
|
|
|
@test("ChatIntentParser detects project_info intent")
|
|
def test_intent_project():
|
|
from chat_intent_parser import ChatIntentParser
|
|
parser = ChatIntentParser()
|
|
result = parser.parse("list projects")
|
|
assert result['intent'] == 'project_info'
|
|
assert 'projects' in result['keywords']
|
|
|
|
@test("ChatIntentParser detects system_status intent")
|
|
def test_intent_status():
|
|
from chat_intent_parser import ChatIntentParser
|
|
parser = ChatIntentParser()
|
|
result = parser.parse("system status")
|
|
assert result['intent'] == 'system_status'
|
|
|
|
@test("ChatIntentParser.extract_search_term works")
|
|
def test_intent_search_term():
|
|
from chat_intent_parser import ChatIntentParser
|
|
parser = ChatIntentParser()
|
|
term = parser.extract_search_term("search for authentication")
|
|
assert term is not None
|
|
assert len(term) > 0
|
|
|
|
test_intent_import()
|
|
test_intent_parse()
|
|
test_intent_project()
|
|
test_intent_status()
|
|
test_intent_search_term()
|
|
|
|
|
|
# =============================================================================
|
|
# Chat Orchestrator Tests
|
|
# =============================================================================
|
|
|
|
print("\n### Chat Orchestrator Tests ###")
|
|
|
|
@test("ChatOrchestrator imports")
|
|
def test_orchestrator_import():
|
|
from chat_orchestrator import ChatOrchestrator
|
|
assert ChatOrchestrator is not None
|
|
|
|
@test("ChatOrchestrator initializes")
|
|
def test_orchestrator_init():
|
|
from chat_orchestrator import ChatOrchestrator
|
|
orch = ChatOrchestrator(timeout_ms=500)
|
|
assert orch.timeout_ms == 500
|
|
|
|
@test("ChatOrchestrator.process_query returns response")
|
|
def test_orchestrator_query():
|
|
from chat_orchestrator import ChatOrchestrator
|
|
orch = ChatOrchestrator()
|
|
result = orch.process_query("help")
|
|
assert 'response' in result
|
|
assert 'status' in result
|
|
assert result['status'] == 'success'
|
|
|
|
@test("ChatOrchestrator handles system status query")
|
|
def test_orchestrator_status():
|
|
from chat_orchestrator import ChatOrchestrator
|
|
orch = ChatOrchestrator()
|
|
result = orch.process_query("system status")
|
|
assert 'response' in result
|
|
assert 'execution_time_ms' in result
|
|
|
|
@test("ChatOrchestrator handles project query")
|
|
def test_orchestrator_projects():
|
|
from chat_orchestrator import ChatOrchestrator
|
|
orch = ChatOrchestrator()
|
|
result = orch.process_query("list projects")
|
|
assert 'response' in result
|
|
assert 'Projects' in result['response'] or 'project' in result['response'].lower()
|
|
|
|
test_orchestrator_import()
|
|
test_orchestrator_init()
|
|
test_orchestrator_query()
|
|
test_orchestrator_status()
|
|
test_orchestrator_projects()
|
|
|
|
|
|
# =============================================================================
|
|
# Chat Response Formatter Tests
|
|
# =============================================================================
|
|
|
|
print("\n### Chat Response Formatter Tests ###")
|
|
|
|
@test("ChatResponseFormatter imports")
|
|
def test_formatter_import():
|
|
from chat_response_formatter import ChatResponseFormatter
|
|
assert ChatResponseFormatter is not None
|
|
|
|
@test("ChatResponseFormatter.format_help returns markdown")
|
|
def test_formatter_help():
|
|
from chat_response_formatter import ChatResponseFormatter
|
|
formatter = ChatResponseFormatter()
|
|
help_text = formatter.format_help()
|
|
assert '# ' in help_text # Has markdown header
|
|
assert len(help_text) > 100
|
|
|
|
@test("ChatResponseFormatter.format_response_time works")
|
|
def test_formatter_time():
|
|
from chat_response_formatter import ChatResponseFormatter
|
|
formatter = ChatResponseFormatter()
|
|
instant = formatter.format_response_time(5)
|
|
assert 'instant' in instant
|
|
fast = formatter.format_response_time(150)
|
|
assert 'fast' in fast.lower() or 'ms' in fast
|
|
|
|
@test("ChatResponseFormatter.format_project_list works")
|
|
def test_formatter_projects():
|
|
from chat_response_formatter import ChatResponseFormatter
|
|
formatter = ChatResponseFormatter()
|
|
data = {'projects': [{'name': 'test', 'type': 'project'}], 'count': 1}
|
|
result = formatter.format_project_list(data)
|
|
assert 'test' in result
|
|
assert 'Project' in result or '1' in result
|
|
|
|
test_formatter_import()
|
|
test_formatter_help()
|
|
test_formatter_time()
|
|
test_formatter_projects()
|
|
|
|
|
|
# =============================================================================
|
|
# Chat Bash Executor Tests
|
|
# =============================================================================
|
|
|
|
print("\n### Chat Bash Executor Tests ###")
|
|
|
|
@test("ChatBashExecutor imports")
|
|
def test_bash_import():
|
|
from chat_bash_executor import ChatBashExecutor
|
|
assert ChatBashExecutor is not None
|
|
|
|
@test("ChatBashExecutor.execute runs uptime")
|
|
def test_bash_uptime():
|
|
from chat_bash_executor import ChatBashExecutor
|
|
executor = ChatBashExecutor()
|
|
result = executor.execute('uptime')
|
|
assert 'success' in result
|
|
assert result['success'] == True
|
|
assert 'output' in result
|
|
|
|
@test("ChatBashExecutor.execute runs disk")
|
|
def test_bash_disk():
|
|
from chat_bash_executor import ChatBashExecutor
|
|
executor = ChatBashExecutor()
|
|
result = executor.execute('disk')
|
|
assert result['success'] == True
|
|
|
|
@test("ChatBashExecutor rejects unknown commands")
|
|
def test_bash_reject():
|
|
from chat_bash_executor import ChatBashExecutor
|
|
executor = ChatBashExecutor()
|
|
result = executor.execute('unknown_dangerous_cmd')
|
|
# Unknown commands return error without success key
|
|
assert 'error' in result
|
|
assert 'not allowed' in result['error'].lower()
|
|
|
|
test_bash_import()
|
|
test_bash_uptime()
|
|
test_bash_disk()
|
|
test_bash_reject()
|
|
|
|
|
|
# =============================================================================
|
|
# Task Watchdog Tests
|
|
# =============================================================================
|
|
|
|
print("\n### Task Watchdog Tests ###")
|
|
|
|
@test("TaskWatchdog imports")
|
|
def test_watchdog_import():
|
|
from task_watchdog import TaskWatchdog
|
|
assert TaskWatchdog is not None
|
|
|
|
@test("TaskWatchdog initializes")
|
|
def test_watchdog_init():
|
|
from task_watchdog import TaskWatchdog
|
|
watchdog = TaskWatchdog()
|
|
assert watchdog.HEARTBEAT_TIMEOUT_SECONDS == 300
|
|
assert watchdog.LOCK_TIMEOUT_SECONDS == 3600
|
|
|
|
@test("TaskWatchdog.check_heartbeats runs")
|
|
def test_watchdog_heartbeats():
|
|
from task_watchdog import TaskWatchdog
|
|
watchdog = TaskWatchdog()
|
|
stuck = watchdog.check_heartbeats()
|
|
assert isinstance(stuck, list)
|
|
|
|
@test("TaskWatchdog.get_project_queue_status returns dict")
|
|
def test_watchdog_queue_status():
|
|
from task_watchdog import TaskWatchdog
|
|
watchdog = TaskWatchdog()
|
|
status = watchdog.get_project_queue_status()
|
|
assert isinstance(status, dict)
|
|
|
|
@test("TaskWatchdog.is_project_blocked returns tuple")
|
|
def test_watchdog_blocked():
|
|
from task_watchdog import TaskWatchdog
|
|
watchdog = TaskWatchdog()
|
|
blocked, reason = watchdog.is_project_blocked('test_nonexistent')
|
|
assert isinstance(blocked, bool)
|
|
|
|
@test("TaskWatchdog.run_check returns summary")
|
|
def test_watchdog_check():
|
|
from task_watchdog import TaskWatchdog
|
|
watchdog = TaskWatchdog()
|
|
summary = watchdog.run_check()
|
|
assert 'timestamp' in summary
|
|
assert 'stuck_tasks' in summary
|
|
assert 'project_status' in summary
|
|
|
|
test_watchdog_import()
|
|
test_watchdog_init()
|
|
test_watchdog_heartbeats()
|
|
test_watchdog_queue_status()
|
|
test_watchdog_blocked()
|
|
test_watchdog_check()
|
|
|
|
|
|
# =============================================================================
|
|
# Task Completion Tests
|
|
# =============================================================================
|
|
|
|
print("\n### Task Completion Tests ###")
|
|
|
|
@test("TaskCompletion imports")
|
|
def test_completion_import():
|
|
from task_completion import TaskCompletion, complete_task, fail_task
|
|
assert TaskCompletion is not None
|
|
assert complete_task is not None
|
|
assert fail_task is not None
|
|
|
|
@test("TaskCompletion initializes")
|
|
def test_completion_init():
|
|
from task_completion import TaskCompletion
|
|
handler = TaskCompletion()
|
|
assert handler.COMPLETED_DIR.exists() or True # May not exist yet
|
|
|
|
@test("TaskCompletion.complete_task handles missing task")
|
|
def test_completion_missing():
|
|
from task_completion import TaskCompletion
|
|
handler = TaskCompletion()
|
|
result = handler.complete_task('nonexistent-task-12345')
|
|
assert result['success'] == False
|
|
assert 'not found' in result.get('error', '').lower()
|
|
|
|
@test("TaskCompletion.fail_task handles missing task")
|
|
def test_fail_missing():
|
|
from task_completion import TaskCompletion
|
|
handler = TaskCompletion()
|
|
result = handler.fail_task('nonexistent-task-12345', 'test error')
|
|
assert result['success'] == False
|
|
|
|
@test("TaskCompletion.set_awaiting_human handles missing task")
|
|
def test_awaiting_missing():
|
|
from task_completion import TaskCompletion
|
|
handler = TaskCompletion()
|
|
result = handler.set_awaiting_human('nonexistent-task-12345', 'question?')
|
|
assert result['success'] == False
|
|
|
|
test_completion_import()
|
|
test_completion_init()
|
|
test_completion_missing()
|
|
test_fail_missing()
|
|
test_awaiting_missing()
|
|
|
|
|
|
# =============================================================================
|
|
# Cockpit Tests
|
|
# =============================================================================
|
|
|
|
print("\n### Cockpit Tests ###")
|
|
|
|
@test("Cockpit module imports")
|
|
def test_cockpit_import():
|
|
from cockpit import cockpit_status, cockpit_start, cockpit_stop, cockpit_send
|
|
assert cockpit_status is not None
|
|
assert cockpit_start is not None
|
|
assert cockpit_stop is not None
|
|
|
|
@test("cockpit_status returns state")
|
|
def test_cockpit_status():
|
|
from cockpit import cockpit_status
|
|
result = cockpit_status('admin')
|
|
assert isinstance(result, dict)
|
|
# Should have status info even if not running
|
|
|
|
@test("container_exists helper works")
|
|
def test_cockpit_container_exists():
|
|
from cockpit import container_exists
|
|
# Test with a non-existent container
|
|
result = container_exists('nonexistent-container-12345')
|
|
assert isinstance(result, bool)
|
|
assert result == False
|
|
|
|
@test("get_container_name generates correct name")
|
|
def test_cockpit_container_name():
|
|
from cockpit import get_container_name
|
|
name = get_container_name('testproject')
|
|
assert 'testproject' in name
|
|
assert 'cockpit' in name.lower()
|
|
|
|
test_cockpit_import()
|
|
test_cockpit_status()
|
|
test_cockpit_container_exists()
|
|
test_cockpit_container_name()
|
|
|
|
|
|
# =============================================================================
|
|
# KG Lookup Tests
|
|
# =============================================================================
|
|
|
|
print("\n### KG Lookup Tests ###")
|
|
|
|
@test("ChatKGLookup imports")
|
|
def test_kg_import():
|
|
from chat_kg_lookup import ChatKGLookup
|
|
assert ChatKGLookup is not None
|
|
|
|
@test("ChatKGLookup.get_kg_statistics returns stats")
|
|
def test_kg_stats():
|
|
from chat_kg_lookup import ChatKGLookup
|
|
lookup = ChatKGLookup()
|
|
stats = lookup.get_kg_statistics()
|
|
assert isinstance(stats, dict)
|
|
|
|
@test("ChatKGLookup.search_all_domains works")
|
|
def test_kg_search():
|
|
from chat_kg_lookup import ChatKGLookup
|
|
lookup = ChatKGLookup()
|
|
results = lookup.search_all_domains('admin', limit=5)
|
|
assert isinstance(results, dict)
|
|
|
|
test_kg_import()
|
|
test_kg_stats()
|
|
test_kg_search()
|
|
|
|
|
|
# =============================================================================
|
|
# CLI Integration Tests
|
|
# =============================================================================
|
|
|
|
print("\n### CLI Integration Tests ###")
|
|
|
|
import subprocess
|
|
|
|
@test("luzia --help works")
|
|
def test_cli_help():
|
|
result = subprocess.run(['luzia', '--help'], capture_output=True, text=True, timeout=10)
|
|
assert result.returncode == 0
|
|
assert 'luzia' in result.stdout.lower() or 'usage' in result.stdout.lower()
|
|
|
|
@test("luzia chat help works")
|
|
def test_cli_chat_help():
|
|
result = subprocess.run(['luzia', 'chat', 'help'], capture_output=True, text=True, timeout=10)
|
|
assert result.returncode == 0
|
|
assert 'Chat' in result.stdout or 'chat' in result.stdout.lower()
|
|
|
|
@test("luzia watchdog status works")
|
|
def test_cli_watchdog():
|
|
result = subprocess.run(['luzia', 'watchdog', 'status'], capture_output=True, text=True, timeout=10)
|
|
assert result.returncode == 0
|
|
assert 'PROJECT' in result.stdout or 'Queue' in result.stdout
|
|
|
|
@test("luzia cockpit status works")
|
|
def test_cli_cockpit():
|
|
result = subprocess.run(['luzia', 'cockpit', 'status'], capture_output=True, text=True, timeout=10)
|
|
assert result.returncode == 0
|
|
|
|
@test("luzia list works")
|
|
def test_cli_list():
|
|
result = subprocess.run(['luzia', 'list'], capture_output=True, text=True, timeout=10)
|
|
assert result.returncode == 0
|
|
|
|
test_cli_help()
|
|
test_cli_chat_help()
|
|
test_cli_watchdog()
|
|
test_cli_cockpit()
|
|
test_cli_list()
|
|
|
|
|
|
# =============================================================================
|
|
# Summary
|
|
# =============================================================================
|
|
|
|
print("\n" + "=" * 60)
|
|
print(f"RESULTS: {RESULTS['passed']} passed, {RESULTS['failed']} failed")
|
|
print("=" * 60)
|
|
|
|
if RESULTS['errors']:
|
|
print("\nFailed tests:")
|
|
for err in RESULTS['errors']:
|
|
print(f" - {err}")
|
|
|
|
sys.exit(0 if RESULTS['failed'] == 0 else 1)
|