2026-05-24 01:03:53 +00:00
|
|
|
"""Tests for the audit writer (upsert into kevin_signal_bridge_state)."""
|
|
|
|
|
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
2026-05-26 20:01:37 +00:00
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
2026-05-24 01:03:53 +00:00
|
|
|
from services.kevin_signal_bridge.audit import AuditWriter
|
|
|
|
|
from shared.models.meet_kevin import (
|
|
|
|
|
KevinAnalysis,
|
|
|
|
|
KevinChannel,
|
|
|
|
|
KevinMarketOutlook,
|
|
|
|
|
KevinStockMention,
|
|
|
|
|
KevinTickerAction,
|
|
|
|
|
KevinTimeHorizon,
|
|
|
|
|
KevinVideo,
|
|
|
|
|
KevinVideoStatus,
|
|
|
|
|
)
|
|
|
|
|
from shared.models.meet_kevin_trading import KevinSignalBridgeState
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _factory(session: AsyncSession):
|
|
|
|
|
class _StaticSessionFactory:
|
|
|
|
|
async def __aenter__(self):
|
|
|
|
|
return session
|
|
|
|
|
|
|
|
|
|
async def __aexit__(self, *args):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def factory():
|
|
|
|
|
return _StaticSessionFactory()
|
|
|
|
|
|
|
|
|
|
return factory
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _seed_mention(session: AsyncSession) -> int:
|
|
|
|
|
channel = KevinChannel(youtube_channel_id="UCa", title="t")
|
|
|
|
|
session.add(channel)
|
|
|
|
|
await session.flush()
|
|
|
|
|
video = KevinVideo(
|
|
|
|
|
channel_id=channel.id,
|
|
|
|
|
youtube_video_id="va",
|
|
|
|
|
title="t",
|
|
|
|
|
published_at=datetime.now(timezone.utc),
|
|
|
|
|
status=KevinVideoStatus.ANALYZED,
|
|
|
|
|
)
|
|
|
|
|
session.add(video)
|
|
|
|
|
await session.flush()
|
|
|
|
|
analysis = KevinAnalysis(
|
|
|
|
|
video_id=video.id,
|
|
|
|
|
model="m",
|
|
|
|
|
prompt_version="v1",
|
|
|
|
|
market_outlook_direction=KevinMarketOutlook.NEUTRAL,
|
|
|
|
|
market_outlook_reasoning="x",
|
|
|
|
|
summary="x",
|
|
|
|
|
prompt_tokens=10,
|
|
|
|
|
completion_tokens=10,
|
|
|
|
|
cost_usd=Decimal("0.01"),
|
|
|
|
|
)
|
|
|
|
|
session.add(analysis)
|
|
|
|
|
await session.flush()
|
|
|
|
|
mention = KevinStockMention(
|
|
|
|
|
video_id=video.id,
|
|
|
|
|
analysis_id=analysis.id,
|
|
|
|
|
symbol="NVDA",
|
|
|
|
|
action=KevinTickerAction.BUY,
|
|
|
|
|
conviction=Decimal("0.7"),
|
|
|
|
|
time_horizon=KevinTimeHorizon.WEEKS,
|
|
|
|
|
rationale_quote="x",
|
|
|
|
|
)
|
|
|
|
|
session.add(mention)
|
|
|
|
|
await session.flush()
|
|
|
|
|
return mention.id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_audit_writer_inserts_new_row(db_session: AsyncSession):
|
|
|
|
|
mention_id = await _seed_mention(db_session)
|
|
|
|
|
writer = AuditWriter(session_factory=_factory(db_session))
|
|
|
|
|
await writer.write(
|
|
|
|
|
mention_id=mention_id,
|
|
|
|
|
bridge_status="emitted",
|
|
|
|
|
effective_conviction=Decimal("0.75"),
|
|
|
|
|
notes="hello",
|
|
|
|
|
)
|
|
|
|
|
row = (
|
|
|
|
|
await db_session.execute(
|
|
|
|
|
select(KevinSignalBridgeState).where(
|
|
|
|
|
KevinSignalBridgeState.mention_id == mention_id
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
).scalar_one()
|
|
|
|
|
assert row.bridge_status.value == "emitted"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_audit_writer_upserts_on_duplicate(db_session: AsyncSession):
|
|
|
|
|
mention_id = await _seed_mention(db_session)
|
|
|
|
|
writer = AuditWriter(session_factory=_factory(db_session))
|
|
|
|
|
await writer.write(mention_id=mention_id, bridge_status="dry_run", notes="first")
|
|
|
|
|
await writer.write(mention_id=mention_id, bridge_status="emitted", notes="second")
|
|
|
|
|
rows = (
|
|
|
|
|
(
|
|
|
|
|
await db_session.execute(
|
|
|
|
|
select(KevinSignalBridgeState).where(
|
|
|
|
|
KevinSignalBridgeState.mention_id == mention_id
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.scalars()
|
|
|
|
|
.all()
|
|
|
|
|
)
|
|
|
|
|
assert len(rows) == 1
|
|
|
|
|
assert rows[0].bridge_status.value == "emitted"
|
|
|
|
|
assert rows[0].notes == "second"
|