Files
myagents/tests/test_sync_workspace.py
T

211 lines
8.6 KiB
Python

"""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"