rebuild(stage-15): complete governance and handoff

This commit is contained in:
leefer
2026-07-30 08:46:12 +08:00
parent fa7a8dde06
commit d1b3b977d2
14 changed files with 498 additions and 13 deletions
+14
View File
@@ -1,5 +1,6 @@
import pytest
from backend.database import MIGRATIONS, Database, MigrationRunner
from tools.database import main
@@ -14,6 +15,19 @@ def test_status_uses_configured_data_directory(tmp_path, monkeypatch, capsys) ->
assert (tmp_path / "xiaobai.db").exists()
def test_status_reads_an_existing_schema_without_mutating_history(
tmp_path, monkeypatch, capsys
) -> None:
database_path = tmp_path / "existing.db"
MigrationRunner(Database(database_path)).upgrade(MIGRATIONS)
monkeypatch.setenv("APP_ENV", "test")
monkeypatch.setenv("APP_DATABASE_PATH", str(database_path))
assert main(["status"]) == 0
assert capsys.readouterr().out.strip() == "available=true schema_version=10"
def test_downgrade_requires_explicit_confirmation(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("APP_ENV", "test")
monkeypatch.setenv("APP_DATA_DIR", str(tmp_path))
+34 -2
View File
@@ -1,3 +1,8 @@
import asyncio
from dataclasses import replace
import httpx
from cryptography.fernet import Fernet
from fastapi import Query
from backend.bootstrap.application import create_application
@@ -17,6 +22,8 @@ def test_health_reports_runtime_environment(tmp_path) -> None:
"components": {"process": "ok", "database": "ok"},
}
assert len(response.headers["X-Request-ID"]) == 32
assert response.headers["X-Content-Type-Options"] == "nosniff"
assert response.headers["X-Frame-Options"] == "DENY"
def test_unknown_failure_uses_safe_error_contract(tmp_path) -> None:
@@ -81,13 +88,38 @@ def test_production_frontend_serves_spa_without_capturing_api_404(tmp_path) -> N
settings.frontend_dist_directory.joinpath("index.html").write_text(
"<main>application</main>", encoding="utf-8"
)
settings.frontend_dist_directory.joinpath("asset.js").write_text(
settings.frontend_dist_directory.joinpath("assets").mkdir()
settings.frontend_dist_directory.joinpath("assets", "asset.js").write_text(
"window.ready=true", encoding="utf-8"
)
application = create_application(settings)
assert request(application, "/review/history").text == "<main>application</main>"
assert request(application, "/asset.js").text == "window.ready=true"
asset_response = request(application, "/assets/asset.js")
assert asset_response.text == "window.ready=true"
assert asset_response.headers["Cache-Control"] == "public,max-age=31536000,immutable"
api_response = request(application, "/api/unknown")
assert api_response.status_code == 404
assert api_response.json()["error"]["code"] == "not_found"
def test_production_security_headers_are_strict_on_https(tmp_path) -> None:
settings = Settings.for_test(tmp_path)
settings = replace(
settings,
environment="production",
encryption_key=Fernet.generate_key().decode("ascii"),
)
application = create_application(settings)
async def get():
transport = httpx.ASGITransport(app=application)
async with application.router.lifespan_context(application):
async with httpx.AsyncClient(
transport=transport, base_url="https://testserver"
) as client:
return await client.get("/api/health")
response = asyncio.run(get())
assert "default-src 'self'" in response.headers["Content-Security-Policy"]
assert response.headers["Strict-Transport-Security"].startswith("max-age=31536000")
+20 -1
View File
@@ -21,7 +21,7 @@ from backend.data.policy import DataPolicyError, DataSourcePolicy
from backend.data.providers.ifind import IfindProvider
from backend.data.providers.tushare import TushareProvider
from backend.data.repository import MarketRepository
from backend.data.sentiment import calculate_sentiment
from backend.data.sentiment import _phase, calculate_sentiment
from backend.database.connection import Database
from backend.database.migrations import MIGRATIONS, MigrationRunner
from backend.features.market.snapshot import build_snapshot
@@ -403,6 +403,25 @@ def test_sentiment_has_all_weighted_components_and_extreme_risk_cap() -> None:
}
def test_sentiment_phase_transitions_require_confirmed_recovery_and_fermentation() -> None:
phase, _reason = _phase(
{"phase": "冰点"}, 28, 3, 40, 45, 50, "修复", False
)
assert phase == "冰点"
phase, _reason = _phase(
{"phase": "修复", "fermentation_signal_count": 0},
55,
8,
60,
55,
60,
"发酵",
False,
)
assert phase == "修复"
def test_incomplete_daily_snapshot_is_rejected_without_overwriting(tmp_path) -> None:
database = Database(tmp_path / "sync.db")
MigrationRunner(database).upgrade(MIGRATIONS)