feat: 审查修复一轮——sync 解引用软链技能/防 CLAUDE.md 崩溃/init 建 agent-tasks;upgrade SHA256 校验+收尾容错;新增 xiaohe switch 回滚;uninstall --all 扩到 ~/.metabot ~/.mytoolkit;下载路径改 xiaohe-agent
This commit is contained in:
@@ -80,6 +80,104 @@ class TestUpgradeCommand:
|
||||
assert "Cancelled" in result.output
|
||||
|
||||
|
||||
def _make_tarball_bytes() -> bytes:
|
||||
import tarfile
|
||||
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w:gz") as tf:
|
||||
data = b"#!/bin/sh\n"
|
||||
info = tarfile.TarInfo("workspace/setup.sh")
|
||||
info.size = len(data)
|
||||
tf.addfile(info, io.BytesIO(data))
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _wire_upgrade(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
payload: bytes,
|
||||
sha: str,
|
||||
) -> None:
|
||||
import hashlib
|
||||
|
||||
def fake_fetch(url: str, user: str, password: str) -> str:
|
||||
if url.endswith("install.sh"):
|
||||
return 'VERSION="v2.0.0"\n'
|
||||
if url.endswith(".sha256"):
|
||||
return f"{sha} xiaohe-agent-latest.tar.gz\n"
|
||||
raise AssertionError(f"unexpected url {url}")
|
||||
|
||||
monkeypatch.setattr(up_mod, "_fetch_text", fake_fetch)
|
||||
monkeypatch.setattr(
|
||||
up_mod, "_download", lambda url, u, p, dest: dest.write_bytes(payload)
|
||||
)
|
||||
monkeypatch.setattr(up_mod, "_install_tools", lambda runtime: [])
|
||||
workspace = tmp_path / "ws"
|
||||
workspace.mkdir()
|
||||
monkeypatch.setattr(up_mod, "get_workspace_root", lambda create=False: workspace)
|
||||
monkeypatch.setattr(
|
||||
up_mod,
|
||||
"sync_workspace",
|
||||
lambda ws: {"added": [], "updated": [], "skipped": [], "removed": []},
|
||||
)
|
||||
|
||||
|
||||
class TestUpgradeFlow:
|
||||
def test_downloads_verifies_and_switches(
|
||||
self, fake_home: Path, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
import hashlib
|
||||
|
||||
_make_runtime(fake_home, "v1.0.0")
|
||||
payload = _make_tarball_bytes()
|
||||
sha = hashlib.sha256(payload).hexdigest()
|
||||
_wire_upgrade(monkeypatch, tmp_path, payload, sha)
|
||||
result = CliRunner().invoke(
|
||||
up_mod.upgrade_cmd, ["--user", "u", "--password", "p"], input="y\n"
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "Upgraded: v1.0.0 -> v2.0.0" in result.output
|
||||
current = fake_home / ".xiaohe" / "runtime" / "current"
|
||||
assert current.resolve().name == "v2.0.0"
|
||||
assert (current / "setup.sh").is_file()
|
||||
|
||||
def test_checksum_mismatch_aborts_before_install(
|
||||
self, fake_home: Path, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
_make_runtime(fake_home, "v1.0.0")
|
||||
payload = _make_tarball_bytes()
|
||||
_wire_upgrade(monkeypatch, tmp_path, payload, "0" * 64)
|
||||
result = CliRunner().invoke(
|
||||
up_mod.upgrade_cmd, ["--user", "u", "--password", "p"], input="y\n"
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "Checksum mismatch" in result.output
|
||||
# Old runtime untouched, no new version dir.
|
||||
current = fake_home / ".xiaohe" / "runtime" / "current"
|
||||
assert current.resolve().name == "v1.0.0"
|
||||
assert not (fake_home / ".xiaohe" / "runtime" / "v2.0.0").exists()
|
||||
|
||||
|
||||
class TestVerifyChecksum:
|
||||
def test_accepts_matching(self, tmp_path: Path) -> None:
|
||||
import hashlib
|
||||
|
||||
blob = tmp_path / "f.tar.gz"
|
||||
blob.write_bytes(b"data")
|
||||
sha = hashlib.sha256(b"data").hexdigest()
|
||||
up_mod._verify_checksum(blob, f"{sha} f.tar.gz\n", "http://x/")
|
||||
|
||||
def test_rejects_mismatch_and_garbage(self, tmp_path: Path) -> None:
|
||||
import click
|
||||
|
||||
blob = tmp_path / "f.tar.gz"
|
||||
blob.write_bytes(b"data")
|
||||
with pytest.raises(click.ClickException, match="Checksum mismatch"):
|
||||
up_mod._verify_checksum(blob, f"{'0' * 64} f.tar.gz\n", "http://x/")
|
||||
with pytest.raises(click.ClickException, match="looks wrong"):
|
||||
up_mod._verify_checksum(blob, "not-a-checksum\n", "http://x/")
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, data: bytes) -> None:
|
||||
self.headers = {"Content-Length": str(len(data))}
|
||||
|
||||
Reference in New Issue
Block a user