Repository navigation
Expand file tree
/
Copy pathtest_migrate_sql.py
More file actions
148 lines (116 loc) · 5.18 KB
/
Copy pathtest_migrate_sql.py
File metadata and controls
148 lines (116 loc) · 5.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
"""Target tests for Pack 0.5.4-MIGRATE-sql.
The SQL logger as a recorder sink: ``project_sql(event)`` is what
``AirlockSQLLogger.record_event`` passes to ``_insert``, and the recorder registers
the sql sink ONLY when ``AIRLOCK_ENABLE_SQL_LOGGER`` is set — as a NORMAL sink
(success+failure, NOT async-only). The ``AIRLOCK_SQL_URL`` disabled-path and the
``_insert`` write path are unchanged. No network/db: events are built in-process and
``_insert`` is monkeypatched.
"""
from __future__ import annotations
import datetime
import pytest
import airlock.callbacks.recorder as recorder_mod
from airlock.callbacks.projections import project_sql
from airlock.callbacks.request_event import build_request_event
from airlock.callbacks.sql_logger import AirlockSQLLogger, proxy_sql_logger
try:
import sqlalchemy as _sa # noqa: F401
_SA_AVAILABLE = True
except ImportError:
_SA_AVAILABLE = False
class _FakeUsage:
def __init__(self, prompt=3, completion=5, total=8) -> None:
self.prompt_tokens = prompt
self.completion_tokens = completion
self.total_tokens = total
class _FakeResponse:
def __init__(self) -> None:
self.usage = _FakeUsage()
def _ts(secs: float) -> datetime.datetime:
return datetime.datetime(
2026, 6, 28, 12, 0, 0, tzinfo=datetime.timezone.utc
) + datetime.timedelta(seconds=secs)
def _kwargs(**over):
metadata = {
"user_api_key_alias": "alice",
"user_api_key_team_alias": "team-a",
"airlock_provider": "openai",
}
metadata.update(over.pop("metadata", {}))
litellm_params = {"metadata": metadata}
base = {
"model": "gpt-4o",
"messages": [{"role": "user", "content": "hello"}],
"litellm_call_id": "call-123",
"litellm_params": litellm_params,
"response_cost": 0.0021,
"headers": {"x-trace": "abc"},
}
base.update(over)
return base
def _event(success=True, **over):
resp = None if not success else _FakeResponse()
return build_request_event(_kwargs(**over), resp, _ts(0), _ts(1), success=success)
# ---------------------------------------------------------------------------
# 1. record_event inserts exactly project_sql(event)
# ---------------------------------------------------------------------------
def test_record_event_inserts_project_sql(monkeypatch):
logger = AirlockSQLLogger()
captured = []
monkeypatch.setattr(logger, "_insert", captured.append)
event = _event()
logger.record_event(event)
assert len(captured) == 1
assert captured[0] == project_sql(event)
def test_record_event_inserts_failure(monkeypatch):
logger = AirlockSQLLogger()
captured = []
monkeypatch.setattr(logger, "_insert", captured.append)
event = _event(success=False, exception=ValueError("boom"))
logger.record_event(event)
assert len(captured) == 1
assert captured[0]["success"] is False
assert captured[0]["error"] == "boom"
assert captured[0]["response"] is None
def test_record_event_messages_response_are_json_strings(monkeypatch):
logger = AirlockSQLLogger()
captured = []
monkeypatch.setattr(logger, "_insert", captured.append)
logger.record_event(_event())
record = captured[0]
assert isinstance(record["messages"], str)
assert isinstance(record["response"], str)
# ---------------------------------------------------------------------------
# 2. AIRLOCK_SQL_URL-unset disabled path is unchanged (no insert, no raise)
# ---------------------------------------------------------------------------
@pytest.mark.skipif(not _SA_AVAILABLE, reason="sqlalchemy not installed")
def test_disabled_path_no_engine_no_raise(monkeypatch):
# No AIRLOCK_SQL_URL set (clean_env autouse removed it)
logger = AirlockSQLLogger()
logger.record_event(_event()) # must not raise
assert logger._engine is None
# ---------------------------------------------------------------------------
# 3. Gating — flag unset → no sql sink
# ---------------------------------------------------------------------------
def test_sql_sink_absent_when_flag_unset(monkeypatch):
monkeypatch.delenv("AIRLOCK_ENABLE_SQL_LOGGER", raising=False)
recorder = recorder_mod._build_recorder()
assert "sql" not in recorder.sink_names
# ---------------------------------------------------------------------------
# 4. Gating — flag set → sql sink present, NORMAL (not async_only)
# ---------------------------------------------------------------------------
def test_sql_sink_present_and_normal_when_flag_set(monkeypatch):
monkeypatch.setenv("AIRLOCK_ENABLE_SQL_LOGGER", "1")
recorder = recorder_mod._build_recorder()
assert "sql" in recorder.sink_names
sql_reg = next(reg for reg in recorder._sinks if reg.name == "sql")
assert sql_reg.async_only is False
def test_sql_sink_fires_on_sync_dispatch(monkeypatch):
"""A NORMAL sink fires on sync dispatch (async_only sinks would be skipped)."""
monkeypatch.setenv("AIRLOCK_ENABLE_SQL_LOGGER", "1")
captured = []
monkeypatch.setattr(proxy_sql_logger, "_insert", captured.append)
recorder = recorder_mod._build_recorder()
event = _event()
recorder.dispatch(event, is_async=False)
assert captured[-1] == project_sql(event)