Files
wehub-resource-sync 2860fb5d18
Security / Dependency review (push) Has been skipped
Scorecard / Scorecard analysis (push) Failing after 0s
Validate / eval (push) Failing after 0s
Security / Dependency audit (push) Failing after 1s
Security / Secret scan (push) Failing after 1s
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

408 lines
17 KiB
Python

"""Tests for the Techmeme source adapter (lib/techmeme.py).
Covers the --json (not --agent) surface choice, header-row filtering,
field mapping, date windowing to the research range, old-binary prose
tolerance, and graceful degradation.
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
import pytest
from lib import techmeme
class _FakeProc:
returncode = 0
stdout = "{}"
stderr = ""
def __init__(self, stdout: str = "{}", returncode: int = 0, stderr: str = ""):
self.stdout = stdout
self.returncode = returncode
self.stderr = stderr
# ---- surface choice ----
def test_search_args_use_json_not_agent():
args = techmeme._build_search_args("AI agents")
assert "--json" in args
# --agent implies --compact, which blanked records pre-PR-1383.
assert "--agent" not in args
assert "--compact" not in args
# Techmeme `search` has no result-limit flag; --max-results breaks it.
assert "--max-results" not in args
assert "search" in args and "AI agents" in args
def test_search_invokes_only_search_no_sync(monkeypatch):
"""search_techmeme must issue exactly one subprocess call -- the search --
and never a `sync` (search hits Techmeme's live archive, not the cache)."""
calls = []
def fake_run(cmd, timeout):
calls.append(list(cmd))
return _FakeProc(stdout=json.dumps([
{"num": 1, "source": "a.com",
"headline": "A real in-window headline about the topic",
"link": "https://t.co/a", "date": "2026-06-15"},
]))
monkeypatch.setattr(techmeme, "_is_available", lambda: True)
monkeypatch.setattr(techmeme.subproc, "run_with_timeout", fake_run)
out = techmeme.search_techmeme("topic", "2026-06-01", "2026-06-27")
assert len(out["results"]) == 1
assert calls == [[techmeme.CLI_BIN, "search", "topic", "--json"]]
assert not any("sync" in c for c in calls)
# ---- date windowing ----
def _search_with_records(monkeypatch, records, from_date="2026-06-01", to_date="2026-06-27"):
monkeypatch.setattr(techmeme, "_is_available", lambda: True)
monkeypatch.setattr(
techmeme.subproc, "run_with_timeout",
lambda cmd, timeout: _FakeProc(stdout=json.dumps(records)),
)
return techmeme.search_techmeme("topic", from_date, to_date)
def test_in_window_record_kept_with_real_date(monkeypatch):
out = _search_with_records(monkeypatch, [
{"num": 1, "source": "a.com",
"headline": "Fresh in-window story about the research topic",
"link": "https://t.co/a", "date": "2026-06-15"},
])
assert len(out["results"]) == 1
items = techmeme.parse_techmeme_response(out, query="topic")
assert len(items) == 1
# The item carries the record's real date, never today's.
assert items[0]["date"] == "2026-06-15"
def test_out_of_window_record_dropped(monkeypatch):
out = _search_with_records(monkeypatch, [
{"num": 1, "source": "a.com",
"headline": "Stale Parler acquisition story from years ago",
"link": "https://t.co/old", "date": "2022-12-02"},
{"num": 2, "source": "b.com",
"headline": "Fresh in-window story about the research topic",
"link": "https://t.co/new", "date": "2026-06-15"},
])
links = [r["link"] for r in out["results"]]
assert links == ["https://t.co/new"]
def test_zero_in_window_records_means_zero_results(monkeypatch):
"""No keep-all fallback: staleness is the bug, so all-stale means empty."""
out = _search_with_records(monkeypatch, [
{"num": 1, "source": "a.com",
"headline": "Stale story number one from the archive",
"link": "https://t.co/1", "date": "2022-12-02"},
{"num": 2, "source": "b.com",
"headline": "Stale story number two from the archive",
"link": "https://t.co/2", "date": "2023-08-24"},
])
assert out["results"] == []
def test_window_endpoints_inclusive(monkeypatch):
"""Records dated exactly from_date or to_date survive the window; a
strict-< regression would silently shave both edges."""
out = _search_with_records(monkeypatch, [
{"num": 1, "source": "a.com",
"headline": "Story published exactly on the window start date",
"link": "https://t.co/start", "date": "2026-06-01"},
{"num": 2, "source": "b.com",
"headline": "Story published exactly on the window end date",
"link": "https://t.co/end", "date": "2026-06-27"},
])
assert [r["link"] for r in out["results"]] == ["https://t.co/start", "https://t.co/end"]
def test_non_string_and_whitespace_dates(monkeypatch):
"""Non-string date values (int/null) are treated as undated and kept;
a valid ISO date with surrounding whitespace still parses."""
out = _search_with_records(monkeypatch, [
{"num": 1, "source": "a.com",
"headline": "Record carrying an integer where the date belongs",
"link": "https://t.co/int", "date": 20260615},
{"num": 2, "source": "b.com",
"headline": "Record carrying an explicit null date value",
"link": "https://t.co/null", "date": None},
{"num": 3, "source": "c.com",
"headline": "Record with whitespace padded valid ISO date",
"link": "https://t.co/ws", "date": " 2026-06-15 "},
])
assert len(out["results"]) == 3
items = techmeme.parse_techmeme_response(out, query="record")
by_url = {it["url"]: it["date"] for it in items}
assert by_url["https://t.co/int"] is None
assert by_url["https://t.co/null"] is None
assert by_url["https://t.co/ws"] == "2026-06-15"
def test_dropped_records_logged(monkeypatch):
logs = []
monkeypatch.setattr(techmeme, "_is_available", lambda: True)
monkeypatch.setattr(techmeme, "_log", lambda msg: logs.append(msg))
monkeypatch.setattr(
techmeme.subproc, "run_with_timeout",
lambda cmd, timeout: _FakeProc(stdout=json.dumps([
{"num": 1, "source": "a.com",
"headline": "Stale story from the deep archive years back",
"link": "https://t.co/old", "date": "2022-12-02"},
])),
)
techmeme.search_techmeme("topic", "2026-06-01", "2026-06-27")
assert "dropped 1 records outside 2026-06-01..2026-06-27" in logs
def test_undated_records_kept_and_never_stamped_today(monkeypatch):
"""Old binaries emit no date key; unparseable dates come through as "" or
junk. All are kept by the window, and parse yields date None -- never
today's date."""
out = _search_with_records(monkeypatch, [
{"num": 1, "source": "a.com",
"headline": "Headline with no date key from an old binary",
"link": "https://t.co/nodate"},
{"num": 2, "source": "b.com",
"headline": "Headline whose date was unparseable upstream",
"link": "https://t.co/empty", "date": ""},
{"num": 3, "source": "c.com",
"headline": "Headline carrying non ISO junk in the date",
"link": "https://t.co/junk", "date": "May 22, 2025"},
])
assert len(out["results"]) == 3
items = techmeme.parse_techmeme_response(out, query="headline")
assert len(items) == 3
today = datetime.now(timezone.utc).date().isoformat()
for item in items:
assert item["date"] is None
assert item["date"] != today
# ---- old-binary prose tolerance ----
def test_no_results_prose_parses_as_empty_without_error(monkeypatch):
"""Old binaries print `No results for "q"` prose to stdout with exit 0 in
JSON mode. That is a zero-hit response, not a decode failure."""
logs = []
monkeypatch.setattr(techmeme, "_is_available", lambda: True)
monkeypatch.setattr(techmeme, "_log", lambda msg: logs.append(msg))
monkeypatch.setattr(
techmeme.subproc, "run_with_timeout",
lambda cmd, timeout: _FakeProc(stdout='No results for "whatever query"\n'),
)
resp = techmeme._run_cli([techmeme.CLI_BIN, "search", "whatever query", "--json"],
timeout=techmeme.SEARCH_TIMEOUT)
assert resp == {"results": []}
assert "error" not in resp
assert not any("decode" in m.lower() for m in logs)
def test_malformed_stdout_still_yields_decode_error(monkeypatch):
"""Genuinely malformed non-JSON stdout (not the prose sentinel) keeps the
existing decode-error envelope."""
logs = []
monkeypatch.setattr(techmeme, "_is_available", lambda: True)
monkeypatch.setattr(techmeme, "_log", lambda msg: logs.append(msg))
monkeypatch.setattr(
techmeme.subproc, "run_with_timeout",
lambda cmd, timeout: _FakeProc(stdout="garbage <<<"),
)
resp = techmeme._run_cli([techmeme.CLI_BIN, "search", "topic", "--json"],
timeout=techmeme.SEARCH_TIMEOUT)
assert resp["results"] == []
assert "json decode" in resp.get("error", "")
assert any("decode" in m.lower() for m in logs)
# ---- header-row filtering ----
def test_story_headline_accepts_sentence():
assert techmeme._is_story_headline("OpenAI ships a new coding agent today", "techcrunch.com")
def test_story_headline_rejects_publication_name_rows():
# Short, publication-name-only rows are section headers, not stories.
assert not techmeme._is_story_headline("TechCrunch", "techcrunch.com")
assert not techmeme._is_story_headline("New York Times", "nytimes.com")
def test_parse_drops_header_rows_keeps_stories():
resp = {
"results": [
{"num": 1, "source": "techcrunch.com", "headline": "TechCrunch",
"link": "http://techcrunch.com/", "date": "2026-06-27"},
{"num": 2, "source": "techcrunch.com",
"headline": "Sakana AI's Fugu claims to rival frontier models",
"link": "https://www.techmeme.com/260627/p2", "date": "2026-06-27"},
]
}
items = techmeme.parse_techmeme_response(resp, query="AI")
assert len(items) == 1
assert items[0]["title"].startswith("Sakana AI")
assert items[0]["url"] == "https://www.techmeme.com/260627/p2"
assert items[0]["source_name"] == "techcrunch.com"
assert items[0]["date"] == "2026-06-27"
def test_parse_drops_records_without_link():
resp = {"results": [{"num": 1, "source": "x.com", "headline": "A real headline sentence here",
"link": "", "date": "2026-06-27"}]}
assert techmeme.parse_techmeme_response(resp, query="x") == []
# ---- relevance ranking ----
def test_more_relevant_headline_ranks_higher():
resp = {
"results": [
{"num": 1, "source": "a.com", "headline": "Unrelated quarterly earnings report released today",
"link": "https://t.co/a", "date": "2026-06-20"},
{"num": 2, "source": "b.com", "headline": "New AI agent framework launches for developers",
"link": "https://t.co/b", "date": "2026-06-20"},
]
}
items = techmeme.parse_techmeme_response(resp, query="AI agent framework")
by_url = {it["url"]: it["relevance"] for it in items}
assert by_url["https://t.co/b"] > by_url["https://t.co/a"]
# ---- envelope tolerance ----
def test_coerce_list_handles_bare_array_and_wrapped():
assert techmeme._coerce_list([{"a": 1}]) == [{"a": 1}]
assert techmeme._coerce_list({"results": [{"a": 1}]}) == [{"a": 1}]
assert techmeme._coerce_list({"nope": 1}) == []
# ---- depth cap + degradation ----
@pytest.mark.parametrize("depth,cap", [("quick", 8), ("default", 16), ("deep", 30)])
def test_depth_cap_truncates_client_side(monkeypatch, depth, cap):
"""Techmeme `search` has no limit flag, so the depth cap is applied after
windowing -- regression guard for the other half of the --max-results fix.
All records are in-window, so the cap alone decides the count."""
records = [
{"num": i, "source": "x.com", "headline": f"A real headline sentence number {i}",
"link": f"https://t.co/{i}", "date": "2026-06-15"}
for i in range(cap + 12)
]
monkeypatch.setattr(techmeme, "_is_available", lambda: True)
monkeypatch.setattr(techmeme, "_run_cli", lambda cmd, timeout: {"results": list(records)})
out = techmeme.search_techmeme("topic", "2026-06-01", "2026-06-27", depth=depth)
assert len(out["results"]) == cap
def test_depth_cap_applies_after_windowing(monkeypatch):
"""Stale records must not consume cap slots: with cap in-window records
plus stale ones interleaved ahead of them, every in-window record
survives."""
cap = techmeme.DEPTH_CONFIG["quick"]
stale = [
{"num": i, "source": "old.com", "headline": f"Stale archive headline number {i}",
"link": f"https://t.co/old{i}", "date": "2022-01-10"}
for i in range(cap)
]
fresh = [
{"num": 100 + i, "source": "new.com", "headline": f"Fresh in window headline number {i}",
"link": f"https://t.co/new{i}", "date": "2026-06-15"}
for i in range(cap)
]
monkeypatch.setattr(techmeme, "_is_available", lambda: True)
monkeypatch.setattr(techmeme, "_run_cli",
lambda cmd, timeout: {"results": stale + fresh})
out = techmeme.search_techmeme("topic", "2026-06-01", "2026-06-27", depth="quick")
links = [r["link"] for r in out["results"]]
assert links == [f"https://t.co/new{i}" for i in range(cap)]
def test_dated_records_take_cap_slots_before_undated(monkeypatch):
"""Undated archive hits must never evict confirmed in-window stories:
dated in-window records fill cap slots first, undated fill the rest."""
cap = techmeme.DEPTH_CONFIG["quick"]
undated = [
{"num": i, "source": "old.com", "headline": f"Undated old binary headline number {i}",
"link": f"https://t.co/u{i}"}
for i in range(cap)
]
dated = [
{"num": 100 + i, "source": "new.com", "headline": f"Dated in window headline number {i}",
"link": f"https://t.co/d{i}", "date": "2026-06-15"}
for i in range(cap)
]
monkeypatch.setattr(techmeme, "_is_available", lambda: True)
# Undated arrive first in CLI order; dated in-window must still win the cap.
monkeypatch.setattr(techmeme, "_run_cli",
lambda cmd, timeout: {"results": undated + dated})
out = techmeme.search_techmeme("topic", "2026-06-01", "2026-06-27", depth="quick")
links = [r["link"] for r in out["results"]]
assert links == [f"https://t.co/d{i}" for i in range(cap)]
def test_all_undated_flood_capped_with_windowing_inactive_hint(monkeypatch):
"""The realistic old-binary case: every record undated. The cap still
applies, and a windowing-inactive hint is logged."""
cap = techmeme.DEPTH_CONFIG["quick"]
logs = []
records = [
{"num": i, "source": "x.com", "headline": f"Undated archive headline number {i}",
"link": f"https://t.co/{i}"}
for i in range(cap + 12)
]
monkeypatch.setattr(techmeme, "_is_available", lambda: True)
monkeypatch.setattr(techmeme, "_log", lambda msg: logs.append(msg))
monkeypatch.setattr(techmeme, "_run_cli", lambda cmd, timeout: {"results": records})
out = techmeme.search_techmeme("topic", "2026-06-01", "2026-06-27", depth="quick")
assert len(out["results"]) == cap
assert any("date windowing inactive" in m for m in logs)
def test_windowing_inactive_hint_absent_when_dates_present(monkeypatch):
"""The hint must not fire when the binary emits dates -- even if every
dated record is out of window (windowing IS active there)."""
logs = []
monkeypatch.setattr(techmeme, "_is_available", lambda: True)
monkeypatch.setattr(techmeme, "_log", lambda msg: logs.append(msg))
monkeypatch.setattr(techmeme, "_run_cli", lambda cmd, timeout: {"results": [
{"num": 1, "source": "a.com", "headline": "Stale but properly dated archive story",
"link": "https://t.co/old", "date": "2022-12-02"},
{"num": 2, "source": "b.com", "headline": "Undated companion record from mixed output",
"link": "https://t.co/mixed"},
]})
techmeme.search_techmeme("topic", "2026-06-01", "2026-06-27")
assert not any("date windowing inactive" in m for m in logs)
@pytest.mark.parametrize("words,expected", [(3, False), (4, True)])
def test_story_headline_word_count_boundary(words, expected):
headline = " ".join(["word"] * words)
assert techmeme._is_story_headline(headline, "x.com") is expected
def test_story_headline_rejects_when_equal_to_source():
# A >=4-word headline that exactly equals its source is still a header row.
assert not techmeme._is_story_headline("the daily example tribune", "the daily example tribune")
def test_binary_absent_returns_empty(monkeypatch):
monkeypatch.setattr(techmeme.shutil, "which", lambda _bin: None)
resp = techmeme.search_techmeme("anything", "2026-06-01", "2026-06-27")
assert resp["results"] == []
assert "error" in resp
def test_empty_topic_returns_empty():
assert techmeme.search_techmeme(" ", "2026-06-01", "2026-06-27") == {"results": []}
def test_parse_handles_non_list_results():
assert techmeme.parse_techmeme_response({"results": "oops"}, query="x") == []
assert techmeme.parse_techmeme_response({}, query="x") == []