release: bump version to 0.59.0
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled

This commit is contained in:
rayd1o
2026-05-16 05:02:05 +08:00
parent 93eb41a9f7
commit 9b913a3b83
86 changed files with 3645 additions and 1198 deletions

View File

@@ -0,0 +1,177 @@
import json
import pytest
from app.core.datasource_defaults import DEFAULT_DATASOURCES
from app.services.collectors.registry import collector_registry
from app.services import earth_boundaries
def write_geojson(path, name="Test"):
path.write_text(
json.dumps(
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {"name": name},
"geometry": {
"type": "Polygon",
"coordinates": [[[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]],
},
}
],
}
),
encoding="utf-8",
)
def patch_paths(monkeypatch, tmp_path):
repo = tmp_path
source_dir = repo / "data/earth-boundary-sources"
boundary_dir = repo / "frontend/public/earth/data/boundaries/v1"
pmtiles = repo / "frontend/public/earth/data/boundaries/earth-boundaries-china-pov-v1.pmtiles"
legacy = repo / "frontend/public/earth/data/countries-admin0.min.geojson"
config = repo / "config/earth-boundary-sources.local.json"
example = repo / "config/earth-boundary-sources.example.json"
policy = repo / "config/earth-boundary-pov-policy.china-v1.json"
for path in (source_dir, boundary_dir, pmtiles.parent, legacy.parent, config.parent):
path.mkdir(parents=True, exist_ok=True)
policy.write_text('{"productionTileFormat":"pmtiles+mvt"}\n', encoding="utf-8")
example.write_text('{"collectorConfigs":{}}\n', encoding="utf-8")
monkeypatch.setattr(earth_boundaries, "REPO_ROOT", repo)
monkeypatch.setattr(earth_boundaries, "SOURCE_OUTPUT_DIR", source_dir)
monkeypatch.setattr(earth_boundaries, "SOURCE_MANIFEST_PATH", source_dir / "manifest.json")
monkeypatch.setattr(earth_boundaries, "BUILD_RESULT_PATH", source_dir / "build-result.json")
monkeypatch.setattr(earth_boundaries, "BUILD_JOB_PATH", source_dir / "build-job.json")
monkeypatch.setattr(earth_boundaries, "BOUNDARY_OUTPUT_DIR", boundary_dir)
monkeypatch.setattr(earth_boundaries, "BOUNDARY_MANIFEST_PATH", boundary_dir / "manifest.json")
monkeypatch.setattr(earth_boundaries, "PMTILES_ARTIFACT_PATH", pmtiles)
monkeypatch.setattr(earth_boundaries, "LEGACY_GEOJSON_PATH", legacy)
monkeypatch.setattr(earth_boundaries, "LOCAL_CONFIG_PATH", config)
monkeypatch.setattr(earth_boundaries, "EXAMPLE_CONFIG_PATH", example)
monkeypatch.setattr(earth_boundaries, "POV_POLICY_PATH", policy)
return {
"repo": repo,
"config": config,
"legacy": legacy,
"pmtiles": pmtiles,
"manifest": boundary_dir / "manifest.json",
}
def test_boundary_status_uses_legacy_provider_when_pmtiles_missing(monkeypatch, tmp_path):
paths = patch_paths(monkeypatch, tmp_path)
write_geojson(paths["legacy"])
status = earth_boundaries.get_boundary_status()
assert status["provider"] == "legacy-geojson"
assert status["fallback_available"] is True
assert status["high_precision_ready"] is False
def test_boundary_status_prefers_high_precision_when_manifest_and_pmtiles_exist(monkeypatch, tmp_path):
paths = patch_paths(monkeypatch, tmp_path)
write_geojson(paths["legacy"])
paths["pmtiles"].write_bytes(b"pmtiles")
paths["manifest"].write_text('{"tileProvider":"pmtiles-mvt"}\n', encoding="utf-8")
status = earth_boundaries.get_boundary_status()
assert status["provider"] == "pmtiles-mvt"
assert status["high_precision_ready"] is True
def test_save_boundary_config_writes_local_config(monkeypatch, tmp_path):
paths = patch_paths(monkeypatch, tmp_path)
payload = {"collectorConfigs": {"earth_admin0_boundaries": {"endpoint": "file:///tmp/a.geojson"}}}
status = earth_boundaries.save_boundary_config(payload)
assert paths["config"].exists()
assert status["config_source"] == "local"
assert status["config"] == payload
@pytest.mark.asyncio
async def test_build_reports_missing_tools_after_source_artifacts(monkeypatch, tmp_path):
paths = patch_paths(monkeypatch, tmp_path)
source_files = {}
for source in earth_boundaries.BOUNDARY_SOURCE_KINDS:
source_path = paths["repo"] / f"{source}.geojson"
write_geojson(source_path, name=source)
source_files[source] = source_path
paths["config"].write_text(
json.dumps(
{
"collectorConfigs": {
source: {
"sourceKind": kind,
"endpoint": str(source_files[source]),
"method": "GET",
}
for source, kind in earth_boundaries.BOUNDARY_SOURCE_KINDS.items()
}
}
),
encoding="utf-8",
)
monkeypatch.setattr(earth_boundaries.shutil, "which", lambda _tool: None)
progress_events = []
status = await earth_boundaries.build_boundary_assets(
lambda progress, phase, message, **_extra: progress_events.append((progress, phase, message))
)
assert status["provider"] == "geojson-high-precision"
assert status["high_precision_ready"] is True
assert (paths["repo"] / "data/earth-boundary-sources/manifest.json").exists()
assert paths["manifest"].exists()
assert any(phase == "download" for _progress, phase, _message in progress_events)
@pytest.mark.asyncio
async def test_start_boundary_build_job_records_geojson_fallback_success(monkeypatch, tmp_path):
paths = patch_paths(monkeypatch, tmp_path)
monkeypatch.setattr(earth_boundaries, "_build_task", None)
monkeypatch.setattr(earth_boundaries, "_build_job_state", {})
source_files = {}
for source in earth_boundaries.BOUNDARY_SOURCE_KINDS:
source_path = paths["repo"] / f"{source}.geojson"
write_geojson(source_path, name=source)
source_files[source] = source_path
paths["config"].write_text(
json.dumps(
{
"collectorConfigs": {
source: {
"sourceKind": kind,
"endpoint": str(source_files[source]),
"method": "GET",
}
for source, kind in earth_boundaries.BOUNDARY_SOURCE_KINDS.items()
}
}
),
encoding="utf-8",
)
monkeypatch.setattr(earth_boundaries.shutil, "which", lambda _tool: None)
response = await earth_boundaries.start_boundary_build_job()
await earth_boundaries._build_task
status = earth_boundaries.get_boundary_build_status()
assert response["accepted"] is True
assert status["job"]["status"] == "succeeded"
assert status["job"]["result"]["provider"] == "geojson-high-precision"
def test_earth_boundary_collectors_are_not_registered_as_datasources():
removed = set(earth_boundaries.BOUNDARY_SOURCE_KINDS) | {"earth_boundary_tiles"}
assert removed.isdisjoint(DEFAULT_DATASOURCES)
for source in removed:
assert collector_registry.get(source) is None

