omnigent-ai--omnigent
146 行
4.8 KiB
Python
146 行
4.8 KiB
Python
"""Unit tests for TCP transport helper functions (no subprocess spawning).
|
|
|
|
Tests the pure-logic helpers in ``omnigent.runner.transports.tcp``:
|
|
socket probing, port allocation, client factory, and subprocess
|
|
configuration — all without launching real uvicorn.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import socket
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from omnigent.runner.transports.tcp import (
|
|
RunnerTCPSubprocess,
|
|
_is_tcp_listening,
|
|
_pick_free_port,
|
|
create_tcp_client,
|
|
)
|
|
|
|
# ── _is_tcp_listening ───────────────────────────────────
|
|
|
|
|
|
def test_is_tcp_listening_returns_false_when_refused() -> None:
|
|
"""Connection-refused on a port that nothing is listening on."""
|
|
# Port 1 is almost certainly not listening on localhost.
|
|
assert _is_tcp_listening("127.0.0.1", 1) is False
|
|
|
|
|
|
def test_is_tcp_listening_returns_true_when_connected() -> None:
|
|
"""A bound TCP socket is detected as listening."""
|
|
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server.bind(("127.0.0.1", 0))
|
|
server.listen(1)
|
|
port = server.getsockname()[1]
|
|
try:
|
|
assert _is_tcp_listening("127.0.0.1", port) is True
|
|
finally:
|
|
server.close()
|
|
|
|
|
|
def test_is_tcp_listening_returns_false_on_os_error() -> None:
|
|
"""OSError (e.g. network unreachable) is caught gracefully."""
|
|
with patch("omnigent.runner.transports.tcp.socket.create_connection", side_effect=OSError):
|
|
assert _is_tcp_listening("192.0.2.1", 9999) is False
|
|
|
|
|
|
# ── _pick_free_port ─────────────────────────────────────
|
|
|
|
|
|
def test_pick_free_port_returns_valid_port() -> None:
|
|
"""The OS allocates a port in the valid range."""
|
|
port = _pick_free_port()
|
|
assert 1 <= port <= 65535
|
|
|
|
|
|
def test_pick_free_port_returns_different_ports() -> None:
|
|
"""Successive calls generally return different ports."""
|
|
ports = {_pick_free_port() for _ in range(5)}
|
|
# With 5 calls, at least 2 should be distinct (astronomically unlikely otherwise).
|
|
assert len(ports) >= 2
|
|
|
|
|
|
# ── create_tcp_client ───────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_tcp_client_returns_async_client() -> None:
|
|
"""Factory returns a correctly-configured httpx.AsyncClient."""
|
|
client = create_tcp_client("http://127.0.0.1:8080")
|
|
try:
|
|
assert isinstance(client, httpx.AsyncClient)
|
|
assert str(client.base_url) == "http://127.0.0.1:8080"
|
|
finally:
|
|
await client.aclose()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_tcp_client_applies_auth_headers() -> None:
|
|
"""Auth headers are injected as defaults on the client."""
|
|
client = create_tcp_client(
|
|
"http://127.0.0.1:8080",
|
|
auth_headers={"Authorization": "Bearer tok-test"},
|
|
)
|
|
try:
|
|
assert client.headers["Authorization"] == "Bearer tok-test"
|
|
finally:
|
|
await client.aclose()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_tcp_client_without_auth_headers() -> None:
|
|
"""No auth headers means an empty default header dict."""
|
|
client = create_tcp_client("http://127.0.0.1:8080")
|
|
try:
|
|
assert "Authorization" not in client.headers
|
|
finally:
|
|
await client.aclose()
|
|
|
|
|
|
# ── RunnerTCPSubprocess config ──────────────────────────
|
|
|
|
|
|
def test_runner_tcp_subprocess_base_url() -> None:
|
|
"""base_url builds correctly from host and port."""
|
|
sub = RunnerTCPSubprocess(host="10.0.0.1", port=9090)
|
|
assert sub.base_url == "http://10.0.0.1:9090"
|
|
|
|
|
|
def test_runner_tcp_subprocess_defaults() -> None:
|
|
"""Default field values are sensible."""
|
|
sub = RunnerTCPSubprocess()
|
|
assert sub.host == "127.0.0.1"
|
|
assert sub.port == 0
|
|
assert sub.startup_timeout_s == 30.0
|
|
assert sub._process is None
|
|
|
|
|
|
def test_runner_tcp_subprocess_kill_noop_when_no_process() -> None:
|
|
"""_kill is safe to call before __enter__."""
|
|
sub = RunnerTCPSubprocess()
|
|
sub._kill() # Should not raise.
|
|
|
|
|
|
def test_runner_tcp_subprocess_kill_noop_when_already_dead() -> None:
|
|
"""_kill handles an already-exited process gracefully."""
|
|
sub = RunnerTCPSubprocess()
|
|
mock_proc = MagicMock()
|
|
mock_proc.poll.return_value = 0 # Already exited
|
|
sub._process = mock_proc
|
|
sub._kill() # Should not raise or call killpg.
|
|
|
|
|
|
def test_runner_tcp_subprocess_exit_calls_kill() -> None:
|
|
"""__exit__ delegates to _kill."""
|
|
sub = RunnerTCPSubprocess()
|
|
mock_proc = MagicMock()
|
|
mock_proc.poll.return_value = 0
|
|
sub._process = mock_proc
|
|
sub.__exit__(None, None, None)
|
|
# Verify _kill was effectively invoked (poll was checked).
|
|
mock_proc.poll.assert_called()
|