diff --git a/docs/remote-hpc.md b/docs/remote-hpc.md index 2b89d13..b95d1f1 100644 --- a/docs/remote-hpc.md +++ b/docs/remote-hpc.md @@ -195,6 +195,33 @@ If you asked for `use_remote=true` and got `local`, the dispatcher fell back. Reasons appear in the response's `warnings` and in `~/.config/uxarray-mcp/logs/`. +Remote results also record the worker's UXarray version as +`remote_uxarray_version`; if it differs from the local `uxarray_version` a +version-drift warning is added so you know the numbers were produced by a +different build. + +--- + +## Live HPC integration tests (developers) + +The remapping tools (`remap_variable`, `regrid_dataset`, `remap_to_rectilinear`) +run the heavy compute on the worker and return compact summaries. An opt-in test +suite validates this end-to-end against a real endpoint. It is **skipped by +default** and only runs when the required environment variables are set: + +```bash +UXMCP_LIVE_ENDPOINT=chrysalis \ +UXMCP_LIVE_GRID=/home/jain/uxarray/test/meshfiles/mpas/QU/480/grid.nc \ +UXMCP_LIVE_DATA=/home/jain/uxarray/test/meshfiles/mpas/QU/480/data.nc \ +UXMCP_LIVE_TARGET_GRID=/home/jain/uxarray/test/meshfiles/mpas/dyamond-30km/gradient_grid_subset.nc \ +UXMCP_LIVE_VARIABLE=bottomDepth \ +uv run pytest tests/test_remote_remap_live.py -v +``` + +Each test asserts `execution_venue == "hpc:"` and that the worker +version is recorded. When the same paths are also readable locally, the suite +cross-checks that remote and local summary statistics agree. + --- ## Troubleshooting diff --git a/tests/test_remote_remap_live.py b/tests/test_remote_remap_live.py new file mode 100644 index 0000000..5133a0e --- /dev/null +++ b/tests/test_remote_remap_live.py @@ -0,0 +1,123 @@ +"""Opt-in live HPC integration tests for remote remapping. + +These tests submit real work to a configured Globus Compute endpoint and are +**skipped by default**. They exist to validate the end-to-end remote remap path +(compute on the worker, compact summary back) against a live cluster — the gap +noted when the remap tools were first added while the endpoint was offline. + +Run them explicitly with:: + + UXMCP_LIVE_ENDPOINT=chrysalis \\ + UXMCP_LIVE_GRID=/home/jain/uxarray/test/meshfiles/mpas/QU/480/grid.nc \\ + UXMCP_LIVE_DATA=/home/jain/uxarray/test/meshfiles/mpas/QU/480/data.nc \\ + UXMCP_LIVE_TARGET_GRID=/home/jain/uxarray/test/meshfiles/mpas/dyamond-30km/gradient_grid_subset.nc \\ + UXMCP_LIVE_VARIABLE=bottomDepth \\ + uv run pytest tests/test_remote_remap_live.py -v + +All five environment variables must be set or the module is skipped. +""" + +from __future__ import annotations + +import os + +import pytest + +_ENDPOINT = os.environ.get("UXMCP_LIVE_ENDPOINT") +_GRID = os.environ.get("UXMCP_LIVE_GRID") +_DATA = os.environ.get("UXMCP_LIVE_DATA") +_TARGET_GRID = os.environ.get("UXMCP_LIVE_TARGET_GRID") +_VARIABLE = os.environ.get("UXMCP_LIVE_VARIABLE") + +pytestmark = pytest.mark.skipif( + not all([_ENDPOINT, _GRID, _DATA, _TARGET_GRID, _VARIABLE]), + reason=( + "live HPC remap test skipped; set UXMCP_LIVE_ENDPOINT, UXMCP_LIVE_GRID, " + "UXMCP_LIVE_DATA, UXMCP_LIVE_TARGET_GRID, UXMCP_LIVE_VARIABLE to run." + ), +) + + +def _assert_remote(result: dict) -> None: + prov = result["_provenance"] + assert prov["execution_venue"] == f"hpc:{_ENDPOINT}", prov["execution_venue"] + # Worker version is recorded for remote runs. + assert "remote_uxarray_version" in prov + + +def test_remote_remap_variable_live(): + from uxarray_mcp.tools.advanced import remap_variable + + result = remap_variable( + target_grid_path=_TARGET_GRID, + variable_name=_VARIABLE, + grid_path=_GRID, + data_path=_DATA, + method="nearest_neighbor", + use_remote=True, + endpoint=_ENDPOINT, + ) + _assert_remote(result) + assert result["stats"]["min"] is not None + assert result["result_shape"] + + +def test_remote_regrid_dataset_live(): + from uxarray_mcp.tools.advanced import regrid_dataset + + result = regrid_dataset( + target_grid_path=_TARGET_GRID, + grid_path=_GRID, + data_path=_DATA, + method="nearest_neighbor", + use_remote=True, + endpoint=_ENDPOINT, + ) + _assert_remote(result) + assert _VARIABLE in result["per_variable_stats"] + + +def test_remote_remap_to_rectilinear_live(): + from uxarray_mcp.tools.advanced import remap_to_rectilinear + + result = remap_to_rectilinear( + variable_name=_VARIABLE, + target_lon=[-90.0, 0.0, 90.0], + target_lat=[-45.0, 0.0, 45.0], + grid_path=_GRID, + data_path=_DATA, + use_remote=True, + endpoint=_ENDPOINT, + ) + _assert_remote(result) + assert result["target_shape"] == [3, 3] + # The small rectilinear array is returned and persisted locally. + assert result["result_handle"] + + +def test_remote_and_local_agree_on_stats(): + """Remote and local remap_variable produce identical summary statistics + when both paths are reachable (guards against silent divergence).""" + from uxarray_mcp.tools.advanced import remap_variable + + remote = remap_variable( + target_grid_path=_TARGET_GRID, + variable_name=_VARIABLE, + grid_path=_GRID, + data_path=_DATA, + method="nearest_neighbor", + use_remote=True, + endpoint=_ENDPOINT, + ) + # Local run only if the same paths are readable locally (else skip). + if not (os.path.exists(_GRID) and os.path.exists(_DATA)): + pytest.skip("grid/data not readable locally; cannot cross-check stats") + local = remap_variable( + target_grid_path=_TARGET_GRID, + variable_name=_VARIABLE, + grid_path=_GRID, + data_path=_DATA, + method="nearest_neighbor", + ) + assert remote["stats"]["min"] == pytest.approx(local["stats"]["min"]) + assert remote["stats"]["max"] == pytest.approx(local["stats"]["max"])