View File

@@ -158,6 +158,56 @@ async def test_enrich_items_with_target_locations_uses_ai_and_geocode(monkeypatc
assert enriched[0].target_ai_error is None
@pytest.mark.asyncio
async def test_enrich_items_with_target_locations_adds_localizations(monkeypatch):
item = ParsedNewsItem(
id="global-scan:localized",
title="Global leaders meet to discuss energy security",
summary="Officials said the talks focused on supply chains and grid resilience.",
url="https://example.com/energy-security",
source="Example Source",
feed_name="Global Monitor / World",
feed_region="global",
homepage_url="https://example.com",
published_at=datetime(2026, 5, 14, 7, 13, 24, tzinfo=UTC),
)
async def fake_geocode(_query: str):
return {
"lat": "50.1109",
"lon": "8.6821",
"display_name": "Frankfurt am Main, Germany",
}
class FakeProviderClient:
async def analyze(self, _request):
class Response:
content = (
'{"location":{"country":"Germany","city":"Frankfurt",'
'"matched_location_name":"Frankfurt, Germany",'
'"latitude":null,"longitude":null,"confidence":0.77},'
'"localizations":{"zh-CN":{"title":"全球领导人讨论能源安全",'
'"summary":"官员表示,会谈聚焦供应链和电网韧性。"}}}'
)
return Response()
monkeypatch.setattr("app.services.earth_news._geocode_target_location", fake_geocode)
enriched = await _enrich_items_with_target_locations(
[item],
provider_client=FakeProviderClient(),
)
payload = _serialize_item(enriched[0], active_region="global")
assert payload["title"] == "Global leaders meet to discuss energy security"
assert payload["summary"] == "Officials said the talks focused on supply chains and grid resilience."
assert payload["localizations"]["zh-CN"]["title"] == "全球领导人讨论能源安全"
assert payload["display_title"] == "全球领导人讨论能源安全"
assert payload["display_summary"] == "官员表示,会谈聚焦供应链和电网韧性。"
assert payload["enrichment_status"] == "success"
@pytest.mark.asyncio
async def test_extract_target_location_from_text_uses_country_hint(monkeypatch):
item = ParsedNewsItem(
@@ -241,7 +291,7 @@ async def test_earth_news_payload_returns_anchor_items_and_enqueues_location_job
enqueued_payloads = []
async def fake_enqueue_target_location_job(payload):
async def fake_enqueue_target_location_job(payload, **_kwargs):
enqueued_payloads.append(payload)
return True
@@ -260,6 +310,8 @@ async def test_earth_news_payload_returns_anchor_items_and_enqueues_location_job
assert len(payload["items"]) == 1
assert payload["items"][0]["id"] == "test-feed:timeout"
assert payload["items"][0]["display_title"] == ""
assert payload["items"][0]["display_summary"] == ""
assert payload["items"][0]["latitude"] == 20.0
assert payload["items"][0]["longitude"] == 0.0
assert payload["items"][0]["location_source"] == "region_anchor"
@@ -359,7 +411,7 @@ async def test_earth_news_payload_initializes_empty_database_from_rss(monkeypatc
async def fake_list_earth_news_items(_db, *, active_region, limit):
return [item]
async def fake_enqueue_target_location_job(payload):
async def fake_enqueue_target_location_job(payload, **_kwargs):
enqueued.append(payload)
return True
@@ -415,7 +467,7 @@ async def test_earth_news_payload_supplements_stale_database_items(monkeypatch):
async def fake_list_earth_news_items(_db, *, active_region, limit):
return [old_item]
async def fake_enqueue_target_location_job(_payload):
async def fake_enqueue_target_location_job(_payload, **_kwargs):
return True
monkeypatch.setattr("app.services.earth_news_store.get_earth_news_freshness", fake_get_earth_news_freshness)
@@ -473,8 +525,11 @@ async def test_earth_news_payload_merges_cached_location_patch(monkeypatch):
async def fake_get_cached_target_location_patch(_item_id):
return cached_patch
async def fake_enqueue_target_location_job(_payload):
raise AssertionError("cached items should not be enqueued")
enqueued = []
async def fake_enqueue_target_location_job(payload, **_kwargs):
enqueued.append(payload)
return True
monkeypatch.setattr("app.services.earth_news.get_sources_for_region", lambda _region: [source])
monkeypatch.setattr("app.services.earth_news._fetch_source", fake_fetch_source)
@@ -493,6 +548,70 @@ async def test_earth_news_payload_merges_cached_location_patch(monkeypatch):
assert payload["items"][0]["longitude"] == 116.3912972
assert payload["items"][0]["verified"] is True
assert payload["items"][0]["location_source"] == "headline_location_hint"
assert enqueued[0]["id"] == "test-feed:cached"
@pytest.mark.asyncio
async def test_earth_news_payload_requeues_cached_failed_localization(monkeypatch):
source = NewsFeedSource(
id="test-feed",
name="Test Feed",
region="global",
homepage_url="https://example.com",
feed_url="https://example.com/rss.xml",
)
item = ParsedNewsItem(
id="test-feed:failed-localization",
title="Failed localization story",
summary="English source summary.",
url="https://example.com/failed-localization",
source="Test Feed",
feed_name="Test Feed",
feed_region="global",
homepage_url="https://example.com",
published_at=datetime(2026, 5, 15, 3, 0, tzinfo=UTC),
)
cached_patch = {
"latitude": 20.0,
"longitude": 0.0,
"location_label": "全球",
"location_source": "region_anchor",
"verified": False,
"location_meta": {"target": None, "anchor": {"region": "global"}},
"content_language": "en",
"localizations": {},
"enrichment_status": "parse_error",
"enrichment_error": "AI response did not contain a parseable JSON object.",
"enriched_at": None,
}
enqueued = []
async def fake_fetch_source(_client, feed_source):
return feed_source, [item], None
async def fake_get_cached_target_location_patch(_item_id):
return cached_patch
async def fake_enqueue_target_location_job(payload, **_kwargs):
enqueued.append(payload)
return True
monkeypatch.setattr("app.services.earth_news.get_sources_for_region", lambda _region: [source])
monkeypatch.setattr("app.services.earth_news._fetch_source", fake_fetch_source)
monkeypatch.setattr(
"app.services.earth_news_queue.get_cached_target_location_patch",
fake_get_cached_target_location_patch,
)
monkeypatch.setattr(
"app.services.earth_news_queue.enqueue_target_location_job",
fake_enqueue_target_location_job,
)
payload = await get_earth_news_payload(provider_client=None)
assert enqueued[0]["id"] == "test-feed:failed-localization"
assert payload["items"][0]["display_title"] == ""
assert payload["items"][0]["enrichment_status"] == "queued"
@pytest.mark.asyncio
@@ -598,6 +717,11 @@ async def test_media_news_archive_collector_maps_news_items(monkeypatch):
location_source="headline_location_hint",
verified=True,
location_meta={"target": {"country": "中国", "city": "Beijing"}},
content_language="en",
localizations={"zh-CN": {"title": "归档新闻", "summary": "归档概要"}},
enrichment_status="success",
enrichment_error=None,
enriched_at=datetime(2026, 5, 15, 3, 6, tzinfo=UTC),
first_seen_at=datetime(2026, 5, 15, 2, 0, tzinfo=UTC),
last_seen_at=datetime(2026, 5, 15, 3, 0, tzinfo=UTC),
resolved_at=datetime(2026, 5, 15, 3, 5, tzinfo=UTC),
@@ -619,3 +743,5 @@ async def test_media_news_archive_collector_maps_news_items(monkeypatch):
assert items[0]["city"] == "Beijing"
assert items[0]["latitude"] == 39.9057136
assert items[0]["metadata"]["verified"] is True
assert "localizations" not in items[0]["metadata"]
assert "enrichment_status" not in items[0]["metadata"]

View File

@@ -0,0 +1,92 @@
from types import SimpleNamespace
import pytest
from app.ai_tasks.prompts import (
get_effective_prompt,
list_effective_prompts,
reset_prompt_override,
save_prompt_override,
)
class _ScalarResult:
def __init__(self, value):
self._value = value
def scalar_one_or_none(self):
return self._value
class _PromptSettingsDB:
def __init__(self, payload=None):
self.record = SimpleNamespace(category="ai_prompts", payload=payload) if payload is not None else None
self.added = None
self.commits = 0
async def execute(self, _statement):
return _ScalarResult(self.record)
def add(self, record):
self.record = record
self.added = record
async def commit(self):
self.commits += 1
@pytest.mark.asyncio
async def test_prompt_defaults_are_loaded_without_override():
db = _PromptSettingsDB()
prompt = await get_effective_prompt(db, "earth.news.enrich")
assert prompt.key == "earth.news.enrich"
assert prompt.is_custom is False
assert "strict JSON" in prompt.prompt
@pytest.mark.asyncio
async def test_prompt_override_save_and_reset():
db = _PromptSettingsDB()
saved = await save_prompt_override(
db,
"alerts.brief",
system_prompt="system custom",
prompt="prompt custom",
)
assert saved.is_custom is True
assert saved.system_prompt == "system custom"
assert saved.prompt == "prompt custom"
assert db.commits == 1
effective = await get_effective_prompt(db, "alerts.brief")
assert effective.prompt == "prompt custom"
reset = await reset_prompt_override(db, "alerts.brief")
assert reset.is_custom is False
assert reset.prompt != "prompt custom"
@pytest.mark.asyncio
async def test_prompt_list_marks_custom_items():
db = _PromptSettingsDB(
{
"overrides": {
"bgp.brief": {
"system_prompt": "",
"prompt": "custom bgp prompt",
"updated_at": "2026-05-16T00:00:00Z",
}
}
}
)
prompts = await list_effective_prompts(db)
by_key = {prompt.key: prompt for prompt in prompts}
assert by_key["bgp.brief"].is_custom is True
assert by_key["bgp.brief"].prompt == "custom bgp prompt"
assert by_key["earth.news.enrich"].is_custom is False