From 85776f6ee1b2205b4f5dbeba51dea9914a0910b9 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Thu, 28 May 2026 11:20:13 +0100 Subject: [PATCH 1/3] add model validation for drift_time < cycle_time --- src/virtualship/models/expedition.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/virtualship/models/expedition.py b/src/virtualship/models/expedition.py index eef23c76..d7badd53 100644 --- a/src/virtualship/models/expedition.py +++ b/src/virtualship/models/expedition.py @@ -293,6 +293,15 @@ class ArgoFloatConfig(_InstrumentConfigMixin, pydantic.BaseModel): model_config = pydantic.ConfigDict(populate_by_name=True) + @pydantic.model_validator(mode="after") + def _drift_days_less_than_cycle_days(self) -> ArgoFloatConfig: + """Ensure drift_days is less than cycle_days.""" + if self.drift_days >= self.cycle_days: + raise ValueError( + f"drift_days ({self.drift_days}) must be less than cycle_days ({self.cycle_days}). " + ) + return self + @register_instrument_config(InstrumentType.ADCP) class ADCPConfig(_InstrumentConfigMixin, pydantic.BaseModel): From 3a4d657a681fafde5c9c0cd9f31567b5b4aca32a Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Thu, 28 May 2026 13:56:00 +0100 Subject: [PATCH 2/3] plan tool explicitly handle user error with drift time --- src/virtualship/cli/_plan.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/virtualship/cli/_plan.py b/src/virtualship/cli/_plan.py index f2e786c7..9446fb6f 100644 --- a/src/virtualship/cli/_plan.py +++ b/src/virtualship/cli/_plan.py @@ -7,6 +7,7 @@ from textual.app import App, ComposeResult from textual.containers import Container, Horizontal, VerticalScroll from textual.dom import NoMatches +from textual.markup import escape from textual.screen import ModalScreen, Screen from textual.validation import Function, Integer from textual.widgets import ( @@ -530,11 +531,25 @@ def _update_instrument_configs(self): kwargs["sensors"] = ( sensors if sensors else _default_sensors(config_class) ) - setattr( - self.expedition.instruments_config, - instrument_name, - config_class(**kwargs), - ) + try: + setattr( + self.expedition.instruments_config, + instrument_name, + config_class(**kwargs), + ) + except (ValueError, Exception) as e: + # catch validation errors, e.g. drift_days >= cycle_days + if isinstance(e, ValueError): + title = info.get("title", instrument_name.replace("_", " ").title()) + raise UserError(f"'{title}' configuration error: {e}") from None + elif ( # pydantic validation error + hasattr(e, "__class__") + and "ValidationError" in e.__class__.__name__ + ): + title = info.get("title", instrument_name.replace("_", " ").title()) + raise UserError(f"'{title}' configuration error: {e}") from None + else: + raise def _update_schedule(self): for i, wp in enumerate(self.expedition.schedule.waypoints): @@ -1150,7 +1165,9 @@ def save_pressed(self) -> None: except Exception as e: self.notify( - f"*** Error saving changes ***:\n\n{e}\n", + escape( + f"*** Error saving changes ***:\n\n{e}\n" + ), # escape avoids issues with special characters being interpreted as markup severity="error", timeout=20, ) From 836856b68d70c53da3010f5cb8604b05aa8ba069 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Thu, 28 May 2026 14:10:16 +0100 Subject: [PATCH 3/3] add test for drift_time < cycle_time --- tests/instruments/test_argo_float.py | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/instruments/test_argo_float.py b/tests/instruments/test_argo_float.py index 403e0457..e37d7420 100644 --- a/tests/instruments/test_argo_float.py +++ b/tests/instruments/test_argo_float.py @@ -229,3 +229,32 @@ def test_argo_config_unsupported_sensor_rejected(): stationkeeping_time_minutes=10, sensors=[SensorConfig(sensor_type=SensorType.OXYGEN)], ) + + +def test_argo_config_drift_days_exceeds_cycle_days(): + """ArgoFloatConfig should reject drift_days >= cycle_days.""" + base_kwargs = { + "min_depth_meter": 0.0, + "max_depth_meter": -2000, + "drift_depth_meter": -1000, + "vertical_speed_meter_per_second": -0.10, + "lifetime": timedelta(days=30), + "stationkeeping_time_minutes": 10, + } + + # drift_days > cycle_days should raise validation error + with pytest.raises( + pydantic.ValidationError, match="drift_days .* must be less than cycle_days" + ): + ArgoFloatConfig(**base_kwargs, cycle_days=10, drift_days=15) + + # drift_days == cycle_days should also raise validation error + with pytest.raises( + pydantic.ValidationError, match="drift_days .* must be less than cycle_days" + ): + ArgoFloatConfig(**base_kwargs, cycle_days=10, drift_days=10) + + # check a valid configuration: drift_days < cycle_days + config = ArgoFloatConfig(**base_kwargs, cycle_days=10, drift_days=9) + assert config.drift_days == 9 + assert config.cycle_days == 10