"""Unit tests for the service cycle: error isolation + apply-still-called. These verify the orchestration contract of ``cycle()`` only. ``run()`` is an infinite loop and is deliberately not tested here; real end-to-end smoke is deferred to the Task 9 pilot. """ from unittest.mock import MagicMock, patch from sync.config import ( SyncConfig, AccessConfig, RuntimeConfig, SqlServerConfig, FileMapping, ) from sync import service def _cfg(): return SyncConfig( sql_server=SqlServerConfig(conn_str="x"), access=AccessConfig(driver="d", roots={"2026": "r"}), runtime=RuntimeConfig(), files=[ FileMapping(file="ok.accdb", root="2026", schema="s", year_suffix="_Y"), FileMapping(file="bad.accdb", root="2026", schema="s", year_suffix="_Y"), ], ) def test_cycle_continues_on_capture_error_and_still_applies_and_cleans(): """A capture failure on one file must not abort the cycle. Verify: (a) capture is attempted for both files, (b) apply is still called, (c) cleanup is attempted for both files, (d) the writer is closed in the finally block. """ cfg = _cfg() with patch.object(service, "SqlWriter") as MW, \ patch.object(service, "AccessReader") as MR, \ patch.object(service, "capture_file", side_effect=[5, RuntimeError("boom")]) as mcapture, \ patch.object(service, "cleanup_file", return_value=0) as mcleanup: writer_inst = MagicMock() MW.return_value = writer_inst MR.return_value = MagicMock() # reader instance mock service.cycle(cfg) # must not raise assert mcapture.call_count == 2 # both files attempted (bad swallowed) writer_inst.call_apply.assert_called_once() # apply still called assert mcleanup.call_count == 2 # cleanup attempted for both files writer_inst.close.assert_called() # writer closed in finally def test_cycle_apply_failure_does_not_block_cleanup(): """Apply failure must not block the cleanup phase.""" cfg = _cfg() with patch.object(service, "SqlWriter") as MW, \ patch.object(service, "AccessReader") as MR, \ patch.object(service, "capture_file", return_value=3) as mcapture, \ patch.object(service, "cleanup_file", return_value=7) as mcleanup: writer_inst = MagicMock() writer_inst.call_apply.side_effect = RuntimeError("apply boom") MW.return_value = writer_inst MR.return_value = MagicMock() service.cycle(cfg) # must not raise assert mcapture.call_count == 2 # both files captured writer_inst.call_apply.assert_called_once() # apply attempted assert mcleanup.call_count == 2 # cleanup still runs despite apply failure writer_inst.close.assert_called() def test_cycle_cleanup_failure_does_not_abort_other_files(): """A cleanup failure on one file must not skip cleanup of the other.""" cfg = _cfg() with patch.object(service, "SqlWriter") as MW, \ patch.object(service, "AccessReader") as MR, \ patch.object(service, "capture_file", return_value=1), \ patch.object(service, "cleanup_file", side_effect=[0, RuntimeError("cleanup boom")]) as mcleanup: writer_inst = MagicMock() MW.return_value = writer_inst MR.return_value = MagicMock() service.cycle(cfg) # must not raise assert mcleanup.call_count == 2 # second file cleanup still attempted writer_inst.close.assert_called()