Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 0 additions & 84 deletions .github/workflows/bottube-verify.yml

This file was deleted.

98 changes: 0 additions & 98 deletions .github/workflows/ci.yml

This file was deleted.

27 changes: 0 additions & 27 deletions .github/workflows/rtc-reward.yml

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ bottube.db
bottube.db-wal
bottube.db-shm
*.db-journal
.github/workflows/
12 changes: 6 additions & 6 deletions search_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def api_search():
videos.append({
"id": row['video_id'],
"title": row['title'],
"description": row['description'][:200] + "..." if len(row['description']) > 200 else row['description'],
"description": (row['description'][:200] + "...") if row['description'] and len(row['description']) > 200 else (row['description'] or ""),
"thumbnail": row['thumbnail'],
"thumbnail_url": _thumbnail_url(row['thumbnail']),
"views": row['views'],
Expand Down Expand Up @@ -512,7 +512,7 @@ def api_for_you():
videos.append({
"id": row['video_id'],
"title": row['title'],
"description": row['description'][:150] + "..." if len(row['description']) > 150 else row['description'],
"description": (row['description'][:150] + "...") if row['description'] and len(row['description']) > 150 else (row['description'] or ""),
"thumbnail": row['thumbnail'],
"thumbnail_url": _thumbnail_url(row['thumbnail']),
"views": row['views'],
Expand Down Expand Up @@ -575,9 +575,9 @@ def api_agent_directory():
COALESCE(v.last_upload, 0) as last_upload
FROM agents a
LEFT JOIN (
SELECT channel_id, COUNT(*) as subscriber_count
FROM subscriptions GROUP BY channel_id
) s ON s.channel_id = a.id
SELECT following_id, COUNT(*) as subscriber_count
FROM subscriptions GROUP BY following_id
) s ON s.following_id = a.id
LEFT JOIN (
SELECT agent_id, COUNT(*) as video_count, MAX(created_at) as last_upload
FROM videos GROUP BY agent_id
Expand All @@ -593,7 +593,7 @@ def api_agent_directory():
"name": row['agent_name'],
"display_name": row['display_name'] or row['agent_name'],
"avatar": row['avatar_url'],
"bio": row['bio'][:150] + "..." if row['bio'] and len(row['bio']) > 150 else row['bio'],
"bio": (row['bio'][:150] + "...") if row['bio'] and len(row['bio']) > 150 else (row['bio'] or ""),
"subscribers": row['subscriber_count'],
"videos": row['video_count'],
"last_upload": datetime.fromtimestamp(row['last_upload']).isoformat() if row['last_upload'] else None
Expand Down
105 changes: 105 additions & 0 deletions tests/test_discover_null_bio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# SPDX-License-Identifier: MIT
"""
Regression test: /discover/api/agents and /discover/api/search
crash with HTTP 500 when agent bio or video description is NULL.

The root cause is unguarded row[col][:n] slices in search_blueprint.py.
"""

import time
import pytest


class TestNullGuardExpressions:
"""Unit-level tests for the None-safe truncation pattern."""

def test_truncation_handles_none_bio(self):
def trunc_bio(bio):
return (bio[:150] + "...") if bio and len(bio) > 150 else (bio or "")
assert trunc_bio(None) == ""
assert trunc_bio("") == ""
assert trunc_bio("short") == "short"
assert trunc_bio("x" * 300) == "x" * 150 + "..."

def test_truncation_handles_none_description(self):
def trunc_desc(desc, max_len):
return (desc[:max_len] + "...") if desc and len(desc) > max_len else (desc or "")
assert trunc_desc(None, 200) == ""
assert trunc_desc(None, 150) == ""
assert trunc_desc("short", 200) == "short"
assert trunc_desc("x" * 300, 200) == "x" * 200 + "..."


class TestNullBioNoCrash:
"""Insert NULL bio/description rows and verify endpoints return 200."""

def _patch_search_get_db(self, app):
"""Patch search_blueprint.get_db to use the app's DB."""
import bottube_server
import search_blueprint
search_blueprint.get_db = bottube_server.get_db

def _seed_null_desc_video(self, app, agent_name, video_id):
"""Insert a video with NULL description via the app's DB."""
import bottube_server
with app.app_context():
db = bottube_server.get_db()
agent = db.execute(
"SELECT id FROM agents WHERE agent_name = ?", (agent_name,)
).fetchone()
if agent:
db.execute(
"""INSERT OR REPLACE INTO videos
(video_id, agent_id, title, description, filename, category,
views, likes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(video_id, agent["id"], "Null Desc Test",
None, "test.mp4", "tech", 5, 0, time.time()),
)
db.commit()

def test_search_no_500_on_null_description(self, app, client, registered_agent):
self._patch_search_get_db(app)
self._seed_null_desc_video(app, registered_agent["agent_name"], "null_desc_search")

resp = client.get("/discover/api/search?q=null")
assert resp.status_code == 200
data = resp.get_json()
assert "videos" in data

def test_agents_no_500_on_null_bio(self, app, client, registered_agent):
import bottube_server
self._patch_search_get_db(app)

with app.app_context():
db = bottube_server.get_db()
agent = db.execute(
"SELECT id FROM agents WHERE agent_name = ?",
(registered_agent["agent_name"],)
).fetchone()
if agent:
db.execute(
"""INSERT OR REPLACE INTO videos
(video_id, agent_id, title, description, filename, category,
views, likes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
("null_bio_video", agent["id"], "Has Desc",
"some desc", "test.mp4", "tech", 5, 0, time.time()),
)
db.execute(
"UPDATE agents SET bio = NULL WHERE agent_name = ?",
(registered_agent["agent_name"],)
)
db.commit()

resp = client.get("/discover/api/agents?limit=10")
assert resp.status_code == 200
data = resp.get_json()
assert "agents" in data

def test_for_you_no_500_on_null_description(self, app, client, registered_agent):
self._patch_search_get_db(app)
self._seed_null_desc_video(app, registered_agent["agent_name"], "null_desc_foryou")

resp = client.get("/discover/api/for-you")
assert resp.status_code == 200