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

584 lines
22 KiB
Python

"""Tests for the remote API path (LAST30DAYS_API_KEY + LAST30DAYS_API_BASE).
Fixtures mirror the remote API contract exactly:
POST {base}/search {"query","depth"} -> {"search_id","status"} | clarify payload
GET {base}/search?id=<uuid> -> pending|running|complete|error rows
401 {"error"} / 402 {"error","requires_credits","balance","needed"} / 429 {"error"}
The endpoint is driven entirely through LAST30DAYS_API_BASE; there is no
built-in default. All keys/hosts in tests are obvious dummy values (see
AGENTS.md security hygiene).
"""
import io
import json
import sys
from contextlib import redirect_stderr, redirect_stdout
from datetime import datetime
from unittest import mock
import pytest
import last30days as cli
from lib import hosted, http, schema
TEST_KEY = "sk_live_DUMMY_TEST_KEY_00000"
# Neutral placeholder endpoint - no product host. Ends in /api/v1 to mirror the
# API-version-root convention the billing-link derivation relies on.
TEST_BASE = "https://api.example.test/api/v1"
SEARCH_ID = "3f6c1c2e-9f6a-4a55-8f8a-2d1a9b8c7d6e"
SUBMIT_OK = {"search_id": SEARCH_ID, "status": "running"}
POLL_RUNNING = {
"id": SEARCH_ID,
"status": "running",
"stderr": (
"[narrate] step=planning queries\n"
"[Reddit] fetched 12 threads\n"
"[narrate] step=searching sources\n"
),
"eta_ms": 45000,
}
POLL_COMPLETE = {
"id": SEARCH_ID,
"status": "complete",
"synthesis_text": "## What happened\nSynthesized report body.",
"raw_markdown": "# Raw markdown\nFull dump.",
}
CLARIFY_RESPONSE = {
"needs_clarification": True,
"clarify_class": "ambiguous_entity",
"question": "Which 'mercury' do you mean?",
"options": ["Mercury the planet", "Mercury the element", "Mercury the band"],
"original_query": "mercury",
}
DIAG = {
"available_sources": ["grounding"],
"providers": {"google": True, "openai": False, "xai": False},
"x_backend": None,
"bird_installed": False,
"bird_authenticated": False,
"bird_username": None,
"native_web_backend": "brave",
}
def make_report(topic: str = "test topic") -> schema.Report:
return schema.Report(
topic=topic,
range_from="2026-06-03",
range_to="2026-07-03",
generated_at="2026-07-03T00:00:00+00:00",
provider_runtime=schema.ProviderRuntime(
reasoning_provider="gemini",
planner_model="gemini-3.1-flash-lite",
rerank_model="gemini-3.1-flash-lite",
),
query_plan=schema.QueryPlan(
intent="overview",
freshness_mode="balanced_recent",
cluster_mode="themes",
raw_topic=topic,
subqueries=[
schema.SubQuery(
label="primary",
search_query=topic.lower(),
ranking_query=f"What happened with {topic}?",
sources=["grounding"],
)
],
source_weights={"grounding": 1.0},
),
clusters=[],
ranked_candidates=[],
items_by_source={"grounding": []},
errors_by_source={},
)
def run_main(argv):
stdout, stderr = io.StringIO(), io.StringIO()
with mock.patch.object(sys, "argv", ["last30days.py", *argv]):
with redirect_stdout(stdout), redirect_stderr(stderr):
rc = cli.main()
return rc, stdout.getvalue(), stderr.getvalue()
@pytest.fixture(autouse=True)
def _clean_env(monkeypatch):
monkeypatch.delenv("LAST30DAYS_API_KEY", raising=False)
monkeypatch.delenv("LAST30DAYS_API_BASE", raising=False)
monkeypatch.delenv("LAST30DAYS_MEMORY_DIR", raising=False)
# ---------------------------------------------------------------------------
# Mode selection in the CLI entrypoint
# ---------------------------------------------------------------------------
def test_env_unset_runs_local_path_with_no_gateway_http(monkeypatch):
"""Without LAST30DAYS_API_KEY the local engine runs; the remote client and
the API are never touched."""
def no_hosted(*args, **kwargs): # pragma: no cover - failure path
raise AssertionError("remote path must not run when env is unset")
def no_http(*args, **kwargs): # pragma: no cover - failure path
raise AssertionError(f"unexpected HTTP call in local test: {args}")
monkeypatch.setattr(hosted, "run_hosted", no_hosted)
monkeypatch.setattr(http, "request", no_http)
fake_progress = mock.Mock()
with mock.patch.object(cli.env, "get_config", return_value={}), \
mock.patch.object(cli.pipeline, "diagnose", return_value=DIAG), \
mock.patch.object(cli.pipeline, "run", return_value=make_report()) as pipeline_run, \
mock.patch.object(cli.ui, "ProgressDisplay", return_value=fake_progress), \
mock.patch.object(cli, "emit_output", return_value="# local rendered"):
rc, out, _err = run_main(["test", "topic"])
assert rc == 0
pipeline_run.assert_called_once()
assert "# local rendered" in out
def test_env_set_routes_to_remote_path(monkeypatch):
# Both vars set -> remote path (KTD-2: key alone no longer activates it).
monkeypatch.setenv("LAST30DAYS_API_KEY", TEST_KEY)
monkeypatch.setenv("LAST30DAYS_API_BASE", TEST_BASE)
calls = []
def fake_run_hosted(topic, depth, *, emit, save_dir, save_suffix):
calls.append({"topic": topic, "depth": depth, "emit": emit,
"save_dir": save_dir, "save_suffix": save_suffix})
return 0
monkeypatch.setattr(hosted, "run_hosted", fake_run_hosted)
with mock.patch.object(cli.env, "get_config", return_value={}), \
mock.patch.object(cli.pipeline, "run",
side_effect=AssertionError("local pipeline must not run")):
rc, _out, _err = run_main(["test", "topic"])
assert rc == 0
assert calls == [{
"topic": "test topic",
"depth": "default",
"emit": "compact",
"save_dir": None,
"save_suffix": "",
}]
def test_register_is_forwarded_to_remote_backend(monkeypatch):
monkeypatch.setenv("LAST30DAYS_API_KEY", TEST_KEY)
monkeypatch.setenv("LAST30DAYS_API_BASE", TEST_BASE)
calls = []
monkeypatch.setattr(
hosted,
"run_hosted",
lambda topic, depth, **kwargs: calls.append((topic, depth, kwargs)) or 0,
)
with mock.patch.object(cli.env, "get_config", return_value={}):
rc, _out, _err = run_main(
["test", "topic", "--register=creator"]
)
assert rc == 0
assert calls == [
(
"test topic",
"default",
{
"emit": "compact",
"save_dir": None,
"save_suffix": "",
"register": "creator",
},
)
]
def test_hosted_submit_adds_only_nondefault_register(remote_env, monkeypatch):
payloads = []
monkeypatch.setattr(
hosted.http,
"post",
lambda _url, *, json_data, **_kwargs: payloads.append(json_data) or SUBMIT_OK,
)
hosted.submit("test topic", "quick")
hosted.submit("test topic", "quick", register="dev")
assert payloads == [
{"query": "test topic", "depth": "quick"},
{"query": "test topic", "depth": "quick", "register": "dev"},
]
def test_remote_json_requires_raw_profile(monkeypatch):
monkeypatch.setenv("LAST30DAYS_API_KEY", TEST_KEY)
monkeypatch.setenv("LAST30DAYS_API_BASE", TEST_BASE)
monkeypatch.setattr(
hosted,
"run_hosted",
lambda *args, **kwargs: pytest.fail("remote request should not start"),
)
with mock.patch.object(cli.env, "get_config", return_value={}):
rc, _out, err = run_main(["test", "topic", "--emit=json"])
assert rc == 2
assert "remote API backend only supports --json-profile=raw" in err
def test_remote_raw_json_preserves_existing_server_contract(monkeypatch):
monkeypatch.setenv("LAST30DAYS_API_KEY", TEST_KEY)
monkeypatch.setenv("LAST30DAYS_API_BASE", TEST_BASE)
calls = []
monkeypatch.setattr(
hosted,
"run_hosted",
lambda topic, depth, **kwargs: calls.append((topic, depth, kwargs)) or 0,
)
with mock.patch.object(cli.env, "get_config", return_value={}):
rc, _out, _err = run_main(
["test", "topic", "--emit=json", "--json-profile=raw"]
)
assert rc == 0
assert calls == [
(
"test topic",
"default",
{"emit": "json", "save_dir": None, "save_suffix": ""},
)
]
@pytest.mark.parametrize(
("flag", "expected_depth"),
[(["--quick"], "quick"), ([], "default"), (["--deep"], "deep")],
)
def test_depth_mapping(monkeypatch, flag, expected_depth):
monkeypatch.setenv("LAST30DAYS_API_KEY", TEST_KEY)
monkeypatch.setenv("LAST30DAYS_API_BASE", TEST_BASE)
depths = []
monkeypatch.setattr(
hosted, "run_hosted",
lambda topic, depth, **kwargs: depths.append(depth) or 0,
)
with mock.patch.object(cli.env, "get_config", return_value={}):
rc, _out, _err = run_main(["test", "topic", *flag])
assert rc == 0
assert depths == [expected_depth]
def test_mock_flag_stays_local_even_with_key(monkeypatch):
monkeypatch.setenv("LAST30DAYS_API_KEY", TEST_KEY)
monkeypatch.setenv("LAST30DAYS_API_BASE", TEST_BASE)
def no_hosted(*args, **kwargs): # pragma: no cover - failure path
raise AssertionError("remote path must not run with --mock")
monkeypatch.setattr(hosted, "run_hosted", no_hosted)
fake_progress = mock.Mock()
with mock.patch.object(cli.env, "get_config", return_value={}), \
mock.patch.object(cli.pipeline, "diagnose", return_value=DIAG), \
mock.patch.object(cli.pipeline, "run", return_value=make_report()) as pipeline_run, \
mock.patch.object(cli.ui, "ProgressDisplay", return_value=fake_progress), \
mock.patch.object(cli, "emit_output", return_value="# local rendered"):
rc, _out, _err = run_main(["test", "topic", "--mock"])
assert rc == 0
pipeline_run.assert_called_once()
def test_key_set_but_base_unset_stays_local(monkeypatch):
"""KTD-2 inertness: with only the key set (no LAST30DAYS_API_BASE), hosted
mode does not activate - the local engine runs and no HTTP is attempted.
This is the leak-proofing guarantee: a key alone can never phone anywhere."""
monkeypatch.setenv("LAST30DAYS_API_KEY", TEST_KEY)
# LAST30DAYS_API_BASE deliberately left unset by the _clean_env fixture.
def no_hosted(*args, **kwargs): # pragma: no cover - failure path
raise AssertionError("remote path must not run without LAST30DAYS_API_BASE")
def no_http(*args, **kwargs): # pragma: no cover - failure path
raise AssertionError(f"unexpected HTTP call when base is unset: {args}")
monkeypatch.setattr(hosted, "run_hosted", no_hosted)
monkeypatch.setattr(http, "request", no_http)
fake_progress = mock.Mock()
with mock.patch.object(cli.env, "get_config", return_value={}), \
mock.patch.object(cli.pipeline, "diagnose", return_value=DIAG), \
mock.patch.object(cli.pipeline, "run", return_value=make_report()) as pipeline_run, \
mock.patch.object(cli.ui, "ProgressDisplay", return_value=fake_progress), \
mock.patch.object(cli, "emit_output", return_value="# local rendered"):
rc, out, _err = run_main(["test", "topic"])
assert rc == 0
pipeline_run.assert_called_once()
assert "# local rendered" in out
# ---------------------------------------------------------------------------
# Remote client: submit -> poll -> complete
# ---------------------------------------------------------------------------
@pytest.fixture()
def remote_env(monkeypatch):
monkeypatch.setenv("LAST30DAYS_API_KEY", TEST_KEY)
monkeypatch.setenv("LAST30DAYS_API_BASE", TEST_BASE)
monkeypatch.setattr(hosted.time, "sleep", lambda _s: None)
def test_happy_path_submit_poll_complete(remote_env, monkeypatch, capsys):
posts, gets = [], []
def fake_post(url, json_data, headers=None, **kwargs):
posts.append({"url": url, "json": json_data, "headers": headers})
return dict(SUBMIT_OK)
poll_rows = [dict(POLL_RUNNING), dict(POLL_RUNNING), dict(POLL_COMPLETE)]
def fake_get(url, headers=None, params=None, **kwargs):
gets.append({"url": url, "headers": headers, "params": params})
return poll_rows.pop(0)
monkeypatch.setattr(hosted.http, "post", fake_post)
monkeypatch.setattr(hosted.http, "get", fake_get)
rc = hosted.run_hosted("test topic", "default", emit="compact",
save_dir=None, save_suffix="")
out = capsys.readouterr().out
assert rc == 0
# Contract: submit
assert posts == [{
"url": f"{TEST_BASE}/search",
"json": {"query": "test topic", "depth": "default"},
"headers": {"Authorization": f"Bearer {TEST_KEY}"},
}]
# Contract: poll same auth, id param
assert all(g["url"] == f"{TEST_BASE}/search" for g in gets)
assert all(g["params"] == {"id": SEARCH_ID} for g in gets)
assert all(g["headers"] == {"Authorization": f"Bearer {TEST_KEY}"} for g in gets)
assert len(gets) == 3
# Synthesis rendered on stdout
assert "Synthesized report body." in out
def test_happy_path_narration_printed_once_and_no_key_echo(remote_env, monkeypatch, capsys):
monkeypatch.setattr(hosted.http, "post", lambda *a, **k: dict(SUBMIT_OK))
poll_rows = [dict(POLL_RUNNING), dict(POLL_RUNNING), dict(POLL_COMPLETE)]
monkeypatch.setattr(hosted.http, "get", lambda *a, **k: poll_rows.pop(0))
rc = hosted.run_hosted("test topic", "default", emit="compact",
save_dir=None, save_suffix="")
captured = capsys.readouterr()
assert rc == 0
# Each narration step printed exactly once even though the stderr blob
# was returned twice by consecutive polls.
assert captured.err.count("[narrate] step=planning queries") == 1
assert captured.err.count("[narrate] step=searching sources") == 1
# Progress line with elapsed/eta shape
assert "eta" in captured.err
# The API key never appears anywhere in output.
assert TEST_KEY not in captured.out
assert TEST_KEY not in captured.err
def test_save_dir_writes_raw_markdown(remote_env, monkeypatch, capsys, tmp_path):
monkeypatch.setattr(hosted.http, "post", lambda *a, **k: dict(SUBMIT_OK))
poll_rows = [dict(POLL_COMPLETE)]
monkeypatch.setattr(hosted.http, "get", lambda *a, **k: poll_rows.pop(0))
rc = hosted.run_hosted("Test Topic!", "default", emit="compact",
save_dir=str(tmp_path), save_suffix="")
captured = capsys.readouterr()
assert rc == 0
saved = tmp_path / "test-topic-raw.md"
assert saved.exists()
assert "# Raw markdown" in saved.read_text(encoding="utf-8")
assert "Saved output to" in captured.err
assert TEST_KEY not in captured.err
def test_save_dir_uses_unique_dated_fallback(remote_env, monkeypatch, capsys, tmp_path):
monkeypatch.setattr(hosted.http, "post", lambda *a, **k: dict(SUBMIT_OK))
poll_rows = [dict(POLL_COMPLETE)]
monkeypatch.setattr(hosted.http, "get", lambda *a, **k: poll_rows.pop(0))
today = datetime.now().strftime("%Y-%m-%d")
base = tmp_path / "test-topic-raw.md"
dated = tmp_path / f"test-topic-raw-{today}.md"
base.write_text("base content", encoding="utf-8")
dated.write_text("dated content", encoding="utf-8")
rc = hosted.run_hosted("Test Topic!", "default", emit="compact",
save_dir=str(tmp_path), save_suffix="")
captured = capsys.readouterr()
saved = tmp_path / f"test-topic-raw-{today}-1.md"
assert rc == 0
assert saved.exists()
assert "# Raw markdown" in saved.read_text(encoding="utf-8")
assert base.read_text(encoding="utf-8") == "base content"
assert dated.read_text(encoding="utf-8") == "dated content"
assert f"Saved output to {saved.resolve()}" in captured.err
assert TEST_KEY not in captured.err
def test_api_base_override(remote_env, monkeypatch, capsys):
monkeypatch.setenv("LAST30DAYS_API_BASE", "https://staging.example.dev/api/v1/")
urls = []
def fake_post(url, json_data, headers=None, **kwargs):
urls.append(url)
return dict(SUBMIT_OK)
monkeypatch.setattr(hosted.http, "post", fake_post)
monkeypatch.setattr(hosted.http, "get", lambda *a, **k: dict(POLL_COMPLETE))
rc = hosted.run_hosted("test topic", "quick", emit="compact",
save_dir=None, save_suffix="")
capsys.readouterr()
assert rc == 0
assert urls == ["https://staging.example.dev/api/v1/search"]
# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------
def test_401_invalid_or_revoked_key(remote_env, monkeypatch, capsys):
def fake_post(*a, **k):
raise http.HTTPError("HTTP 401: Unauthorized", 401,
json.dumps({"error": "Invalid API key"}))
monkeypatch.setattr(hosted.http, "post", fake_post)
rc = hosted.run_hosted("test topic", "default", emit="compact",
save_dir=None, save_suffix="")
captured = capsys.readouterr()
assert rc == 1
assert "invalid or revoked" in captured.err.lower()
assert TEST_KEY not in captured.err
def test_402_shows_balance_needed_and_billing_url(remote_env, monkeypatch, capsys):
body = {"error": "Insufficient credits", "requires_credits": True,
"balance": 40, "needed": 200}
def fake_post(*a, **k):
raise http.HTTPError("HTTP 402: Payment Required", 402, json.dumps(body))
monkeypatch.setattr(hosted.http, "post", fake_post)
rc = hosted.run_hosted("test topic", "deep", emit="compact",
save_dir=None, save_suffix="")
captured = capsys.readouterr()
assert rc == 1
# Balance and needed shown verbatim from the API response.
assert "40" in captured.err
assert "200" in captured.err
# Billing link is derived from the configured base (base minus /api/v1,
# plus /dashboard/billing) - never hardcoded.
assert "https://api.example.test/dashboard/billing" in captured.err
assert TEST_KEY not in captured.err
def test_429_rate_limited(remote_env, monkeypatch, capsys):
def fake_post(*a, **k):
raise http.HTTPError("HTTP 429: Too Many Requests", 429,
json.dumps({"error": "Rate limit exceeded"}))
monkeypatch.setattr(hosted.http, "post", fake_post)
rc = hosted.run_hosted("test topic", "default", emit="compact",
save_dir=None, save_suffix="")
captured = capsys.readouterr()
assert rc == 1
assert "rate limit" in captured.err.lower()
def test_clarify_response_prints_question_options_distinct_exit(remote_env, monkeypatch, capsys):
monkeypatch.setattr(hosted.http, "post", lambda *a, **k: dict(CLARIFY_RESPONSE))
def no_get(*a, **k): # pragma: no cover - failure path
raise AssertionError("must not poll on clarify")
monkeypatch.setattr(hosted.http, "get", no_get)
rc = hosted.run_hosted("mercury", "default", emit="compact",
save_dir=None, save_suffix="")
captured = capsys.readouterr()
assert rc == hosted.EXIT_CLARIFY
assert rc not in (0, 1)
assert "Which 'mercury' do you mean?" in captured.err
assert "Mercury the planet" in captured.err
assert "Mercury the band" in captured.err
assert "re-run" in captured.err.lower()
def test_error_status_run_prints_server_message(remote_env, monkeypatch, capsys):
monkeypatch.setattr(hosted.http, "post", lambda *a, **k: dict(SUBMIT_OK))
error_row = {"id": SEARCH_ID, "status": "error",
"error": "Synthesis failed upstream"}
monkeypatch.setattr(hosted.http, "get", lambda *a, **k: dict(error_row))
rc = hosted.run_hosted("test topic", "default", emit="compact",
save_dir=None, save_suffix="")
captured = capsys.readouterr()
assert rc == 1
assert "Synthesis failed upstream" in captured.err
def test_network_timeout_mid_poll_retries_get(remote_env, monkeypatch, capsys):
monkeypatch.setattr(hosted.http, "post", lambda *a, **k: dict(SUBMIT_OK))
attempts = []
def flaky_get(*a, **k):
attempts.append(1)
if len(attempts) < 3:
raise http.HTTPError("Connection error: TimeoutError: timed out")
return dict(POLL_COMPLETE)
monkeypatch.setattr(hosted.http, "get", flaky_get)
rc = hosted.run_hosted("test topic", "default", emit="compact",
save_dir=None, save_suffix="")
captured = capsys.readouterr()
assert rc == 0
assert len(attempts) == 3
assert "Synthesized report body." in captured.out
def test_persistent_network_failure_gives_up_with_message(remote_env, monkeypatch, capsys):
monkeypatch.setattr(hosted.http, "post", lambda *a, **k: dict(SUBMIT_OK))
def dead_get(*a, **k):
raise http.HTTPError("Connection error: TimeoutError: timed out")
monkeypatch.setattr(hosted.http, "get", dead_get)
rc = hosted.run_hosted("test topic", "default", emit="compact",
save_dir=None, save_suffix="")
captured = capsys.readouterr()
assert rc == 1
assert "poll" in captured.err.lower()
assert TEST_KEY not in captured.err
def test_emit_json_prints_terminal_row_without_stderr(remote_env, monkeypatch, capsys):
monkeypatch.setattr(hosted.http, "post", lambda *a, **k: dict(SUBMIT_OK))
monkeypatch.setattr(hosted.http, "get", lambda *a, **k: dict(POLL_COMPLETE))
rc = hosted.run_hosted("test topic", "default", emit="json",
save_dir=None, save_suffix="")
captured = capsys.readouterr()
assert rc == 0
payload = json.loads(captured.out)
assert payload["status"] == "complete"
assert payload["synthesis_text"].startswith("## What happened")
assert payload["raw_markdown"].startswith("# Raw markdown")
assert "stderr" not in payload
assert TEST_KEY not in captured.out