项目文件夹

文件
wehub-resource-sync 2860fb5d18
Security / Dependency review (push) Has been skipped
Scorecard / Scorecard analysis (push) Failing after 0s
Security / Dependency audit (push) Failing after 1s
Security / Secret scan (push) Failing after 1s
Validate / eval (push) Failing after 0s
Validate / tests (push) Failing after 0s
Validate / mcp-tests (push) Failing after 1s
GitHub Actions Security Analysis with zizmor 🌈 / zizmor (push) Failing after 1s
Security / SAST scan (push) Failing after 13m52s
chore: import upstream snapshot with attribution
2026-07-13 12:05:33 +08:00

663 行
22 KiB
Python

import json
import os
import subprocess
import sys
from pathlib import Path
from unittest import mock
import pytest
import last30days as cli
from lib import pipeline, planner, reddit_listing, render, rerank, schema
REPO_ROOT = Path(__file__).resolve().parents[1]
def _item(
item_id: str,
source: str,
title: str,
*,
published_at: str = "2026-07-09",
engagement: dict[str, int | float] | None = None,
) -> schema.SourceItem:
return schema.SourceItem(
item_id=item_id,
source=source,
title=title,
body=title,
url=f"https://{source}.example/{item_id}",
published_at=published_at,
engagement=engagement or {},
snippet=f"Evidence about {title}",
)
def _candidate(item: schema.SourceItem) -> schema.Candidate:
return schema.Candidate(
candidate_id=f"candidate-{item.item_id}",
item_id=item.item_id,
source=item.source,
title=item.title,
url=item.url,
snippet=item.snippet,
subquery_labels=["discovery-listings"],
native_ranks={f"discovery-listings:{item.source}": 1},
local_relevance=0.9,
freshness=95,
engagement=100,
source_quality=0.8,
rrf_score=0.1,
sources=[item.source],
source_items=[item],
final_score=80,
)
def test_discovery_plan_reuses_category_peer_mapping():
plan = planner.build_discovery_plan(
"AI agents",
available_sources=["reddit", "hackernews"],
)
assert plan.category == "ai_agent_framework"
assert plan.subreddits == ["LangChain", "LocalLLaMA", "AI_Agents", "MachineLearning"]
assert plan.sources == ["reddit", "hackernews"]
def test_discovery_plan_keeps_keyless_reddit_for_unknown_domains():
plan = planner.build_discovery_plan(
"urban gardening",
available_sources=["reddit", "hackernews"],
)
assert plan.category is None
assert plan.subreddits == ["all"]
assert plan.sources == ["reddit", "hackernews"]
def test_uncategorized_discovery_uses_parseable_r_all_listing_paths():
card = (
'<shreddit-post permalink="/r/gardening/comments/abc123/urban_garden/" '
'post-title="Urban gardening is taking off" score="42" comment-count="7" '
'author="gardener" subreddit-name="gardening" '
'created-timestamp="2026-07-09T12:00:00+00:00">'
)
requested_urls: list[str] = []
def fake_get(url, **_kwargs):
requested_urls.append(url)
return card
with mock.patch.object(reddit_listing.http, "reddit_keyless_get_text", side_effect=fake_get):
result = reddit_listing.fetch_discovery_listings(
["all"], query="urban gardening",
)
assert len(result["items"]) == 1
assert any("/r/all/rising/" in url for url in requested_urls)
assert any("/r/all/top/?t=week" in url for url in requested_urls)
assert all("name=all" not in url for url in requested_urls)
def test_velocity_scoring_favors_a_recent_spike_over_static_bigness():
recent = _item(
"recent",
"reddit",
"Recent spike",
published_at="2026-07-09",
engagement={"score": 100, "num_comments": 10},
)
old = _item(
"old",
"reddit",
"Older large thread",
published_at="2026-06-20",
engagement={"score": 300, "num_comments": 10},
)
assert rerank.engagement_velocity_score(recent, as_of_date="2026-07-10") > (
rerank.engagement_velocity_score(old, as_of_date="2026-07-10")
)
def test_domain_filter_ignores_generic_ai_only_matches():
assert pipeline._matches_discovery_domain(
"AI agents", "An AI agent bankrupted its operator"
)
assert not pipeline._matches_discovery_domain(
"AI agents", "Global dialogue on AI governance"
)
@pytest.mark.parametrize(
("domain", "listing_title"),
[
("城市园艺", "城市园艺技巧与社区花园"),
("גינון עירוני", "מדריך חדש לגינון עירוני"),
],
)
def test_domain_filter_tokenizes_non_latin_domains(domain, listing_title):
assert pipeline._matches_discovery_domain(domain, listing_title)
def test_x_velocity_excludes_views_and_bookmarks():
xquik_item = _item(
"xquik",
"x",
"X backend reach",
engagement={
"likes": 10,
"reposts": 3,
"replies": 2,
"quotes": 1,
"views": 100_000,
"bookmarks": 5_000,
},
)
standard_item = _item(
"standard",
"x",
"X backend interactions",
engagement={"likes": 10, "reposts": 3, "replies": 2, "quotes": 1},
)
assert rerank.discovery_engagement_total(xquik_item) == 16
assert rerank.engagement_velocity_score(
xquik_item, as_of_date="2026-07-10"
) == rerank.engagement_velocity_score(standard_item, as_of_date="2026-07-10")
def test_discovery_topic_name_uses_entities_shared_across_sources():
reddit = _candidate(_item("r1", "reddit", "OpenAI Agent SDK launch details"))
hn = _candidate(_item("h1", "hackernews", "OpenAI Agent SDK reaches developers"))
candidates = {reddit.candidate_id: reddit, hn.candidate_id: hn}
cluster = schema.Cluster(
cluster_id="cluster-1",
title=reddit.title,
candidate_ids=list(candidates),
representative_ids=[reddit.candidate_id],
sources=["hackernews", "reddit"],
score=80,
)
assert pipeline.discovery_topic_name(cluster, candidates, "AI agents") == "OpenAI Agent SDK"
def test_discovery_renderer_snapshot():
report = schema.DiscoveryReport(
domain="AI agents",
range_from="2026-06-10",
range_to="2026-07-10",
generated_at="2026-07-10T00:00:00+00:00",
plan=schema.DiscoveryPlan(
domain="AI agents",
category="ai_agent_framework",
subreddits=["AI_Agents"],
sources=["reddit", "hackernews"],
),
topics=[schema.DiscoveryTopic(
rank=1,
name="Agent memory protocols",
why_spiking="Two independent listing items accelerated this week.",
momentum="new-this-week",
velocity_score=123.45,
sources=["hackernews", "reddit"],
engagement_by_source={
"reddit": {"score": 120, "num_comments": 30},
"hackernews": {"points": 80},
},
command='/last30days "Agent memory protocols"',
)],
)
with mock.patch.object(render, "_render_badge", return_value=["BADGE", ""]):
rendered = render.render_discovery(report)
assert rendered == (
"BADGE\n\n"
"# Trending discovery: AI agents\n\n"
"Window: 2026-06-10 to 2026-07-10\n"
"Feeds: reddit, hackernews\n"
"Communities: r/AI_Agents\n\n"
"## 1. Agent memory protocols\n\n"
"**Momentum:** New this week · velocity 123.45\n\n"
"Two independent listing items accelerated this week.\n\n"
"**Evidence:** Reddit: score 120, num comments 30 · Hacker News: points 80\n\n"
"**Research next:** `/last30days \"Agent memory protocols\"`\n"
)
def test_keyless_discovery_degrades_without_digg():
def fake_fetch(source, plan, *, from_date, to_date, depth, mock, config):
return pipeline._mock_discovery_items(source, plan.domain, to_date), None
with mock.patch.object(pipeline, "available_sources", return_value=["reddit", "hackernews"]), \
mock.patch.object(pipeline, "_fetch_discovery_source", side_effect=fake_fetch):
report = pipeline.run_discover(
domain="AI agents",
config={},
as_of_date="2026-07-10",
)
assert 5 <= len(report.topics) <= 10
assert report.source_status["reddit"].state == "ok"
assert report.source_status["hackernews"].state == "ok"
assert report.source_status["digg"].state == "skipped-unconfigured"
assert report.source_status["x"].state == "skipped-unconfigured"
assert all(topic.command.startswith('/last30days "') for topic in report.topics)
def test_discovery_drops_zero_velocity_clusters():
raw_item = {
"id": "zero-engagement",
"text": "AI agent launch with no interactions",
"url": "https://x.com/example/status/1",
"author_handle": "example",
"date": "2026-07-09",
"engagement": {"likes": 0, "reposts": 0, "replies": 0, "quotes": 0},
"relevance": 0.9,
}
with mock.patch.object(pipeline, "available_sources", return_value=["x"]), \
mock.patch.object(pipeline, "_fetch_discovery_source", return_value=([raw_item], None)):
report = pipeline.run_discover(
domain="AI agents",
config={},
as_of_date="2026-07-10",
)
assert report.topics == []
assert "Fewer than five topic clusters survived this domain sweep." in report.warnings
def test_explicit_unavailable_discovery_source_does_not_widen_to_other_sources():
with mock.patch.object(pipeline, "available_sources", return_value=[]), \
mock.patch.object(pipeline, "_fetch_discovery_source") as fetch:
with pytest.raises(ValueError, match="No listing sources are available"):
pipeline.run_discover(
domain="AI agents",
config={},
requested_sources=["digg"],
as_of_date="2026-07-10",
)
fetch.assert_not_called()
def test_discovery_reads_browser_credentials_and_does_not_schedule_pending_x():
parser = cli.build_parser()
args, extra = parser.parse_known_args(["--discover", "AI agents"])
assert cli._config_policy_for_args(args, "", extra).browser_cookies == "read"
no_cookies_args, extra = parser.parse_known_args(
["--no-browser-cookies", "--discover", "AI agents"]
)
assert cli._config_policy_for_args(no_cookies_args, "", extra).browser_cookies == "off"
fetched_sources: list[str] = []
def fake_available_sources(config, requested_sources, *, x_pending=None, local_only=False):
assert x_pending is False
return ["reddit", "hackernews"] + (["x"] if x_pending is not False else [])
def fake_fetch(source, plan, *, from_date, to_date, depth, mock, config):
fetched_sources.append(source)
return pipeline._mock_discovery_items(source, plan.domain, to_date), None
with mock.patch.object(pipeline, "available_sources", side_effect=fake_available_sources), \
mock.patch.object(pipeline, "_fetch_discovery_source", side_effect=fake_fetch):
report = pipeline.run_discover(
domain="AI agents",
config={"FROM_BROWSER": "firefox", "_BROWSER_COOKIE_MODE": "plan_only"},
as_of_date="2026-07-10",
)
assert "x" not in fetched_sources
assert report.source_status["x"].state == "skipped-unconfigured"
def test_authenticated_x_discovery_uses_available_backend():
plan = planner.build_discovery_plan(
"AI agents",
available_sources=["x"],
)
raw = pipeline._mock_discovery_items("x", plan.domain, "2026-07-10")
with mock.patch.object(pipeline.env, "x_backend_chain", return_value=["bird"]), \
mock.patch.object(pipeline, "_fetch_x_backend", return_value=(raw, "")) as fetch:
items, error = pipeline._fetch_discovery_source(
"x",
plan,
from_date="2026-06-10",
to_date="2026-07-10",
depth="default",
mock=False,
config={"AUTH_TOKEN": "dummy", "CT0": "dummy"},
)
assert error is None
assert len(items) == 6
fetch.assert_called_once()
def test_listing_failure_is_not_reported_as_clean_no_results():
def fake_fetch(source, plan, *, from_date, to_date, depth, mock, config):
if source == "reddit":
return [], "connection timed out"
return pipeline._mock_discovery_items(source, plan.domain, to_date), None
with mock.patch.object(pipeline, "available_sources", return_value=["reddit", "hackernews"]), \
mock.patch.object(pipeline, "_fetch_discovery_source", side_effect=fake_fetch):
report = pipeline.run_discover(
domain="AI agents",
config={},
as_of_date="2026-07-10",
)
assert report.source_status["reddit"].state == "timeout"
assert report.source_status["reddit"].detail == "connection timed out"
def test_reddit_discovery_adapter_preserves_partial_feed_errors():
item = {
"url": "https://reddit.com/r/example/comments/1",
"title": "AI agent launch",
}
with mock.patch.object(
reddit_listing,
"_fetch_one_with_status",
side_effect=[([], "rising timed out"), ([item], None)],
):
result = reddit_listing.fetch_discovery_listings(
["AI_Agents"], query="AI agents",
)
assert result["items"] == [item]
assert result["errors"] == ["r/AI_Agents rising: rising timed out"]
def test_discovery_cli_json_contract_and_mutual_exclusion():
result = subprocess.run(
[
sys.executable,
"skills/last30days/scripts/last30days.py",
"--discover",
"AI agents",
"--mock",
"--emit=json",
],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr
payload = json.loads(result.stdout)
assert payload["schema_version"] == "1.0"
assert payload["kind"] == "discovery"
assert 5 <= len(payload["results"]) <= 10
assert payload["results"][0]["command"].startswith('/last30days "')
invalid = subprocess.run(
[
sys.executable,
"skills/last30days/scripts/last30days.py",
"topic",
"--discover",
"AI agents",
"--mock",
],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
assert invalid.returncode == 2
assert "cannot be combined with a positional topic" in invalid.stderr
drill_conflict = subprocess.run(
[
sys.executable,
"skills/last30days/scripts/last30days.py",
"--discover",
"AI agents",
"--drill",
"1",
"--mock",
],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
assert drill_conflict.returncode == 2
assert "mutually exclusive" in drill_conflict.stderr
def test_discovery_cli_rejects_historical_as_of():
result = subprocess.run(
[
sys.executable,
"skills/last30days/scripts/last30days.py",
"--discover",
"AI agents",
"--as-of",
"2026-06-01",
"--mock",
],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 2
assert "--as-of cannot be used with --discover" in result.stderr
assert "current live listings" in result.stderr
def test_discovery_filters_incompatible_default_sources_but_rejects_explicit_only():
default_result = subprocess.run(
[
sys.executable,
"skills/last30days/scripts/last30days.py",
"--discover",
"AI agents",
"--mock",
"--emit=json",
],
cwd=REPO_ROOT,
env={**os.environ, "LAST30DAYS_DEFAULT_SEARCH": "reddit,x,youtube,hn"},
capture_output=True,
text=True,
check=False,
)
assert default_result.returncode == 0, default_result.stderr
explicit_result = subprocess.run(
[
sys.executable,
"skills/last30days/scripts/last30days.py",
"--discover",
"AI agents",
"--search=youtube",
"--mock",
],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
assert explicit_result.returncode == 2
assert "unsupported: youtube" in explicit_result.stderr
def test_detect_category_rejects_suffix_false_positives():
from lib import categories
assert categories.detect_category("Dubai agents") is None
assert categories.detect_category("Thai agents real estate") is None
assert categories.detect_category("AI agents") == "ai_agent_framework"
assert categories.detect_category("what's new in ai agent frameworks") == "ai_agent_framework"
def test_discovery_engagement_excludes_rank_metadata():
from lib import pipeline, schema
items = [
schema.SourceItem(
item_id=f"digg-{i}", source="digg", title="t", body="b",
url=f"https://di.gg/{i}", published_at="2026-07-05", snippet="s",
engagement={"postCount": 5, "rank": 100 * (i + 1), "rank_score": 0.5},
)
for i in range(3)
]
totals = pipeline._discovery_engagement(items)
assert totals["digg"]["postCount"] == 15
assert "rank" not in totals["digg"]
assert "rank_score" not in totals["digg"]
def test_domain_matching_preserves_non_plural_anchors():
from lib import pipeline
assert pipeline._matches_discovery_domain("AI bias", "Addressing AI bias in models")
assert pipeline._matches_discovery_domain("supply chain crisis", "The crisis deepens for chip supply")
# Plural matching still works both directions.
assert pipeline._matches_discovery_domain("AI agents", "The best AI agent stacks")
def test_x_fallback_success_is_clean(monkeypatch):
from lib import pipeline, env
calls = []
def fake_fetch(backend, subquery, from_date, to_date, depth, config):
calls.append(backend)
if backend == "bird":
return [], "cookie expired"
return [object()], None
monkeypatch.setattr(pipeline, "_fetch_x_backend", fake_fetch)
monkeypatch.setattr(env, "x_backend_chain", lambda config: ["bird", "xquik"])
plan = pipeline.schema.DiscoveryPlan(
domain="ai agents", category=None, subreddits=[], sources=["x"],
)
items, error = pipeline._fetch_discovery_source(
"x", plan,
from_date="2026-06-11", to_date="2026-07-11", depth="quick",
mock=False, config={},
)
assert error is None
assert len(items) == 1
assert calls == ["bird", "xquik"]
def _digg_envelope(*clusters: dict) -> dict:
return {"results": list(clusters)}
def _digg_cluster(cluster_id: str, title: str, tldr: str = "") -> dict:
return {
"clusterUrlId": cluster_id,
"title": title,
"tldr": tldr,
"rank": 5,
"postCount": 12,
"uniqueAuthors": 8,
}
def test_digg_discovery_drops_off_domain_clusters(monkeypatch):
"""Regression: a crypto sweep surfaced AI stories because the Digg
branch (an AI-only leaderboard feed) applied no domain filter."""
envelope = _digg_envelope(
_digg_cluster("c1", "Bitcoin crypto rally accelerates"),
_digg_cluster("c2", "OpenAI ships a new frontier model"),
)
monkeypatch.setattr(pipeline.digg, "search_digg", lambda *a, **k: envelope)
plan = schema.DiscoveryPlan(
domain="crypto", category=None, subreddits=[], sources=["digg"],
)
items, error = pipeline._fetch_discovery_source(
"digg", plan,
from_date="2026-06-11", to_date="2026-07-11", depth="quick",
mock=False, config={},
)
assert error is None
titles = [item["title"] for item in items]
assert titles == ["Bitcoin crypto rally accelerates"]
def test_digg_discovery_keeps_domain_matching_clusters(monkeypatch):
envelope = _digg_envelope(
_digg_cluster("c1", "AI agents reshape support desks"),
_digg_cluster("c2", "The best AI agent stacks compared"),
)
monkeypatch.setattr(pipeline.digg, "search_digg", lambda *a, **k: envelope)
plan = schema.DiscoveryPlan(
domain="AI agents", category=None, subreddits=[], sources=["digg"],
)
items, error = pipeline._fetch_discovery_source(
"digg", plan,
from_date="2026-06-11", to_date="2026-07-11", depth="quick",
mock=False, config={},
)
assert error is None
assert len(items) == 2
def test_digg_discovery_all_filtered_is_clean_no_results(monkeypatch):
envelope = _digg_envelope(
_digg_cluster("c1", "OpenAI ships a new frontier model"),
_digg_cluster("c2", "Anthropic updates its agent SDK"),
)
monkeypatch.setattr(pipeline.digg, "search_digg", lambda *a, **k: envelope)
plan = schema.DiscoveryPlan(
domain="crypto", category=None, subreddits=[], sources=["digg"],
)
items, error = pipeline._fetch_discovery_source(
"digg", plan,
from_date="2026-06-11", to_date="2026-07-11", depth="quick",
mock=False, config={},
)
assert error is None
assert items == []
def test_x_discovery_preserves_producing_backends_own_error(monkeypatch):
"""A backend that returns items plus its own error is a partial outcome;
only earlier failed-over backends' errors are observability-only."""
monkeypatch.setattr(
pipeline, "_fetch_x_backend",
lambda *a, **k: ([{"id": "x-1", "title": "t"}], "rate limited after page 1"),
)
monkeypatch.setattr(pipeline.env, "x_backend_chain", lambda config: ["bird"])
plan = schema.DiscoveryPlan(
domain="ai agents", category=None, subreddits=[], sources=["x"],
)
items, error = pipeline._fetch_discovery_source(
"x", plan,
from_date="2026-06-11", to_date="2026-07-11", depth="quick",
mock=False, config={},
)
assert len(items) == 1
assert error == "rate limited after page 1"
def test_discovery_exits_when_configured_sources_have_no_discovery_feed(monkeypatch, capsys):
"""A configured source boundary must hold: never silently widen a sweep
to feeds the user filtered out."""
monkeypatch.setattr(
cli.env, "get_config", lambda **_kwargs: {"LAST30DAYS_DEFAULT_SEARCH": "youtube"}
)
monkeypatch.setattr(sys, "argv", ["last30days.py", "--discover", "AI agents", "--mock"])
with mock.patch.object(pipeline, "run_discover") as run:
assert cli.main() == 2
run.assert_not_called()
err = capsys.readouterr().err
assert "no discovery-capable sources" in err
assert "reddit" in err