second-brain/tests/test_claim.py
Travis Herbranson 913d4f0335 deploy/tower + claim race + transcribe smoke tests
deploy/tower/:
- second-brain-transcribe.service — systemd unit. User=herbyadmin,
  Type=simple, After=/Wants= wg-quick@wg-lan.service so the WG tunnel
  must come up first. Restart=always with a StartLimitBurst guard.
- second-brain-transcribe.env.example — env file template documenting
  the SECOND_BRAIN_DATABASE_URL form for db.wg.herbylab.dev (10.99.0.1)
  and the optional WHISPER_* overrides.
- README.md — EndeavourOS install steps (nvidia/cuda/cudnn, ffmpeg, uv
  + tower extra, model pre-warm), WG topology reference, validation
  checklist for what to confirm once the tunnel is live, and a
  follow-ups section flagging the local-disk → NAS media migration as
  out-of-scope-for-this-round.

Tests:
- tests/test_claim.py — live-DB race test. Two threads call
  claim_next_source against a single PULLED video row; SKIP LOCKED
  must give exactly one of them the row, the other gets None. Also
  asserts the claimed_by/at columns land + release nulls them.
  Auto-skips when no SECOND_BRAIN_DATABASE_URL is set.
- tests/test_transcribe.py — pure-Python coverage of resolve_settings
  (cpu→int8, cuda→float16, env-over-block) and write_srt; plus a CPU
  smoke test that synthesizes a numpy audio array and runs the `tiny`
  model on cpu/int8 (auto-skipped when faster-whisper isn't installed,
  i.e. on the dev side without --extra tower).
2026-05-25 10:20:46 -04:00

108 lines
3.3 KiB
Python

"""Live-DB test: claim_next_source is race-safe against concurrent callers.
Skips when no DB URL is configured. This is the integration-level guard
for the SELECT ... FOR UPDATE SKIP LOCKED dance — unit-testing the SQL
without Postgres would prove nothing.
"""
from __future__ import annotations
import os
import threading
import uuid
import psycopg
import pytest
def _db_reachable() -> bool:
url = os.environ.get("SECOND_BRAIN_DATABASE_URL") or os.environ.get(
"HERBYLAB_DATABASE_URL"
)
if not url:
return False
raw = url.replace("postgresql+psycopg://", "postgresql://", 1)
try:
with psycopg.connect(raw, connect_timeout=2):
return True
except Exception:
return False
pytestmark = pytest.mark.skipif(
not _db_reachable(),
reason="claim race test needs SECOND_BRAIN_DATABASE_URL pointed at a real petalbrain",
)
def test_two_workers_never_grab_the_same_row():
from second_brain.claim import claim_next_source, release_claim
from second_brain.database import Database, reset_database_singleton
from second_brain.models import Source, SourceStatus, SourceType, utcnow
reset_database_singleton()
db = Database()
# Seed a single TRANSCRIBED-stage candidate.
unique = uuid.uuid4().hex[:8]
url = f"https://example.test/claim-race/{unique}"
with db.session() as sess:
src = Source(
url=url,
title=f"claim-race-{unique}",
domain="development",
source_type=SourceType.VIDEO,
status=SourceStatus.PULLED,
ingested_at=utcnow(),
)
sess.add(src)
sess.flush()
source_id = src.id
results: list[int | None] = []
lock = threading.Lock()
def worker(name: str):
with db.session() as sess:
claimed = claim_next_source(
sess,
worker_id=name,
statuses=[SourceStatus.PENDING, SourceStatus.PULLED],
source_type=SourceType.VIDEO,
)
with lock:
results.append(claimed)
t1 = threading.Thread(target=worker, args=("worker-A",))
t2 = threading.Thread(target=worker, args=("worker-B",))
t1.start()
t2.start()
t1.join()
t2.join()
try:
# Exactly one worker should have grabbed the row; the other gets None.
# (SKIP LOCKED guarantees this even if both transactions overlap.)
non_none = [r for r in results if r is not None]
assert non_none == [source_id], (
f"expected exactly one claimant of {source_id}, got results={results}"
)
# Confirm claim columns landed.
with db.session() as sess:
row = sess.get(Source, source_id)
assert row.claimed_by in {"worker-A", "worker-B"}
assert row.claimed_at is not None
# Releasing the claim should null both columns.
with db.session() as sess:
release_claim(sess, source_id)
with db.session() as sess:
row = sess.get(Source, source_id)
assert row.claimed_by is None
assert row.claimed_at is None
finally:
with db.session() as sess:
sess.query(Source).filter(Source.id == source_id).delete()
reset_database_singleton()