Files
PolyWeather/tests/test_deployment_runtime_config.py
T
2026-07-02 20:25:40 +08:00

458 lines
21 KiB
Python

from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_frontend_dockerfile_uses_standalone_multistage_runtime():
dockerfile = (ROOT / "frontend" / "Dockerfile").read_text(encoding="utf-8")
next_config = (ROOT / "frontend" / "next.config.mjs").read_text(encoding="utf-8")
assert 'output: "standalone"' in next_config
assert "AS deps" in dockerfile
assert "AS builder" in dockerfile
assert "AS runner" in dockerfile
assert "npm ci" in dockerfile
assert "npm prune --omit=dev" in dockerfile or "npm ci --omit=dev" in dockerfile
assert ".next/standalone" in dockerfile
assert "CMD [\"node\", \"server.js\"]" in dockerfile
def test_frontend_dockerfile_accepts_turnstile_site_key_build_arg():
dockerfile = (ROOT / "frontend" / "Dockerfile").read_text(encoding="utf-8")
assert "ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY" in dockerfile
assert "ENV NEXT_PUBLIC_TURNSTILE_SITE_KEY=$NEXT_PUBLIC_TURNSTILE_SITE_KEY" in dockerfile
def test_nginx_proxy_buffers_cover_supabase_auth_cookies():
nginx_conf = (ROOT / "deploy" / "nginx" / "polyweather.conf").read_text(
encoding="utf-8"
)
assert "proxy_buffer_size 16k;" in nginx_conf
assert "proxy_buffers 8 16k;" in nginx_conf
assert "proxy_busy_buffers_size 32k;" in nginx_conf
def test_scan_terminal_prewarm_is_lazy_by_default():
app_factory = (ROOT / "web" / "app_factory.py").read_text(encoding="utf-8")
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED" in app_factory
assert "start_scan_terminal_prewarm()" not in app_factory.replace(
"if _scan_terminal_prewarm_enabled():\n start_scan_terminal_prewarm()",
"",
)
def test_scan_terminal_prewarm_only_runs_for_web_service(monkeypatch):
from web import app_factory
monkeypatch.setenv("POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED", "true")
monkeypatch.delenv("POLYWEATHER_SERVICE_ROLE", raising=False)
assert app_factory._scan_terminal_prewarm_enabled() is False
monkeypatch.setenv("POLYWEATHER_SERVICE_ROLE", "bot")
assert app_factory._scan_terminal_prewarm_enabled() is False
monkeypatch.setenv("POLYWEATHER_SERVICE_ROLE", "web")
assert app_factory._scan_terminal_prewarm_enabled() is True
def test_observation_collector_only_runs_for_collector_service(monkeypatch):
from web import app_factory
monkeypatch.delenv("POLYWEATHER_SERVICE_ROLE", raising=False)
monkeypatch.delenv("POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED", raising=False)
assert app_factory._observation_collector_enabled() is False
monkeypatch.setenv("POLYWEATHER_SERVICE_ROLE", "bot")
assert app_factory._observation_collector_enabled() is False
monkeypatch.setenv("POLYWEATHER_SERVICE_ROLE", "web")
assert app_factory._observation_collector_enabled() is False
monkeypatch.setenv("POLYWEATHER_SERVICE_ROLE", "collector")
assert app_factory._observation_collector_enabled() is True
monkeypatch.setenv("POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED", "false")
assert app_factory._observation_collector_enabled() is False
def test_docker_compose_isolates_collector_from_web_and_bot_services():
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
bot_block = compose.split(" polyweather:", 1)[1].split(
"\n polyweather_frontend:",
1,
)[0]
web_block = compose.split(" polyweather_web:", 1)[1].split(
"\n polyweather_collector:",
1,
)[0]
collector_block = compose.split(" polyweather_collector:", 1)[1].split(
"\n polyweather_warmer:",
1,
)[0]
warmer_block = compose.split(" polyweather_warmer:", 1)[1].split(
"\n polyweather_training_settlement:",
1,
)[0]
training_settlement_block = compose.split(
" polyweather_training_settlement:",
1,
)[1].split(
"\n polyweather_weathernext2_worker:",
1,
)[0]
weathernext2_block = compose.split(
" polyweather_weathernext2_worker:",
1,
)[1].split(
"\nx-polyweather-base:",
1,
)[0]
assert "POLYWEATHER_SERVICE_ROLE: web" in compose
assert "POLYWEATHER_SERVICE_ROLE: bot" in compose
assert "POLYWEATHER_SERVICE_ROLE: collector" in collector_block
assert "POLYWEATHER_SERVICE_ROLE: warmer" in warmer_block
assert "POLYWEATHER_SERVICE_ROLE: training_settlement" in training_settlement_block
assert "POLYWEATHER_SERVICE_ROLE: weathernext2_worker" in weathernext2_block
assert "redis-server --appendonly yes --maxmemory ${POLYWEATHER_REDIS_MAXMEMORY:-512mb} --maxmemory-policy noeviction" in compose
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in bot_block
assert "POLYWEATHER_EVENT_STORE: ${POLYWEATHER_EVENT_STORE:-redis}" in web_block
assert "POLYWEATHER_REDIS_REQUIRED: ${POLYWEATHER_REDIS_REQUIRED:-true}" in web_block
assert "POLYWEATHER_REDIS_STREAM_MAXLEN: ${POLYWEATHER_REDIS_STREAM_MAXLEN:-100000}" in web_block
assert "POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED: ${POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED:-true}" in web_block
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: ${POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED:-false}" in web_block
assert "POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC: '30'" in web_block
assert "POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS: ${POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS:-4}" in web_block
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in collector_block
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in warmer_block
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in bot_block
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in web_block
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'true'" in collector_block
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in warmer_block
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in training_settlement_block
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in weathernext2_block
assert "command: python -m web.training_settlement_worker" in training_settlement_block
assert "command: python -m web.weathernext2_worker" in weathernext2_block
assert (
"POLYWEATHER_TRAINING_SETTLEMENT_INTERVAL_SEC: "
"${POLYWEATHER_TRAINING_SETTLEMENT_INTERVAL_SEC:-21600}"
in training_settlement_block
)
assert (
"POLYWEATHER_TRAINING_SETTLEMENT_LOOKBACK_DAYS: "
"${POLYWEATHER_TRAINING_SETTLEMENT_LOOKBACK_DAYS:-10}"
in training_settlement_block
)
assert "POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY:-3}" in web_block
assert "POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY:-3}" in web_block
assert "POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS:-3000}" in web_block
assert "POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS:-8000}" in web_block
assert "UVICORN_WORKERS: ${UVICORN_WORKERS:-2}" in web_block
assert "WEATHERNEXT2_ENABLED: ${WEATHERNEXT2_ENABLED:-1}" in web_block
assert "WEATHERNEXT2_ENABLED: ${WEATHERNEXT2_ENABLED:-1}" in weathernext2_block
assert "WEATHERNEXT2_BACKEND: ${WEATHERNEXT2_BACKEND:-gcs_zarr}" in weathernext2_block
assert (
"WEATHERNEXT2_GCS_ZARR_URI: "
"${WEATHERNEXT2_GCS_ZARR_URI:-gs://weathernext/weathernext_2_0_0/zarr}"
in weathernext2_block
)
assert (
"WEATHERNEXT2_MODEL_DIR: "
"${WEATHERNEXT2_MODEL_DIR:-/app/data/models/weathernext2_calibrator}"
in weathernext2_block
)
assert (
"GOOGLE_APPLICATION_CREDENTIALS: "
"${GOOGLE_APPLICATION_CREDENTIALS:-/app/secrets/gcp-sa.json}"
in weathernext2_block
)
assert "./secrets:/app/secrets:ro" in weathernext2_block
assert "POLYWEATHER_COLLECTOR_PATCH_ENDPOINT: ''" in bot_block
assert "POLYWEATHER_COLLECTOR_PATCH_ENDPOINT: ''" in web_block
assert (
"POLYWEATHER_COLLECTOR_PATCH_ENDPOINT: "
"${POLYWEATHER_COLLECTOR_PATCH_ENDPOINT:-http://polyweather_web:8000/api/internal/collector-patch}"
in collector_block
)
assert "command: python -m web.observation_collector_worker" in collector_block
assert "command: python -m web.cache_warmer_worker" in warmer_block
assert "POLYWEATHER_OBSERVATION_COLLECTOR_AMSC_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_AMSC_SEC:-60}" in collector_block
assert "POLYWEATHER_OBSERVATION_COLLECTOR_CACHE_REFRESH_WORKERS: ${POLYWEATHER_OBSERVATION_COLLECTOR_CACHE_REFRESH_WORKERS:-2}" in collector_block
assert "POLYWEATHER_WARMER_ENABLED: ${POLYWEATHER_WARMER_ENABLED:-true}" in warmer_block
assert "POLYWEATHER_WARMER_TICK_SEC: ${POLYWEATHER_WARMER_TICK_SEC:-30}" in warmer_block
assert "POLYWEATHER_WARMER_SCAN_INTERVAL_SEC: ${POLYWEATHER_WARMER_SCAN_INTERVAL_SEC:-120}" in warmer_block
assert "POLYWEATHER_WARMER_CITY_INTERVAL_SEC: ${POLYWEATHER_WARMER_CITY_INTERVAL_SEC:-30}" in warmer_block
assert "POLYWEATHER_WARMER_CITY_BATCH_SIZE: ${POLYWEATHER_WARMER_CITY_BATCH_SIZE:-16}" in warmer_block
assert "cpus: ${POLYWEATHER_WARMER_CPUS:-0.75}" in warmer_block
assert "TELEGRAM_AIRPORT_PUSH_INTERVAL_SEC: ${POLYWEATHER_BOT_AIRPORT_PUSH_INTERVAL_SEC:-60}" in bot_block
assert "POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC:-300}" in collector_block
def test_docker_compose_exposes_cloudflare_free_runtime_env():
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
frontend_block = compose.split(" polyweather_frontend:", 1)[1].split(
"\n polyweather_web:",
1,
)[0]
web_block = compose.split(" polyweather_web:", 1)[1].split(
"\n polyweather_collector:",
1,
)[0]
assert "NEXT_PUBLIC_TURNSTILE_SITE_KEY: ${NEXT_PUBLIC_TURNSTILE_SITE_KEY:-}" in frontend_block
assert "POLYWEATHER_TURNSTILE_SECRET_KEY: ${POLYWEATHER_TURNSTILE_SECRET_KEY:-}" in frontend_block
assert "POLYWEATHER_TURNSTILE_BYPASS: ${POLYWEATHER_TURNSTILE_BYPASS:-false}" in frontend_block
assert "POLYWEATHER_R2_ACCOUNT_ID: ${POLYWEATHER_R2_ACCOUNT_ID:-}" in web_block
assert "POLYWEATHER_R2_BUCKET: ${POLYWEATHER_R2_BUCKET:-}" in web_block
assert "POLYWEATHER_R2_ACCESS_KEY_ID: ${POLYWEATHER_R2_ACCESS_KEY_ID:-}" in web_block
assert "POLYWEATHER_R2_SECRET_ACCESS_KEY: ${POLYWEATHER_R2_SECRET_ACCESS_KEY:-}" in web_block
def test_scan_terminal_backend_timeout_returns_before_next_proxy_abort():
import web.services.scan_terminal_config as scan_terminal_config
route_source = (
ROOT / "frontend" / "app" / "api" / "scan" / "terminal" / "route.ts"
).read_text(encoding="utf-8")
config_source = (
ROOT / "web" / "services" / "scan_terminal_config.py"
).read_text(encoding="utf-8")
assert 'POLYWEATHER_SCAN_TERMINAL_PROXY_TIMEOUT_MS || "60000"' in route_source
assert '"POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC",\n 30,' in config_source
assert '"POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS",\n 1,' in config_source
assert (
'"POLYWEATHER_SCAN_TERMINAL_PREWARM_PAYLOAD_TIMEOUT_SEC",\n 30,'
in config_source
)
assert scan_terminal_config.SCAN_TERMINAL_BUILD_TIMEOUT_SEC <= 30
assert (
scan_terminal_config.SCAN_TERMINAL_PREWARM_PAYLOAD_TIMEOUT_SEC
>= scan_terminal_config.SCAN_TERMINAL_BUILD_TIMEOUT_SEC
)
def test_deploy_workflow_applies_cloudflare_rules_when_token_is_available():
workflow = (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8")
assert "cloudflare-cache-rules:" in workflow
assert "needs: [python-quality]" in workflow
assert "CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}" in workflow
assert "CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}" in workflow
assert "CLOUDFLARE_ZONE_ID is not configured" in workflow
assert "python scripts/configure_cloudflare_free.py --apply" in workflow
assert workflow.index("cloudflare-cache-rules:") < workflow.index("deploy:")
def test_deploy_workflow_passes_turnstile_site_key_to_frontend_build():
workflow = (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8")
assert "NEXT_PUBLIC_TURNSTILE_SITE_KEY: ${{ secrets.NEXT_PUBLIC_TURNSTILE_SITE_KEY || '' }}" in workflow
assert '--build-arg "NEXT_PUBLIC_TURNSTILE_SITE_KEY=${NEXT_PUBLIC_TURNSTILE_SITE_KEY}"' in workflow
def test_probability_engine_uses_enriched_multi_model_snapshot():
source = (ROOT / "web" / "analysis_service.py").read_text(encoding="utf-8")
assert 'raw["multi_model"] = mm' in source
def test_city_detail_peak_window_uses_shared_multi_model_resolver():
source = (ROOT / "web" / "analysis_service.py").read_text(encoding="utf-8")
assert "from src.analysis.trend_engine import _resolve_peak_hours" in source
assert "peak_hours = _resolve_peak_hours(" in source
def test_deploy_script_retries_image_pull_for_registry_propagation():
script = (ROOT / "deploy.sh").read_text(encoding="utf-8")
assert "for pull_attempt in $(seq 1 6)" in script
assert "docker compose pull && pull_ok=1 && break" in script
def test_deploy_script_exports_backend_supabase_env_from_env_file():
script = (ROOT / "deploy.sh").read_text(encoding="utf-8")
assert "read_env_file_value()" in script
assert "resolve_env_value()" in script
assert 'resolve_env_value "SUPABASE_URL" "NEXT_PUBLIC_SUPABASE_URL"' in script
assert 'resolve_env_value "SUPABASE_ANON_KEY" "NEXT_PUBLIC_SUPABASE_ANON_KEY"' in script
assert 'export SUPABASE_URL="$resolved_supabase_url"' in script
assert 'export SUPABASE_ANON_KEY="$resolved_supabase_anon_key"' in script
assert "unset SUPABASE_URL" in script
assert "unset SUPABASE_ANON_KEY" in script
assert script.index("read_env_file_value()") < script.index('export IMAGE_TAG="$NEW_TAG"')
assert script.index('resolve_env_value "SUPABASE_URL"') < script.index("pull_ok=0")
def test_deploy_script_syncs_city_thread_ids_into_runtime_volume():
script = (ROOT / "deploy.sh").read_text(encoding="utf-8")
assert "sync_city_thread_ids()" in script
assert 'repo_file="$COMPOSE_DIR/data/city_thread_ids.json"' in script
assert 'target_file="$runtime_dir/city_thread_ids.json"' in script
assert "merged.update(target_data)" in script
assert script.index("sync_city_thread_ids") < script.index("Updating Redis dependency")
def test_deploy_script_retries_startup_smoke_checks():
script = (ROOT / "deploy.sh").read_text(encoding="utf-8")
assert "smoke_check()" in script
assert "wait_for_scan_terminal_snapshot()" in script
assert '"status":"ready"' in script
assert '"status":"stale"' in script
assert "stale snapshot available" in script
assert "http=401" in script
assert '"stale_reason":"市场扫描快照正在初始化"' in script
assert "initializing after attempt" in script
assert 'wait_for_scan_terminal_snapshot "scan terminal snapshot" "http://127.0.0.1:3001/api/scan/terminal"' in script
assert script.index("wait_for_scan_terminal_snapshot") < script.index("run_public_smoke_checks")
assert 'smoke_check "healthz" "https://api.polyweather.top/healthz" 15 3 5' in script
assert 'warm_public_route "local cities recent stats" "http://127.0.0.1:8000/api/cities?refresh_deb_recent=1"' in script
assert 'warm_public_route "scan terminal" "https://polyweather.top/api/scan/terminal"' in script
assert 'smoke_check "local cities" "http://127.0.0.1:8000/api/cities" 10 6 3' not in script
assert 'smoke_check "frontend cities" "https://polyweather.top/api/cities" 20 5 5' in script
assert 'smoke_check "frontend" "https://www.polyweather.top/" 15 3 5' in script
def test_deploy_script_rechecks_public_smoke_before_rollback():
script = (ROOT / "deploy.sh").read_text(encoding="utf-8")
assert "run_public_smoke_checks()" in script
assert "Initial public smoke failed; retrying before rollback" in script
assert "PUBLIC_SMOKE_RECHECK_DELAY_SEC" in script
assert 'smoke_check "healthz recheck" "https://api.polyweather.top/healthz"' in script
assert 'smoke_check "frontend cities recheck" "https://polyweather.top/api/cities"' in script
assert 'smoke_check "frontend recheck" "https://www.polyweather.top/"' in script
def test_deploy_script_retries_compose_recreate_races():
script = (ROOT / "deploy.sh").read_text(encoding="utf-8")
assert "compose_up_retry()" in script
assert "removal of container .* is already in progress" in script
assert 'compose_up_retry "backend services" -d --no-deps polyweather_web polyweather' in script
assert 'compose_up_retry "observation collector" -d --no-deps polyweather_collector' in script
assert 'compose_up_retry "cache warmer" -d --no-deps polyweather_warmer' in script
assert 'compose_up_retry "training settlement" -d --no-deps polyweather_training_settlement' in script
assert 'compose_up_retry "WeatherNext2 worker" -d --no-deps polyweather_weathernext2_worker' in script
assert 'compose_up_retry "frontend" -d --no-deps polyweather_frontend' in script
def test_deploy_token_is_passed_over_stdin_not_process_args():
script = (ROOT / "deploy.sh").read_text(encoding="utf-8")
workflow = (ROOT / ".github" / "workflows" / "ci.yml").read_text(
encoding="utf-8"
)
assert 'NEW_TAG="${1:-latest}"' in script
assert 'GHCR_PAT="$1"' not in script
assert "read -r GHCR_PAT" in script
assert 'printf \'%s\' "$GHCR_PAT" | docker login' in script
assert "GHCR_PAT: ${{ secrets.GHCR_PAT }}" in workflow
assert 'printf \'%s\\n\' "$GHCR_PAT" | ssh' in workflow
assert "bash /tmp/deploy.sh '${{ github.sha }}'" in workflow
assert "bash /tmp/deploy.sh '${{ secrets.GHCR_PAT }}'" not in workflow
def test_deployment_helpers_do_not_reference_retired_vps_ip():
retired_ip = "38.54.27.70"
deploy_ps1 = (ROOT / "deploy.ps1").read_text(encoding="utf-8")
cache_script = (ROOT / "scripts" / "validate_frontend_cache.sh").read_text(
encoding="utf-8"
)
assert retired_ip not in deploy_ps1
assert retired_ip not in cache_script
assert 'BASE_URL="${1:-https://polyweather.top}"' in cache_script
def test_docker_compose_keeps_polyweather_ports_on_loopback():
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
assert "127.0.0.1:3001:3000" in compose
assert "127.0.0.1:8000:8000" in compose
assert "\n - 3001:3000" not in compose
assert "\n - 8000:8000" not in compose
def test_frontend_proxy_uses_internal_backend_url_not_public_site():
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
script = (ROOT / "deploy.sh").read_text(encoding="utf-8")
frontend_block = compose.split(" polyweather_frontend:", 1)[1].split(
"\n polyweather_web:",
1,
)[0]
assert "POLYWEATHER_API_BASE_URL: http://polyweather_web:8000" in frontend_block
assert (
"POLYWEATHER_API_BASE_URL: ${POLYWEATHER_API_BASE_URL:-http://polyweather_web:8000}"
not in frontend_block
)
assert 'export POLYWEATHER_API_BASE_URL="${POLYWEATHER_FRONTEND_INTERNAL_API_BASE_URL:-http://polyweather_web:8000}"' in script
assert script.index("export POLYWEATHER_API_BASE_URL=") < script.rindex(
"validate_frontend_api_base_url"
)
def test_frontend_and_web_share_supabase_forwarded_identity_secrets():
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
script = (ROOT / "deploy.sh").read_text(encoding="utf-8")
frontend_block = compose.split(" polyweather_frontend:", 1)[1].split(
"\n polyweather_web:",
1,
)[0]
web_block = compose.split(" polyweather_web:", 1)[1].split(
"\n polyweather_collector:",
1,
)[0]
assert (
"POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN: ${POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN}"
in frontend_block
)
assert "SUPABASE_URL: ${SUPABASE_URL}" in web_block
assert "SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY}" in web_block
assert "SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY}" in web_block
assert (
"POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN: ${POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN}"
in web_block
)
assert 'resolve_env_value "SUPABASE_URL" "NEXT_PUBLIC_SUPABASE_URL"' in script
assert 'export SUPABASE_URL="$resolved_supabase_url"' in script
assert (
'resolve_env_value "SUPABASE_ANON_KEY" "NEXT_PUBLIC_SUPABASE_ANON_KEY"'
in script
)
assert 'export SUPABASE_ANON_KEY="$resolved_supabase_anon_key"' in script
def test_web_container_raises_open_file_limit_for_sse_and_proxy_load():
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
web_block = compose.split(" polyweather_web:", 1)[1].split(
"\nx-polyweather-base:",
1,
)[0]
assert "ulimits:" in web_block
assert "nofile:" in web_block
assert "soft: 65535" in web_block
assert "hard: 65535" in web_block
def test_city_detail_builds_deb_hourly_consensus_before_peak_window():
source = (ROOT / "web" / "analysis_service.py").read_text(encoding="utf-8")
assert "from src.analysis.deb_hourly_consensus import build_deb_hourly_consensus_path" in source
assert "deb_hourly_consensus = build_deb_hourly_consensus_path(" in source
assert '"hourly_consensus": deb_hourly_consensus' in source
assert 'deb_base_source = "deb_hourly_consensus"' in source
assert "base_source=deb_base_source" in source