403Webshell
Server IP : 3.147.158.171  /  Your IP : 216.73.216.88
Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5
System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64
User : ec2-user ( 1000)
PHP Version : 8.4.21
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /tsai/repo/api/tests/unit/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /tsai/repo/api/tests/unit/test_comments_scoring.py
"""Tests for cron article-selection scoring in app.api.agents.comments."""

from collections import Counter
from datetime import datetime, timedelta, timezone

import pytest

from app.api.agents.comments import (
    DECAY_DAYS,
    REPLY_DECAY_HOURS,
    _article_age_days,
    _article_score,
    _comment_age_hours,
    _comment_score,
    _count_replies_per_comment,
    _is_fact_check_comment,
    _is_self_comment,
    _pick_article_weighted,
    _pick_reply_target,
)
from app.appTypes import Character


def _article(article_id: int, age_days: float) -> dict:
    published = datetime.now(timezone.utc) - timedelta(days=age_days)
    return {
        'id': article_id,
        'date_gmt': published.strftime('%Y-%m-%dT%H:%M:%S'),
        'title': {'rendered': f'Article {article_id}'},
    }


def test_age_days_parses_iso_naive_as_utc():
    article = _article(1, 0.5)
    age = _article_age_days(article)
    assert age is not None
    assert 0.4 < age < 0.6


def test_age_days_returns_none_when_missing():
    assert _article_age_days({}) is None
    assert _article_age_days({'date_gmt': 'not-a-date'}) is None


def test_fresh_zero_comments_scores_close_to_one():
    article = _article(1, 0)
    score = _article_score(article, {1: 0})
    assert score > 0.99


def test_old_zero_comment_beats_fresh_saturated():
    fresh_saturated = _article(1, 0)
    old_zero = _article(2, DECAY_DAYS)  # ~0.5 recency, 1.0 under-commented = 0.5
    counts = {1: 5, 2: 0}
    assert _article_score(old_zero, counts) > _article_score(fresh_saturated, counts)


def test_score_zero_when_date_missing():
    assert _article_score({'id': 1}, {1: 0}) == 0.0


def test_picker_returns_none_for_empty_pool():
    assert _pick_article_weighted([], {}) is None


def test_picker_falls_back_to_uniform_when_all_weights_zero():
    bad = [{'id': 1}, {'id': 2}, {'id': 3}]  # no date_gmt -> all zero weight
    picked = _pick_article_weighted(bad, {})
    assert picked in bad


def test_picker_favors_higher_scores_over_many_trials():
    fresh_zero = _article(1, 0)
    old_saturated = _article(2, 30)
    pool = [fresh_zero, old_saturated]
    counts = {1: 0, 2: 8}

    import random as _random

    _random.seed(42)
    tally = Counter()
    for _ in range(2000):
        picked = _pick_article_weighted(pool, counts)
        tally[picked['id']] += 1

    # fresh+0 should dominate old+saturated by a wide margin
    assert tally[1] > tally[2] * 20


def test_picker_spreads_when_scores_close():
    a = _article(1, 0)
    b = _article(2, 0)
    pool = [a, b]
    counts = {1: 0, 2: 0}

    import random as _random

    _random.seed(7)
    tally = Counter()
    for _ in range(2000):
        picked = _pick_article_weighted(pool, counts)
        tally[picked['id']] += 1

    # equal scores should produce ~50/50; allow generous slack
    assert 800 < tally[1] < 1200
    assert 800 < tally[2] < 1200


# --------------------------------------------------------------------
# Reply-target selection
# --------------------------------------------------------------------

def _comment(
    comment_id: int,
    age_hours: float,
    author_name: str = 'Alice',
    parent: int = 0,
) -> dict:
    published = datetime.now(timezone.utc) - timedelta(hours=age_hours)
    return {
        'id': comment_id,
        'parent': parent,
        'date_gmt': published.strftime('%Y-%m-%dT%H:%M:%S'),
        'author_name': author_name,
        'content': {'rendered': f'<p>comment {comment_id}</p>'},
    }


