feat: улучшить agentic-readiness — добавить CI, проверки конфигурации и тесты
This commit is contained in:
+139
-115
@@ -64,16 +64,18 @@ async def test_analyze_finding_timeout():
|
||||
import guarddog_nexus.config
|
||||
from guarddog_nexus.core.llm import analyze_finding
|
||||
|
||||
guarddog_nexus.config.config.llm_api_key = "sk-test"
|
||||
guarddog_nexus.config.config.llm_timeout = 1
|
||||
original_api_key = guarddog_nexus.config.config.llm_api_key
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_api_key = "sk-test"
|
||||
guarddog_nexus.config.config.llm_timeout = 1
|
||||
|
||||
import httpx
|
||||
import httpx
|
||||
|
||||
with patch("httpx.AsyncClient.post", side_effect=httpx.TimeoutException("timeout")):
|
||||
result = await analyze_finding({"rule": "test", "severity": "WARNING"})
|
||||
assert result is None
|
||||
|
||||
guarddog_nexus.config.config.llm_api_key = ""
|
||||
with patch("httpx.AsyncClient.post", side_effect=httpx.TimeoutException("timeout")):
|
||||
result = await analyze_finding({"rule": "test", "severity": "WARNING"})
|
||||
assert result is None
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_api_key = original_api_key
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -81,14 +83,16 @@ async def test_analyze_finding_api_error():
|
||||
import guarddog_nexus.config
|
||||
from guarddog_nexus.core.llm import analyze_finding
|
||||
|
||||
guarddog_nexus.config.config.llm_api_key = "sk-test"
|
||||
guarddog_nexus.config.config.llm_timeout = 30
|
||||
original_api_key = guarddog_nexus.config.config.llm_api_key
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_api_key = "sk-test"
|
||||
guarddog_nexus.config.config.llm_timeout = 30
|
||||
|
||||
with patch("httpx.AsyncClient.post", side_effect=Exception("connection refused")):
|
||||
result = await analyze_finding({"rule": "test", "severity": "WARNING"})
|
||||
assert result is None
|
||||
|
||||
guarddog_nexus.config.config.llm_api_key = ""
|
||||
with patch("httpx.AsyncClient.post", side_effect=Exception("connection refused")):
|
||||
result = await analyze_finding({"rule": "test", "severity": "WARNING"})
|
||||
assert result is None
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_api_key = original_api_key
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -96,29 +100,31 @@ async def test_analyze_finding_success():
|
||||
import guarddog_nexus.config
|
||||
from guarddog_nexus.core.llm import analyze_finding
|
||||
|
||||
guarddog_nexus.config.config.llm_api_key = "sk-test"
|
||||
guarddog_nexus.config.config.llm_timeout = 30
|
||||
original_api_key = guarddog_nexus.config.config.llm_api_key
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_api_key = "sk-test"
|
||||
guarddog_nexus.config.config.llm_timeout = 30
|
||||
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status.return_value = None
|
||||
mock_resp.json.return_value = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": '{"verdict":"safe","summary":"ok",'
|
||||
'"analysis":"fine","severity_rating":"low"}',
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status.return_value = None
|
||||
mock_resp.json.return_value = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": '{"verdict":"safe","summary":"ok",'
|
||||
'"analysis":"fine","severity_rating":"low"}',
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with patch("guarddog_nexus.core.llm.httpx.AsyncClient.post", return_value=mock_resp):
|
||||
result = await analyze_finding({"rule": "test"})
|
||||
assert result is not None
|
||||
assert result["verdict"] == "safe"
|
||||
assert result["severity_rating"] == "low"
|
||||
|
||||
guarddog_nexus.config.config.llm_api_key = ""
|
||||
with patch("guarddog_nexus.core.llm.httpx.AsyncClient.post", return_value=mock_resp):
|
||||
result = await analyze_finding({"rule": "test"})
|
||||
assert result is not None
|
||||
assert result["verdict"] == "safe"
|
||||
assert result["severity_rating"] == "low"
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_api_key = original_api_key
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -126,27 +132,29 @@ async def test_analyze_finding_markdown_unwrap():
|
||||
import guarddog_nexus.config
|
||||
from guarddog_nexus.core.llm import analyze_finding
|
||||
|
||||
guarddog_nexus.config.config.llm_api_key = "sk-test"
|
||||
original_api_key = guarddog_nexus.config.config.llm_api_key
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_api_key = "sk-test"
|
||||
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status.return_value = None
|
||||
mock_resp.json.return_value = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": '```json\n{"verdict":"suspicious","summary":"hm",'
|
||||
'"analysis":"...","severity_rating":"medium"}\n```',
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.raise_for_status.return_value = None
|
||||
mock_resp.json.return_value = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": '```json\n{"verdict":"suspicious","summary":"hm",'
|
||||
'"analysis":"...","severity_rating":"medium"}\n```',
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with patch("guarddog_nexus.core.llm.httpx.AsyncClient.post", return_value=mock_resp):
|
||||
result = await analyze_finding({"rule": "test"})
|
||||
assert result is not None
|
||||
assert result["verdict"] == "suspicious"
|
||||
|
||||
guarddog_nexus.config.config.llm_api_key = ""
|
||||
with patch("guarddog_nexus.core.llm.httpx.AsyncClient.post", return_value=mock_resp):
|
||||
result = await analyze_finding({"rule": "test"})
|
||||
assert result is not None
|
||||
assert result["verdict"] == "suspicious"
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_api_key = original_api_key
|
||||
|
||||
|
||||
# --- T1: analyze_finding_htmx endpoint ---
|
||||
@@ -156,80 +164,90 @@ async def test_analyze_finding_markdown_unwrap():
|
||||
async def test_analyze_endpoint_llm_disabled(client, sample_finding):
|
||||
import guarddog_nexus.config
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = False
|
||||
original_enabled = guarddog_nexus.config.config.llm_enabled
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_enabled = False
|
||||
|
||||
resp = await client.post(f"/api/v1/findings/{sample_finding.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "disabled" in resp.text.lower()
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = False
|
||||
resp = await client.post(f"/api/v1/findings/{sample_finding.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "disabled" in resp.text.lower()
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_enabled = original_enabled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_endpoint_not_found(client):
|
||||
import guarddog_nexus.config
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
original_enabled = guarddog_nexus.config.config.llm_enabled
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
|
||||
resp = await client.post("/api/v1/findings/99999/analyze")
|
||||
assert resp.status_code == 404
|
||||
assert "not found" in resp.text.lower()
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = False
|
||||
resp = await client.post("/api/v1/findings/99999/analyze")
|
||||
assert resp.status_code == 404
|
||||
assert "not found" in resp.text.lower()
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_enabled = original_enabled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_endpoint_idempotent_already_analyzed(client, sample_finding_with_report):
|
||||
import guarddog_nexus.config
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
original_enabled = guarddog_nexus.config.config.llm_enabled
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
|
||||
resp = await client.post(f"/api/v1/findings/{sample_finding_with_report.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "safe" in resp.text
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = False
|
||||
resp = await client.post(f"/api/v1/findings/{sample_finding_with_report.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "safe" in resp.text
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_enabled = original_enabled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_endpoint_success(client, sample_finding):
|
||||
import guarddog_nexus.config
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
original_enabled = guarddog_nexus.config.config.llm_enabled
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
|
||||
fake_report = {
|
||||
"verdict": "malicious",
|
||||
"summary": "bad",
|
||||
"analysis": "evil",
|
||||
"severity_rating": "critical",
|
||||
}
|
||||
fake_report = {
|
||||
"verdict": "malicious",
|
||||
"summary": "bad",
|
||||
"analysis": "evil",
|
||||
"severity_rating": "critical",
|
||||
}
|
||||
|
||||
async def mock_analyze(data):
|
||||
return fake_report
|
||||
async def mock_analyze(data):
|
||||
return fake_report
|
||||
|
||||
with patch("guarddog_nexus.core.llm.analyze_finding", mock_analyze):
|
||||
resp = await client.post(f"/api/v1/findings/{sample_finding.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "malicious" in resp.text
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = False
|
||||
with patch("guarddog_nexus.core.llm.analyze_finding", mock_analyze):
|
||||
resp = await client.post(f"/api/v1/findings/{sample_finding.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "malicious" in resp.text
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_enabled = original_enabled
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_endpoint_failure(client, sample_finding):
|
||||
import guarddog_nexus.config
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
original_enabled = guarddog_nexus.config.config.llm_enabled
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
|
||||
async def mock_analyze(data):
|
||||
return None
|
||||
async def mock_analyze(data):
|
||||
return None
|
||||
|
||||
with patch("guarddog_nexus.core.llm.analyze_finding", mock_analyze):
|
||||
resp = await client.post(f"/api/v1/findings/{sample_finding.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "failed" in resp.text.lower()
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = False
|
||||
with patch("guarddog_nexus.core.llm.analyze_finding", mock_analyze):
|
||||
resp = await client.post(f"/api/v1/findings/{sample_finding.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "failed" in resp.text.lower()
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_enabled = original_enabled
|
||||
|
||||
|
||||
# --- GET /analyze polling endpoint ---
|
||||
@@ -245,25 +263,29 @@ class TestAnalyzeStatusEndpoint:
|
||||
async def test_status_returns_report_when_complete(self, client, sample_finding_with_report):
|
||||
import guarddog_nexus.config
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
original_enabled = guarddog_nexus.config.config.llm_enabled
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
|
||||
resp = await client.get(f"/api/v1/findings/{sample_finding_with_report.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "safe" in resp.text
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = False
|
||||
resp = await client.get(f"/api/v1/findings/{sample_finding_with_report.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "safe" in resp.text
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_enabled = original_enabled
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status_returns_spinner_when_no_report(self, client, sample_finding):
|
||||
import guarddog_nexus.config
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
original_enabled = guarddog_nexus.config.config.llm_enabled
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_enabled = True
|
||||
|
||||
resp = await client.get(f"/api/v1/findings/{sample_finding.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "hx-get" in resp.text.lower()
|
||||
|
||||
guarddog_nexus.config.config.llm_enabled = False
|
||||
resp = await client.get(f"/api/v1/findings/{sample_finding.id}/analyze")
|
||||
assert resp.status_code == 200
|
||||
assert "hx-get" in resp.text.lower()
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_enabled = original_enabled
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status_returns_spinner_when_analyzing(self, client, db_session, sample_finding):
|
||||
@@ -288,16 +310,18 @@ async def test_analyze_finding_exhausts_all_retries():
|
||||
import guarddog_nexus.config
|
||||
from guarddog_nexus.core.llm import analyze_finding
|
||||
|
||||
guarddog_nexus.config.config.llm_api_key = "sk-test"
|
||||
original_api_key = guarddog_nexus.config.config.llm_api_key
|
||||
try:
|
||||
guarddog_nexus.config.config.llm_api_key = "sk-test"
|
||||
|
||||
with patch("guarddog_nexus.core.llm._attempt_llm_call", return_value=None):
|
||||
with patch("guarddog_nexus.core.llm.asyncio.sleep") as mock_sleep:
|
||||
result = await analyze_finding({"rule": "test-rule"}, max_retries=2)
|
||||
with patch("guarddog_nexus.core.llm._attempt_llm_call", return_value=None):
|
||||
with patch("guarddog_nexus.core.llm.asyncio.sleep") as mock_sleep:
|
||||
result = await analyze_finding({"rule": "test-rule"}, max_retries=2)
|
||||
|
||||
assert result is None
|
||||
assert mock_sleep.call_count == 1
|
||||
|
||||
guarddog_nexus.config.config.llm_api_key = ""
|
||||
assert result is None
|
||||
assert mock_sleep.call_count == 1
|
||||
finally:
|
||||
guarddog_nexus.config.config.llm_api_key = original_api_key
|
||||
|
||||
|
||||
# --- LLM lock cleanup ---
|
||||
|
||||
Reference in New Issue
Block a user