Files
WealthySmart/backend/tests/test_chat_audit.py
2026-07-14 22:34:33 -06:00

128 lines
4.4 KiB
Python

"""Chat-run audit persistence and redacted HTTP access logs."""
import asyncio
from datetime import timedelta
from agent_framework_ag_ui import AGUIThreadSnapshot
from starlette.requests import Request
from starlette.responses import Response
from sqlmodel import select
import app.agent.chat_audit as chat_audit
import app.agent.snapshot_store as snapshot_store_module
from app.agent.snapshot_store import PostgresSnapshotStore
from app.models.models import ChatRunLog
from app.timeutil import utcnow
def _audit_store(monkeypatch, engine):
monkeypatch.setattr(chat_audit, "engine", engine)
monkeypatch.setattr(snapshot_store_module, "engine", engine)
return PostgresSnapshotStore()
def test_snapshot_completes_chat_run_with_tools_and_response(monkeypatch, engine, session):
store = _audit_store(monkeypatch, engine)
run_id = chat_audit.create_chat_run(
request_id="req-1",
agui_run_id="run-1",
thread_id="main",
messages=[{"role": "user", "content": "¿Cuál es mi saldo?"}],
)
token = chat_audit.set_current_chat_run(run_id)
try:
asyncio.run(
store.save(
scope="default",
thread_id="resp_123",
snapshot=AGUIThreadSnapshot(
messages=[
{"id": "u1", "role": "user", "content": "¿Cuál es mi saldo?"},
{
"id": "a1",
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call-1",
"type": "function",
"function": {"name": "get_net_worth", "arguments": "{}"},
}
],
},
{"id": "t1", "role": "tool", "tool_call_id": "call-1", "content": "{\"net_crc\": 1}"},
{"id": "a2", "role": "assistant", "content": "Tu saldo es ₡1."},
]
),
)
)
finally:
chat_audit.reset_current_chat_run(token)
row = session.get(ChatRunLog, run_id)
assert row is not None
assert row.status == "completed"
assert row.agui_run_id == "run-1"
assert row.response_thread_id == "resp_123"
assert row.user_prompt == "¿Cuál es mi saldo?"
assert row.assistant_response == "Tu saldo es ₡1."
assert row.tool_calls == [
{
"id": "call-1",
"name": "get_net_worth",
"arguments": "{}",
"result": '{"net_crc": 1}',
}
]
def test_failure_and_retention_cleanup(monkeypatch, engine, session):
_audit_store(monkeypatch, engine)
failed_id = chat_audit.create_chat_run(
request_id="req-failed", agui_run_id="run-failed", thread_id="main", messages=[]
)
chat_audit.mark_chat_run_failed(failed_id, "RuntimeError: provider unavailable")
assert session.get(ChatRunLog, failed_id).status == "failed"
expired = ChatRunLog(
request_id="req-expired",
model="gpt-5.6-luna",
expires_at=utcnow() - timedelta(seconds=1),
)
session.add(expired)
session.commit()
assert chat_audit.purge_expired_chat_runs() == 1
assert session.exec(select(ChatRunLog).where(ChatRunLog.request_id == "req-expired")).first() is None
def test_api_access_log_never_contains_request_body(monkeypatch):
from app.main import log_api_request
logged = []
monkeypatch.setattr("app.main.log_event", lambda event, **fields: logged.append((event, fields)))
scope = {
"type": "http",
"method": "POST",
"path": "/api/auth/login",
"raw_path": b"/api/auth/login",
"query_string": b"",
"headers": [],
"scheme": "http",
"server": ("testserver", 80),
"client": ("testclient", 1234),
}
request = Request(scope)
async def call_next(_request):
return Response(status_code=201)
response = asyncio.run(log_api_request(request, call_next))
assert response.headers["X-Request-ID"]
assert logged[0][0] == "api_request"
fields = logged[0][1]
assert fields["path"] == "/api/auth/login"
assert fields["status_code"] == 201
assert "password" not in fields
assert "body" not in fields