Files
luzia/tests/test_time_metrics.py
admin ec33ac1936 Refactor cockpit to use DockerTmuxController pattern
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>
2026-01-14 10:42:16 -03:00

437 lines
15 KiB
Python

#!/usr/bin/env python3
"""
Test cases for time_metrics module.
Run with: pytest /opt/server-agents/orchestrator/tests/test_time_metrics.py -v
"""
import json
import os
import sys
import tempfile
import time
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import patch, MagicMock
# Add lib to path
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
import pytest
# Import module under test
from time_metrics import (
get_utc_now,
get_utc_now_with_offset,
parse_iso_timestamp,
calculate_duration_seconds,
format_duration,
format_duration_human,
elapsed_since,
convert_to_local_time,
format_timestamp_with_local,
get_system_load,
get_memory_usage,
get_disk_usage,
capture_system_context,
TaskTimeTracker,
create_task_time_metadata,
update_task_completion_metadata,
format_job_with_timing,
format_logs_header,
DEFAULT_TIMEZONE
)
class TestTimestampFunctions:
"""Test timestamp generation and parsing."""
def test_get_utc_now_format(self):
"""UTC timestamp should be in ISO 8601 format with Z suffix."""
ts = get_utc_now()
assert ts.endswith("Z")
# Should be parseable
dt = datetime.fromisoformat(ts[:-1])
assert dt is not None
def test_get_utc_now_with_offset_format(self):
"""UTC timestamp with offset should end with +00:00."""
ts = get_utc_now_with_offset()
assert "+00:00" in ts
def test_parse_iso_timestamp_z_suffix(self):
"""Parse timestamp with Z suffix."""
ts = "2026-01-11T03:31:57Z"
dt = parse_iso_timestamp(ts)
assert dt is not None
assert dt.year == 2026
assert dt.month == 1
assert dt.day == 11
assert dt.hour == 3
assert dt.minute == 31
assert dt.second == 57
def test_parse_iso_timestamp_with_offset(self):
"""Parse timestamp with timezone offset."""
ts = "2026-01-11T00:31:57-03:00"
dt = parse_iso_timestamp(ts)
assert dt is not None
assert dt.hour == 0 # Local hour
def test_parse_iso_timestamp_no_tz(self):
"""Parse timestamp without timezone (assume UTC)."""
ts = "2026-01-11T03:31:57"
dt = parse_iso_timestamp(ts)
assert dt is not None
assert dt.hour == 3
def test_parse_iso_timestamp_none(self):
"""None input should return None."""
assert parse_iso_timestamp(None) is None
assert parse_iso_timestamp("") is None
def test_parse_iso_timestamp_invalid(self):
"""Invalid timestamp should return None."""
assert parse_iso_timestamp("not-a-timestamp") is None
assert parse_iso_timestamp("2026-99-99T99:99:99Z") is None
class TestDurationCalculations:
"""Test duration calculation and formatting."""
def test_calculate_duration_seconds(self):
"""Calculate duration between two timestamps."""
start = "2026-01-11T10:00:00Z"
end = "2026-01-11T10:01:00Z"
duration = calculate_duration_seconds(start, end)
assert duration == 60.0
def test_calculate_duration_hours(self):
"""Calculate duration spanning hours."""
start = "2026-01-11T10:00:00Z"
end = "2026-01-11T12:30:00Z"
duration = calculate_duration_seconds(start, end)
assert duration == 2.5 * 3600 # 2.5 hours
def test_calculate_duration_negative(self):
"""Duration can be negative if end is before start."""
start = "2026-01-11T12:00:00Z"
end = "2026-01-11T10:00:00Z"
duration = calculate_duration_seconds(start, end)
assert duration < 0
def test_calculate_duration_none(self):
"""Invalid inputs should return None."""
assert calculate_duration_seconds(None, "2026-01-11T10:00:00Z") is None
assert calculate_duration_seconds("2026-01-11T10:00:00Z", None) is None
def test_format_duration_seconds(self):
"""Format durations under a minute."""
assert format_duration(0) == "00:00:00"
assert format_duration(45) == "00:00:45"
assert format_duration(59) == "00:00:59"
def test_format_duration_minutes(self):
"""Format durations in minutes."""
assert format_duration(60) == "00:01:00"
assert format_duration(125) == "00:02:05"
assert format_duration(3599) == "00:59:59"
def test_format_duration_hours(self):
"""Format durations in hours."""
assert format_duration(3600) == "01:00:00"
assert format_duration(3661) == "01:01:01"
assert format_duration(7200) == "02:00:00"
def test_format_duration_none(self):
"""None or negative should return placeholder."""
assert format_duration(None) == "--:--:--"
assert format_duration(-1) == "--:--:--"
def test_format_duration_human_seconds(self):
"""Human-readable format for seconds."""
assert format_duration_human(0) == "0s"
assert format_duration_human(45) == "45s"
assert format_duration_human(59) == "59s"
def test_format_duration_human_minutes(self):
"""Human-readable format for minutes."""
assert format_duration_human(60) == "1m 0s"
assert format_duration_human(125) == "2m 5s"
assert format_duration_human(3599) == "59m 59s"
def test_format_duration_human_hours(self):
"""Human-readable format for hours."""
assert format_duration_human(3600) == "1h 0m 0s"
assert format_duration_human(3661) == "1h 1m 1s"
assert format_duration_human(7200) == "2h 0m 0s"
def test_format_duration_human_days(self):
"""Human-readable format for days."""
assert format_duration_human(86400) == "1d 0h 0m"
assert format_duration_human(90061) == "1d 1h 1m"
def test_format_duration_human_none(self):
"""None or negative should return 'unknown'."""
assert format_duration_human(None) == "unknown"
assert format_duration_human(-1) == "unknown"
class TestTimezoneConversion:
"""Test timezone conversion functions."""
def test_convert_to_local_time_montevideo(self):
"""Convert UTC to Montevideo time (UTC-3)."""
utc_ts = "2026-01-11T03:31:57Z"
local = convert_to_local_time(utc_ts, "America/Montevideo")
# Should be 00:31:57 local (UTC-3)
assert "00:31:57" in local
def test_convert_to_local_time_invalid(self):
"""Invalid timestamp should return original."""
result = convert_to_local_time("invalid", "America/Montevideo")
assert "invalid" in result
def test_format_timestamp_with_local(self):
"""Format timestamp showing both UTC and local."""
utc_ts = "2026-01-11T03:31:57Z"
result = format_timestamp_with_local(utc_ts, "America/Montevideo")
assert "2026-01-11T03:31:57Z" in result
assert "America/Montevideo" in result
class TestSystemContext:
"""Test system context capture functions."""
def test_get_system_load_returns_tuple(self):
"""System load should return 3-element tuple."""
load = get_system_load()
assert isinstance(load, tuple)
assert len(load) == 3
assert all(isinstance(l, (int, float)) for l in load)
def test_get_memory_usage_keys(self):
"""Memory usage should have expected keys."""
mem = get_memory_usage()
assert "total_mb" in mem
assert "available_mb" in mem
assert "used_mb" in mem
assert "used_percent" in mem
assert 0 <= mem["used_percent"] <= 100
def test_get_disk_usage_keys(self):
"""Disk usage should have expected keys."""
disk = get_disk_usage("/")
assert "total_gb" in disk
assert "free_gb" in disk
assert "used_gb" in disk
assert "used_percent" in disk
assert 0 <= disk["used_percent"] <= 100
def test_capture_system_context_structure(self):
"""System context should have complete structure."""
ctx = capture_system_context()
assert "timestamp" in ctx
assert "system_load" in ctx
assert "memory" in ctx
assert "disk" in ctx
assert isinstance(ctx["system_load"], list)
assert len(ctx["system_load"]) == 3
assert "used_percent" in ctx["memory"]
assert "available_mb" in ctx["memory"]
assert "used_percent" in ctx["disk"]
assert "free_gb" in ctx["disk"]
class TestTaskTimeTracker:
"""Test TaskTimeTracker class."""
def test_tracker_initialization(self):
"""Tracker should initialize with task_id and project."""
tracker = TaskTimeTracker("test-001", "admin")
assert tracker.task_id == "test-001"
assert tracker.project == "admin"
assert tracker.dispatch_time is None
assert tracker.completion_time is None
def test_mark_dispatched(self):
"""mark_dispatched should record dispatch time and context."""
tracker = TaskTimeTracker("test-001", "admin")
result = tracker.mark_dispatched()
assert tracker.dispatch_time is not None
assert tracker.dispatch_context is not None
assert "dispatch" in result
assert "utc_time" in result["dispatch"]
assert "system_load" in result["dispatch"]
assert "memory_percent" in result["dispatch"]
def test_mark_started(self):
"""mark_started should record start time."""
tracker = TaskTimeTracker("test-001", "admin")
tracker.mark_dispatched()
result = tracker.mark_started()
assert tracker.start_time is not None
assert "start_time" in result
def test_mark_completed(self):
"""mark_completed should calculate duration."""
tracker = TaskTimeTracker("test-001", "admin")
tracker.mark_dispatched()
time.sleep(1.1) # Delay for measurable duration (must be > 1 sec for second resolution)
result = tracker.mark_completed(exit_code=0)
assert tracker.completion_time is not None
assert "completion" in result
assert "utc_time" in result["completion"]
assert "duration_seconds" in result["completion"]
# Duration should be at least 1 second
assert result["completion"]["duration_seconds"] >= 1.0
assert "exit_code" in result["completion"]
assert result["completion"]["exit_code"] == 0
def test_get_full_metrics_running(self):
"""get_full_metrics for running task should show elapsed."""
tracker = TaskTimeTracker("test-001", "admin")
tracker.mark_dispatched()
metrics = tracker.get_full_metrics()
assert metrics["status"] == "running"
assert "elapsed" in metrics
assert "dispatch" in metrics
def test_get_full_metrics_completed(self):
"""get_full_metrics for completed task should show duration."""
tracker = TaskTimeTracker("test-001", "admin")
tracker.mark_dispatched()
tracker.mark_completed(0)
metrics = tracker.get_full_metrics()
assert metrics["status"] == "completed"
assert "completion" in metrics
assert "duration_seconds" in metrics["completion"]
class TestMetadataFunctions:
"""Test metadata helper functions."""
def test_create_task_time_metadata(self):
"""create_task_time_metadata should return dispatch info."""
meta = create_task_time_metadata("test-001", "admin")
assert "time_metrics" in meta
assert "time_tracker_data" in meta
assert "dispatch" in meta["time_metrics"]
assert meta["time_tracker_data"]["task_id"] == "test-001"
assert meta["time_tracker_data"]["project"] == "admin"
def test_update_task_completion_metadata(self):
"""update_task_completion_metadata should add completion info."""
# Create initial metadata
meta = create_task_time_metadata("test-001", "admin")
time.sleep(0.1)
# Update with completion
updated = update_task_completion_metadata(meta, exit_code=0)
assert "time_metrics" in updated
assert "completion" in updated["time_metrics"]
assert updated["time_metrics"]["completion"]["exit_code"] == 0
class TestOutputFormatters:
"""Test output formatting functions."""
def test_format_job_with_timing_complete(self):
"""Format job with timing info should include all fields."""
job = {
"id": "123456-abcd",
"project": "admin",
"status": "completed",
"time_metrics": {
"dispatch": {
"utc_time": "2026-01-11T10:00:00Z",
"system_load": [0.5, 0.6, 0.7]
},
"completion": {
"duration_formatted": "00:05:30"
}
}
}
result = format_job_with_timing(job)
assert "123456-abcd" in result
assert "admin" in result
assert "completed" in result
assert "10:00:00" in result
assert "00:05:30" in result
def test_format_job_with_timing_running(self):
"""Format running job should show elapsed time."""
job = {
"id": "123456-abcd",
"project": "admin",
"status": "running",
"time_metrics": {
"dispatch": {
"utc_time": "2026-01-11T10:00:00Z",
"system_load": [0.5, 0.6, 0.7]
}
}
}
result = format_job_with_timing(job)
assert "123456-abcd" in result
assert "running" in result
def test_format_logs_header_structure(self):
"""Logs header should contain timing sections."""
job = {
"id": "123456-abcd",
"project": "admin",
"status": "completed",
"time_metrics": {
"dispatch": {
"utc_time": "2026-01-11T10:00:00Z",
"system_load": [0.5, 0.6, 0.7],
"memory_percent": 65,
"disk_percent": 45
},
"completion": {
"utc_time": "2026-01-11T10:05:30Z",
"duration_formatted": "00:05:30"
}
}
}
header = format_logs_header(job)
assert "" in header # Box drawing
assert "Job:" in header
assert "Agent: admin" in header
assert "Dispatched:" in header
assert "Status:" in header
assert "System:" in header
class TestElapsedSince:
"""Test elapsed_since function."""
def test_elapsed_since_recent(self):
"""elapsed_since should calculate time from now."""
# Use a timestamp 5 seconds ago
past = datetime.utcnow() - timedelta(seconds=5)
past_ts = past.strftime("%Y-%m-%dT%H:%M:%SZ")
elapsed = elapsed_since(past_ts)
# Should be around 5s (allow some tolerance)
assert "s" in elapsed # Should have seconds format
if __name__ == "__main__":
pytest.main([__file__, "-v"])