mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-21 16:46:26 +03:00
fix: relocate binary analysis skill
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
"""Unit tests for the bootstrap command.
|
||||
|
||||
Validates VAL-CLI-004, VAL-CLI-005, VAL-CLI-006, VAL-CLI-007, VAL-SAFE-006:
|
||||
- Bootstrap --plan shows install targets without mutation
|
||||
- Bootstrap --plan on healthy system reports nothing needed
|
||||
- Bootstrap --apply downloads, installs, verifies
|
||||
- Bootstrap --apply partial failure reports success=false, partial=true
|
||||
- Checksum verification fails closed on mismatch
|
||||
"""
|
||||
|
||||
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 hashlib
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from binary_analysis.bootstrap.deps import Dependency
|
||||
from binary_analysis.cli.bootstrap import (
|
||||
_apply_mode,
|
||||
_build_plan,
|
||||
_plan_mode,
|
||||
_verify_checksum,
|
||||
)
|
||||
from binary_analysis.domain.enums import ExitCode
|
||||
|
||||
|
||||
class TestBuildPlan:
|
||||
"""Tests for the build_plan function."""
|
||||
|
||||
def test_plan_lists_missing_components(self) -> None:
|
||||
"""Missing deps should have status=missing, action=install, source."""
|
||||
deps = [
|
||||
Dependency(
|
||||
name="java",
|
||||
status="missing",
|
||||
message="Java not found",
|
||||
remediation="Install Java",
|
||||
),
|
||||
Dependency(
|
||||
name="ghidra",
|
||||
status="present",
|
||||
version="12.1.2",
|
||||
path="/opt/ghidra",
|
||||
message="Ghidra found",
|
||||
remediation="",
|
||||
),
|
||||
]
|
||||
plan = _build_plan(deps)
|
||||
assert len(plan) == 2
|
||||
|
||||
java = plan[0]
|
||||
assert java["name"] == "java"
|
||||
assert java["status"] == "missing"
|
||||
assert java["action"] == "install"
|
||||
assert "source" in java
|
||||
assert "remediation" in java
|
||||
|
||||
ghidra = plan[1]
|
||||
assert ghidra["name"] == "ghidra"
|
||||
assert ghidra["status"] == "present"
|
||||
assert ghidra["action"] == "none"
|
||||
|
||||
def test_plan_all_present(self) -> None:
|
||||
"""All-present deps should show all status=present, action=none."""
|
||||
deps = [
|
||||
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="",
|
||||
),
|
||||
]
|
||||
plan = _build_plan(deps)
|
||||
assert len(plan) == 3
|
||||
for item in plan:
|
||||
assert item["status"] == "present"
|
||||
assert item["action"] == "none"
|
||||
|
||||
|
||||
class TestPlanMode:
|
||||
"""Tests for the _plan_mode function."""
|
||||
|
||||
def test_plan_mode_has_missing_returns_false_success(self) -> None:
|
||||
"""When deps are missing, plan mode returns success=false."""
|
||||
deps = [
|
||||
Dependency(
|
||||
name="java",
|
||||
status="missing",
|
||||
message="Java not found",
|
||||
remediation="Install Java",
|
||||
),
|
||||
]
|
||||
result = _plan_mode(deps)
|
||||
assert result["success"] is False
|
||||
assert result["_exit_code"] == ExitCode.DEPENDENCY_MISSING
|
||||
|
||||
def test_plan_mode_all_present_returns_true_success(self) -> None:
|
||||
"""When all deps present, plan mode returns success=true."""
|
||||
deps = [
|
||||
Dependency(
|
||||
name="java",
|
||||
status="present",
|
||||
version="21.0.1",
|
||||
path="/usr/bin/java",
|
||||
message="Java found",
|
||||
remediation="",
|
||||
),
|
||||
]
|
||||
result = _plan_mode(deps)
|
||||
assert result["success"] is True
|
||||
assert "_exit_code" not in result
|
||||
|
||||
def test_plan_mode_no_filesystem_mutation(self, tmp_path) -> None:
|
||||
"""Plan mode must not create any files."""
|
||||
import os
|
||||
|
||||
before = set(os.listdir(tmp_path))
|
||||
|
||||
deps = [
|
||||
Dependency(
|
||||
name="java",
|
||||
status="missing",
|
||||
message="Java not found",
|
||||
remediation="Install Java",
|
||||
),
|
||||
]
|
||||
_plan_mode(deps)
|
||||
|
||||
# Working in tmp_path — nothing should change
|
||||
after = set(os.listdir(tmp_path))
|
||||
assert after == before, "Plan mode must not create files"
|
||||
|
||||
def test_plan_mode_diagnostics_for_missing(self) -> None:
|
||||
"""Missing deps must produce ERROR diagnostics."""
|
||||
deps = [
|
||||
Dependency(
|
||||
name="java",
|
||||
status="missing",
|
||||
message="Java not found",
|
||||
remediation="Install Java",
|
||||
),
|
||||
Dependency(
|
||||
name="ghidra",
|
||||
status="present",
|
||||
version="12.1.2",
|
||||
path="/opt/ghidra",
|
||||
message="Ghidra found",
|
||||
remediation="",
|
||||
),
|
||||
]
|
||||
result = _plan_mode(deps)
|
||||
diags = result["diagnostics"]
|
||||
|
||||
# Only missing deps get diagnostics
|
||||
assert len(diags) == 1
|
||||
assert diags[0]["severity"] == "ERROR"
|
||||
assert diags[0]["component"] == "java"
|
||||
|
||||
|
||||
class TestApplyMode:
|
||||
"""Tests for the _apply_mode function."""
|
||||
|
||||
def test_apply_mode_all_present(self) -> None:
|
||||
"""When all deps present, apply returns success=true."""
|
||||
deps = [
|
||||
Dependency(
|
||||
name="java",
|
||||
status="present",
|
||||
version="21.0.1",
|
||||
path="/usr/bin/java",
|
||||
message="Java found",
|
||||
remediation="",
|
||||
),
|
||||
]
|
||||
result = _apply_mode(deps)
|
||||
assert result["success"] is True
|
||||
assert "_exit_code" not in result
|
||||
|
||||
def test_apply_mode_requires_manual(self) -> None:
|
||||
"""Deps requiring manual install produce WARNING diagnostics."""
|
||||
deps = [
|
||||
Dependency(
|
||||
name="java",
|
||||
status="missing",
|
||||
message="Java not found",
|
||||
remediation="Install Java JDK 17+",
|
||||
),
|
||||
]
|
||||
result = _apply_mode(deps)
|
||||
# Java installation requires manual steps
|
||||
components = result["data"]["components"]
|
||||
java = components[0]
|
||||
assert java["status"] == "requires_manual"
|
||||
|
||||
|
||||
class TestChecksumVerification:
|
||||
"""Tests for checksum verification (VAL-SAFE-006)."""
|
||||
|
||||
def test_verify_checksum_match_passes(self) -> None:
|
||||
"""Matching checksums should not raise."""
|
||||
data = b"hello world"
|
||||
expected = hashlib.sha256(data).hexdigest()
|
||||
# Should not raise
|
||||
_verify_checksum(data, expected)
|
||||
|
||||
def test_verify_checksum_mismatch_raises(self) -> None:
|
||||
"""Mismatched checksums should raise ValueError."""
|
||||
data = b"hello world"
|
||||
expected = "a" * 64 # Deliberately wrong
|
||||
with pytest.raises(ValueError, match="Checksum mismatch"):
|
||||
_verify_checksum(data, expected)
|
||||
|
||||
def test_verify_checksum_empty_data(self) -> None:
|
||||
"""Empty data should still verify checksum."""
|
||||
data = b""
|
||||
expected = hashlib.sha256(data).hexdigest()
|
||||
_verify_checksum(data, expected)
|
||||
|
||||
def test_verify_checksum_case_insensitive(self) -> None:
|
||||
"""Checksum comparison should be case-insensitive."""
|
||||
data = b"test"
|
||||
expected = hashlib.sha256(data).hexdigest().upper()
|
||||
_verify_checksum(data, expected)
|
||||
|
||||
|
||||
class TestBootstrapCLI:
|
||||
"""Integration-style tests for bootstrap command via main()."""
|
||||
|
||||
def test_bootstrap_plan_json_produces_envelope(self, capsys: pytest.CaptureFixture) -> None:
|
||||
"""bootstrap --plan --json must produce valid JSON envelope."""
|
||||
from binary_analysis.cli.main import main
|
||||
|
||||
main(["--json", "bootstrap", "--plan"])
|
||||
captured = capsys.readouterr()
|
||||
parsed = json.loads(captured.out)
|
||||
|
||||
# Must have all envelope fields
|
||||
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"] == "bootstrap"
|
||||
|
||||
def test_bootstrap_plan_json_has_components(self, capsys: pytest.CaptureFixture) -> None:
|
||||
"""bootstrap --plan --json must list components."""
|
||||
from binary_analysis.cli.main import main
|
||||
|
||||
main(["--json", "bootstrap", "--plan"])
|
||||
captured = capsys.readouterr()
|
||||
parsed = json.loads(captured.out)
|
||||
|
||||
assert "components" in parsed["data"]
|
||||
assert isinstance(parsed["data"]["components"], list)
|
||||
|
||||
def test_bootstrap_plan_json_components_have_required_fields(
|
||||
self, capsys: pytest.CaptureFixture
|
||||
) -> None:
|
||||
"""Each component in plan must have name, status, action, source."""
|
||||
from binary_analysis.cli.main import main
|
||||
|
||||
main(["--json", "bootstrap", "--plan"])
|
||||
captured = capsys.readouterr()
|
||||
parsed = json.loads(captured.out)
|
||||
|
||||
for comp in parsed["data"]["components"]:
|
||||
assert "name" in comp
|
||||
assert "status" in comp
|
||||
assert "action" in comp
|
||||
|
||||
def test_bootstrap_apply_json_produces_envelope(self, capsys: pytest.CaptureFixture) -> None:
|
||||
"""bootstrap --apply --json must produce valid JSON envelope."""
|
||||
from binary_analysis.cli.main import main
|
||||
|
||||
main(["--json", "bootstrap", "--apply"])
|
||||
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}"
|
||||
Reference in New Issue
Block a user