Merge pull request #416 from tmchow/worktree-inherited-discovering-pebble

fix(ci): run full pytest suite, repair 13 rotted tests
This commit is contained in:
Trevin Chow
2026-05-16 22:11:34 -07:00
committed by GitHub
5 changed files with 64 additions and 37 deletions
+3 -3
View File
@@ -10,7 +10,7 @@ permissions:
contents: read contents: read
jobs: jobs:
plugin-contract: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -22,5 +22,5 @@ jobs:
- name: Set up Python - name: Set up Python
run: uv python install 3.12 run: uv python install 3.12
- name: Run plugin contract tests - name: Run test suite
run: uv run pytest tests/test_plugin_contract.py tests/test_version_consistency.py run: uv run pytest
+22 -18
View File
@@ -14,7 +14,7 @@ import argparse
import json import json
import sqlite3 import sqlite3
import sys import sys
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@@ -519,7 +519,7 @@ def get_daily_cost(date: Optional[str] = None) -> float:
conn = _connect() conn = _connect()
try: try:
if not date: if not date:
date = datetime.now().strftime("%Y-%m-%d") date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
row = conn.execute( row = conn.execute(
"""SELECT COALESCE(SUM(token_cost), 0) as total """SELECT COALESCE(SUM(token_cost), 0) as total
FROM research_runs FROM research_runs
@@ -575,7 +575,7 @@ def get_stats() -> Dict[str, Any]:
topic_count = conn.execute("SELECT COUNT(*) FROM topics WHERE enabled = 1").fetchone()[0] topic_count = conn.execute("SELECT COUNT(*) FROM topics WHERE enabled = 1").fetchone()[0]
finding_count = conn.execute("SELECT COUNT(*) FROM findings").fetchone()[0] finding_count = conn.execute("SELECT COUNT(*) FROM findings").fetchone()[0]
week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") week_ago = (datetime.now(timezone.utc) - timedelta(days=7)).strftime("%Y-%m-%d")
runs_7d = conn.execute( runs_7d = conn.execute(
"SELECT COUNT(*) FROM research_runs WHERE run_date >= ?", (week_ago,) "SELECT COUNT(*) FROM research_runs WHERE run_date >= ?", (week_ago,)
).fetchone()[0] ).fetchone()[0]
@@ -621,7 +621,7 @@ def get_trending(days: int = 7) -> List[Dict[str, Any]]:
"""Get topics ranked by recent finding activity.""" """Get topics ranked by recent finding activity."""
conn = _connect() conn = _connect()
try: try:
since = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") since = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
rows = conn.execute( rows = conn.execute(
"""SELECT t.name, t.id, """SELECT t.name, t.id,
COUNT(f.id) as new_findings, COUNT(f.id) as new_findings,
@@ -673,27 +673,31 @@ def findings_from_report(
limit: Optional[int] = None, limit: Optional[int] = None,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Convert report into persisted findings. """Convert report into persisted findings.
Uses ranked candidates (post-rerank) when available for quality scores and explanations. Uses ranked candidates (post-rerank) when available for quality scores and explanations.
Supplements with raw items from items_by_source for HN/PM that didn't rank highly Supplements with raw items from items_by_source for HN/PM that didn't rank highly
but are valuable for watchlist persistence. but are valuable for watchlist persistence. When ranked_candidates is empty
(degraded path — rerank failed or was skipped), falls back to supplementing
all sources from items_by_source so findings aren't silently dropped.
""" """
findings = [] findings = []
seen_urls = set() seen_urls = set()
# Phase 1: Process ranked candidates (high-quality data with explanations and corroboration)
for candidate in report.ranked_candidates: for candidate in report.ranked_candidates:
finding = finding_from_candidate(candidate) findings.append(finding_from_candidate(candidate))
findings.append(finding)
seen_urls.add(candidate.url) seen_urls.add(candidate.url)
# Phase 2: Add HN/PM items not already captured in ranked candidates supplement_sources = (
for source_name in ["hackernews", "polymarket"]: list(report.items_by_source)
if not report.ranked_candidates
else ["hackernews", "polymarket"]
)
for source_name in supplement_sources:
if source_name not in report.items_by_source: if source_name not in report.items_by_source:
continue continue
for item in report.items_by_source[source_name]: for item in report.items_by_source[source_name]:
if item.url in seen_urls: if item.url in seen_urls:
continue # Already captured with rich data continue
findings.append({ findings.append({
"source": source_name, "source": source_name,
"source_url": item.url, "source_url": item.url,
@@ -705,8 +709,7 @@ def findings_from_report(
"relevance_score": item.local_relevance or 0.5, "relevance_score": item.local_relevance or 0.5,
}) })
seen_urls.add(item.url) seen_urls.add(item.url)
# Apply global limit after collecting all findings (fix: was per-source, now global)
return findings[:limit] if limit is not None else findings return findings[:limit] if limit is not None else findings
@@ -722,9 +725,10 @@ def _cli_query(args):
since = None since = None
if args.since: if args.since:
# Parse duration like "7d", "30d" # Parse duration like "7d", "30d". Use UTC to match SQLite's
# datetime('now') which writes first_seen in UTC.
days = int(args.since.rstrip("d")) days = int(args.since.rstrip("d"))
since = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") since = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
findings = get_new_findings(topic["id"], since) findings = get_new_findings(topic["id"], since)
print(json.dumps({"topic": topic["name"], "findings": findings, "count": len(findings)}, default=str)) print(json.dumps({"topic": topic["name"], "findings": findings, "count": len(findings)}, default=str))
+23 -4
View File
@@ -6,6 +6,7 @@ from __future__ import annotations
import os import os
import subprocess import subprocess
import sys import sys
import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
@@ -27,13 +28,31 @@ class FooterNudgeSuppressionTests(unittest.TestCase):
"--emit=md", "--emit=md",
*argv, *argv,
] ]
env = {**os.environ, "LAST30DAYS_SKIP_PREFLIGHT": "1"} env = {
**os.environ,
"LAST30DAYS_SKIP_PREFLIGHT": "1",
# Skip ~/.config/last30days/.env so a contributor's saved
# BRAVE/EXA/SERPER/PARALLEL key doesn't make grounding "available"
# and suppress the promo we're checking for.
"LAST30DAYS_CONFIG_DIR": "",
# Pin X as available so _missing_sources_for_promo selects "web"
# (otherwise the "x" promo wins and the BRAVE_API_KEY string never
# appears).
"XAI_API_KEY": "test-stub",
}
# Strip any grounded-web keys the host might have so the promo path # Strip any grounded-web keys the host might have so the promo path
# triggers deterministically in mock + no-backend. # triggers deterministically in mock + no-backend. Also strip X cookie
# credentials so XAI_API_KEY is the unambiguous X backend.
for key in ("BRAVE_API_KEY", "EXA_API_KEY", "SERPER_API_KEY", for key in ("BRAVE_API_KEY", "EXA_API_KEY", "SERPER_API_KEY",
"PARALLEL_API_KEY", "OPENROUTER_API_KEY"): "PARALLEL_API_KEY", "OPENROUTER_API_KEY",
"AUTH_TOKEN", "CT0", "LAST30DAYS_X_BACKEND"):
env.pop(key, None) env.pop(key, None)
return subprocess.run(cmd, capture_output=True, text=True, env=env) # Run from a tmpdir so _find_project_env() can't walk up into any
# .claude/last30days.env above the repo on the contributor's machine.
with tempfile.TemporaryDirectory() as tmp:
return subprocess.run(
cmd, capture_output=True, text=True, env=env, cwd=tmp,
)
def test_bare_run_emits_web_promo(self): def test_bare_run_emits_web_promo(self):
result = self._run(topic="OpenAI") result = self._run(topic="OpenAI")
+8 -4
View File
@@ -213,8 +213,10 @@ class TestPollDeviceAuth:
@patch("lib.setup_wizard.urlopen") @patch("lib.setup_wizard.urlopen")
def test_timeout_returns_none(self, mock_urlopen, mock_time): def test_timeout_returns_none(self, mock_urlopen, mock_time):
"""Returns None when timeout is exceeded.""" """Returns None when timeout is exceeded."""
# Simulate time passing beyond deadline # poll_device_auth calls time.time() for deadline init, last_reminder init,
mock_time.time = MagicMock(side_effect=[0, 301]) # then once per while-loop iteration. Three values are enough for one check
# that exceeds the deadline.
mock_time.time = MagicMock(side_effect=[0, 0, 301])
mock_time.sleep = MagicMock() mock_time.sleep = MagicMock()
result = setup_wizard.poll_device_auth("dc-123", interval=5, timeout=300) result = setup_wizard.poll_device_auth("dc-123", interval=5, timeout=300)
@@ -224,7 +226,9 @@ class TestPollDeviceAuth:
@patch("lib.setup_wizard.urlopen") @patch("lib.setup_wizard.urlopen")
def test_expired_token_returns_none(self, mock_urlopen, mock_time): def test_expired_token_returns_none(self, mock_urlopen, mock_time):
"""Returns None on expired_token error.""" """Returns None on expired_token error."""
mock_time.time = MagicMock(side_effect=[0, 0]) # Loop terminates via urlopen response, not the clock — pin time to 0
# so the deadline check stays a non-event regardless of call count.
mock_time.time = MagicMock(return_value=0)
mock_time.sleep = MagicMock() mock_time.sleep = MagicMock()
expired_resp = MagicMock() expired_resp = MagicMock()
@@ -243,7 +247,7 @@ class TestPollDeviceAuth:
"""HTTP 400 during polling continues (authorization pending).""" """HTTP 400 during polling continues (authorization pending)."""
from urllib.error import HTTPError from urllib.error import HTTPError
mock_time.time = MagicMock(side_effect=[0, 0, 0]) mock_time.time = MagicMock(return_value=0)
mock_time.sleep = MagicMock() mock_time.sleep = MagicMock()
success_resp = MagicMock() success_resp = MagicMock()
+8 -8
View File
@@ -3,7 +3,7 @@
import json import json
import sqlite3 import sqlite3
import tempfile import tempfile
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -503,16 +503,16 @@ def test_get_new_findings_filters_by_date(temp_db, sample_report):
findings = store.findings_from_report(sample_report) findings = store.findings_from_report(sample_report)
store.store_findings(run_id, topic["id"], findings) store.store_findings(run_id, topic["id"], findings)
# Get findings since tomorrow (should be empty) # Use UTC because store writes first_seen via SQLite's datetime('now') (UTC).
tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") # Local-time math here would flake near midnight UTC.
tomorrow = (datetime.now(timezone.utc) + timedelta(days=1)).strftime("%Y-%m-%d")
new_findings = store.get_new_findings(topic["id"], since=tomorrow) new_findings = store.get_new_findings(topic["id"], since=tomorrow)
assert len(new_findings) == 0 assert len(new_findings) == 0
# Get findings since yesterday (should have all) yesterday = (datetime.now(timezone.utc) - timedelta(days=1)).strftime("%Y-%m-%d")
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
new_findings = store.get_new_findings(topic["id"], since=yesterday) new_findings = store.get_new_findings(topic["id"], since=yesterday)
assert len(new_findings) == 4 assert len(new_findings) == 4