"""Tests for myagents.commands.sync_workspace.""" import json from pathlib import Path import click import pytest from myagents.commands import sync_workspace as sync_mod @pytest.fixture() def runtime(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: rt = tmp_path / "runtime" (rt / ".agents" / "skills" / "demo").mkdir(parents=True) (rt / ".agents" / "skills" / "demo" / "SKILL.md").write_text("demo v1") (rt / "agents.md").write_text("agent rules v1") (rt / "VERSION").write_text("v0.0.0-test\n") monkeypatch.setattr(sync_mod, "get_runtime_root", lambda: rt) return rt @pytest.fixture() def workspace(tmp_path: Path) -> Path: ws = tmp_path / "workspace" ws.mkdir() return ws def _read_stamp(ws: Path) -> dict: return json.loads((ws / sync_mod.STAMP_NAME).read_text()) class TestSyncWorkspace: def test_new_files_copied_and_symlinks_made( self, runtime: Path, workspace: Path ) -> None: report = sync_mod.sync_workspace(workspace) skill = workspace / ".agents" / "skills" / "demo" / "SKILL.md" assert skill.read_text() == "demo v1" assert (workspace / "agents.md").read_text() == "agent rules v1" assert (workspace / "CLAUDE.md").is_symlink() assert (workspace / ".claude").is_symlink() assert report["added"] and not report["skipped"] assert _read_stamp(workspace)["version"] == "v0.0.0-test" def test_second_run_is_noop(self, runtime: Path, workspace: Path) -> None: sync_mod.sync_workspace(workspace) report = sync_mod.sync_workspace(workspace) assert report == {"added": [], "updated": [], "skipped": [], "removed": []} def test_upgrade_overwrites_untouched_file( self, runtime: Path, workspace: Path ) -> None: sync_mod.sync_workspace(workspace) (runtime / "agents.md").write_text("agent rules v2") report = sync_mod.sync_workspace(workspace) assert (workspace / "agents.md").read_text() == "agent rules v2" assert "agents.md" in report["updated"] def test_user_modified_file_is_skipped( self, runtime: Path, workspace: Path ) -> None: sync_mod.sync_workspace(workspace) (workspace / "agents.md").write_text("my local edits") (runtime / "agents.md").write_text("agent rules v2") report = sync_mod.sync_workspace(workspace) assert (workspace / "agents.md").read_text() == "my local edits" assert "agents.md" in report["skipped"] def test_removed_from_runtime_is_removed_from_workspace( self, runtime: Path, workspace: Path ) -> None: sync_mod.sync_workspace(workspace) (runtime / ".agents" / "skills" / "demo" / "SKILL.md").unlink() report = sync_mod.sync_workspace(workspace) assert not (workspace / ".agents" / "skills" / "demo" / "SKILL.md").exists() assert report["removed"] def test_git_checkout_refused_without_force( self, runtime: Path, workspace: Path ) -> None: (workspace / ".git").mkdir() with pytest.raises(click.ClickException): sync_mod.sync_workspace(workspace) report = sync_mod.sync_workspace(workspace, force=True) assert report["added"] def test_symlinked_skill_dir_is_dereferenced( self, runtime: Path, workspace: Path ) -> None: # Skills living in contrib/ are linked into .agents/skills (relative # symlink); the workspace must receive real self-contained files. real = runtime / "contrib" / "mytoolkit" / ".claude" / "skills" / "mytoolkit" real.mkdir(parents=True) (real / "SKILL.md").write_text("toolkit skill v1") import os rel_target = os.path.relpath(real, runtime / ".agents" / "skills") (runtime / ".agents" / "skills" / "mytoolkit").symlink_to( rel_target, target_is_directory=True ) report = sync_mod.sync_workspace(workspace) copied = workspace / ".agents" / "skills" / "mytoolkit" / "SKILL.md" assert copied.is_file() and not copied.is_symlink() assert copied.read_text() == "toolkit skill v1" assert ".agents/skills/mytoolkit/SKILL.md" in report["added"] # Second run: content identical -> no spurious updates. assert sync_mod.sync_workspace(workspace) == { "added": [], "updated": [], "skipped": [], "removed": [] } def test_existing_real_managed_symlink_target_is_kept( self, runtime: Path, workspace: Path ) -> None: # A pre-existing real CLAUDE.md must not be replaced by a symlink. (workspace / "CLAUDE.md").write_text("my own claude rules") sync_mod.sync_workspace(workspace) claude_md = workspace / "CLAUDE.md" assert not claude_md.is_symlink() assert claude_md.read_text() == "my own claude rules" assert (workspace / ".claude").is_symlink() # other links still made class TestCreateLauncher: @pytest.fixture() def fake_home(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: home = tmp_path / "home" (home / "Desktop").mkdir(parents=True) monkeypatch.setattr(Path, "home", staticmethod(lambda: home)) return home def test_macos_app_bundle_with_icon( self, runtime: Path, fake_home: Path, monkeypatch: pytest.MonkeyPatch ) -> None: (runtime / "assets" / "icon").mkdir(parents=True) (runtime / "assets" / "icon" / "XiaoheAgent.icns").write_bytes(b"icns") legacy_cmd = fake_home / "Desktop" / "XiaoheAgent.command" legacy_cmd.write_text("#!/bin/sh\n") legacy_app = fake_home / "Desktop" / "XiaoheAgent.app" legacy_app.mkdir() monkeypatch.setattr( sync_mod.os, "uname", lambda: type("U", (), {"sysname": "Darwin"}) ) sync_mod._create_launcher() app = fake_home / "Desktop" / "Xiaohe Agent.app" executable = app / "Contents" / "MacOS" / "XiaoheAgent" plist = (app / "Contents" / "Info.plist").read_text() assert "Xiaohe Agent" in plist assert (app / "Contents" / "Resources" / "XiaoheAgent.icns").is_file() assert executable.stat().st_mode & 0o111 assert "xiaohe" in executable.read_text() assert not legacy_cmd.exists() # superseded .command removed assert not legacy_app.exists() # renamed from XiaoheAgent.app def test_linux_desktop_entry_with_icon( self, runtime: Path, fake_home: Path, monkeypatch: pytest.MonkeyPatch ) -> None: (runtime / "assets" / "icon").mkdir(parents=True) (runtime / "assets" / "icon" / "xiaohe-icon-512.png").write_bytes(b"png") monkeypatch.setattr( sync_mod.os, "uname", lambda: type("U", (), {"sysname": "Linux"}) ) sync_mod._create_launcher() entry = ( fake_home / ".local" / "share" / "applications" / "Xiaohe Agent.desktop" ) text = entry.read_text() assert "Name=Xiaohe Agent" in text assert "Icon=xiaohe-agent" in text class TestInitCommand: @pytest.fixture() def fake_home(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: home = tmp_path / "home" home.mkdir() monkeypatch.setattr(Path, "home", staticmethod(lambda: home)) monkeypatch.setattr(sync_mod, "get_setting", lambda *a, **kw: "") monkeypatch.setattr(sync_mod, "set_setting", lambda *a, **kw: None) return home def test_agent_tasks_created_from_template( self, runtime: Path, workspace: Path, fake_home: Path ) -> None: from click.testing import CliRunner template = runtime / "assistant" / "agent-tasks.template.md" template.parent.mkdir(parents=True) template.write_text("# task board template") result = CliRunner().invoke(sync_mod.init_cmd, [str(workspace)]) assert result.exit_code == 0, result.output live = workspace / "assistant" / "agent-tasks.md" assert live.read_text() == "# task board template" assert (workspace / "tmp").is_dir() # skeleton dirs created def test_existing_agent_tasks_untouched( self, runtime: Path, workspace: Path, fake_home: Path ) -> None: from click.testing import CliRunner template = runtime / "assistant" / "agent-tasks.template.md" template.parent.mkdir(parents=True) template.write_text("# task board template") live = workspace / "assistant" / "agent-tasks.md" live.parent.mkdir(parents=True) live.write_text("# my live board") result = CliRunner().invoke(sync_mod.init_cmd, [str(workspace)]) assert result.exit_code == 0, result.output assert live.read_text() == "# my live board"