@@ -169,6 +169,30 @@ def test_publish_succeeds_without_user_flag(self, temp_project_dir):
169169 content = (temp_project_dir / ".evolve" / "public" / "guideline" / "my-tip.md" ).read_text ()
170170 assert "visibility: public" in content
171171
172+ def test_publish_fails_when_public_entity_already_exists (self , temp_project_dir ):
173+ guideline_dir = temp_project_dir / ".evolve" / "entities" / "guideline"
174+ guideline_dir .mkdir (parents = True )
175+ source = guideline_dir / "my-tip.md"
176+ source .write_text ("---\n type: guideline\n ---\n \n Prefer composition.\n " )
177+
178+ public_path = temp_project_dir / ".evolve" / "public" / "guideline" / "my-tip.md"
179+ public_path .parent .mkdir (parents = True )
180+ existing_content = "---\n type: guideline\n visibility: public\n ---\n \n Existing public content.\n "
181+ public_path .write_text (existing_content )
182+
183+ result = run_script (
184+ PUBLISH_SCRIPT ,
185+ project_dir = temp_project_dir ,
186+ args = ["--entity" , "my-tip.md" ],
187+ evolve_dir = temp_project_dir / ".evolve" ,
188+ expect_success = False ,
189+ )
190+
191+ assert result .returncode != 0
192+ assert "already published" in result .stderr
193+ assert public_path .read_text () == existing_content
194+ assert source .exists ()
195+
172196 def test_publish_rejects_path_traversal_in_entity_name (self , temp_project_dir ):
173197 guideline_dir = temp_project_dir / ".evolve" / "entities" / "guideline"
174198 guideline_dir .mkdir (parents = True )
@@ -240,6 +264,37 @@ def test_subscribe_rejects_path_traversal_in_name(self, temp_project_dir, local_
240264 assert result .returncode != 0
241265 assert "invalid subscription name" in result .stderr
242266
267+ @pytest .mark .parametrize ("name" , ["" , "." ])
268+ def test_subscribe_rejects_empty_or_dot_name (self , temp_project_dir , local_repo , name ):
269+ result = run_script (
270+ SUBSCRIBE_SCRIPT ,
271+ project_dir = temp_project_dir ,
272+ args = ["--name" , name , "--remote" , str (local_repo ["bare" ]), "--branch" , "main" ],
273+ evolve_dir = temp_project_dir / ".evolve" ,
274+ expect_success = False ,
275+ )
276+
277+ assert result .returncode != 0
278+ assert "invalid subscription name" in result .stderr
279+
280+ def test_subscribe_fails_when_destination_already_exists (self , temp_project_dir , local_repo ):
281+ evolve_dir = temp_project_dir / ".evolve"
282+ existing_dest = evolve_dir / "subscribed" / "alice"
283+ existing_dest .mkdir (parents = True )
284+
285+ result = run_script (
286+ SUBSCRIBE_SCRIPT ,
287+ project_dir = temp_project_dir ,
288+ args = ["--name" , "alice" , "--remote" , str (local_repo ["bare" ]), "--branch" , "main" ],
289+ evolve_dir = evolve_dir ,
290+ expect_success = False ,
291+ )
292+
293+ assert result .returncode != 0
294+ assert "destination already exists" in result .stderr
295+ config_path = temp_project_dir / "evolve.config.yaml"
296+ assert not config_path .exists ()
297+
243298 def test_unsubscribe_list_and_not_found (self , temp_project_dir , local_repo ):
244299 evolve_dir = temp_project_dir / ".evolve"
245300 run_script (
@@ -315,6 +370,78 @@ def test_sync_no_subscriptions_exits_cleanly(self, temp_project_dir):
315370 assert "No subscriptions" in result .stdout
316371 assert "evolve-lite:subscribe" in result .stdout
317372
373+ def test_sync_skips_invalid_subscription_name (self , temp_project_dir ):
374+ evolve_dir = temp_project_dir / ".evolve"
375+ (temp_project_dir / "evolve.config.yaml" ).write_text (
376+ 'subscriptions:\n '
377+ ' - name: "."\n '
378+ ' remote: "https://example.com/repo.git"\n '
379+ ' branch: "main"\n '
380+ )
381+
382+ result = run_script (
383+ SYNC_SCRIPT ,
384+ project_dir = temp_project_dir ,
385+ evolve_dir = evolve_dir ,
386+ expect_success = False ,
387+ )
388+
389+ assert result .returncode == 0
390+ assert ". (invalid subscription name)" in result .stdout
391+ assert not (evolve_dir / "entities" / "subscribed" ).exists ()
392+
393+ def test_manual_sync_runs_even_when_on_session_start_is_false (self , temp_project_dir , local_repo ):
394+ evolve_dir = temp_project_dir / ".evolve"
395+ run_script (
396+ SUBSCRIBE_SCRIPT ,
397+ project_dir = temp_project_dir ,
398+ args = ["--name" , "alice" , "--remote" , str (local_repo ["bare" ]), "--branch" , "main" ],
399+ evolve_dir = evolve_dir ,
400+ )
401+ (temp_project_dir / "evolve.config.yaml" ).write_text (
402+ 'subscriptions:\n '
403+ f' - name: "alice"\n remote: "{ local_repo ["bare" ]} "\n branch: "main"\n '
404+ 'sync:\n '
405+ ' on_session_start: false\n '
406+ )
407+
408+ result = run_script (
409+ SYNC_SCRIPT ,
410+ project_dir = temp_project_dir ,
411+ evolve_dir = evolve_dir ,
412+ expect_success = False ,
413+ )
414+
415+ assert result .returncode == 0
416+ assert "Synced 1 repo(s):" in result .stdout
417+
418+ def test_session_start_sync_respects_on_session_start_false (self , temp_project_dir , local_repo ):
419+ evolve_dir = temp_project_dir / ".evolve"
420+ run_script (
421+ SUBSCRIBE_SCRIPT ,
422+ project_dir = temp_project_dir ,
423+ args = ["--name" , "alice" , "--remote" , str (local_repo ["bare" ]), "--branch" , "main" ],
424+ evolve_dir = evolve_dir ,
425+ )
426+ (temp_project_dir / "evolve.config.yaml" ).write_text (
427+ 'subscriptions:\n '
428+ f' - name: "alice"\n remote: "{ local_repo ["bare" ]} "\n branch: "main"\n '
429+ 'sync:\n '
430+ ' on_session_start: false\n '
431+ )
432+
433+ result = run_script (
434+ SYNC_SCRIPT ,
435+ project_dir = temp_project_dir ,
436+ args = ["--quiet" , "--session-start" ],
437+ evolve_dir = evolve_dir ,
438+ expect_success = False ,
439+ )
440+
441+ assert result .returncode == 0
442+ mirrored = evolve_dir / "entities" / "subscribed" / "alice" / "guideline" / "tip-one.md"
443+ assert not mirrored .exists ()
444+
318445 def test_sync_writes_audit_log (self , temp_project_dir , local_repo ):
319446 evolve_dir = temp_project_dir / ".evolve"
320447 run_script (
@@ -351,6 +478,29 @@ def test_sync_picks_up_new_entity_after_push(self, temp_project_dir, local_repo)
351478 assert mirrored .exists ()
352479 assert "Delete dead code promptly." in mirrored .read_text ()
353480
481+ def test_sync_skips_symlinked_markdown_files (self , temp_project_dir , local_repo ):
482+ evolve_dir = temp_project_dir / ".evolve"
483+ run_script (
484+ SUBSCRIBE_SCRIPT ,
485+ project_dir = temp_project_dir ,
486+ args = ["--name" , "alice" , "--remote" , str (local_repo ["bare" ]), "--branch" , "main" ],
487+ evolve_dir = evolve_dir ,
488+ )
489+
490+ git_env = local_repo ["env" ]
491+ target = local_repo ["work" ] / "guideline" / "tip-one.md"
492+ symlink = local_repo ["work" ] / "guideline" / "tip-link.md"
493+ symlink .symlink_to (target .name )
494+ subprocess .run (["git" , "-C" , str (local_repo ["work" ]), "add" , "." ], check = True , env = git_env )
495+ subprocess .run (["git" , "-C" , str (local_repo ["work" ]), "commit" , "-m" , "add tip symlink" ], check = True , env = git_env )
496+ subprocess .run (["git" , "-C" , str (local_repo ["work" ]), "push" , "origin" , "main" ], check = True , env = git_env )
497+
498+ run_script (SYNC_SCRIPT , project_dir = temp_project_dir , evolve_dir = evolve_dir )
499+
500+ mirrored_dir = evolve_dir / "entities" / "subscribed" / "alice" / "guideline"
501+ assert (mirrored_dir / "tip-one.md" ).exists ()
502+ assert not (mirrored_dir / "tip-link.md" ).exists ()
503+
354504 def test_sync_removed_entity_disappears_after_sync (self , temp_project_dir , local_repo ):
355505 evolve_dir = temp_project_dir / ".evolve"
356506 run_script (
0 commit comments