S57-S58 compliance sprint: - Checker consolidation: testing→error_handling, test_coverage→test_quality v4.0 - 14-branch dispatch + night shift agent fixes - New tests: json_handler, cli_routing, error_resilience, contracts across all branches - Bypass configs for justified architectural choices (daemon plugins, flow watchdog, prax public API) - Deep nesting refactors, unused function cleanup, architecture fixes - Commons: 68→100% test_quality (279 tests), full bypass config - README updated: 100% compliance, 2000+ tests Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
395 lines
14 KiB
Python
395 lines
14 KiB
Python
# ===================AIPASS====================
|
|
# META DATA HEADER
|
|
# Name: test_curation.py - Curation Subsystem Tests
|
|
# Date: 2026-03-28
|
|
# Version: 1.0.0
|
|
# Category: commons/tests
|
|
#
|
|
# CHANGELOG (Max 5 entries):
|
|
# - v1.0.0 (2026-03-28): Initial creation — reactions, pins, trending tests
|
|
#
|
|
# CODE STANDARDS:
|
|
# - Pytest function style (no unittest classes)
|
|
# - Uses initialized_db fixture from conftest.py for DB isolation
|
|
# - Mocks prax logger and json_handler to avoid side-effect dependencies
|
|
# =============================================
|
|
|
|
"""
|
|
Unit tests for the curation subsystem.
|
|
|
|
Covers:
|
|
- reaction_queries: add, remove, get counts, get detailed, summary string
|
|
- pin_queries: pin, unpin, get pinned, is_pinned checks
|
|
- trending_queries: empty results and engagement-based ranking
|
|
"""
|
|
|
|
import sqlite3
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from commons.apps.handlers.curation.reaction_queries import (
|
|
add_reaction,
|
|
remove_reaction,
|
|
get_reactions,
|
|
get_reactions_detailed,
|
|
get_reaction_summary,
|
|
REACTION_EMOJI,
|
|
VALID_REACTIONS,
|
|
)
|
|
from commons.apps.handlers.curation.pin_queries import (
|
|
pin_post,
|
|
unpin_post,
|
|
get_pinned_posts,
|
|
is_pinned,
|
|
)
|
|
from commons.apps.handlers.curation.trending_queries import get_trending_posts
|
|
|
|
|
|
# =============================================================================
|
|
# HELPERS
|
|
# =============================================================================
|
|
|
|
|
|
def _seed_agent_and_post(conn: sqlite3.Connection) -> int:
|
|
"""Insert a test agent and post, return the post ID."""
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO agents (branch_name, display_name) VALUES (?, ?)",
|
|
("TEST_BRANCH", "Test"),
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO posts (title, content, room_name, author) "
|
|
"VALUES (?, ?, ?, ?)",
|
|
("Test Post", "Content", "general", "TEST_BRANCH"),
|
|
)
|
|
conn.commit()
|
|
row = conn.execute("SELECT last_insert_rowid()").fetchone()
|
|
post_id: int = row[0]
|
|
return post_id
|
|
|
|
|
|
def _seed_comment(conn: sqlite3.Connection, post_id: int) -> int:
|
|
"""Insert a test comment on a post, return the comment ID."""
|
|
conn.execute(
|
|
"INSERT INTO comments (post_id, author, content) VALUES (?, ?, ?)",
|
|
(post_id, "TEST_BRANCH", "A test comment"),
|
|
)
|
|
conn.commit()
|
|
row = conn.execute("SELECT last_insert_rowid()").fetchone()
|
|
comment_id: int = row[0]
|
|
return comment_id
|
|
|
|
|
|
# =============================================================================
|
|
# REACTION QUERIES — add_reaction
|
|
# =============================================================================
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.reaction_queries.json_handler")
|
|
def test_add_reaction_new_returns_true(
|
|
mock_json: object, initialized_db: object
|
|
) -> None:
|
|
"""Adding a new reaction to a post should return True."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
result = add_reaction(conn, "TEST_BRANCH", "thumbsup", post_id=post_id)
|
|
assert result is True
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.reaction_queries.json_handler")
|
|
def test_add_reaction_duplicate_returns_false(
|
|
mock_json: object, initialized_db: object
|
|
) -> None:
|
|
"""Adding the same reaction a second time should return False."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
add_reaction(conn, "TEST_BRANCH", "thumbsup", post_id=post_id)
|
|
result = add_reaction(conn, "TEST_BRANCH", "thumbsup", post_id=post_id)
|
|
assert result is False
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.reaction_queries.json_handler")
|
|
def test_add_reaction_invalid_type_returns_false(
|
|
mock_json: object, initialized_db: object
|
|
) -> None:
|
|
"""An invalid reaction name should be rejected immediately."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
result = add_reaction(conn, "TEST_BRANCH", "invalid_emoji", post_id=post_id)
|
|
assert result is False
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.reaction_queries.json_handler")
|
|
def test_add_reaction_comment_target(
|
|
mock_json: object, initialized_db: object
|
|
) -> None:
|
|
"""Reactions can target a comment instead of a post."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
comment_id = _seed_comment(conn, post_id)
|
|
|
|
result = add_reaction(conn, "TEST_BRANCH", "agree", comment_id=comment_id)
|
|
assert result is True
|
|
|
|
# Verify the reaction is stored against the comment, not the post
|
|
counts = get_reactions(conn, comment_id=comment_id)
|
|
assert counts.get("agree") == 1
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.reaction_queries.json_handler")
|
|
def test_add_reaction_both_targets_returns_false(
|
|
mock_json: object, initialized_db: object
|
|
) -> None:
|
|
"""Providing both post_id and comment_id should be rejected."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
result = add_reaction(conn, "TEST_BRANCH", "agree", post_id=post_id, comment_id=99)
|
|
assert result is False
|
|
|
|
|
|
# =============================================================================
|
|
# REACTION QUERIES — remove_reaction
|
|
# =============================================================================
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.reaction_queries.json_handler")
|
|
def test_remove_reaction_existing_returns_true(
|
|
mock_json: object, initialized_db: object
|
|
) -> None:
|
|
"""Removing an existing reaction should return True."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
add_reaction(conn, "TEST_BRANCH", "celebrate", post_id=post_id)
|
|
result = remove_reaction(conn, "TEST_BRANCH", "celebrate", post_id=post_id)
|
|
assert result is True
|
|
|
|
|
|
def test_remove_reaction_nonexistent_returns_false(initialized_db: object) -> None:
|
|
"""Removing a reaction that was never added should return False."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
result = remove_reaction(conn, "TEST_BRANCH", "thinking", post_id=post_id)
|
|
assert result is False
|
|
|
|
|
|
# =============================================================================
|
|
# REACTION QUERIES — get_reactions / get_reactions_detailed / summary
|
|
# =============================================================================
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.reaction_queries.json_handler")
|
|
def test_get_reactions_returns_correct_counts(
|
|
mock_json: object, initialized_db: object
|
|
) -> None:
|
|
"""get_reactions should return accurate per-type counts."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
# Second agent
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO agents (branch_name, display_name) VALUES (?, ?)",
|
|
("AGENT_B", "Agent B"),
|
|
)
|
|
conn.commit()
|
|
|
|
add_reaction(conn, "TEST_BRANCH", "thumbsup", post_id=post_id)
|
|
add_reaction(conn, "AGENT_B", "thumbsup", post_id=post_id)
|
|
add_reaction(conn, "TEST_BRANCH", "thinking", post_id=post_id)
|
|
|
|
counts = get_reactions(conn, post_id=post_id)
|
|
assert counts["thumbsup"] == 2
|
|
assert counts["thinking"] == 1
|
|
assert "agree" not in counts
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.reaction_queries.json_handler")
|
|
def test_get_reactions_detailed_returns_agent_names(
|
|
mock_json: object, initialized_db: object
|
|
) -> None:
|
|
"""get_reactions_detailed should map reaction types to agent name lists."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO agents (branch_name, display_name) VALUES (?, ?)",
|
|
("AGENT_B", "Agent B"),
|
|
)
|
|
conn.commit()
|
|
|
|
add_reaction(conn, "TEST_BRANCH", "agree", post_id=post_id)
|
|
add_reaction(conn, "AGENT_B", "agree", post_id=post_id)
|
|
|
|
detailed = get_reactions_detailed(conn, post_id=post_id)
|
|
assert "agree" in detailed
|
|
assert set(detailed["agree"]) == {"TEST_BRANCH", "AGENT_B"}
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.reaction_queries.json_handler")
|
|
def test_get_reaction_summary_formatted_string(
|
|
mock_json: object, initialized_db: object
|
|
) -> None:
|
|
"""get_reaction_summary should return an emoji-count formatted string."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
add_reaction(conn, "TEST_BRANCH", "thumbsup", post_id=post_id)
|
|
add_reaction(conn, "TEST_BRANCH", "celebrate", post_id=post_id)
|
|
|
|
summary = get_reaction_summary(conn, post_id=post_id)
|
|
assert REACTION_EMOJI["thumbsup"] + "1" in summary
|
|
assert REACTION_EMOJI["celebrate"] + "1" in summary
|
|
|
|
|
|
def test_get_reaction_summary_empty_returns_empty_string(
|
|
initialized_db: object,
|
|
) -> None:
|
|
"""get_reaction_summary with no reactions should return an empty string."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
summary = get_reaction_summary(conn, post_id=post_id)
|
|
assert summary == ""
|
|
|
|
|
|
# =============================================================================
|
|
# PIN QUERIES
|
|
# =============================================================================
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.pin_queries.json_handler")
|
|
def test_pin_post_success(mock_json: object, initialized_db: object) -> None:
|
|
"""Pinning an existing post should return True and set pinned=1."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
result = pin_post(conn, post_id)
|
|
assert result is True
|
|
assert is_pinned(conn, post_id) is True
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.pin_queries.json_handler")
|
|
def test_unpin_post_success(mock_json: object, initialized_db: object) -> None:
|
|
"""Unpinning a pinned post should return True and set pinned=0."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
pin_post(conn, post_id)
|
|
result = unpin_post(conn, post_id)
|
|
assert result is True
|
|
assert is_pinned(conn, post_id) is False
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.pin_queries.json_handler")
|
|
def test_get_pinned_posts_returns_only_pinned(
|
|
mock_json: object, initialized_db: object
|
|
) -> None:
|
|
"""get_pinned_posts should return only posts with pinned=1."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
# Before pinning, list should be empty
|
|
pinned = get_pinned_posts(conn)
|
|
assert len(pinned) == 0
|
|
|
|
pin_post(conn, post_id)
|
|
pinned = get_pinned_posts(conn)
|
|
assert len(pinned) == 1
|
|
assert pinned[0]["id"] == post_id
|
|
assert pinned[0]["title"] == "Test Post"
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.pin_queries.json_handler")
|
|
def test_get_pinned_posts_filters_by_room(
|
|
mock_json: object, initialized_db: object
|
|
) -> None:
|
|
"""get_pinned_posts with room_name should filter to that room only."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
_seed_agent_and_post(conn) # post in "general"
|
|
|
|
# Create a second post in "dev"
|
|
conn.execute(
|
|
"INSERT INTO posts (title, content, room_name, author) "
|
|
"VALUES (?, ?, ?, ?)",
|
|
("Dev Post", "Dev content", "dev", "TEST_BRANCH"),
|
|
)
|
|
conn.commit()
|
|
dev_post_id: int = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
|
|
|
# Pin both
|
|
pin_post(conn, 1)
|
|
pin_post(conn, dev_post_id)
|
|
|
|
general_pinned = get_pinned_posts(conn, room_name="general")
|
|
assert len(general_pinned) == 1
|
|
assert general_pinned[0]["room_name"] == "general"
|
|
|
|
dev_pinned = get_pinned_posts(conn, room_name="dev")
|
|
assert len(dev_pinned) == 1
|
|
assert dev_pinned[0]["room_name"] == "dev"
|
|
|
|
|
|
def test_is_pinned_false_for_unpinned_post(initialized_db: object) -> None:
|
|
"""is_pinned should return False for a post that has not been pinned."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
assert is_pinned(conn, post_id) is False
|
|
|
|
|
|
def test_is_pinned_false_for_nonexistent_post(initialized_db: object) -> None:
|
|
"""is_pinned should return False for a post ID that does not exist."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
assert is_pinned(conn, 99999) is False
|
|
|
|
|
|
# =============================================================================
|
|
# TRENDING QUERIES
|
|
# =============================================================================
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.trending_queries.json_handler")
|
|
def test_get_trending_posts_empty(mock_json: object, initialized_db: object) -> None:
|
|
"""get_trending_posts with no engagement data should return an empty list."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
_seed_agent_and_post(conn)
|
|
|
|
trending = get_trending_posts(conn, hours=24, min_engagement=1)
|
|
assert trending == []
|
|
|
|
|
|
@patch("commons.apps.handlers.curation.trending_queries.json_handler")
|
|
@patch("commons.apps.handlers.curation.reaction_queries.json_handler")
|
|
def test_get_trending_posts_with_engagement(
|
|
mock_reaction_json: object,
|
|
mock_trending_json: object,
|
|
initialized_db: object,
|
|
) -> None:
|
|
"""Posts with enough recent engagement should appear in trending results."""
|
|
conn: sqlite3.Connection = initialized_db # type: ignore[assignment]
|
|
post_id = _seed_agent_and_post(conn)
|
|
|
|
# Add agents for engagement
|
|
for name in ("AGENT_A", "AGENT_B", "AGENT_C"):
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO agents (branch_name, display_name) VALUES (?, ?)",
|
|
(name, name),
|
|
)
|
|
conn.commit()
|
|
|
|
# Add reactions (3 total = meets min_engagement=3)
|
|
add_reaction(conn, "AGENT_A", "thumbsup", post_id=post_id)
|
|
add_reaction(conn, "AGENT_B", "agree", post_id=post_id)
|
|
add_reaction(conn, "AGENT_C", "celebrate", post_id=post_id)
|
|
|
|
trending = get_trending_posts(conn, hours=24, min_engagement=3)
|
|
assert len(trending) == 1
|
|
assert trending[0]["id"] == post_id
|
|
assert trending[0]["reaction_count"] == 3
|
|
assert trending[0]["engagement_count"] == 3
|