@@ -158,6 +158,57 @@ def fail_once_for_candidate_system(path: Path):
158158 assert {name : path .read_bytes () for name , path in paths .items ()} == baseline
159159
160160
161+ def test_commit_removes_temp_file_when_replace_fails (tmp_path : Path , monkeypatch ):
162+ target = tmp_path / "system.txt"
163+ baseline = b"system baseline"
164+ target .write_bytes (baseline )
165+ snapshot = snapshot_prompt_files ({"system" : target })
166+ original_replace = os .replace
167+ replace_calls = 0
168+
169+ def fail_first_replace (source : str | bytes | Path , destination : str | bytes | Path ):
170+ nonlocal replace_calls
171+ replace_calls += 1
172+ if replace_calls == 1 :
173+ raise OSError ("candidate replace failed" )
174+ return original_replace (source , destination )
175+
176+ monkeypatch .setattr (writeback .os , "replace" , fail_first_replace )
177+
178+ result = commit_prompt_bundle (snapshot , {"system" : "candidate system" })
179+
180+ assert result .status == "rolled_back"
181+ assert target .read_bytes () == baseline
182+ assert list (tmp_path .glob (f".{ target .name } .*.tmp" )) == []
183+
184+
185+ def test_atomic_replace_preserves_replace_error_when_temp_unlink_fails (tmp_path : Path , monkeypatch ):
186+ target = tmp_path / "system.txt"
187+ baseline = b"system baseline"
188+ target .write_bytes (baseline )
189+ original_unlink = Path .unlink
190+
191+ def fail_replace (source : str | bytes | Path , destination : str | bytes | Path ):
192+ raise OSError ("primary replace failed" )
193+
194+ def fail_temp_unlink (path : Path , missing_ok : bool = False ):
195+ if path .parent == tmp_path and path .name .startswith (f".{ target .name } ." ) and path .suffix == ".tmp" :
196+ raise OSError ("temp unlink failed" )
197+ return original_unlink (path , missing_ok = missing_ok )
198+
199+ with monkeypatch .context () as injected_failure :
200+ injected_failure .setattr (writeback .os , "replace" , fail_replace )
201+ injected_failure .setattr (Path , "unlink" , fail_temp_unlink )
202+ with pytest .raises (OSError ) as error_info :
203+ writeback ._atomic_replace_bytes (target , b"candidate system" )
204+
205+ for temp_path in tmp_path .glob (f".{ target .name } .*.tmp" ):
206+ original_unlink (temp_path )
207+
208+ assert str (error_info .value ) == "primary replace failed"
209+ assert target .read_bytes () == baseline
210+
211+
161212def test_commit_rolls_back_all_files_when_second_replace_fails (tmp_path : Path , monkeypatch ):
162213 paths , baseline = _prompt_files (tmp_path )
163214 snapshot = snapshot_prompt_files (paths )
0 commit comments