Files
geointel/backend/tests/test_sprint234_project_lifecycle_cleanup.py
T
JensandClaude Opus 5 6572e4ad5f scope frontend contracts to the feature, not to one file
93 test files read a single frontend source and asserted identifiers in it. The
MapWorkspace split showed what that costs: 24 tests went red for a move that
changed no behaviour at all. A contract belongs to the feature — a container,
its hooks, its domain layer — not to whichever file currently holds it.

232 read sites now resolve through read_feature(). The distinction that makes
this safe is direction: a *positive* contract ("this is wired") may widen,
because the identifier must still exist somewhere in the feature; a *negative*
one ("this component performs no transport") is a statement about one file, and
widening it would quietly weaken the check. The 73 single-file reads that
remain are exactly those, and a guard now enforces the rule for new tests.

Verified rather than assumed: of the 732 migrated positive assertions, 644 still
match exactly one module — as specific as before — and the other 86 already
spanned a container and its hook by nature. Two apparent misses are an artefact
of the checking regex reading an escaped newline literally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:05:43 +02:00

172 lines
6.0 KiB
Python

from __future__ import annotations
import importlib.util
from pathlib import Path
import subprocess
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
from tests.frontend_contract import read_feature
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_cleanup_script_can_start_as_a_direct_operator_command() -> None:
result = subprocess.run(
[sys.executable, str(ROOT / "scripts/archive_technical_projects.py"), "--help"],
cwd=ROOT,
capture_output=True,
check=False,
text=True,
timeout=20,
)
assert result.returncode == 0, result.stderr
assert "--apply" in result.stdout
assert "--show-names" in result.stdout
def test_frontend_lifecycle_and_component_boundaries_are_wired() -> None:
app_source = read_feature("shell")
project_panel = (ROOT / "frontend/src/components/project/ProjectPanel.tsx").read_text(encoding="utf-8")
detection_lab = read_feature("detection")
segmentation_lab = read_feature("segmentation")
map_workspace = read_feature("map_workspace")
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 "persistedDetectionModelLabel" in detection_lab
assert "Lokaal gebouwmodel" in detection_lab
assert "persistedSegmentationModelLabel" in segmentation_lab
assert "Testsegmentatie" in segmentation_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