from __future__ import annotations import importlib.util from pathlib import Path import sys from types import SimpleNamespace from uuid import uuid4 import pytest from fastapi import HTTPException from fastapi.testclient import TestClient from app.api.routes import projects as project_routes from app.main import app from app.schemas.project import ProjectRead, ProjectUpdate from app.services.project_service import ProjectService ROOT = Path(__file__).resolve().parents[2] def load_cleanup_module(): script = ROOT / "scripts" / "archive_technical_projects.py" spec = importlib.util.spec_from_file_location("archive_technical_projects_test", script) assert spec is not None and spec.loader is not None module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module def test_project_list_defaults_to_active_and_can_request_archived(monkeypatch) -> None: captured: list[str] = [] def fake_list(_db, *, limit, offset, name, project_status): del limit, offset, name captured.append(project_status) return [ ProjectRead( id=uuid4(), name=f"{project_status} project", region="Kempen", status=project_status if project_status != "all" else "active", ) ], 1 monkeypatch.setattr(ProjectService, "list_projects", fake_list) client = TestClient(app) assert client.get("/api/v1/projects").status_code == 200 assert client.get("/api/v1/projects", params={"status": "archived"}).status_code == 200 assert captured == ["active", "archived"] def test_project_update_schema_allows_only_active_or_archived() -> None: from pydantic import ValidationError from app.schemas.project import ProjectUpdate assert ProjectUpdate(status="archived").status == "archived" try: ProjectUpdate(status="deleted") except ValidationError: pass else: raise AssertionError("ProjectUpdate must not expose deleted as an ordinary lifecycle state") def test_project_update_returns_404_when_project_is_missing(monkeypatch) -> None: monkeypatch.setattr(ProjectService, "update_project", lambda *_args, **_kwargs: None) with pytest.raises(HTTPException) as exc_info: project_routes.update_project(uuid4(), ProjectUpdate(status="archived"), db=SimpleNamespace()) assert exc_info.value.status_code == 404 def test_cleanup_allowlist_preserves_real_workspaces() -> None: module = load_cleanup_module() assert module.is_technical_project_name("GeoIntel Detection Quality Matrix 42") assert module.is_technical_project_name("GeoIntel hard-negative Mol 20260709") assert module.is_technical_project_name("GeoIntel Detection Calibration 0.15 20260709T090018Z") assert module.is_technical_project_name("GeoIntel Real Data Validation 20260707T000620Z") assert module.is_technical_project_name("GeoIntel Operational YOLO Geel Smoke 20260711T133656Z") assert module.is_technical_project_name("GeoIntel Demo - Building QA") assert not module.is_technical_project_name("Kempen Regional Workbench") assert not module.is_technical_project_name("Mol Municipality Workbench") assert not module.is_technical_project_name("Vrij project van een gebruiker") def test_cleanup_plan_selects_active_allowlisted_projects_only() -> None: module = load_cleanup_module() rows = [ SimpleNamespace( id=uuid4(), name="GeoIntel Detection Quality Matrix 1", status="active", ), SimpleNamespace( id=uuid4(), name="Kempen Regional Workbench", status="active", ), ] class Query: def filter(self, *_args): return self def order_by(self, *_args): return self def all(self): return rows class Session: def query(self, _model): return Query() plan = module.build_archive_plan(Session()) assert plan.count == 1 assert plan.names == ("GeoIntel Detection Quality Matrix 1",) def test_cleanup_script_is_packaged_and_readiness_checked() -> None: dockerfile = (ROOT / "deploy/unraid/Dockerfile.all-in-one").read_text(encoding="utf-8") readiness = (ROOT / "scripts/run_readiness_check.sh").read_text(encoding="utf-8") assert "COPY scripts/archive_technical_projects.py" in dockerfile assert "py_compile scripts/archive_technical_projects.py" in readiness def test_frontend_lifecycle_and_component_boundaries_are_wired() -> None: app_source = (ROOT / "frontend/src/App.tsx").read_text(encoding="utf-8") project_panel = (ROOT / "frontend/src/components/project/ProjectPanel.tsx").read_text(encoding="utf-8") detection_lab = (ROOT / "frontend/src/components/detection/DetectionLab.tsx").read_text(encoding="utf-8") map_workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8") premium_css = (ROOT / "frontend/src/styles/premium.css").read_text(encoding="utf-8") assert "OverviewWorkspace" in app_source assert "onArchiveProject={archiveProject}" in app_source assert "DetectionModelManagement" in detection_lab assert "from './mapWorkspaceUtils'" in map_workspace assert "Werkruimte archiveren" in project_panel assert "PROTECTED_PROJECT_NAMES" in project_panel assert ".technical-inline-details" in premium_css assert ".workspace-grid-ai" in premium_css