deer-flow/backend/tests/test_lark_skill_manifest_atomic.py
IronMurphy 6103c7b6c2
fix(lark): publish the skill file and manifest atomically (#6683)
* fix(lark): publish the skill file and manifest atomically

_write_skill_file and _write_manifest wrote their final paths with a truncating
write, while the runtime files in the same module already go through
_publish_bytes_atomically. A partial skill file or manifest is unparseable, and
the manifest carries the content hash the install is verified against, so a
half-written one cannot be repaired. Route both through the existing helper.

* fix(lark): address skill publication review feedback

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-11 17:46:29 +08:00

97 lines
4.3 KiB
Python

# SPDX-License-Identifier: MIT
"""Exercise file publication and rollback of the installed Lark skill pack."""
from __future__ import annotations
import json
import os
import zipfile
from pathlib import Path
import pytest
from deerflow.config import paths as paths_module
from deerflow.config.paths import Paths
from deerflow.integrations import lark_cli
@pytest.mark.parametrize("file_kind", ["skill", "manifest"])
def test_failed_file_publish_preserves_previous_content(tmp_path, monkeypatch, file_kind):
root = tmp_path / "lark-cli"
skill_file = root / "lark-shared" / "SKILL.md"
skill_file.parent.mkdir(parents=True)
skill_file.write_text("---\nname: lark-shared\ndescription: 飞书说明\n---\n\n旧版正文\n", encoding="utf-8")
manifest_file = root / lark_cli.LARK_CLI_MANIFEST_FILE
manifest_file.write_text('{"content_sha256": "previous-digest"}\n', encoding="utf-8")
target = skill_file if file_kind == "skill" else manifest_file
previous = target.read_bytes()
real_replace = os.replace
def fail_target_replace(source, destination):
if Path(destination) == target:
raise OSError("injected file publication failure")
return real_replace(source, destination)
monkeypatch.setattr(lark_cli.os, "replace", fail_target_replace)
with pytest.raises(OSError, match="injected file publication failure"):
if file_kind == "skill":
lark_cli._append_deerflow_lark_shared_guidance(root)
else:
lark_cli._write_manifest(root, {"lark-shared"}, version="v1.0.65", content_sha256="new-digest")
assert target.read_bytes() == previous
assert not list(root.rglob("*.tmp"))
def _make_archive(tmp_path: Path, revision: str) -> Path:
archive = tmp_path / f"lark-{revision}.zip"
with zipfile.ZipFile(archive, "w") as zf:
for skill_name in lark_cli.LARK_SKILL_NAMES:
content = f"---\nname: {skill_name}\ndescription: 飞书技能 {revision}\n---\n\n# {skill_name}\n\n{revision} 正文\n"
zf.writestr(f"cli-1.0.65/skills/{skill_name}/SKILL.md", content)
zf.writestr(f"cli-1.0.65/skills/{skill_name}/references/readme.md", f"{revision} 参考\n")
return archive
def _snapshot(root: Path) -> dict[str, bytes]:
return {path.relative_to(root).as_posix(): path.read_bytes() for path in root.rglob("*") if path.is_file()}
@pytest.mark.parametrize("file_kind", ["skill", "manifest"])
def test_failed_reinstall_keeps_installed_pack_and_discards_staging(tmp_path, monkeypatch, file_kind):
monkeypatch.setattr(paths_module, "_paths", Paths(base_dir=tmp_path / "home"))
previous_archive = _make_archive(tmp_path, "previous")
replacement_archive = _make_archive(tmp_path, "replacement")
installed, digest = lark_cli._install_lark_skills_from_archive("alice", previous_archive)
root = lark_cli.lark_integration_root("alice")
previous = _snapshot(root)
manifest = json.loads(previous[lark_cli.LARK_CLI_MANIFEST_FILE])
assert manifest["content_sha256"] == digest
assert set(manifest["skills"]) == set(installed)
failed_destination: Path | None = None
real_replace = os.replace
def fail_staged_replace(source, destination):
nonlocal failed_destination
destination = Path(destination)
is_selected_file = destination.name == lark_cli.LARK_CLI_MANIFEST_FILE if file_kind == "manifest" else destination.parent.name == "lark-shared" and destination.name == "SKILL.md"
if is_selected_file:
failed_destination = destination
# The publish is still private, and the live body/manifest pair is intact.
assert destination.is_relative_to(root.parent)
assert not destination.is_relative_to(root)
assert _snapshot(root) == previous
raise OSError("injected staged publication failure")
return real_replace(source, destination)
monkeypatch.setattr(lark_cli.os, "replace", fail_staged_replace)
with pytest.raises(OSError, match="injected staged publication failure"):
lark_cli._install_lark_skills_from_archive("alice", replacement_archive)
assert failed_destination is not None
assert _snapshot(root) == previous
assert not failed_destination.exists()
assert not list(root.parent.glob(".installing-lark-cli-*"))
assert not list(root.parent.glob(".replacing-*"))