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:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -676,24 +676,28 @@ def findings_from_report(
|
|||||||
|
|
||||||
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,
|
||||||
@@ -706,7 +710,6 @@ def findings_from_report(
|
|||||||
})
|
})
|
||||||
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))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
+5
-5
@@ -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,14 +503,14 @@ 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
|
||||||
|
|||||||
Reference in New Issue
Block a user