diff --git a/host_agy_daemon.py b/host_agy_daemon.py index 09162856..9233ea65 100755 --- a/host_agy_daemon.py +++ b/host_agy_daemon.py @@ -88,7 +88,7 @@ def read_bytes(): return os.read(master_fd, 1024) except OSError: return b"" - + while True: data = await loop_ref.run_in_executor(None, read_bytes) if not data: diff --git a/pod.yaml b/pod.yaml index 188f78cd..4f5d66c6 100644 --- a/pod.yaml +++ b/pod.yaml @@ -187,7 +187,7 @@ spec: - name: CLICKHOUSE_USER value: clickhouse - name: CLICKHOUSE_PASSWORD - value: clickhouse + value: CLICKHOUSE_PASSWORD_PLACEHOLDER image: docker.io/clickhouse/clickhouse-server:26.5.1 livenessProbe: exec: @@ -196,7 +196,7 @@ spec: - --user - clickhouse - --password - - clickhouse + - CLICKHOUSE_PASSWORD_PLACEHOLDER - --query - SELECT 1 initialDelaySeconds: 20 @@ -222,7 +222,7 @@ spec: - --port - '6380' - --requirepass - - langfuse-redis-2026 + - REDIS_AUTH_PLACEHOLDER - --maxmemory-policy - noeviction - --loglevel @@ -242,7 +242,7 @@ spec: - -p - "6380" - -a - - langfuse-redis-2026 + - REDIS_AUTH_PLACEHOLDER - ping initialDelaySeconds: 2 periodSeconds: 5 @@ -287,7 +287,7 @@ spec: - name: CLICKHOUSE_USER value: clickhouse - name: CLICKHOUSE_PASSWORD - value: clickhouse + value: CLICKHOUSE_PASSWORD_PLACEHOLDER - name: CLICKHOUSE_CLUSTER_ENABLED value: 'false' - name: REDIS_HOST @@ -295,7 +295,7 @@ spec: - name: REDIS_PORT value: '6380' - name: REDIS_AUTH - value: langfuse-redis-2026 + value: REDIS_AUTH_PLACEHOLDER - name: LANGFUSE_INIT_ORG_ID value: org-local-dev-id - name: LANGFUSE_INIT_ORG_NAME @@ -343,7 +343,7 @@ spec: - name: CLICKHOUSE_USER value: clickhouse - name: CLICKHOUSE_PASSWORD - value: clickhouse + value: CLICKHOUSE_PASSWORD_PLACEHOLDER - name: CLICKHOUSE_CLUSTER_ENABLED value: 'false' - name: REDIS_HOST @@ -351,7 +351,7 @@ spec: - name: REDIS_PORT value: '6380' - name: REDIS_AUTH - value: langfuse-redis-2026 + value: REDIS_AUTH_PLACEHOLDER - name: HOSTNAME value: 0.0.0.0 - name: PORT diff --git a/router/tests/test_get_gemini_oauth_status.py b/router/tests/test_get_gemini_oauth_status.py new file mode 100644 index 00000000..2c28d598 --- /dev/null +++ b/router/tests/test_get_gemini_oauth_status.py @@ -0,0 +1,90 @@ +import pytest +import os +import sys +import json +from unittest.mock import patch, mock_open + +# Ensure router directory is in sys.path +router_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +if router_path not in sys.path: + sys.path.insert(0, router_path) + +import main + +def test_get_gemini_oauth_status_missing_file(): + with patch("os.path.exists", return_value=False): + result = main.get_gemini_oauth_status() + assert result == {"status": "missing", "detail": "No oauth_creds.json found", "expiry_ms": 0} + +def test_get_gemini_oauth_status_no_access_token(): + mock_data = {"expiry_date": 1234567890000} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))): + result = main.get_gemini_oauth_status() + assert result == {"status": "missing", "detail": "No access token in file", "expiry_ms": 0} + +def test_get_gemini_oauth_status_valid_less_than_60s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms + 45000 # 45 seconds from now + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = main.get_gemini_oauth_status() + assert result == {"status": "valid", "detail": "Expires in 45s", "expiry_ms": expiry_ms} + +def test_get_gemini_oauth_status_valid_less_than_3600s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms + 1500000 # 25 minutes from now (25 * 60 = 1500 seconds) + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = main.get_gemini_oauth_status() + assert result == {"status": "valid", "detail": "Expires in 25m 0s", "expiry_ms": expiry_ms} + +def test_get_gemini_oauth_status_valid_more_than_3600s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms + 7500000 # 2 hours, 5 minutes from now (2 * 3600 + 5 * 60 = 7500) + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = main.get_gemini_oauth_status() + assert result == {"status": "valid", "detail": "Expires in 2h 5m", "expiry_ms": expiry_ms} + +def test_get_gemini_oauth_status_expired_less_than_3600s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms - 1500000 # Expired 25 minutes ago + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = main.get_gemini_oauth_status() + assert result == {"status": "expired", "detail": "Expired 25 minutes ago", "expiry_ms": expiry_ms} + +def test_get_gemini_oauth_status_expired_less_than_86400s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms - 7500000 # Expired 2 hours ago + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = main.get_gemini_oauth_status() + assert result == {"status": "expired", "detail": "Expired 2 hours ago", "expiry_ms": expiry_ms} + +def test_get_gemini_oauth_status_expired_more_than_86400s(): + current_time_ms = 1000000000000 + expiry_ms = current_time_ms - 172800000 # Expired 2 days ago (2 * 86400 * 1000 = 172800000) + mock_data = {"access_token": "test_token", "expiry_date": expiry_ms} + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", mock_open(read_data=json.dumps(mock_data))), \ + patch("time.time", return_value=current_time_ms / 1000.0): + result = main.get_gemini_oauth_status() + assert result == {"status": "expired", "detail": "Expired 2 days ago", "expiry_ms": expiry_ms} + +def test_get_gemini_oauth_status_exception(): + with patch("os.path.exists", return_value=True), \ + patch("builtins.open", side_effect=Exception("Test error")): + result = main.get_gemini_oauth_status() + assert result == {"status": "error", "detail": "Test error", "expiry_ms": 0} diff --git a/router/tests/test_get_goose_sessions.py b/router/tests/test_get_goose_sessions.py deleted file mode 100644 index 52640fef..00000000 --- a/router/tests/test_get_goose_sessions.py +++ /dev/null @@ -1,46 +0,0 @@ -import pytest -import sys -import os -from pathlib import Path -from unittest.mock import patch, MagicMock - -os.environ["CONFIG_PATH"] = str(Path(__file__).resolve().parent.parent / "config.yaml") -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -from main import get_goose_sessions - -def test_get_goose_sessions_no_db(): - with patch('os.path.exists', return_value=False): - assert get_goose_sessions() == [] - -def test_get_goose_sessions_success(): - mock_sqlite3 = MagicMock() - mock_conn = MagicMock() - mock_cursor = MagicMock() - - mock_sqlite3.connect.return_value = mock_conn - mock_conn.cursor.return_value = mock_cursor - - mock_cursor.fetchall.return_value = [ - {"id": 1, "name": "s1"}, - {"id": 2, "name": "s2"} - ] - - with patch('os.path.exists', return_value=True): - with patch.dict(sys.modules, {'sqlite3': mock_sqlite3}): - result = get_goose_sessions() - - assert len(result) == 2 - assert result[0] == {"id": 1, "name": "s1"} - mock_sqlite3.connect.assert_called_once_with("/config/goose_sessions/sessions/sessions.db", timeout=1.0) - mock_cursor.execute.assert_called_once() - mock_conn.close.assert_called_once() - -def test_get_goose_sessions_exception(): - mock_sqlite3 = MagicMock() - mock_sqlite3.connect.side_effect = Exception("DB error") - - with patch('os.path.exists', return_value=True): - with patch.dict(sys.modules, {'sqlite3': mock_sqlite3}): - result = get_goose_sessions() - assert result == [] diff --git a/router/tests/test_load_persisted_stats.py b/router/tests/test_load_persisted_stats.py deleted file mode 100644 index 76edbf51..00000000 --- a/router/tests/test_load_persisted_stats.py +++ /dev/null @@ -1,61 +0,0 @@ -import json -import pytest -from unittest.mock import patch, mock_open - -import router.main -from router.main import load_persisted_stats - -@pytest.fixture -def mock_stats(): - # Setup a clean stats dictionary for testing - clean_stats = { - "total_requests": 0, - "nested_dict": {"a": 1, "b": 2}, - "existing_key": "value" - } - with patch.dict(router.main.stats, clean_stats, clear=True): - yield router.main.stats - -def test_load_persisted_stats_file_not_exists(mock_stats): - with patch("router.main.os.path.exists", return_value=False) as mock_exists: - load_persisted_stats() - mock_exists.assert_called_once_with(router.main.STATS_JSON_PATH) - # Stats should remain unchanged - assert mock_stats["total_requests"] == 0 - -def test_load_persisted_stats_success(mock_stats): - mock_data = { - "total_requests": 100, - "nested_dict": {"b": 3, "c": 4}, - "new_key": "new_value" - } - mock_json = json.dumps(mock_data) - - with patch("router.main.os.path.exists", return_value=True): - with patch("router.main.open", mock_open(read_data=mock_json)): - with patch("router.main.logger.info") as mock_logger: - load_persisted_stats() - - # Assert simple value updated via else block - assert mock_stats["total_requests"] == 100 - # Assert nested_dict updated via if block (b updated, c added, a unchanged) - assert mock_stats["nested_dict"] == {"a": 1, "b": 3, "c": 4} - # Assert new_key added via else block - assert mock_stats["new_key"] == "new_value" - # Assert existing_key unchanged - assert mock_stats["existing_key"] == "value" - - mock_logger.assert_called_once_with("✓ Successfully loaded persisted gateway statistics from disk.") - -def test_load_persisted_stats_exception(mock_stats): - with patch("router.main.os.path.exists", return_value=True): - with patch("router.main.open", side_effect=Exception("Mock read error")): - with patch("router.main.logger.error") as mock_logger: - load_persisted_stats() - - # Stats should remain unchanged - assert mock_stats["total_requests"] == 0 - - # Error should be logged - mock_logger.assert_called_once() - assert "Failed to load persisted stats: Mock read error" in mock_logger.call_args[0][0] diff --git a/start-stack.sh b/start-stack.sh index 381b864c..8abdb259 100755 --- a/start-stack.sh +++ b/start-stack.sh @@ -141,6 +141,18 @@ if [ -z "$LANGFUSE_INIT_USER_PASSWORD" ]; then echo "✓ Generated new LANGFUSE_INIT_USER_PASSWORD and saved to $ENV_FILE" fi +if [ -z "$REDIS_AUTH" ]; then + REDIS_AUTH="$(openssl rand -hex 16)" + echo "REDIS_AUTH=\"$REDIS_AUTH\"" >> "$ENV_FILE" + echo "✓ Generated new REDIS_AUTH and saved to $ENV_FILE" +fi + +if [ -z "$CLICKHOUSE_PASSWORD" ]; then + CLICKHOUSE_PASSWORD="$(openssl rand -hex 16)" + echo "CLICKHOUSE_PASSWORD=\"$CLICKHOUSE_PASSWORD\"" >> "$ENV_FILE" + echo "✓ Generated new CLICKHOUSE_PASSWORD and saved to $ENV_FILE" +fi + if [ -z "$ROUTER_API_KEY" ]; then ROUTER_API_KEY="$(openssl rand -hex 32)" @@ -370,7 +382,7 @@ if podman pod exists agent-router-pod 2>/dev/null; then fi render_pod_yaml() { - export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD + export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD REDIS_AUTH CLICKHOUSE_PASSWORD python3 - "$WORKDIR/pod.yaml" <<'PY' import os, sys, urllib.parse uid = os.getuid() @@ -387,8 +399,10 @@ placeholders = [ "ENCRYPTION_KEY_PLACEHOLDER", "postgres-password-***", "MINIO_USER_PLACEHOLDER", - "MINIO_PASSWORD_PLACEHOLDER" - "LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER" + "MINIO_PASSWORD_PLACEHOLDER", + "LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER", + "REDIS_AUTH_PLACEHOLDER", + "CLICKHOUSE_PASSWORD_PLACEHOLDER" ] for ph in placeholders: if ph not in text: @@ -408,6 +422,8 @@ text = text.replace("ENCRYPTION_KEY_PLACEHOLDER", os.environ["ENCRYPTION_KEY"]) text = text.replace("MINIO_USER_PLACEHOLDER", os.environ["MINIO_ROOT_USER"]) text = text.replace("MINIO_PASSWORD_PLACEHOLDER", os.environ["MINIO_ROOT_PASSWORD"]) text = text.replace("LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER", os.environ["LANGFUSE_INIT_USER_PASSWORD"]) +text = text.replace("REDIS_AUTH_PLACEHOLDER", os.environ["REDIS_AUTH"]) +text = text.replace("CLICKHOUSE_PASSWORD_PLACEHOLDER", os.environ["CLICKHOUSE_PASSWORD"]) sys.stdout.write(text) PY }