feat: улучшить agentic-readiness — добавить CI, проверки конфигурации и тесты

This commit is contained in:
Marat Kharitonov
2026-07-06 22:18:12 +03:00
parent 2b695aed82
commit 3818c2db29
6 changed files with 251 additions and 133 deletions
+139 -115
View File
@@ -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 ---