feat: xiaohe 命令组 + settings.json 拆分 + workspace 同步引擎
- xiaohe:默认转发 default_agent(settings.json default_agent,兼容 myclaude 拼写), 子命令 init(首用引导 workspace 路径+骨架+sync+启动器)/ sync(runtime→workspace 哈希三态同步:新增/覆盖/本地改动跳过;git 检出拒绝) - settings.py:~/.xiaohe/agent/settings.json 优先,config.json settings 段兜底 - get_workspace_root:env > settings > project_root/workspace > ~/workspace - build_cli 增 offer_install:缺二进制时交互提示安装(claude/codex 有 install_cmd) - 测试 +11(settings/sync 引擎三态/幂等/git 拒绝)
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""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"]
|
||||
Reference in New Issue
Block a user