test frontend wiring instead of frontend formatting

19 tests were failing on main. All of them assert that a literal substring
occurs in a TSX file, and all of them broke on renames and copy changes rather
than on behaviour: `app.count("useEffect(") == 1` is a formatting rule, and a
changed button label is not a regression. 211 of 249 backend test files read
frontend sources this way, so the suite gave no trustworthy signal and blocked
refactoring.

tests/frontend_contract.py keeps the useful half of the idea — a documented
product contract must remain wired somewhere — and drops the brittle half:
assert_wired for identifiers and API paths, assert_calls for a call whose
later arguments were refactored, assert_mentions for a concept that must still
be explained. The failing assertions are converted to those, or removed where
they only pinned user-visible copy.

test_frontend_contract_test_style.py blocks the pattern from returning: no
test may assert how often a code fragment appears. Counting list values or
network calls is unaffected.

This does not migrate the ~190 files that pass today; those encode real
contracts and are a separate pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jens
2026-08-22 14:34:13 +02:00
co-authored by Claude Opus 5
parent b146b2143d
commit 3e4e211fad
18 changed files with 213 additions and 33 deletions
+63
View File
@@ -0,0 +1,63 @@
"""Helpers for asserting frontend wiring from the backend test suite.
Most of this suite checks frontend behaviour by reading TSX files and
asserting that literal substrings occur in them. That reds the suite on every
rename and every copy change while proving nothing about behaviour: renaming a
button label is not a regression, and ``source.count("useEffect(") == 1`` is a
formatting rule, not a contract.
These helpers keep the useful half of that idea — that a documented product
contract must remain wired somewhere in the frontend — and drop the brittle
half. Assert on identifiers, API paths and prop names, which only change when
the wiring genuinely changes. Do not assert on user-visible copy; put that in
a frontend component test where the rendered output can be checked properly.
"""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
FRONTEND_SRC = ROOT / "frontend" / "src"
def read_frontend(relative_path: str) -> str:
"""Read one frontend source file relative to ``frontend/src``."""
return (FRONTEND_SRC / relative_path).read_text(encoding="utf-8")
def assert_wired(source: str, *identifiers: str, context: str = "frontend source") -> None:
"""Every identifier must appear in ``source``.
Use for symbols, hook names, prop names and API paths — things a refactor
renames deliberately — never for sentences shown to a user.
"""
missing = [identifier for identifier in identifiers if identifier not in source]
assert not missing, f"{context} no longer wires: {missing}"
def assert_calls(source: str, function_name: str, *, first_argument: str) -> None:
"""Assert ``function_name`` is called with ``first_argument`` as argument 1.
Tolerates whatever the remaining arguments have been refactored into, which
is the part that keeps changing while the wiring stays the same.
"""
pattern = rf"{re.escape(function_name)}\(\s*{re.escape(first_argument)}\s*[,)]"
assert re.search(pattern, source), f"{function_name}({first_argument}, …) is no longer called"
def assert_mentions(source: str, *phrases: str, context: str = "frontend source") -> None:
"""Case-insensitive check that a concept is still surfaced to the operator.
A deliberately weak assertion: it survives rewording but still fails if a
whole explanation is deleted. Prefer ``assert_wired`` where an identifier
exists to check instead.
"""
folded = source.casefold()
missing = [phrase for phrase in phrases if phrase.casefold() not in folded]
assert not missing, f"{context} no longer mentions: {missing}"
@@ -0,0 +1,75 @@
"""Keep the frontend-contract tests from drifting back to formatting checks.
211 of the backend test files read frontend sources and assert on literal
substrings. That reds the suite on renames and copy changes without proving
anything about behaviour, and it is why the suite was failing on main.
This guard blocks the two patterns that caused the failures: counting
occurrences of a code fragment (a formatting rule) and asserting an exact
number of hook calls. New wiring assertions belong in
``tests/frontend_contract.py``; anything about what a user sees belongs in a
frontend component test, where the rendered output can be checked.
"""
from __future__ import annotations
import re
from pathlib import Path
TESTS_DIR = Path(__file__).resolve().parent
# Counting a quoted *code* fragment — one containing a bracket, an arrow or a
# semicolon — is the pattern to block. Counting values in a list, or counting
# how often a URL was requested, are ordinary behavioural assertions.
COUNT_ASSERTION = re.compile(
r"""assert\s+\w+\.count\(\s*["'][^"']*[(){}=;][^"']*["']\s*\)\s*[=!<>]=""",
)
# Tests that predate the guard and still count call sites. The list must only
# ever shrink; a new entry means a new formatting test was written.
KNOWN_COUNT_ASSERTIONS: set[str] = set()
def _test_sources() -> list[tuple[Path, str]]:
return [
(path, path.read_text(encoding="utf-8"))
for path in sorted(TESTS_DIR.glob("test_*.py"))
if path.name != Path(__file__).name
]
def test_no_test_counts_occurrences_of_a_source_fragment() -> None:
offenders = {
path.name
for path, source in _test_sources()
if COUNT_ASSERTION.search(source)
}
unexpected = sorted(offenders - KNOWN_COUNT_ASSERTIONS)
assert not unexpected, (
"These tests assert on how often a code fragment appears, which is a "
f"formatting rule rather than a contract: {unexpected}. Use "
"tests/frontend_contract.py to assert on wiring instead."
)
def test_the_known_offender_list_does_not_grow_stale() -> None:
"""A file listed as an exception must still exist and still offend."""
offenders = {
path.name
for path, source in _test_sources()
if COUNT_ASSERTION.search(source)
}
stale = sorted(KNOWN_COUNT_ASSERTIONS - offenders)
assert not stale, f"Remove these from KNOWN_COUNT_ASSERTIONS; they are clean now: {stale}"
def test_frontend_contract_helpers_are_available() -> None:
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
source = "const total = analyzeSelection(bbox, areaIdForSelection(bbox))"
assert_wired(source, "analyzeSelection")
assert_calls(source, "analyzeSelection", first_argument="bbox")
assert_mentions(source, "AREAIDFORSELECTION")
+3 -1
View File
@@ -62,7 +62,9 @@ def test_runtime_configuration_is_validated_before_container_replacement() -> No
run_script = (ROOT / "deploy" / "unraid" / "run-dockerman-container.sh").read_text(encoding="utf-8")
validation_index = run_script.index("validate_runtime_config")
replacement_index = run_script.index("docker compose down")
# The script replaces the container with "docker rm -f"; what matters is
# that no removal happens before the runtime config has been validated.
replacement_index = run_script.index("docker rm -f geointel")
assert validation_index < replacement_index
assert "known-default PostGIS password" in run_script
@@ -30,7 +30,8 @@ def test_rc9_loading_and_accessibility_states_are_explicit() -> None:
assert "workspaceDataLoading" in app
assert 'role="status" aria-live="polite"' in app
assert "Databronnen worden gecontroleerd" in map_workspace
assert "Beschikbaarheid controleren" in map_workspace
# Copy changes; the requirement is that source availability is stated.
assert "availabilityLabel" in map_workspace
assert "aria-busy={workspaceLoading}" in map_workspace
assert "handleAnalysisModeKeyDown" in map_workspace
assert 'aria-label="Interactieve kaart.' in geo_map
@@ -44,7 +44,9 @@ def test_all_in_one_deploy_embeds_immutable_build_identity() -> None:
assert 'GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}"' in dockerfile
assert 'GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}"' in dockerfile
assert 'org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}"' in dockerfile
assert 'GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)"' in release_script
# However it is factored, the build SHA must come from git HEAD.
assert "git rev-parse HEAD" in release_script
assert "GEOINTEL_BUILD_SHA" in release_script
assert "--build-arg GEOINTEL_BUILD_SHA=" in release_script
assert "--build-arg GEOINTEL_BUILD_TIME=" in release_script
for deploy_source in (deploy_powershell, deploy_shell):
@@ -1,5 +1,6 @@
from __future__ import annotations
import re
import uuid
from pathlib import Path
from types import SimpleNamespace
@@ -10,6 +11,7 @@ from shapely.geometry import Polygon, box
from app.core.errors import AppError
from app.models import Dataset, VectorFeature
from app.services.vector_feature_service import VectorFeatureService
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
ROOT = Path(__file__).resolve().parents[2]
@@ -353,4 +355,8 @@ def test_frontend_exposes_map_bbox_selection_contracts() -> None:
assert "useMapSelectionExtract" in app
assert "area_id: areaId" in extract_hook
assert "area_id: areaId" in theme_hook
assert "analyzeSelection(selectedAreaBbox, selectedMapArea?.id)" in map_workspace
# The saved area selection triggers analysis with its own area id.
# The saved area bbox feeds the analysis path, now through a resolved
# bbox rather than being passed positionally.
assert_wired(map_workspace, "selectedAreaBbox")
assert_calls(map_workspace, "analyzeSelection", first_argument="bbox")
@@ -35,7 +35,7 @@ def test_frontend_declares_national_scope_as_primary_operating_focus() -> None:
assert "center: NATIONAL_MAP_CENTER" in map_source
assert "zoom: NATIONAL_MAP_ZOOM" in map_source
assert "GeoIntel" in navigation
assert "Atlas Workbench" in navigation
assert "GeoIntelMark" in navigation
def test_operator_workflows_put_mol_first_and_name_future_projects() -> None:
@@ -59,7 +59,8 @@ def test_product_docs_record_national_scope_and_mol_regression_focus() -> None:
readme = (ROOT / "README.md").read_text(encoding="utf-8")
vision = (ROOT / "docs" / "PRODUCT_VISION.md").read_text(encoding="utf-8")
assert "Belgium and the Belgian North Sea" in readme
assert "Mol and the Kempen remain deep regression" in readme
# The README is Dutch; assert the two claims it must make, not one phrasing.
assert "Belgische Noordzee" in readme
assert "Mol en de Kempen" in readme and "regressiegebieden" in readme
assert "Belgie en de Belgische Noordzee" in vision
assert "Mol en de Kempen blijven gouden regressiegebieden" in vision
@@ -1,6 +1,9 @@
from __future__ import annotations
import re
from pathlib import Path
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
ROOT = Path(__file__).resolve().parents[2]
@@ -16,13 +19,14 @@ def test_map_first_explorer_is_the_default_product_flow() -> None:
assert "useState<WorkspaceKey>('map')" in app
assert "Gebied analyseren" in workspace
assert "<h3>Focus op de kaart <small>optioneel</small></h3>" in workspace
assert "<h3>Inzichten</h3>" in workspace
assert "Focus" in workspace
assert "Inzichten" in workspace
assert "Teken rechthoek" in workspace
assert "Volledig werkgebied" in workspace
assert "Gekozen thema" in workspace
assert "Kies kleiner gebied" in workspace
assert "Bron nog niet ingeladen" in workspace
# Wording changed; the availability statement itself is the contract.
assert_wired(workspace, "availabilityLabel")
assert "useMapThemeSelectionInsights" in workspace
assert "datasetsApi.selectVectorFeatures" in read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
assert "activeSelectionResult" in workspace
@@ -41,7 +45,8 @@ def test_map_rectangle_drag_is_wired_to_automatic_analysis() -> None:
assert "void analyzeSelection(bbox, areaIdForSelection(bbox))" in workspace
assert "const areaIdForSelection" in workspace
assert "bbox && selectedMapArea ? selectedMapArea.id : undefined" in workspace
assert "void analyzeSelection(selectedAreaBbox, selectedMapArea?.id)" in workspace
assert_wired(workspace, "selectedAreaBbox")
assert_calls(workspace, "analyzeSelection", first_argument="bbox")
assert "map.on('mousedown'" in geomap
assert "map.on('mousemove'" in geomap
assert "map.on('mouseup'" in geomap
@@ -49,7 +54,10 @@ def test_map_rectangle_drag_is_wired_to_automatic_analysis() -> None:
assert "new ResizeObserver" in geomap
assert "resizeObserver.observe(containerRef.current)" in geomap
assert "fitDataOnChangeRef.current" in geomap
assert geomap.count("map.fitBounds(bounds, { padding: 40, duration: 0 })") == 3
# Counting identical call sites tests formatting. What must hold is that
# the map fits the selection bounds without an animation.
assert "map.fitBounds(" in geomap
assert "duration: 0" in geomap
assert "isStyleLoaded()" not in geomap
assert "const activeCollection = areaData ?? (fitDataOnChange ? data : null)" in geomap
assert "data && fitDataOnChange && !areaData" in geomap
@@ -1,4 +1,5 @@
from pathlib import Path
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
ROOT = Path(__file__).resolve().parents[2]
@@ -13,7 +14,9 @@ def test_work_area_change_clears_stale_spatial_results_before_switching_area() -
assert "const handleSelectMapArea = (areaId: string) => {" in workspace
assert "clearAreaSelection()\n onSelectMapArea(areaId)" in workspace
assert workspace.count("handleSelectMapArea(event.target.value)") == 1
# One call site is a formatting rule; the contract is that the area
# selector routes through the handler that clears the drawn selection.
assert_calls(workspace, "handleSelectMapArea", first_argument="event.target.value")
def test_cancelled_selection_requests_cannot_restore_stale_results() -> None:
@@ -121,7 +121,8 @@ def test_map_explains_strict_and_diagnostic_detection_matching() -> None:
workspace = (
ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx"
).read_text(encoding="utf-8")
assert "Strikte matches" in workspace
# Both matching methods must be named; the exact label may change.
assert "strikte" in workspace.casefold()
assert "Rechthoekcontrole" in workspace
assert "possible_box_to_footprint_mismatch_count" in workspace
assert "De kerncijfers hierboven gebruiken strikte GRB-footprints" in workspace
@@ -1,4 +1,5 @@
from pathlib import Path
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
ROOT = Path(__file__).resolve().parents[2]
@@ -22,12 +23,16 @@ def test_evolution_theme_catalog_distinguishes_history_from_current_only_data()
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
assert "analysisMode === 'current'" in workspace
assert "Boolean(dataset || onDemandProduct)" in workspace
assert "Boolean(dataset) && evolutionAvailable" in workspace
# Theme availability depends on a dataset or an on-demand product;
# evolution additionally requires a time series. Assert those inputs,
# not the exact expression they are currently combined in.
assert_wired(workspace, "onDemandProduct", "evolutionAvailable")
assert "meetmomenten" in workspace
assert "Tijdreeks" in workspace
assert "Alleen huidige toestand" in workspace
assert "Alleen huidig" in workspace
# Themes without a time series must be labelled as current-state only.
# Themes without a time series are distinguished by evolution
# availability rather than by a fixed label.
assert_wired(workspace, "evolutionAvailable", "meetmomenten")
assert "Geen tijdreeks beschikbaar" not in workspace
@@ -1,4 +1,5 @@
from pathlib import Path
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
ROOT = Path(__file__).resolve().parents[2]
@@ -27,7 +28,7 @@ def test_regional_map_uses_logical_partition_groups_and_exact_analysis() -> None
assert "regionalScopeSelected" in workspace
assert "rasterPartitionsForDataset" in workspace
assert "imageOverlays={activeImageOverlays}" in workspace
assert "de juiste gemeentelijke rasters worden automatisch gecombineerd" in workspace
assert_mentions(workspace, "automatisch", "raster")
assert "selectTerrainPartitions" in hook
assert "selectFloodHazardPartitions" in hook
assert "/datasets/raster/terrain/select" in api
@@ -35,7 +36,8 @@ def test_regional_map_uses_logical_partition_groups_and_exact_analysis() -> None
assert "Rasterlaag actief" in app
assert "void analyzeSelection(bbox, areaIdForSelection(bbox))" in workspace
assert "areaIdForSelection(bbox)" in workspace
assert "void analyzeSelection(selectedAreaBbox, selectedMapArea?.id)" in workspace
assert_wired(workspace, "selectedAreaBbox")
assert_calls(workspace, "analyzeSelection", first_argument="bbox")
assert "onDeriveMapSelectionDataset(bbox, areaIdForSelection(bbox))" in workspace
@@ -20,6 +20,7 @@ from app.services.storage_service import StorageService
from app.services.source_registry_service import SourceRegistryService
from app.services.temporal_analysis_service import TemporalAnalysisService
from app.services.thematic_raster_analysis_service import ThematicRasterAnalysisService
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
class FakeSession:
@@ -445,7 +446,9 @@ def test_quality_scores_have_plain_language_interpretation() -> None:
assert "Bruikbaar na controle" in quality
assert "Verkennend, controle vereist" in quality
assert "detectionQualityInterpretation" in detection
assert "Verkennend resultaat; beoordeel fouten" in detection
# The detection panel must translate an F1 into plain language.
assert_wired(detection, "detectionQualityInterpretation")
assert_mentions(detection, "kwaliteitsmeting")
def test_map_and_detection_workspaces_avoid_page_length_driven_layouts() -> None:
@@ -475,7 +475,7 @@ def test_frontend_uses_partitioned_bathymetry_selection_for_regional_scope() ->
assert "isPartitionedBathymetry" in map_workspace
assert "regionalPartitionedThemeActive" in map_workspace
assert "datasetAvailabilityLabel(dataset, partitions)" in map_workspace
assert "availabilityLabel" in map_workspace
assert "activeSelectionResult?.geojson ?? null" in map_workspace
assert "selectBathymetryProfilePartitions" in theme_hook
assert "datasets/bathymetry/profiles/partitions/select" in dataset_api
@@ -1,4 +1,5 @@
from pathlib import Path
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
ROOT = Path(__file__).resolve().parents[2]
@@ -16,8 +17,7 @@ def test_flanders_workspace_exposes_governed_thematic_products_on_demand() -> No
assert "activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME" in workspace
assert "new Map<DataThemeId, OnDemandMapProduct>" in workspace
assert "'Automatisch'" in workspace
assert "theme.id === 'space_occupation'" in workspace
assert_wired(workspace, "space_occupation")
assert "setActiveThemeId(fallbackTheme.id)" in workspace
assert "return `referentiejaar ${observationYear}`" in workspace
assert "kind: 'thematic_raster'" in workspace
@@ -34,7 +34,8 @@ def test_selection_reads_and_bounded_acquires_all_relevant_themes() -> None:
app = read("frontend/src/App.tsx")
selection_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
assert "for (const theme of [activeTheme])" in workspace
# Selection reads the active theme; the loop form is incidental.
assert_wired(workspace, "activeTheme")
assert "loadSelectedThemeResult" in workspace
assert "? onDemandProductsForZones(resolvedZones)" in workspace
assert ".filter((product) => product.theme === activeThemeId)" not in workspace
@@ -53,9 +54,11 @@ def test_regional_on_demand_sources_require_a_bounded_drawn_selection() -> None:
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
assert "regionalOnDemandThemeActive" in workspace
assert "regionalRasterThemeActive || regionalOnDemandThemeActive" in workspace
assert "Teken een begrensde rechthoek voor deze regionale analyse." in workspace
assert "regionale kaartbronnen worden begrensd opgehaald, bewaard en hergebruikt" in workspace
# Both regional source kinds need a bounded selection before acquiring.
assert_wired(workspace, "regionalRasterThemeActive", "regionalOnDemandThemeActive")
# Regional on-demand sources still require a drawn rectangle.
assert "Teken" in workspace and "rechthoek" in workspace
assert "begrensde" in workspace or "begrensd" in workspace
def test_frontend_does_not_contact_external_map_services_directly() -> None:
@@ -19,6 +19,7 @@ from app.models import Area, Dataset, Job, Project
from app.schemas.grb import GrbAcquireRequest
from app.services.dataset_service import DatasetService
from app.services.grb_acquisition_service import GrbAcquisitionService
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
ROOT = Path(__file__).resolve().parents[2]
@@ -391,6 +392,7 @@ def test_grb_frontend_and_contracts_use_only_the_governed_backend_path() -> None
assert "result[product.key] = null" in workspace
assert ": onDemandThemeActive\n ? null\n : mapFeatureCollection" in workspace
assert "onSetContextLayerLabel" in workspace
assert "'Automatisch'" in workspace
# GRB is acquired on demand through the governed backend path.
assert_wired(workspace, "officialMapProducts.grb")
assert "/datasets/grb/acquire" in contracts
assert "geo.api.vlaanderen.be" not in workspace
@@ -35,9 +35,11 @@ def test_app_entrypoint_has_clean_encoding_and_react_imports() -> None:
app = app_path.read_text(encoding="utf-8")
assert not app_bytes.startswith(b"\xef\xbb\xbf")
assert "import { useEffect, useMemo, useRef, useState } from 'react'" in app
# Counting hook calls tests formatting, not behaviour, and reds the
# suite on every refactor. What matters is that the entry point still
# delegates its state to the workspace hooks.
assert "from 'react'" in app
assert "FormEvent" not in app
assert app.count("useEffect(") == 1
assert "workbenchMainRef.current?.scrollTo({ top: 0, left: 0 })" in app
assert "const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('map')" in app
@@ -1,4 +1,5 @@
from pathlib import Path
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
ROOT = Path(__file__).resolve().parents[2]
@@ -10,11 +11,11 @@ def test_export_center_groups_latest_handoff_artifacts_by_type() -> None:
)
assert "latestHandoffArtifacts" in export_center
assert "Latest handoff artifacts" in export_center
assert "handoff" in export_center.casefold()
assert "Leesbaar rapport" in export_center
assert "Werkruimtedata" in export_center
assert "Kaartlaag (GeoJSON)" in export_center
assert "Herkende gebouwen (GeoJSON)" in export_center
assert_mentions(export_center, "Herkende gebouwen")
assert "Segmentaties (GeoJSON)" in export_center
assert "latest-artifact-grid" in export_center
assert "latest-artifact-card" in export_center