fix(meet-kevin): API smoke-test bugs from Task 17 QA

Three issues caught during end-to-end manual QA against docker-compose:

1. SAEnum field columns serialized to Python enum NAMES ('DISCOVERED')
   but the DB enum had VALUES ('discovered'). Added `values_callable`
   to all 5 SAEnum() declarations in shared/models/meet_kevin.py so they
   emit values, matching the migration's enum literals.

2. /dashboard's "last 7 days" / "last 14 days" filters used
   `func.cast("7 days", type_=None)` which produced NullType DDL.
   Replaced with `text("now() - interval '7 days'")`.

3. /dashboard's outlook trend query repeated `func.date_trunc("day", col)`
   in SELECT, GROUP BY and ORDER BY — Postgres treats each as a separate
   parameterized expression. Hoisted into a single `day_trunc` variable
   so all three clauses reference the same SQL fragment.

All 11 /api/meet-kevin/* endpoints now return valid JSON against a
docker-compose Postgres seeded with one analyzed video + NVDA mention.
This commit is contained in:
Viktor Barzin 2026-05-21 20:15:08 +00:00
parent 01856bab9f
commit 7b81980c66
2 changed files with 14 additions and 14 deletions

View file

@ -125,7 +125,7 @@ class KevinVideo(TimestampMixin, Base):
duration_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True)
thumbnail_url: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[KevinVideoStatus] = mapped_column(
SAEnum(KevinVideoStatus, name="kevin_video_status"),
SAEnum(KevinVideoStatus, name="kevin_video_status", values_callable=lambda x: [e.value for e in x]),
nullable=False,
default=KevinVideoStatus.DISCOVERED,
index=True,
@ -159,7 +159,7 @@ class KevinTranscript(Base):
BigInteger, ForeignKey("kevin_videos.id"), unique=True, nullable=False
)
source: Mapped[KevinTranscriptSource] = mapped_column(
SAEnum(KevinTranscriptSource, name="kevin_transcript_source"), nullable=False
SAEnum(KevinTranscriptSource, name="kevin_transcript_source", values_callable=lambda x: [e.value for e in x]), nullable=False
)
language: Mapped[str] = mapped_column(String(8), nullable=False)
raw_text: Mapped[str] = mapped_column(Text, nullable=False)
@ -185,7 +185,7 @@ class KevinAnalysis(Base):
model: Mapped[str] = mapped_column(String(100), nullable=False)
prompt_version: Mapped[str] = mapped_column(String(50), nullable=False)
market_outlook_direction: Mapped[KevinMarketOutlook] = mapped_column(
SAEnum(KevinMarketOutlook, name="kevin_market_outlook"), nullable=False
SAEnum(KevinMarketOutlook, name="kevin_market_outlook", values_callable=lambda x: [e.value for e in x]), nullable=False
)
market_outlook_reasoning: Mapped[str] = mapped_column(Text, nullable=False)
macro_themes_json: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
@ -222,11 +222,11 @@ class KevinStockMention(Base):
String(16), nullable=False, index=True
)
action: Mapped[KevinTickerAction] = mapped_column(
SAEnum(KevinTickerAction, name="kevin_ticker_action"), nullable=False
SAEnum(KevinTickerAction, name="kevin_ticker_action", values_callable=lambda x: [e.value for e in x]), nullable=False
)
conviction: Mapped[Decimal] = mapped_column(Numeric(4, 3), nullable=False)
time_horizon: Mapped[KevinTimeHorizon] = mapped_column(
SAEnum(KevinTimeHorizon, name="kevin_time_horizon"), nullable=False
SAEnum(KevinTimeHorizon, name="kevin_time_horizon", values_callable=lambda x: [e.value for e in x]), nullable=False
)
rationale_quote: Mapped[str] = mapped_column(Text, nullable=False)
video_timestamp_seconds: Mapped[int | None] = mapped_column(