feat: add optional ai and mcp automation
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.setting import Setting
|
||||
from app.services.api_tokens import MCP_READ_SCOPE, create_api_token
|
||||
from app.services.report_store import ReportStore
|
||||
|
||||
DOMAIN = "example.com"
|
||||
|
||||
REPORT = {
|
||||
"domain": DOMAIN,
|
||||
"report_id": "ai-001",
|
||||
"org_name": "Receiver",
|
||||
"policy": {"p": "none", "pct": "100"},
|
||||
"records": [
|
||||
{
|
||||
"source_ip": "192.0.2.10",
|
||||
"count": 8,
|
||||
"disposition": "none",
|
||||
"dkim_result": "fail",
|
||||
"spf_result": "fail",
|
||||
},
|
||||
{
|
||||
"source_ip": "192.0.2.11",
|
||||
"count": 2,
|
||||
"disposition": "none",
|
||||
"dkim_result": "pass",
|
||||
"spf_result": "pass",
|
||||
},
|
||||
],
|
||||
"summary": {"total_count": 10, "passed_count": 2, "failed_count": 8},
|
||||
}
|
||||
|
||||
|
||||
def _seed_report_store() -> None:
|
||||
ReportStore.get_instance().add_report(REPORT)
|
||||
|
||||
|
||||
def _set_setting(db: Session, key: str, value: str, category: str) -> None:
|
||||
row = db.query(Setting).filter(Setting.key == key).first()
|
||||
if row is None:
|
||||
row = Setting(key=key, value=value, category=category, value_type="string")
|
||||
db.add(row)
|
||||
else:
|
||||
row.value = value
|
||||
db.commit()
|
||||
|
||||
|
||||
def test_ai_settings_are_seeded(authed_client: TestClient):
|
||||
response = authed_client.get("/api/v1/settings")
|
||||
assert response.status_code == 200
|
||||
keys = {row["key"] for row in response.json()}
|
||||
assert "ai.enabled" in keys
|
||||
assert "ai.provider" in keys
|
||||
assert "ai.action_tools_enabled" in keys
|
||||
assert "mcp.enabled" in keys
|
||||
|
||||
|
||||
def test_ai_summary_requires_explicit_opt_in(authed_client: TestClient):
|
||||
_seed_report_store()
|
||||
response = authed_client.get(f"/api/v1/ai/domains/{DOMAIN}/summary")
|
||||
assert response.status_code == 403
|
||||
assert "AI assistance is disabled" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_ai_summary_is_evidence_first_and_redacted(
|
||||
authed_client: TestClient,
|
||||
db_session: Session,
|
||||
):
|
||||
_seed_report_store()
|
||||
_set_setting(db_session, "ai.enabled", "true", "ai")
|
||||
_set_setting(db_session, "ai.redaction_mode", "strict", "ai")
|
||||
|
||||
response = authed_client.get(f"/api/v1/ai/domains/{DOMAIN}/summary")
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()["summary"]
|
||||
assert body["summary"]["domain"] == DOMAIN
|
||||
assert body["summary"]["failed_messages"] == 8
|
||||
assert body["recommendations"]
|
||||
assert body["recommendations"][0]["evidence"]
|
||||
assert "**redacted**" not in body["safe_context"]["domain"]
|
||||
|
||||
|
||||
def test_action_proposals_are_reviewable_and_not_mutating(
|
||||
authed_client: TestClient,
|
||||
db_session: Session,
|
||||
):
|
||||
_seed_report_store()
|
||||
_set_setting(db_session, "ai.enabled", "true", "ai")
|
||||
|
||||
response = authed_client.get(f"/api/v1/ai/domains/{DOMAIN}/action-proposals")
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["domain"] == DOMAIN
|
||||
assert body["proposals"]
|
||||
proposal = body["proposals"][0]
|
||||
assert proposal["requires_human_confirmation"] is True
|
||||
assert proposal["mutates_state"] is False
|
||||
assert proposal["confirmation_text"] == proposal["proposal_id"]
|
||||
|
||||
|
||||
def test_action_confirmation_requires_action_tools_enabled(
|
||||
authed_client: TestClient,
|
||||
db_session: Session,
|
||||
):
|
||||
_seed_report_store()
|
||||
_set_setting(db_session, "ai.enabled", "true", "ai")
|
||||
proposal = authed_client.get(f"/api/v1/ai/domains/{DOMAIN}/action-proposals").json()[
|
||||
"proposals"
|
||||
][0]
|
||||
|
||||
denied = authed_client.post(
|
||||
f"/api/v1/ai/domains/{DOMAIN}/action-proposals/confirm",
|
||||
json={
|
||||
"proposal_id": proposal["proposal_id"],
|
||||
"confirmation_text": proposal["proposal_id"],
|
||||
},
|
||||
)
|
||||
assert denied.status_code == 403
|
||||
|
||||
_set_setting(db_session, "ai.action_tools_enabled", "true", "ai")
|
||||
confirmed = authed_client.post(
|
||||
f"/api/v1/ai/domains/{DOMAIN}/action-proposals/confirm",
|
||||
json={
|
||||
"proposal_id": proposal["proposal_id"],
|
||||
"confirmation_text": proposal["proposal_id"],
|
||||
"note": "Reviewed by operator",
|
||||
},
|
||||
)
|
||||
assert confirmed.status_code == 200
|
||||
assert confirmed.json()["status"] == "confirmed"
|
||||
assert confirmed.json()["applied"] is False
|
||||
|
||||
|
||||
def test_mcp_requires_enabled_scoped_token(client: TestClient, db_session: Session):
|
||||
_seed_report_store()
|
||||
token = create_api_token(db_session, name="mcp client", scopes=[MCP_READ_SCOPE])
|
||||
|
||||
disabled = client.post(
|
||||
"/api/v1/mcp",
|
||||
headers={"X-API-Key": token.secret},
|
||||
json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"},
|
||||
)
|
||||
assert disabled.status_code == 403
|
||||
|
||||
_set_setting(db_session, "mcp.enabled", "true", "mcp")
|
||||
listed = client.post(
|
||||
"/api/v1/mcp",
|
||||
headers={"X-API-Key": token.secret},
|
||||
json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"},
|
||||
)
|
||||
assert listed.status_code == 200
|
||||
assert listed.json()["result"]["tools"][0]["readOnlyHint"] is True
|
||||
|
||||
called = client.post(
|
||||
"/api/v1/mcp",
|
||||
headers={"X-API-Key": token.secret},
|
||||
json={
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {"name": "domain_summary", "arguments": {"domain": DOMAIN}},
|
||||
},
|
||||
)
|
||||
assert called.status_code == 200
|
||||
result = called.json()["result"]["content"][0]["json"]
|
||||
assert result["summary"]["domain"] == DOMAIN
|
||||
Reference in New Issue
Block a user