mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* 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>
97 lines
4.3 KiB
Python
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-*"))
|