33
44from __future__ import annotations
55
6- from unittest .mock import Mock
6+ from collections .abc import Sequence
7+ from unittest .mock import Mock , patch
78
89import pytest
910
1011from data_designer .config .column_configs import LLMTextColumnConfig , SamplerColumnConfig
12+ from data_designer .config .column_types import ColumnConfigT
1113from data_designer .config .config_builder import DataDesignerConfigBuilder
1214from data_designer .config .custom_column import custom_column_generator
15+ from data_designer .config .models import ModelConfig
1316from data_designer .config .sampler_params import SamplerType , UUIDSamplerParams
1417from data_designer .engine import flags
1518from data_designer .engine .dataset_builders .errors import DatasetGenerationError
@@ -27,7 +30,12 @@ def _force_sync_engine(monkeypatch: pytest.MonkeyPatch) -> None:
2730 monkeypatch .setattr (flags , "DATA_DESIGNER_ASYNC_ENGINE" , False )
2831
2932
30- def _build_columns (* , model_configs , llm_columns : list [tuple [str , str ]] = (), include_sampler : bool = True ):
33+ def _build_columns (
34+ * ,
35+ model_configs : list [ModelConfig ],
36+ llm_columns : Sequence [tuple [str , str ]] = (),
37+ include_sampler : bool = True ,
38+ ) -> list [ColumnConfigT ]:
3139 """Build a ``DataDesignerConfig`` and return its (already-flat) column configs.
3240
3341 ``llm_columns`` is a list of ``(name, model_alias)`` pairs. ``include_sampler``
@@ -231,3 +239,131 @@ def test_run_readiness_check_no_models_no_tools_is_noop(
231239
232240 stub_resource_provider .model_registry .run_health_check .assert_not_called ()
233241 mock_mcp_registry .run_health_check .assert_not_called ()
242+
243+
244+ # ---------------------------------------------------------------------------
245+ # Column-type coverage
246+ # ---------------------------------------------------------------------------
247+
248+
249+ def test_run_readiness_check_collects_image_model_aliases (
250+ stub_resource_provider ,
251+ stub_model_configs ,
252+ ) -> None :
253+ """Image-generation columns contribute their model aliases like LLM columns do.
254+
255+ The dataset builder dispatches probes by ``model_generation_type`` inside
256+ ``ModelRegistry.run_health_check``; readiness is generation-type-agnostic
257+ and must surface every alias regardless of column kind.
258+ """
259+ from data_designer .config .column_configs import ImageColumnConfig
260+
261+ stub_resource_provider .model_registry .run_health_check = Mock ()
262+ stub_resource_provider .mcp_registry = None
263+
264+ builder = DataDesignerConfigBuilder (model_configs = stub_model_configs )
265+ builder .add_column (SamplerColumnConfig (name = "seed_id" , sampler_type = SamplerType .UUID , params = UUIDSamplerParams ()))
266+ builder .add_column (LLMTextColumnConfig (name = "caption" , prompt = "x" , model_alias = "stub-text" ))
267+ builder .add_column (ImageColumnConfig (name = "picture" , prompt = "y" , model_alias = "stub-image" ))
268+
269+ run_readiness_check (builder .build ().columns , stub_resource_provider )
270+
271+ stub_resource_provider .model_registry .run_health_check .assert_called_once ()
272+ (called_aliases ,), _ = stub_resource_provider .model_registry .run_health_check .call_args
273+ assert set (called_aliases ) == {"stub-text" , "stub-image" }
274+
275+
276+ def test_run_readiness_check_passes_skip_flagged_aliases_to_registry (
277+ stub_resource_provider ,
278+ stub_model_configs ,
279+ ) -> None :
280+ """Readiness does not pre-filter ``skip_health_check=True`` aliases.
281+
282+ The skip decision lives in ``ModelRegistry.run_health_check`` (covered by
283+ ``test_model_registry``). Readiness's contract is "pass every referenced
284+ alias through and let the registry decide" — verified here so future edits
285+ don't accidentally start filtering at this layer.
286+ """
287+ stub_resource_provider .model_registry .run_health_check = Mock ()
288+ stub_resource_provider .mcp_registry = None
289+
290+ columns = _build_columns (
291+ model_configs = stub_model_configs ,
292+ llm_columns = [("col" , "stub-text" )],
293+ )
294+
295+ run_readiness_check (columns , stub_resource_provider )
296+
297+ stub_resource_provider .model_registry .run_health_check .assert_called_once_with (["stub-text" ])
298+
299+
300+ # ---------------------------------------------------------------------------
301+ # Async dispatch
302+ # ---------------------------------------------------------------------------
303+
304+
305+ def test_run_readiness_check_dispatches_to_async_registry_under_async_engine (
306+ stub_resource_provider ,
307+ stub_model_configs ,
308+ monkeypatch : pytest .MonkeyPatch ,
309+ ) -> None :
310+ """When the async engine is selected, model probes route through ``arun_health_check``.
311+
312+ The autouse fixture pins sync; this test overrides for the async path so the
313+ branch in ``readiness._run_model_health_check`` gets coverage.
314+ """
315+ monkeypatch .setattr (flags , "DATA_DESIGNER_ASYNC_ENGINE" , True )
316+ stub_resource_provider .model_registry .arun_health_check = Mock ()
317+ stub_resource_provider .mcp_registry = None
318+
319+ columns = _build_columns (
320+ model_configs = stub_model_configs ,
321+ llm_columns = [("col" , "stub-text" )],
322+ )
323+
324+ # ``run_coroutine_threadsafe`` returns a Future; we want the readiness wrapper
325+ # to call ``.result(timeout=...)`` on it, so install a Mock future whose
326+ # ``.result`` returns ``None`` (success).
327+ sentinel_future = Mock ()
328+ sentinel_future .result .return_value = None
329+
330+ fake_loop = Mock ()
331+
332+ with (
333+ patch ("data_designer.engine.readiness.ensure_async_engine_loop" , return_value = fake_loop , create = True ),
334+ patch ("asyncio.run_coroutine_threadsafe" , return_value = sentinel_future ) as mock_submit ,
335+ ):
336+ run_readiness_check (columns , stub_resource_provider )
337+
338+ # The async coroutine was created from arun_health_check and submitted to the loop.
339+ stub_resource_provider .model_registry .arun_health_check .assert_called_once_with (["stub-text" ])
340+ mock_submit .assert_called_once ()
341+ sentinel_future .result .assert_called_once_with (timeout = 180 )
342+
343+
344+ def test_run_readiness_check_cancels_future_and_reraises_on_timeout (
345+ stub_resource_provider ,
346+ stub_model_configs ,
347+ monkeypatch : pytest .MonkeyPatch ,
348+ ) -> None :
349+ """A 180-second timeout cancels the future and re-raises ``TimeoutError``."""
350+ monkeypatch .setattr (flags , "DATA_DESIGNER_ASYNC_ENGINE" , True )
351+ stub_resource_provider .model_registry .arun_health_check = Mock ()
352+ stub_resource_provider .mcp_registry = None
353+
354+ columns = _build_columns (
355+ model_configs = stub_model_configs ,
356+ llm_columns = [("col" , "stub-text" )],
357+ )
358+
359+ sentinel_future = Mock ()
360+ sentinel_future .result .side_effect = TimeoutError ()
361+
362+ with (
363+ patch ("data_designer.engine.readiness.ensure_async_engine_loop" , return_value = Mock (), create = True ),
364+ patch ("asyncio.run_coroutine_threadsafe" , return_value = sentinel_future ),
365+ pytest .raises (TimeoutError ),
366+ ):
367+ run_readiness_check (columns , stub_resource_provider )
368+
369+ sentinel_future .cancel .assert_called_once ()
0 commit comments