@@ -406,3 +406,67 @@ def _boom(src, dst):
406406 assert (tmp_path / "voice_sample.mp3" ).read_bytes () == b"old-audio"
407407 leftovers = [p .name for p in tmp_path .iterdir () if p .name .endswith (".tmp" )]
408408 assert leftovers == [], f"失败路径留下了暂存文件:{ leftovers } "
409+
410+
411+ def test_a_same_extension_replace_rolls_back_when_the_manifest_fails (tmp_path , monkeypatch ):
412+ """New audio must not survive under the old manifest.
413+
414+ Replacing a .wav with another .wav reuses the filename, so os.replace
415+ overwrites the old audio before the manifest is written. If the manifest
416+ write then fails, resolution still finds a "valid" pair — new audio wearing
417+ the old prefix / language / provider — while the upload answered 500. A
418+ silent mismatch is worse than a loud failure, so the previous audio is
419+ staged aside and restored.
420+ """
421+ from main_routers .workshop_router import voice_refs
422+
423+ (tmp_path / "voice_sample.wav" ).write_bytes (b"old-audio" )
424+ (tmp_path / WORKSHOP_VOICE_MANIFEST_NAME ).write_text (
425+ json .dumps ({"version" : 1 , "reference_audio" : "voice_sample.wav" , "prefix" : "old" }),
426+ encoding = "utf-8" ,
427+ )
428+
429+ def _boom (* args , ** kwargs ):
430+ raise OSError (28 , "No space left on device" )
431+
432+ monkeypatch .setattr (voice_refs , "atomic_write_json" , _boom )
433+
434+ with pytest .raises (OSError ):
435+ voice_refs ._replace_voice_reference (
436+ str (tmp_path ),
437+ str (tmp_path / "voice_sample.wav" ),
438+ b"new-audio" ,
439+ str (tmp_path / WORKSHOP_VOICE_MANIFEST_NAME ),
440+ {"version" : 1 , "reference_audio" : "voice_sample.wav" , "prefix" : "new" },
441+ )
442+
443+ assert (tmp_path / "voice_sample.wav" ).read_bytes () == b"old-audio" , (
444+ "新音频留在了旧 manifest 底下——同名替换失败后必须回滚"
445+ )
446+ assert _manifest (tmp_path )["prefix" ] == "old"
447+ leftovers = sorted (p .name for p in tmp_path .iterdir () if ".tmp" in p .name )
448+ assert leftovers == [], f"失败路径留下了暂存/备份文件:{ leftovers } "
449+
450+
451+ def test_a_successful_replace_leaves_no_backup_behind (tmp_path ):
452+ """The staged backup must not outlive a successful swap."""
453+ from main_routers .workshop_router import voice_refs
454+
455+ (tmp_path / "voice_sample.wav" ).write_bytes (b"old-audio" )
456+ (tmp_path / WORKSHOP_VOICE_MANIFEST_NAME ).write_text (
457+ json .dumps ({"version" : 1 , "reference_audio" : "voice_sample.wav" , "prefix" : "old" }),
458+ encoding = "utf-8" ,
459+ )
460+
461+ voice_refs ._replace_voice_reference (
462+ str (tmp_path ),
463+ str (tmp_path / "voice_sample.wav" ),
464+ b"new-audio" ,
465+ str (tmp_path / WORKSHOP_VOICE_MANIFEST_NAME ),
466+ {"version" : 1 , "reference_audio" : "voice_sample.wav" , "prefix" : "new" },
467+ )
468+
469+ assert (tmp_path / "voice_sample.wav" ).read_bytes () == b"new-audio"
470+ assert _manifest (tmp_path )["prefix" ] == "new"
471+ leftovers = sorted (p .name for p in tmp_path .iterdir () if ".tmp" in p .name )
472+ assert leftovers == [], f"成功路径留下了暂存/备份文件:{ leftovers } "
0 commit comments