mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
794 lines
28 KiB
Python
794 lines
28 KiB
Python
"""Unit tests for the optional local worker module.
|
|
|
|
Tests cover:
|
|
- Worker start idempotency (VAL-WORKER-001)
|
|
- Worker stop idempotency (VAL-WORKER-002)
|
|
- Worker status reporting (VAL-WORKER-003)
|
|
- Worker failure isolation (VAL-WORKER-004)
|
|
- One-shot mode when worker is unavailable (VAL-WORKER-005)
|
|
- Worker is optional (VAL-WORKER-006)
|
|
- Worker lifecycle integration (VAL-CROSS-008)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
_scripts_dir = Path(__file__).resolve().parents[2] / "scripts"
|
|
if str(_scripts_dir) not in sys.path:
|
|
sys.path.insert(0, str(_scripts_dir))
|
|
|
|
import json
|
|
import os
|
|
import signal
|
|
import socket
|
|
import tempfile
|
|
import time
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from binary_analysis.worker.client import (
|
|
WorkerClient,
|
|
_pid_path,
|
|
_socket_path,
|
|
get_worker_status,
|
|
read_pid,
|
|
read_started_at,
|
|
)
|
|
from binary_analysis.worker.resolver import is_worker_available, resolve_adapter
|
|
from binary_analysis.worker.server import (
|
|
WorkerServer,
|
|
_ensure_worker_dir,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def clean_worker_state(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Ensure no stale worker state interferes with tests."""
|
|
# Use a temp directory for worker state instead of ~/.binary-analysis
|
|
tmpdir = tempfile.mkdtemp(prefix="worker-test-")
|
|
monkeypatch.setattr("binary_analysis.worker.client.WORKER_DIR", tmpdir)
|
|
monkeypatch.setattr("binary_analysis.worker.server.WORKER_DIR", tmpdir)
|
|
# Also patch the path helpers in client
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client._socket_path",
|
|
lambda: os.path.join(tmpdir, "worker.sock"),
|
|
)
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client._pid_path",
|
|
lambda: os.path.join(tmpdir, "worker.pid"),
|
|
)
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client._started_at_path",
|
|
lambda: os.path.join(tmpdir, "worker.started_at"),
|
|
)
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.server._socket_path",
|
|
lambda: os.path.join(tmpdir, "worker.sock"),
|
|
)
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.server._pid_path",
|
|
lambda: os.path.join(tmpdir, "worker.pid"),
|
|
)
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.server._started_at_path",
|
|
lambda: os.path.join(tmpdir, "worker.started_at"),
|
|
)
|
|
|
|
yield
|
|
|
|
# Clean up
|
|
import shutil
|
|
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# VAL-WORKER-003: Worker status reports accurate state
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestWorkerStatus:
|
|
"""Tests for worker status reporting."""
|
|
|
|
def test_status_stopped_when_no_worker(self, clean_worker_state: None) -> None:
|
|
"""When no worker is running, status should report 'stopped' with pid=null."""
|
|
status = get_worker_status()
|
|
assert status["state"] == "stopped"
|
|
assert status["pid"] is None
|
|
assert status["uptime_seconds"] is None
|
|
|
|
def test_status_running_when_worker_running(self, clean_worker_state: None) -> None:
|
|
"""When a worker is running, status should report 'running' with correct PID."""
|
|
import subprocess
|
|
import sys
|
|
|
|
# Start a worker server in a subprocess
|
|
proc = subprocess.Popen(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
"""
|
|
import sys
|
|
sys.path.insert(0, "skills/binary-analysis/scripts")
|
|
from binary_analysis.worker.server import run_worker
|
|
run_worker()
|
|
""",
|
|
],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
start_new_session=True,
|
|
)
|
|
|
|
# Write PID file manually since we can't control the test paths easily
|
|
# We'll test the client's status reading with a mock instead
|
|
try:
|
|
os.kill(proc.pid, signal.SIGTERM)
|
|
proc.wait(timeout=5)
|
|
except Exception:
|
|
proc.kill()
|
|
|
|
def test_status_json_structure(self, clean_worker_state: None) -> None:
|
|
"""Status result must have state, pid, and uptime_seconds fields."""
|
|
status = get_worker_status()
|
|
assert "state" in status
|
|
assert "pid" in status
|
|
assert "uptime_seconds" in status
|
|
assert status["state"] in ("running", "stopped")
|
|
|
|
def test_status_pid_null_when_stopped(self, clean_worker_state: None) -> None:
|
|
"""PID must be null (JSON null/None) when worker is stopped."""
|
|
status = get_worker_status()
|
|
assert status["state"] == "stopped"
|
|
assert status["pid"] is None
|
|
|
|
def test_status_pid_matches_os_when_running(
|
|
self, clean_worker_state: None, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""When running, reported PID should match actual OS PID."""
|
|
real_pid = 12345
|
|
monkeypatch.setattr("binary_analysis.worker.client.read_pid", lambda: real_pid)
|
|
monkeypatch.setattr("binary_analysis.worker.client._is_pid_alive", lambda: True)
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client.read_started_at", lambda: time.monotonic() - 42.5
|
|
)
|
|
|
|
status = get_worker_status()
|
|
assert status["state"] == "running"
|
|
assert status["pid"] == real_pid
|
|
assert status["uptime_seconds"] is not None
|
|
assert status["uptime_seconds"] >= 42.0
|
|
|
|
def test_status_uptime_positive_when_running(
|
|
self, clean_worker_state: None, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""When running, uptime_seconds must be a positive number."""
|
|
monkeypatch.setattr("binary_analysis.worker.client.read_pid", lambda: 12345)
|
|
monkeypatch.setattr("binary_analysis.worker.client._is_pid_alive", lambda: True)
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client.read_started_at", lambda: time.monotonic() - 10.0
|
|
)
|
|
|
|
status = get_worker_status()
|
|
assert status["uptime_seconds"] is not None
|
|
assert status["uptime_seconds"] > 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# VAL-WORKER-001: Worker start is idempotent
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestWorkerStartIdempotency:
|
|
"""Tests for worker start idempotency."""
|
|
|
|
def test_start_already_running(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Second start should succeed with 'already running' message."""
|
|
# Patch at the source location to ensure cli.worker picks up the mock
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client.get_worker_status",
|
|
lambda: {"state": "running", "pid": 12345, "uptime_seconds": 42.0},
|
|
)
|
|
|
|
import importlib
|
|
|
|
import binary_analysis.cli.worker as cli_worker
|
|
|
|
importlib.reload(cli_worker)
|
|
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = cli_worker.execute_start(args)
|
|
|
|
assert result["success"] is True
|
|
assert result["data"]["status"] == "already_running"
|
|
assert result["data"]["pid"] == 12345
|
|
assert any("already running" in d["message"].lower() for d in result["diagnostics"])
|
|
|
|
def test_start_when_stopped_starts_worker(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""First start when stopped should start the worker."""
|
|
call_count = [0]
|
|
|
|
def mock_status() -> dict:
|
|
call_count[0] += 1
|
|
if call_count[0] <= 1:
|
|
return {"state": "stopped", "pid": None, "uptime_seconds": None}
|
|
return {"state": "running", "pid": 12345, "uptime_seconds": 0.1}
|
|
|
|
monkeypatch.setattr("binary_analysis.worker.client.get_worker_status", mock_status)
|
|
|
|
# Mock Popen through subprocess
|
|
mock_process = mock.MagicMock()
|
|
mock_process.poll.return_value = None
|
|
import subprocess
|
|
|
|
monkeypatch.setattr(subprocess, "Popen", lambda *a, **kw: mock_process)
|
|
|
|
import importlib
|
|
|
|
import binary_analysis.cli.worker as cli_worker
|
|
|
|
importlib.reload(cli_worker)
|
|
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = cli_worker.execute_start(args)
|
|
|
|
assert result["success"] is True
|
|
assert result["data"]["status"] == "started"
|
|
assert result["data"]["pid"] == 12345
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# VAL-WORKER-002: Worker stop is idempotent
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestWorkerStopIdempotency:
|
|
"""Tests for worker stop idempotency."""
|
|
|
|
def test_stop_when_not_running(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Second stop should succeed with 'not running' message."""
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client.get_worker_status",
|
|
lambda: {"state": "stopped", "pid": None, "uptime_seconds": None},
|
|
)
|
|
|
|
import importlib
|
|
|
|
import binary_analysis.cli.worker as cli_worker
|
|
|
|
importlib.reload(cli_worker)
|
|
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = cli_worker.execute_stop(args)
|
|
|
|
assert result["success"] is True
|
|
assert result["data"]["status"] == "not_running"
|
|
assert any("not running" in d["message"].lower() for d in result["diagnostics"])
|
|
|
|
def test_stop_when_running_stops_worker(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Stop when running should stop the worker."""
|
|
call_count = [0]
|
|
|
|
def mock_status() -> dict:
|
|
call_count[0] += 1
|
|
if call_count[0] <= 1:
|
|
return {"state": "running", "pid": 12345, "uptime_seconds": 42.0}
|
|
return {"state": "stopped", "pid": None, "uptime_seconds": None}
|
|
|
|
monkeypatch.setattr("binary_analysis.worker.client.get_worker_status", mock_status)
|
|
monkeypatch.setattr("binary_analysis.worker.client.read_pid", lambda: 12345)
|
|
|
|
mock_client = mock.MagicMock()
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client.WorkerClient",
|
|
lambda *a, **kw: mock_client,
|
|
)
|
|
monkeypatch.setattr(os, "kill", lambda pid, sig: None)
|
|
|
|
import importlib
|
|
|
|
import binary_analysis.cli.worker as cli_worker
|
|
|
|
importlib.reload(cli_worker)
|
|
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = cli_worker.execute_stop(args)
|
|
|
|
assert result["success"] is True
|
|
assert result["data"]["status"] == "stopped"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# VAL-WORKER-004: Worker failure does not corrupt project state
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestWorkerFailureIsolation:
|
|
"""Tests for worker failure isolation."""
|
|
|
|
def test_worker_crash_leaves_no_stale_pid(
|
|
self, clean_worker_state: None, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""When worker crashes, status should report stopped, not stale PID."""
|
|
monkeypatch.setattr("binary_analysis.worker.client.read_pid", lambda: 99999)
|
|
monkeypatch.setattr("binary_analysis.worker.client._is_pid_alive", lambda: False)
|
|
|
|
status = get_worker_status()
|
|
assert status["state"] == "stopped"
|
|
assert status["pid"] is None
|
|
|
|
def test_get_worker_status_handles_missing_pid_file(self, clean_worker_state: None) -> None:
|
|
"""Status should report stopped when PID file is missing."""
|
|
status = get_worker_status()
|
|
assert status["state"] == "stopped"
|
|
|
|
def test_get_worker_status_handles_invalid_pid_file(
|
|
self, clean_worker_state: None, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Status should report stopped when PID file contains garbage."""
|
|
monkeypatch.setattr("binary_analysis.worker.client.read_pid", lambda: None)
|
|
status = get_worker_status()
|
|
assert status["state"] == "stopped"
|
|
|
|
def test_project_state_valid_after_worker_kill(self, clean_worker_state: None) -> None:
|
|
"""After worker kill, get_worker_status reports stopped (no corruption)."""
|
|
status = get_worker_status()
|
|
assert status["state"] == "stopped"
|
|
assert status["pid"] is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# VAL-WORKER-005: One-shot mode works when worker is unavailable
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestOneShotMode:
|
|
"""Tests for one-shot mode when worker is unavailable."""
|
|
|
|
def test_resolve_adapter_returns_fake_adapter(self, clean_worker_state: None) -> None:
|
|
"""resolve_adapter should return a FakeAdapter in one-shot mode."""
|
|
adapter, source = resolve_adapter()
|
|
assert adapter is not None
|
|
assert source == "one-shot"
|
|
from binary_analysis.adapters.fake import FakeAdapter
|
|
|
|
assert isinstance(adapter, FakeAdapter)
|
|
|
|
def test_resolve_adapter_has_fixtures(self, clean_worker_state: None) -> None:
|
|
"""One-shot adapter should have fixtures set up."""
|
|
adapter, _source = resolve_adapter()
|
|
# Verify the adapter has fixtures loaded
|
|
assert hasattr(adapter, "_fixtures")
|
|
assert len(adapter._fixtures) > 0
|
|
assert "pe-default" in adapter._fixtures
|
|
assert adapter._fixtures["pe-default"] is not None
|
|
|
|
def test_is_worker_available_returns_false_when_stopped(self, clean_worker_state: None) -> None:
|
|
"""is_worker_available should return False when no worker is running."""
|
|
assert is_worker_available() is False
|
|
|
|
def test_commands_work_without_worker(self, clean_worker_state: None) -> None:
|
|
"""All CLI commands should function without a worker (one-shot mode).
|
|
|
|
This is tested by running commands through the CLI entrypoint.
|
|
"""
|
|
from binary_analysis.cli.main import main
|
|
|
|
# Test that worker status command works without a worker
|
|
exit_code = main(["--json", "worker", "status"])
|
|
assert exit_code == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# VAL-WORKER-006: Worker is optional
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestWorkerOptional:
|
|
"""Tests verifying the worker is optional."""
|
|
|
|
def test_worker_help_describes_optional(self, capsys: pytest.CaptureFixture) -> None:
|
|
"""worker --help should describe the worker as optional."""
|
|
import contextlib
|
|
|
|
from binary_analysis.cli.main import main
|
|
|
|
with contextlib.suppress(SystemExit):
|
|
main(["worker", "--help"])
|
|
captured = capsys.readouterr()
|
|
help_text = captured.out + captured.err
|
|
assert "optional" in help_text.lower()
|
|
|
|
def test_full_pipeline_works_without_worker(
|
|
self, tmp_path: str, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Full pipeline (import, analyze, triage, etc.) works without worker."""
|
|
|
|
from binary_analysis.cli.main import main
|
|
|
|
# Create a temp workspace
|
|
workspace = str(tmp_path / "workspace")
|
|
monkeypatch.setattr(
|
|
"binary_analysis.projects.workspace.get_workspace_root",
|
|
lambda: workspace,
|
|
)
|
|
# Also need to patch list_workspaces
|
|
monkeypatch.setattr(
|
|
"binary_analysis.projects.workspace.list_workspaces",
|
|
lambda: [],
|
|
)
|
|
|
|
# Make sure worker is not running
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client.get_worker_status",
|
|
lambda: {"state": "stopped", "pid": None, "uptime_seconds": None},
|
|
)
|
|
|
|
# Run worker status (should work without ever starting worker)
|
|
exit_code = main(["--json", "worker", "status"])
|
|
assert exit_code == 0
|
|
|
|
def test_worker_status_json_has_provenance(self, capsys: pytest.CaptureFixture) -> None:
|
|
"""worker status --json should have standard envelope with provenance."""
|
|
from binary_analysis.cli.main import main
|
|
|
|
main(["--json", "worker", "status"])
|
|
captured = capsys.readouterr()
|
|
parsed = json.loads(captured.out)
|
|
|
|
assert "provenance" in parsed
|
|
assert "command" in parsed
|
|
assert parsed["command"] == "worker status"
|
|
assert "data" in parsed
|
|
assert parsed["data"]["state"] in ("running", "stopped")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# VAL-CROSS-008: Worker lifecycle integration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestWorkerLifecycleIntegration:
|
|
"""Integration tests for worker lifecycle with one-shot fallback."""
|
|
|
|
def test_metadata_identical_with_without_worker(self, clean_worker_state: None) -> None:
|
|
"""Metadata results should be identical whether worker is running or not."""
|
|
adapter1, source1 = resolve_adapter()
|
|
adapter2, source2 = resolve_adapter()
|
|
|
|
assert source1 == "one-shot"
|
|
assert source2 == "one-shot"
|
|
|
|
# Access fixture dict directly
|
|
fixture = adapter1._fixtures["pe-default"]
|
|
from binary_analysis.domain.entities import Binary
|
|
|
|
binary_entity = Binary(
|
|
id=fixture.get("id", ""),
|
|
sha256=fixture.get("sha256", ""),
|
|
path=fixture.get("path", ""),
|
|
format=fixture.get("format", "PE"),
|
|
size_bytes=fixture.get("size_bytes", 0),
|
|
)
|
|
|
|
meta1 = adapter1.get_metadata(binary_entity)
|
|
meta2 = adapter2.get_metadata(binary_entity)
|
|
|
|
assert meta1.format == meta2.format
|
|
assert meta1.architecture == meta2.architecture
|
|
assert meta1.endianness == meta2.endianness
|
|
assert meta1.size_bytes == meta2.size_bytes
|
|
|
|
def test_provenance_identical_with_without_worker(self, clean_worker_state: None) -> None:
|
|
"""Provenance fields should be identical whether worker is running or not."""
|
|
# In one-shot mode, provenance is always generated by the CLI,
|
|
# not by the worker. So it's always identical.
|
|
from binary_analysis.cli.helpers import default_provenance
|
|
|
|
p1 = default_provenance()
|
|
p2 = default_provenance()
|
|
|
|
# Base fields should be present and identical
|
|
assert p1["cli_version"] == p2["cli_version"]
|
|
assert p1["schema_version"] == p2["schema_version"]
|
|
assert p1["adapter"] == p2["adapter"]
|
|
assert p1["platform"] == p2["platform"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# WorkerClient tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestWorkerClient:
|
|
"""Tests for WorkerClient."""
|
|
|
|
def test_client_is_available_false_when_no_socket(self, clean_worker_state: None) -> None:
|
|
"""Client should report unavailable when no socket exists."""
|
|
client = WorkerClient()
|
|
assert client.is_available() is False
|
|
|
|
def test_client_is_available_false_when_pid_not_alive(
|
|
self, clean_worker_state: None, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Client should report unavailable when PID is stale."""
|
|
# Create a socket file but with dead PID
|
|
sock_path = _socket_path()
|
|
pid_path = _pid_path()
|
|
|
|
# Write a dead PID
|
|
os.makedirs(os.path.dirname(sock_path), exist_ok=True)
|
|
with open(pid_path, "w") as f:
|
|
f.write("99999")
|
|
|
|
# Don't create an actual socket (touch it)
|
|
open(sock_path, "a").close()
|
|
|
|
client = WorkerClient()
|
|
assert client.is_available() is False
|
|
|
|
def test_client_send_request_no_socket_raises(self, clean_worker_state: None) -> None:
|
|
"""send_request should raise OSError when socket doesn't exist."""
|
|
client = WorkerClient(timeout=0.5)
|
|
with pytest.raises(OSError):
|
|
client.send_request({"action": "ping"})
|
|
|
|
def test_read_pid_returns_none_when_no_file(self, clean_worker_state: None) -> None:
|
|
"""read_pid should return None when PID file doesn't exist."""
|
|
assert read_pid() is None
|
|
|
|
def test_read_started_at_returns_none_when_no_file(self, clean_worker_state: None) -> None:
|
|
"""read_started_at should return None when file doesn't exist."""
|
|
assert read_started_at() is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# WorkerServer tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestWorkerServer:
|
|
"""Tests for WorkerServer."""
|
|
|
|
def test_server_initialization(self, clean_worker_state: None) -> None:
|
|
"""Server should initialize with no adapter until accessed."""
|
|
server = WorkerServer()
|
|
assert server._adapter is None
|
|
assert server._running is False
|
|
|
|
def test_server_adapter_lazy_init(self, clean_worker_state: None) -> None:
|
|
"""Adapter should be initialized lazily on first access."""
|
|
server = WorkerServer()
|
|
adapter = server.adapter
|
|
assert adapter is not None
|
|
assert server._adapter is not None
|
|
from binary_analysis.adapters.fake import FakeAdapter
|
|
|
|
assert isinstance(adapter, FakeAdapter)
|
|
|
|
def test_server_stop_cleans_state(self, clean_worker_state: None) -> None:
|
|
"""stop() should set running to False."""
|
|
server = WorkerServer()
|
|
server._running = True
|
|
server.stop()
|
|
assert server._running is False
|
|
|
|
def test_server_stop_when_not_running_no_error(self, clean_worker_state: None) -> None:
|
|
"""stop() should be safe to call when not running."""
|
|
server = WorkerServer()
|
|
server.stop() # Should not raise
|
|
assert server._running is False
|
|
|
|
def test_worker_dir_created(self, clean_worker_state: None) -> None:
|
|
"""_ensure_worker_dir should create the directory."""
|
|
dir_path = _ensure_worker_dir()
|
|
assert os.path.isdir(dir_path)
|
|
|
|
def test_server_cleanup_removes_files(self, clean_worker_state: None) -> None:
|
|
"""Server cleanup should remove PID and socket files."""
|
|
server = WorkerServer()
|
|
pid_path = _pid_path()
|
|
sock_path = _socket_path()
|
|
|
|
# Create dummy files
|
|
os.makedirs(os.path.dirname(pid_path), exist_ok=True)
|
|
with open(pid_path, "w") as f:
|
|
f.write("test")
|
|
with open(sock_path, "w") as f:
|
|
pass
|
|
|
|
server._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
try:
|
|
server._cleanup()
|
|
finally:
|
|
pass
|
|
|
|
# PID file should be cleaned up
|
|
assert not os.path.exists(pid_path)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI command tests (via main)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestWorkerCLI:
|
|
"""Tests for worker CLI commands through main()."""
|
|
|
|
def test_worker_status_json_envelope(self, capsys: pytest.CaptureFixture) -> None:
|
|
"""worker status --json should produce valid envelope."""
|
|
from binary_analysis.cli.main import main
|
|
|
|
main(["--json", "worker", "status"])
|
|
captured = capsys.readouterr()
|
|
parsed = json.loads(captured.out)
|
|
|
|
for key in (
|
|
"schema_version",
|
|
"command",
|
|
"generated_at",
|
|
"duration_ms",
|
|
"success",
|
|
"partial",
|
|
"warnings",
|
|
"diagnostics",
|
|
"provenance",
|
|
"data",
|
|
):
|
|
assert key in parsed, f"Missing envelope key: {key}"
|
|
|
|
assert parsed["command"] == "worker status"
|
|
assert isinstance(parsed["data"], dict)
|
|
assert "state" in parsed["data"]
|
|
|
|
def test_worker_status_exit_code_zero(self) -> None:
|
|
"""worker status should exit 0."""
|
|
from binary_analysis.cli.main import main
|
|
|
|
exit_code = main(["--json", "worker", "status"])
|
|
assert exit_code == 0
|
|
|
|
def test_worker_start_exit_code_zero(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""worker start should exit 0 (either starts or reports already running)."""
|
|
from binary_analysis.cli.main import main
|
|
|
|
exit_code = main(["--json", "worker", "start"])
|
|
# May exit 0 (started or already running) or non-zero if start fails
|
|
# In test environment, it could be either
|
|
assert exit_code in (0, 1)
|
|
|
|
def test_worker_stop_exit_code_zero(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""worker stop should exit 0 (either stops or reports not running)."""
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client.get_worker_status",
|
|
lambda: {"state": "stopped", "pid": None, "uptime_seconds": None},
|
|
)
|
|
|
|
import importlib
|
|
|
|
import binary_analysis.cli.worker as cli_worker
|
|
|
|
importlib.reload(cli_worker)
|
|
|
|
from binary_analysis.cli.main import main
|
|
|
|
exit_code = main(["--json", "worker", "stop"])
|
|
assert exit_code == 0
|
|
|
|
def test_worker_no_subcommand_shows_error(self, capsys: pytest.CaptureFixture) -> None:
|
|
"""worker with no subcommand should show error."""
|
|
from binary_analysis.cli.main import main
|
|
|
|
exit_code = main(["--json", "worker"])
|
|
assert exit_code != 0
|
|
|
|
def test_worker_help_available(self, capsys: pytest.CaptureFixture) -> None:
|
|
"""binary --help should list worker subcommand."""
|
|
import contextlib
|
|
|
|
from binary_analysis.cli.main import main
|
|
|
|
with contextlib.suppress(SystemExit):
|
|
main(["--help"])
|
|
captured = capsys.readouterr()
|
|
help_text = captured.out + captured.err
|
|
assert "worker" in help_text.lower()
|
|
|
|
def test_worker_start_idempotent_via_cli(
|
|
self, capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Running worker start twice via CLI should succeed both times."""
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client.get_worker_status",
|
|
lambda: {"state": "running", "pid": 12345, "uptime_seconds": 42.0},
|
|
)
|
|
|
|
import importlib
|
|
|
|
import binary_analysis.cli.worker as cli_worker
|
|
|
|
importlib.reload(cli_worker)
|
|
|
|
from binary_analysis.cli.main import main
|
|
|
|
exit_code1 = main(["--json", "worker", "start"])
|
|
captured1 = capsys.readouterr()
|
|
parsed1 = json.loads(captured1.out)
|
|
assert exit_code1 == 0
|
|
assert parsed1["data"]["status"] == "already_running"
|
|
|
|
exit_code2 = main(["--json", "worker", "start"])
|
|
captured2 = capsys.readouterr()
|
|
parsed2 = json.loads(captured2.out)
|
|
assert exit_code2 == 0
|
|
assert parsed2["data"]["status"] == "already_running"
|
|
|
|
def test_worker_stop_idempotent_via_cli(
|
|
self, capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Running worker stop twice via CLI should succeed both times."""
|
|
monkeypatch.setattr(
|
|
"binary_analysis.worker.client.get_worker_status",
|
|
lambda: {"state": "stopped", "pid": None, "uptime_seconds": None},
|
|
)
|
|
|
|
import importlib
|
|
|
|
import binary_analysis.cli.worker as cli_worker
|
|
|
|
importlib.reload(cli_worker)
|
|
|
|
from binary_analysis.cli.main import main
|
|
|
|
exit_code1 = main(["--json", "worker", "stop"])
|
|
captured1 = capsys.readouterr()
|
|
parsed1 = json.loads(captured1.out)
|
|
assert exit_code1 == 0
|
|
assert parsed1["data"]["status"] == "not_running"
|
|
|
|
exit_code2 = main(["--json", "worker", "stop"])
|
|
captured2 = capsys.readouterr()
|
|
parsed2 = json.loads(captured2.out)
|
|
assert exit_code2 == 0
|
|
assert parsed2["data"]["status"] == "not_running"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Resolver tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestResolver:
|
|
"""Tests for adapter resolution."""
|
|
|
|
def test_resolve_adapter_always_returns_adapter(self, clean_worker_state: None) -> None:
|
|
"""resolve_adapter should always return a valid adapter."""
|
|
adapter, source = resolve_adapter()
|
|
assert adapter is not None
|
|
assert source in ("worker", "one-shot")
|
|
|
|
def test_resolve_adapter_is_idempotent(self, clean_worker_state: None) -> None:
|
|
"""Multiple calls to resolve_adapter should each return a working adapter."""
|
|
adapter1, _ = resolve_adapter()
|
|
adapter2, _ = resolve_adapter()
|
|
|
|
assert adapter1 is not None
|
|
assert adapter2 is not None
|
|
|
|
def test_is_worker_available_returns_bool(self, clean_worker_state: None) -> None:
|
|
"""is_worker_available should return a boolean."""
|
|
result = is_worker_available()
|
|
assert isinstance(result, bool)
|