Advertisement

← refacto-feature-scout

tests/test_scan_digest.py

4.7 KB Download file
import json
from datetime import date
from pathlib import Path

from scout.claude_runner import ClaudeResult, extract_json
from scout.projects import Project
from scout.reddit import parse_atom, render
from scout.scan import build_prompt, is_duplicate, parse_ideas, run_scan

IDEAS = [
    {"title": "Whale trade feed", "summary": "Live list of trades over $10k.", "user_value": "Traders ask for it.",
     "evidence": [{"url": "https://example.com/a", "note": "thread asking for whale feed"}], "effort": "S",
     "category": "feature", "spec": "Poll trades; filter notional > 10k."},
    {"title": "No evidence idea", "summary": "Something", "evidence": []},
    {"title": "Weird effort", "summary": "x", "evidence": ["https://example.com/b"], "effort": "huge", "category": "nope"},
]


def test_parse_ideas_filters_and_normalises():
    text = "Here you go:\n```json\n" + json.dumps(IDEAS) + "\n```"
    ideas = parse_ideas(text)
    assert [i["title"] for i in ideas] == ["Whale trade feed", "Weird effort"]
    assert ideas[1]["effort"] == "M" and ideas[1]["category"] == "feature"
    assert ideas[1]["evidence"] == [{"url": "https://example.com/b", "note": ""}]


def test_extract_json_bare_and_wrapped():
    assert extract_json('[{"a":1}]') == [{"a": 1}]
    assert extract_json('Sure. {"status":"done"} thanks') == {"status": "done"}


def test_duplicate_detection():
    assert is_duplicate("Whale trade feed", ["Live whale trade feed"])
    assert not is_duplicate("Brier score tracker", ["Whale trade feed"])


def fake_project(tmp_path: Path) -> Project:
    (tmp_path / "README.md").write_text("# Fake app\nHas a watchlist.")
    return Project(key="terminal", name="PolyTerm", areas=["Polymarket"], folder=tmp_path, url=None,
                   what_it_is="fake", context_files=["README.md"],
                   sources={"urls": ["https://board"], "reddit": ["https://feed"]})


def test_run_scan_stores_new_ideas_and_skips_repeats(store, tmp_path):
    p = fake_project(tmp_path)
    calls = {}

    def runner(prompt, **kw):
        calls["prompt"] = prompt
        return ClaudeResult(ok=True, text=json.dumps(IDEAS), cost_usd=0.5)

    r = run_scan(p, store, runner=runner, fetch=lambda url: "<html><body>Board: users want alerts</body></html>",
                 reddit_posts=[{"title": "Give me alerts", "url": "https://reddit.com/r/x/1", "text": "please"}])
    assert r["ok"] and r["added"] == 2 and r["found"] == 2
    assert "Has a watchlist" in calls["prompt"] and "users want alerts" in calls["prompt"]
    assert "Give me alerts" in calls["prompt"]
    # second day: same ideas come back → all duplicates, nothing added
    r2 = run_scan(p, store, runner=runner, fetch=lambda url: "", reddit_posts=[])
    assert r2["added"] == 0 and r2["duplicates"] == 2
    assert "Whale trade feed [proposed]" in build_prompt(p, "", "", "\n".join(f"- {t} [{s}]" for t, s in store.all_titles("terminal")))


def test_run_scan_reports_failure_not_silence(store, tmp_path):
    p = fake_project(tmp_path)
    r = run_scan(p, store, runner=lambda *a, **k: ClaudeResult(ok=False, error="boom"), fetch=lambda u: "", reddit_posts=[])
    assert r["ok"] is False and r["found"] == 0 and "boom" in r["error"]


ATOM = """<?xml version="1.0"?><feed xmlns="http://www.w3.org/2005/Atom">
<entry><author><name>/u/bob</name></author><title>Need a whale tracker</title>
<link href="https://www.reddit.com/r/Polymarket/comments/abc/x/"/>
<published>2099-01-01T00:00:00+00:00</published>
<content type="html">&lt;div&gt;Anyone know a tool? submitted by /u/bob [link] [comments]&lt;/div&gt;</content></entry></feed>"""


def test_reddit_parse_and_render():
    posts = parse_atom(ATOM)
    assert posts[0]["title"] == "Need a whale tracker" and posts[0]["author"] == "/u/bob"
    assert posts[0]["text"] == "Anyone know a tool?"
    assert "Need a whale tracker (https://www.reddit.com/r/Polymarket/comments/abc/x/)" in render(posts)


# The old check here read the operator's own folders off this disk, so it could only pass on
# his Mac. What it was really guarding — that the live project's settings are what we think
# they are — is now in tests/test_frozen_terminal.py, which needs no files at all.


def test_reddit_collect_retries_once_on_429():
    import urllib.error
    from scout import reddit as rd
    calls = []

    def fetch(url):
        calls.append(url)
        if len(calls) == 1:
            raise urllib.error.HTTPError(url, 429, "Too Many", {}, None)
        return ATOM

    posts, problems = rd.collect(["https://a"], fetch=fetch, sleep=lambda s: None)
    assert len(posts) == 1 and problems == [] and calls == ["https://a", "https://a"]