mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
291 lines
10 KiB
Python
291 lines
10 KiB
Python
"""Unit tests for the doctor command.
|
|
|
|
Validates VAL-CLI-001, VAL-CLI-002, VAL-CLI-003:
|
|
- Doctor reports missing dependencies with severity, component, message, remediation
|
|
- Doctor reports all-clear when everything is healthy
|
|
- Doctor JSON envelope contains all required fields
|
|
"""
|
|
|
|
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 pytest
|
|
from binary_analysis.bootstrap.deps import Dependency, discover_dependencies
|
|
from binary_analysis.cli.doctor import execute
|
|
from binary_analysis.domain.enums import ExitCode
|
|
|
|
|
|
class TestDoctorExecute:
|
|
"""Tests for doctor command execute function."""
|
|
|
|
def test_doctor_result_has_required_fields(self) -> None:
|
|
"""Doctor result must have success, partial, warnings, diagnostics, data."""
|
|
# We can't create a real argparse.Namespace easily, but we can
|
|
# test the execute function directly with a mock
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = execute(args)
|
|
|
|
assert "success" in result
|
|
assert "partial" in result
|
|
assert "warnings" in result
|
|
assert "diagnostics" in result
|
|
assert "data" in result
|
|
|
|
def test_doctor_data_has_components(self) -> None:
|
|
"""Doctor data must contain components list."""
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = execute(args)
|
|
|
|
assert "components" in result["data"]
|
|
assert isinstance(result["data"]["components"], list)
|
|
assert len(result["data"]["components"]) >= 1 # At least checks Java
|
|
|
|
def test_doctor_diagnostics_have_required_fields(self) -> None:
|
|
"""Each diagnostic must have severity, component, and message."""
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = execute(args)
|
|
diagnostics = result["diagnostics"]
|
|
|
|
# There should be at least one diagnostic entry
|
|
assert len(diagnostics) >= 1
|
|
|
|
for diag in diagnostics:
|
|
assert "severity" in diag
|
|
assert "component" in diag
|
|
assert "message" in diag
|
|
assert diag["severity"] in ("INFO", "WARNING", "ERROR")
|
|
|
|
def test_doctor_check_component_coverage(self) -> None:
|
|
"""Doctor should check java, ghidra, and pyghidra."""
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = execute(args)
|
|
components = result["data"]["components"]
|
|
component_names = {c["name"] for c in components}
|
|
|
|
# All three required components should be checked
|
|
assert "java" in component_names, "Java must be checked"
|
|
assert "ghidra" in component_names, "Ghidra must be checked"
|
|
assert "pyghidra" in component_names, "PyGhidra must be checked"
|
|
|
|
def test_doctor_exit_code_when_deps_missing(self) -> None:
|
|
"""When deps are missing, result should include _exit_code = 3."""
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = execute(args)
|
|
|
|
# If all deps are missing (common in test env), expect exit code 3
|
|
has_error = any(d["status"] == "missing" for d in result["data"]["components"])
|
|
if has_error:
|
|
assert result.get("_exit_code") == ExitCode.DEPENDENCY_MISSING
|
|
assert result["success"] is False
|
|
|
|
def test_doctor_success_when_all_present(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""When all deps are present, success should be True."""
|
|
# Mock discover_dependencies to return all present
|
|
monkeypatch.setattr(
|
|
"binary_analysis.cli.doctor.discover_dependencies",
|
|
lambda: [
|
|
Dependency(
|
|
name="java",
|
|
status="present",
|
|
version="21.0.1",
|
|
path="/usr/bin/java",
|
|
message="Java found",
|
|
remediation="",
|
|
),
|
|
Dependency(
|
|
name="ghidra",
|
|
status="present",
|
|
version="12.1.2",
|
|
path="/opt/ghidra",
|
|
message="Ghidra found",
|
|
remediation="",
|
|
),
|
|
Dependency(
|
|
name="pyghidra",
|
|
status="present",
|
|
version="3.1.0",
|
|
path="/venv/lib/pyghidra",
|
|
message="PyGhidra found",
|
|
remediation="",
|
|
),
|
|
],
|
|
)
|
|
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = execute(args)
|
|
|
|
assert result["success"] is True
|
|
assert "_exit_code" not in result # No explicit exit code needed for success
|
|
# All diagnostics should be INFO
|
|
for diag in result["diagnostics"]:
|
|
assert diag["severity"] == "INFO"
|
|
# No ERROR diagnostics
|
|
assert not any(d["severity"] == "ERROR" for d in result["diagnostics"])
|
|
|
|
def test_doctor_missing_deps_have_remediation(self) -> None:
|
|
"""Missing deps must have remediation hints."""
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = execute(args)
|
|
diagnostics = result["diagnostics"]
|
|
|
|
error_diags = [d for d in diagnostics if d["severity"] == "ERROR"]
|
|
if error_diags:
|
|
for diag in error_diags:
|
|
assert "remediation" in diag
|
|
assert len(diag["remediation"]) > 0, f"Missing remediation for {diag['component']}"
|
|
|
|
def test_doctor_components_have_status(self) -> None:
|
|
"""Each component must have a status field."""
|
|
import argparse
|
|
|
|
args = argparse.Namespace()
|
|
result = execute(args)
|
|
components = result["data"]["components"]
|
|
|
|
for comp in components:
|
|
assert "status" in comp
|
|
assert comp["status"] in ("present", "missing", "error")
|
|
assert "name" in comp
|
|
assert "message" in comp
|
|
assert "remediation" in comp
|
|
|
|
|
|
class TestDoctorCLI:
|
|
"""Integration-style tests for doctor command via main()."""
|
|
|
|
def test_doctor_json_produces_valid_envelope(self, capsys: pytest.CaptureFixture) -> None:
|
|
"""doctor --json must produce valid JSON envelope."""
|
|
from binary_analysis.cli.main import main
|
|
|
|
main(["--json", "doctor"])
|
|
captured = capsys.readouterr()
|
|
parsed = json.loads(captured.out)
|
|
|
|
# All envelope fields must be present
|
|
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"] == "doctor"
|
|
|
|
def test_doctor_json_diagnostics_structure(self, capsys: pytest.CaptureFixture) -> None:
|
|
"""doctor --json diagnostics must have severity, component, message."""
|
|
from binary_analysis.cli.main import main
|
|
|
|
main(["--json", "doctor"])
|
|
captured = capsys.readouterr()
|
|
parsed = json.loads(captured.out)
|
|
|
|
for diag in parsed["diagnostics"]:
|
|
assert "severity" in diag
|
|
assert "component" in diag
|
|
assert "message" in diag
|
|
|
|
|
|
class TestDependencyClass:
|
|
"""Tests for the Dependency dataclass."""
|
|
|
|
def test_dependency_to_dict(self) -> None:
|
|
"""Dependency.to_dict() should produce expected keys."""
|
|
dep = Dependency(
|
|
name="java",
|
|
status="present",
|
|
version="21.0.1",
|
|
path="/usr/bin/java",
|
|
message="Java found",
|
|
remediation="",
|
|
)
|
|
d = dep.to_dict()
|
|
assert d["name"] == "java"
|
|
assert d["status"] == "present"
|
|
assert d["version"] == "21.0.1"
|
|
assert d["path"] == "/usr/bin/java"
|
|
|
|
def test_dependency_missing_to_dict(self) -> None:
|
|
"""Missing dependency to_dict should have null version/path."""
|
|
dep = Dependency(
|
|
name="ghidra",
|
|
status="missing",
|
|
message="Ghidra not found",
|
|
remediation="Install Ghidra from https://ghidra-sre.org/",
|
|
)
|
|
d = dep.to_dict()
|
|
assert d["name"] == "ghidra"
|
|
assert d["status"] == "missing"
|
|
assert d["version"] is None
|
|
assert d["path"] is None
|
|
assert len(d["remediation"]) > 0
|
|
|
|
|
|
class TestDiscoverDependencies:
|
|
"""Tests for the discover_dependencies function."""
|
|
|
|
def test_discover_returns_list(self) -> None:
|
|
"""discover_dependencies must return a list."""
|
|
deps = discover_dependencies()
|
|
assert isinstance(deps, list)
|
|
assert len(deps) >= 1
|
|
|
|
def test_discover_checks_all_components(self) -> None:
|
|
"""discover_dependencies must check java, ghidra, pyghidra."""
|
|
deps = discover_dependencies()
|
|
names = {d.name for d in deps}
|
|
assert "java" in names
|
|
assert "ghidra" in names
|
|
assert "pyghidra" in names
|
|
|
|
def test_discover_deps_are_dependency_instances(self) -> None:
|
|
"""Each item must be a Dependency instance."""
|
|
deps = discover_dependencies()
|
|
for dep in deps:
|
|
assert isinstance(dep, Dependency)
|
|
|
|
def test_discover_deps_have_valid_status(self) -> None:
|
|
"""Each dependency must have a valid status."""
|
|
deps = discover_dependencies()
|
|
valid_statuses = {"present", "missing", "error"}
|
|
for dep in deps:
|
|
assert dep.status in valid_statuses
|
|
|
|
def test_java_detection_with_java_home(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""When JAVA_HOME points to a valid JDK, Java should be present."""
|
|
import shutil
|
|
|
|
java_path = shutil.which("java")
|
|
if java_path:
|
|
monkeypatch.setenv("JAVA_HOME", "/usr") # Won't match exactly but tests the flow
|
|
deps = discover_dependencies()
|
|
java_dep = next(d for d in deps if d.name == "java")
|
|
assert java_dep is not None
|