Files
geointel/backend/tests/test_docker_runtime_config.py
T
Jens be2e092b33
GeoIntel release gates / Compile, test, contracts and builds (push) Failing after 20s
GeoIntel release gates / Python and npm vulnerability policy (push) Failing after 22s
GeoIntel release gates / GIS image, SBOM and container scan (push) Failing after 2m31s
Recover governed runtime provenance for legacy YOLO models
2026-08-23 23:22:02 +02:00

439 lines
20 KiB
Python

from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_backend_dockerfile_copies_package_sources_before_pip_install() -> None:
dockerfile = ROOT / "backend" / "Dockerfile"
lines = dockerfile.read_text(encoding="utf-8").splitlines()
pip_install_index = lines.index('RUN extras=".[gis]" \\')
preceding = "\n".join(lines[:pip_install_index])
assert "COPY pyproject.toml README.md /app/" in preceding
assert "COPY app /app/app" in preceding
def test_backend_dockerfile_installs_approved_gis_runtime_stack() -> None:
dockerfile = (ROOT / "backend" / "Dockerfile").read_text(encoding="utf-8")
assert "ARG GEOINTEL_INSTALL_AI=false" in dockerfile
assert 'extras=".[gis]"' in dockerfile
assert 'extras=".[gis,ai]"' in dockerfile
assert "RUN python scripts/gis_import_smoke.py" in dockerfile
assert "gdal-bin" in dockerfile
assert "libgdal-dev" in dockerfile
assert "libgeos-dev" in dockerfile
assert "libproj-dev" in dockerfile
assert "proj-bin" in dockerfile
assert "libxcb1" in dockerfile
assert "libgl1" in dockerfile
assert "libglib2.0-0" in dockerfile
def test_backend_pyproject_exposes_gis_optional_dependency_group() -> None:
pyproject = (ROOT / "backend" / "pyproject.toml").read_text(encoding="utf-8")
assert "gis = [" in pyproject
assert '"rasterio>=1.4.3"' in pyproject
assert '"geopandas>=1.0.1"' in pyproject
assert '"pyogrio>=0.10.0"' in pyproject
assert '"ultralytics>=8.3,<9"' not in pyproject.split("gis = [", 1)[1].split("]", 1)[0]
def test_all_in_one_dockerfile_can_opt_into_ai_dependencies_without_base_install() -> None:
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
assert "ARG GEOINTEL_INSTALL_AI=false" in dockerfile
assert "COPY backend/pyproject.toml /app/" in dockerfile
assert "COPY backend/requirements-runtime.lock /app/" in dockerfile
assert "COPY backend/pyproject.toml backend/README.md /app/" not in dockerfile
assert "GeoIntel backend package metadata" in dockerfile
assert "--require-hashes -r requirements-runtime.lock" in dockerfile
assert "ARG GEOINTEL_ULTRALYTICS_VERSION=" in dockerfile
assert '"ultralytics==$GEOINTEL_ULTRALYTICS_VERSION"' in dockerfile
assert "python scripts/gis_import_smoke.py" in dockerfile
assert "yolo_preflight.py" in dockerfile
assert "libxcb1" in dockerfile
assert "libgl1" in dockerfile
assert "libglib2.0-0" in dockerfile
def test_all_in_one_dockerfile_copies_operator_scripts_for_runtime_use() -> None:
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
for line in dockerfile.splitlines():
if line.startswith("COPY scripts/"):
source_path = line.split()[1]
assert (ROOT / source_path).is_file()
required_runtime_scripts = {
"prepare_operator_real_data_samples.py",
"export_operator_yolo_tile_dataset.py",
"audit_operator_yolo_dataset_quality.py",
"render_operator_yolo_label_qa_contact_sheets.py",
"train_operator_yolo_detector.sh",
"training_dataset_eligibility.py",
"training_release_manifest.py",
"verify_real_data_detection_qa_workflow.sh",
"run_detection_quality_matrix.sh",
"run_multi_sample_detection_quality_matrix.sh",
"run_mol_operational_validation.sh",
"export_detection_calibration_evidence.sh",
"assemble_detection_calibration_evidence_portfolio.sh",
"build_fixed_threshold_evidence_portfolio_inputs.py",
"audit_detection_false_negative_evidence.py",
"audit_detection_false_positive_evidence.py",
"render_detection_false_positive_review_contact_sheets.py",
"render_detection_false_negative_review_contact_sheets.py",
"validate_detection_false_positive_review_decisions.py",
"validate_detection_false_negative_review_decisions.py",
"run_operator_hard_negative_detection_matrix.sh",
"run_background_corpus_split_matrix.sh",
"build_background_corpus_split_report.py",
"build_detection_model_promotion_report.py",
"run_split_background_promotion_workflow.sh",
"activate_promoted_yolo_candidate.py",
"migrate_runtime_model_provenance.py",
"manage_grb_refresh.py",
"orthophoto_release_preflight.py",
"provision_walous_sources.py",
"provision_spw_terrain_source.py",
}
for script_name in required_runtime_scripts:
assert f"COPY scripts/{script_name} /app/scripts/{script_name}" in dockerfile
def test_all_in_one_dockerfile_copies_operator_scripts_after_dependency_install() -> None:
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
dependency_install_index = dockerfile.index('/usr/bin/python3.11 -m venv /opt/geointel/venv \\')
operator_copy_index = dockerfile.index(
"COPY scripts/render_operator_yolo_label_qa_contact_sheets.py "
"/app/scripts/render_operator_yolo_label_qa_contact_sheets.py"
)
assert operator_copy_index > dependency_install_index
def test_compose_does_not_require_missing_root_env_file() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
assert "env_file:" not in compose
assert "DATABASE_URL: postgresql+psycopg://${GEOINTEL_POSTGRES_USER:-geointel}" in compose
def test_compose_exposes_frontend_on_configurable_host_port_with_cors_origin() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
env_example = (ROOT / ".env.example").read_text(encoding="utf-8")
assert '"${GEOINTEL_FRONTEND_PORT:-1202}:80"' in compose
assert '"${GEOINTEL_BACKEND_PORT:-8000}:8000"' in compose
assert "CORS_ORIGINS: ${GEOINTEL_CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}" in compose
assert "GEOINTEL_FRONTEND_PORT=1202" in env_example
assert "GEOINTEL_BACKEND_PORT=8000" in env_example
assert "http://localhost:1202" in env_example
assert "http://127.0.0.1:1202" in env_example
def test_env_example_uses_runtime_env_names_read_by_backend_and_frontend() -> None:
env_example = (ROOT / ".env.example").read_text(encoding="utf-8")
assert "GEOINTEL_INSTALL_AI=false" in env_example
assert "YOLO_ENABLED=false" in env_example
assert "YOLO_MODELS_DIR=/app/models" in env_example
assert "YOLO_MODEL_PATH=" in env_example
assert "YOLO_CONFIG_DIR=./storage/ultralytics" in env_example
assert "YOLO_MAX_TILES=100" in env_example
assert "YOLO_MAX_DETECTIONS=1000" in env_example
assert "YOLO_DUPLICATE_IOU_THRESHOLD=0.5" in env_example
assert "ENABLE_YOLO" not in env_example
assert "ENABLE_SAM" not in env_example
assert "VITE_API_BASE_URL=" in env_example
assert "VITE_API_PROXY_TARGET=http://localhost:8000" in env_example
def test_walloon_runtime_settings_are_editable_in_compose_and_unraid() -> None:
files = [
(ROOT / "docker-compose.yml").read_text(encoding="utf-8"),
(ROOT / "docker-compose.unraid.yml").read_text(encoding="utf-8"),
(ROOT / "deploy" / "unraid" / "geointel.env.example").read_text(encoding="utf-8"),
(ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8"),
(ROOT / "deploy" / "unraid" / "geointel-unraid-template.xml").read_text(encoding="utf-8"),
]
for content in files:
assert "SPW_FLOOD_HAZARD_ENABLED" in content
assert "SPW_FLOOD_HAZARD_MAPSERVER_URL" in content
assert "WALOUS_ENABLED" in content
assert "WALOUS_SOURCE_DIR" in content
assert "WALOUS_ANALYSIS_RESOLUTION_M" in content
assert "WALOUS_MAX_SIDE_M" in content
assert "WALOUS_MAX_PIXELS" in content
def test_frontend_uses_same_origin_api_proxy_by_default() -> None:
api_client = (ROOT / "frontend" / "src" / "services" / "api" / "client.ts").read_text(encoding="utf-8")
nginx_config = (ROOT / "frontend" / "nginx.conf").read_text(encoding="utf-8")
dockerfile = (ROOT / "frontend" / "Dockerfile").read_text(encoding="utf-8")
assert '?? ""' in api_client
assert "http://localhost:8000" not in api_client
assert "FROM nginx:" in dockerfile
assert "COPY --from=build /app/dist /usr/share/nginx/html" in dockerfile
assert "location /api/" in nginx_config
assert "proxy_pass http://backend:8000/api/" in nginx_config
assert "location = /health" in nginx_config
assert 'add_header Cache-Control "no-cache"' in nginx_config
assert "location /assets/" in nginx_config
assert "try_files $uri $uri/ /index.html" in nginx_config
def test_nginx_runtime_allows_real_gis_upload_payloads() -> None:
frontend_nginx = (ROOT / "frontend" / "nginx.conf").read_text(encoding="utf-8")
all_in_one_nginx = (ROOT / "deploy" / "unraid" / "nginx-all-in-one.conf").read_text(encoding="utf-8")
start_script = (ROOT / "deploy" / "unraid" / "all-in-one-start.sh").read_text(encoding="utf-8")
assert "client_max_body_size 250m;" in frontend_nginx
assert "client_max_body_size __GEOINTEL_MAX_UPLOAD_MB__m;" in all_in_one_nginx
assert 'sed -i "s/__GEOINTEL_MAX_UPLOAD_MB__/${MAX_UPLOAD_MB}/g"' in start_script
def test_nginx_runtime_allows_long_ai_and_qa_requests() -> None:
frontend_nginx = (ROOT / "frontend" / "nginx.conf").read_text(encoding="utf-8")
all_in_one_nginx = (ROOT / "deploy" / "unraid" / "nginx-all-in-one.conf").read_text(encoding="utf-8")
for config in (frontend_nginx, all_in_one_nginx):
assert "proxy_read_timeout 600s;" in config
assert "proxy_send_timeout 600s;" in config
def test_compose_does_not_publish_postgis_on_default_host_port() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
assert '"5432:5432"' not in compose
def test_compose_waits_for_healthy_database_and_applies_migrations() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
assert "pg_isready -U ${GEOINTEL_POSTGRES_USER:-geointel} -d ${GEOINTEL_POSTGRES_DB:-geointel}" in compose
assert "condition: service_healthy" in compose
assert "sh /app/docker_start.sh" in compose
def test_compose_mounts_demo_fixtures_for_backend_runtime() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
assert "./fixtures:/app/fixtures:ro" in compose
def test_compose_has_backend_and_frontend_healthchecks() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
assert "http://127.0.0.1:8000/health/ready" in compose
assert "urllib.request.urlopen" in compose
assert "http://127.0.0.1/health/ready" in compose
assert "wget -q -O -" in compose
assert "start_period: 30s" in compose
assert "start_period: 10s" in compose
def test_frontend_waits_for_healthy_backend_in_compose() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
frontend_section = compose.split(" frontend:", 1)[1]
assert "backend:" in frontend_section
assert "condition: service_healthy" in frontend_section
def test_backend_docker_start_script_waits_for_sql_connection_before_migrations() -> None:
script = (ROOT / "backend" / "docker_start.sh").read_text(encoding="utf-8")
assert "Waiting for database connection" in script
assert "create_engine(settings.database_url" in script
assert "SELECT 1" in script
assert "python -m alembic upgrade head" in script
assert "uvicorn app.main:app --host 0.0.0.0 --port 8000" in script
def test_runtime_sets_writable_ultralytics_config_directory() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
unraid_compose = (ROOT / "docker-compose.unraid.yml").read_text(encoding="utf-8")
start_script = (ROOT / "deploy" / "unraid" / "all-in-one-start.sh").read_text(encoding="utf-8")
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
unraid_env = (ROOT / "deploy" / "unraid" / "geointel.env.example").read_text(encoding="utf-8")
assert "YOLO_CONFIG_DIR: ${YOLO_CONFIG_DIR:-/app/storage/ultralytics}" in compose
assert "YOLO_CONFIG_DIR: ${YOLO_CONFIG_DIR:-/app/storage/ultralytics}" in unraid_compose
assert 'export YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-$STORAGE_ROOT/ultralytics}"' in start_script
assert 'mkdir -p "$PGDATA" "$STORAGE_ROOT" "$YOLO_CONFIG_DIR"' in start_script
assert 'YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-/app/storage/ultralytics}"' in run_script
assert '-e YOLO_CONFIG_DIR="$YOLO_CONFIG_DIR"' in run_script
assert "YOLO_CONFIG_DIR=/app/storage/ultralytics" in unraid_env
def test_regional_official_vector_sources_are_configurable_in_every_runtime() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
unraid_compose = (ROOT / "docker-compose.unraid.yml").read_text(encoding="utf-8")
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(
encoding="utf-8"
)
env_example = (ROOT / "deploy" / "unraid" / "geointel.env.example").read_text(
encoding="utf-8"
)
template = (ROOT / "deploy" / "unraid" / "geointel-unraid-template.xml").read_text(
encoding="utf-8"
)
for key in (
"SPW_PICC_ENABLED",
"SPW_PICC_MAPSERVER_URL",
"URBIS_ENABLED",
"URBIS_WFS_URL",
):
assert key in compose
assert key in unraid_compose
assert f'{key}="${{{key}:-' in run_script
assert f'-e {key}="${key}"' in run_script
assert f"{key}=" in env_example
assert f'Target="{key}"' in template
def test_segmentation_and_mdk_acquisition_are_configurable_in_every_runtime() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
unraid_compose = (ROOT / "docker-compose.unraid.yml").read_text(encoding="utf-8")
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
env_example = (ROOT / ".env.example").read_text(encoding="utf-8")
unraid_env = (ROOT / "deploy" / "unraid" / "geointel.env.example").read_text(encoding="utf-8")
template = (ROOT / "deploy" / "unraid" / "geointel-unraid-template.xml").read_text(encoding="utf-8")
for key in (
"YOLO_SEG_ENABLED",
"YOLO_SEG_MODEL_PATH",
"SAM_ENABLED",
"SAM_MODEL_PATH",
"SEGMENTATION_MAX_MASKS_PER_TILE",
"SEGMENTATION_DUPLICATE_IOU_THRESHOLD",
"MDK_BATHYMETRY_ACQUISITION_ENABLED",
"MDK_BATHYMETRY_COVERAGE_ID",
"MDK_BATHYMETRY_MAX_BBOX_DEG2",
):
assert key in compose, key
assert key in unraid_compose, key
assert f'{key}="${{{key}:-' in run_script, key
assert f'-e {key}="${key}"' in run_script, key
assert f"{key}=" in env_example, key
assert f"{key}=" in unraid_env, key
assert f'Target="{key}"' in template, key
def test_compose_reconciles_interrupted_runs_after_restart_like_unraid_runtime() -> None:
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
start_script = (ROOT / "deploy" / "unraid" / "all-in-one-start.sh").read_text(encoding="utf-8")
assert (
"GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP: "
"${GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP:-true}"
) in compose
assert (
'GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP='
'"${GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP:-true}"'
) in start_script
def test_docker_build_contexts_exclude_vendor_build_and_cache_outputs() -> None:
required_patterns = {
"node_modules",
"dist",
"__pycache__",
"*.pyc",
".pytest_cache",
}
for relative_path in ("backend/.dockerignore", "frontend/.dockerignore"):
content = (ROOT / relative_path).read_text(encoding="utf-8")
for pattern in required_patterns:
assert pattern in content
def test_browser_runtime_verification_script_detects_proxy_contract() -> None:
script = (ROOT / "scripts" / "verify_browser_runtime.sh").read_text(encoding="utf-8")
assert "/api/v1/projects" in script
assert "<!doctype html" in script
assert "canonical GeoIntel envelope" in script
assert '"status":"ok"' in script
def test_gis_runtime_verification_script_detects_required_capabilities() -> None:
script = (ROOT / "scripts" / "verify_gis_runtime.sh").read_text(encoding="utf-8")
assert "/api/v1/system/capabilities" in script
assert '"postgis":true' in script
assert '"rasterio":true' in script
assert '"geopandas":true' in script
assert "<!doctype html" in script
def test_gis_import_smoke_script_checks_runtime_imports() -> None:
docker_script = (ROOT / "backend" / "scripts" / "gis_import_smoke.py").read_text(encoding="utf-8")
root_wrapper = (ROOT / "scripts" / "gis_import_smoke.py").read_text(encoding="utf-8")
assert 'REQUIRED_MODULES = ("rasterio", "geopandas", "pyogrio")' in docker_script
assert "importlib.import_module" in docker_script
assert '"gis_imports"' in docker_script
assert 'ROOT / "backend" / "scripts"' in root_wrapper
assert "from gis_import_smoke import main" in root_wrapper
def test_backend_docker_context_contains_gis_import_smoke_script() -> None:
assert (ROOT / "backend" / "scripts" / "gis_import_smoke.py").exists()
def test_all_in_one_dockerfile_caches_dependencies_and_pins_driver_compatible_cuda_torch() -> None:
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
metadata_copy_index = dockerfile.index("COPY backend/pyproject.toml /app/")
placeholder_readme_index = dockerfile.index("GeoIntel backend package metadata")
dependency_install_index = dockerfile.index('/usr/bin/python3.11 -m venv /opt/geointel/venv \\')
backend_copy_index = dockerfile.index("COPY backend/ /app/")
smoke_index = dockerfile.index("RUN python scripts/gis_import_smoke.py")
assert metadata_copy_index < placeholder_readme_index < dependency_install_index < backend_copy_index < smoke_index
assert "GEOINTEL_TORCH_INDEX_URL=https://download.pytorch.org/whl/cu128" in dockerfile
assert "GEOINTEL_TORCH_VERSION=2.11.0" in dockerfile
assert "GEOINTEL_TORCHVISION_VERSION=0.26.0" in dockerfile
assert '--index-url "$GEOINTEL_TORCH_INDEX_URL"' in dockerfile
def test_unraid_deploy_passes_ai_build_arg_and_yolo_runtime_env() -> None:
deploy_ps1 = (ROOT / "scripts" / "deploy_tower.ps1").read_text(encoding="utf-8")
deploy_sh = (ROOT / "scripts" / "deploy_tower.sh").read_text(encoding="utf-8")
release_script = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text(encoding="utf-8")
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
assert 'DEPLOY_GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-}"' in deploy_sh
assert "--build-arg GEOINTEL_INSTALL_AI=" in release_script
assert "DEPLOY_GEOINTEL_INSTALL_AI" in deploy_ps1
assert "[string]$InstallAi" in deploy_ps1
assert 'YOLO_ENABLED="${YOLO_ENABLED:-false}"' in run_script
assert '-e YOLO_ENABLED="$YOLO_ENABLED"' in run_script
assert 'YOLO_MODELS_DIR="${YOLO_MODELS_DIR:-/app/models}"' in run_script
assert '-e YOLO_MODELS_DIR="$YOLO_MODELS_DIR"' in run_script
assert '-e YOLO_MODEL_PATH="$YOLO_MODEL_PATH"' in run_script
assert '-e YOLO_MAX_TILES="$YOLO_MAX_TILES"' in run_script
assert '-e YOLO_MAX_DETECTIONS="$YOLO_MAX_DETECTIONS"' in run_script
assert '-e YOLO_DUPLICATE_IOU_THRESHOLD="$YOLO_DUPLICATE_IOU_THRESHOLD"' in run_script
assert "-v \"${GEOINTEL_MODELS_PATH}:/app/models\"" in run_script
def test_unraid_ai_runtime_requests_nvidia_and_fails_closed() -> None:
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
assert "--gpus all" in run_script
assert 'YOLO_DEVICE="${YOLO_DEVICE:-cuda:0}"' in run_script
assert 'YOLO_REQUIRE_CUDA="${YOLO_REQUIRE_CUDA:-true}"' in run_script
assert '-e YOLO_REQUIRE_CUDA="$YOLO_REQUIRE_CUDA"' in run_script
assert "https://download.pytorch.org/whl/cu128" in dockerfile