def _character(name: str = 'Sarah') -> Character:
    return Character(
        name=name,
        email=f'{name.lower()}@example.test',
        description='test character',
        enabled=True,
        date='2025-01-01',
    )


def test_is_fact_check_comment_detects_prefix_case_insensitive():
    assert _is_fact_check_comment({'author_name': 'Fact-Check (via OpenAI gpt-4)'})
    assert _is_fact_check_comment({'author_name': 'fact-check (via Anthropic claude)'})
    assert not _is_fact_check_comment({'author_name': 'Sarah'})
    assert not _is_fact_check_comment({})


def test_is_self_comment_case_insensitive():
    char = _character('Sarah')
    assert _is_self_comment({'author_name': 'Sarah'}, char)
    assert _is_self_comment({'author_name': 'sarah'}, char)
    assert not _is_self_comment({'author_name': 'Bob'}, char)
    assert not _is_self_comment({}, char)


def test_count_replies_per_comment():
    comments = [
        _comment(1, 1, parent=0),
        _comment(2, 1, parent=1),
        _comment(3, 1, parent=1),
        _comment(4, 1, parent=2),
        _comment(5, 1, parent=0),
    ]
    counts = _count_replies_per_comment(comments)
    assert counts == {1: 2, 2: 1}


def test_comment_age_hours_parses():
    c = _comment(1, age_hours=2.0)
    age = _comment_age_hours(c)
    assert age is not None
    assert 1.9 < age < 2.1


def test_fresh_top_level_zero_replies_scores_close_to_one():
    c = _comment(1, age_hours=0, parent=0)
    assert _comment_score(c, {}) > 0.99


def test_nested_penalized_vs_top_level():
    top = _comment(1, age_hours=0, parent=0)
    nested = _comment(2, age_hours=0, parent=99)
    assert _comment_score(top, {}) > _comment_score(nested, {}) * 1.9


def test_underreplied_penalty():
    c = _comment(1, age_hours=0, parent=0)
    no_replies = _comment_score(c, {})
    saturated = _comment_score(c, {1: 3})
    assert no_replies > saturated * 3.5  # 1.0 vs 0.25


def test_old_top_level_zero_beats_fresh_top_level_saturated():
    fresh_sat = _comment(1, age_hours=0, parent=0)
    old_zero = _comment(2, age_hours=REPLY_DECAY_HOURS, parent=0)
    counts = {1: 5, 2: 0}
    assert _comment_score(old_zero, counts) > _comment_score(fresh_sat, counts)


def test_pick_reply_target_returns_none_when_all_filtered():
    char = _character('Sarah')
    comments = [
        _comment(1, 1, author_name='Sarah'),
        _comment(2, 1, author_name='Fact-Check (via OpenAI gpt-4)'),
    ]
    assert _pick_reply_target(comments, char) is None


def test_pick_reply_target_excludes_self_and_fact_check():
    char = _character('Sarah')
    comments = [
        _comment(1, 1, author_name='Sarah'),
        _comment(2, 1, author_name='Fact-Check (via OpenAI gpt-4)'),
        _comment(3, 1, author_name='Bob'),
    ]
    import random as _random

    _random.seed(0)
    for _ in range(50):
        picked = _pick_reply_target(comments, char)
        assert picked is not None
        assert picked['id'] == 3


def test_pick_reply_target_favors_fresh_top_level_zero_over_old_nested_replied():
    char = _character('Sarah')
    fresh_top = _comment(1, age_hours=0, author_name='Bob', parent=0)
    old_nested = _comment(2, age_hours=24 * 7, author_name='Carol', parent=1)
    # add filler replies under old_nested to inflate its reply count
    fillers = [_comment(100 + i, 1, author_name='X', parent=2) for i in range(5)]
    pool = [fresh_top, old_nested] + fillers

    import random as _random

    _random.seed(123)
    tally = Counter()
    for _ in range(2000):
        picked = _pick_reply_target(pool, char)
        tally[picked['id']] += 1

    # fresh+top+0 should dominate old+nested+saturated by a wide margin
    assert tally[1] > tally[2] * 20

Youez - 2016 - github.com/yon3zu
LinuXploit