@@ -224,6 +224,79 @@ def test_janitor_warn_on_delete_failure_downgrades_aggregated_error(
224224 assert "Janitor completed with failures" in warn_spy .call_args [0 ][0 ]
225225
226226
227+ def test_janitor_force_delete_removes_environment_state_despite_drop_failure (
228+ mocker : MockerFixture , tmp_path : Path
229+ ):
230+ models_dir = tmp_path / "models"
231+ models_dir .mkdir ()
232+ (models_dir / "model1.sql" ).write_text ("MODEL(name test.model1, kind FULL); SELECT 1 AS col" )
233+
234+ ctx = Context (
235+ paths = [tmp_path ],
236+ config = Config (model_defaults = ModelDefaultsConfig (dialect = "duckdb" )),
237+ )
238+ ctx .plan ("dev" , no_prompts = True , auto_apply = True )
239+ ctx .invalidate_environment ("dev" )
240+
241+ mocker .patch (
242+ "sqlmesh.core.context.cleanup_expired_views" ,
243+ return_value = ["view drop error" ],
244+ )
245+ mocker .patch (
246+ "sqlmesh.core.janitor.iter_expired_snapshot_batches" ,
247+ return_value = iter ([]),
248+ )
249+
250+ # without force_delete the environment is retained for retry
251+ with pytest .raises (SQLMeshError ):
252+ ctx ._run_janitor (ignore_ttl = True , force_delete = False )
253+ assert ctx .state_sync .get_environment ("dev" ) is not None
254+
255+ # with force_delete the environment state is purged even though drops failed
256+ with pytest .raises (SQLMeshError ):
257+ ctx ._run_janitor (ignore_ttl = True , force_delete = True )
258+ assert ctx .state_sync .get_environment ("dev" ) is None
259+
260+
261+ def test_janitor_force_delete_removes_snapshot_state_despite_cleanup_failure (
262+ mocker : MockerFixture , tmp_path : Path
263+ ):
264+ models_dir = tmp_path / "models"
265+ models_dir .mkdir ()
266+ model1_path = models_dir / "model1.sql"
267+ model1_path .write_text ("MODEL(name test.model1, kind FULL); SELECT 1 AS col" )
268+
269+ # using warn_on_delete_failure so the janitor completes and we can inspect the state after
270+ ctx = Context (
271+ paths = [tmp_path ],
272+ config = Config (
273+ model_defaults = ModelDefaultsConfig (dialect = "duckdb" ),
274+ janitor = JanitorConfig (warn_on_delete_failure = True ),
275+ ),
276+ )
277+ ctx .plan ("dev" , no_prompts = True , auto_apply = True )
278+ model1_snapshot = ctx .get_snapshot ("test.model1" )
279+
280+ # simulating a zombie snapshot
281+ model1_path .unlink ()
282+ ctx .load ()
283+ ctx .plan ("dev" , no_prompts = True , auto_apply = True )
284+ ctx .invalidate_environment ("dev" )
285+
286+ mocker .patch (
287+ "sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.cleanup" ,
288+ side_effect = Exception ("table cleanup error" ),
289+ )
290+
291+ # without force_delete the snapshot state is retained for retry
292+ ctx ._run_janitor (ignore_ttl = True , force_delete = False )
293+ assert ctx .state_sync .get_snapshots ([model1_snapshot .snapshot_id ]) # type: ignore
294+
295+ # with force_delete the snapshot state record is purged even though cleanup failed
296+ ctx ._run_janitor (ignore_ttl = True , force_delete = True )
297+ assert not ctx .state_sync .get_snapshots ([model1_snapshot .snapshot_id ]) # type: ignore
298+
299+
227300@use_terminal_console
228301def test_destroy (copy_to_temp_path ):
229302 # Testing project with two gateways to verify cleanup is performed across engines
0 commit comments