mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 05:06:59 +00:00
* test: fix five Windows-host failures across sandbox, read_file, lark shim, and extension checks Windows dev hosts currently fail five backend test cases that CI (ubuntu) never exercises: - test_local_sandbox_command_timeout: the fixture forces a bare /bin/sh shell and the Windows dispatch spawns it directly (WinError 2), and make(1) is not a given on Windows. Stub both runners; the contract under test is the timeout marker assembly, not the runner. - test_local_sandbox_provider_mounts: the Popen-capture mock delegated to the real spawn, which cannot execute a POSIX shell path on Windows. Capture at the runner seam instead and assert the rewritten path shape-agnostically (the Windows runners pass the command string, not a [shell, -c, command] list). - test_read_file_tool_binary: text-mode write_text leaks CRLF into the fixture on Windows; write the mount fixture as bytes (#6042 follow-up). - test_lark_shim_atomic: skip the exact 0o755/0o644 assertions on Windows hosts, whose stat mode approximates POSIX bits (#6257 pattern). - test_extension_check_script: zipfile normalizes os.sep in entry names on Windows reads, so a writestr-built fixture can never carry a verbatim backslash entry there. Inject the hostile name into namelist unless the archive already stores it, pinning the guard contract identically. On a Windows 11 host these five files go from five failed cases to green (102 passed, ruff check/format clean). Two more Windows-host failures (skill-review NUL probe, jina shared-deadline classification) need deeper root-causing and are left for follow-ups. * test(skill-review): review the committed skill tree, not local residue The video-generation review test scans the live directory, so untracked build residue inside skills/public/video-generation fails it: once bytecode caching drops a scripts/__pycache__ directory into the tree (importing the skill script writes one), skillscan's package-undecodable-script finding is correct and the test can never pass again until the residue is deleted by hand. Reproduced on Windows: a planted scripts/__pycache__ pyc fails the test; deleting it makes it pass. - Materialize the tracked skill tree (git ls-files) into a temp dir and review that, so the test reviews exactly the committed package. - tests/skills/skill_loader.py disables bytecode caching around exec_module, so the root skill suite stops polluting the committed tree. Follows up on #6786 (item 6). * docs(test): state the argv-list contract on both sandbox runners The Windows dispatch also passes the resolved command as an argv list, so the string fallback was unreachable on every platform. --------- Co-authored-by: xiaodu55 <xiaodu55@users.noreply.github.com>
155 lines
5.9 KiB
Python
155 lines
5.9 KiB
Python
"""Packaging checks catch defects before a trusted extension is installed."""
|
|
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
from zipfile import ZipFile
|
|
|
|
import pytest
|
|
|
|
SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "check_extension.py"
|
|
spec = importlib.util.spec_from_file_location("extension_check_script", SCRIPT)
|
|
checker = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(checker)
|
|
|
|
|
|
def wheel(
|
|
tmp_path,
|
|
*,
|
|
entry="example = example:install",
|
|
requirement="deerflow-extension-api>=0.2,<0.3",
|
|
files=None,
|
|
):
|
|
path = tmp_path / "example-0.1.0-py3-none-any.whl"
|
|
with ZipFile(path, "w") as archive:
|
|
archive.writestr(
|
|
"example-0.1.0.dist-info/METADATA",
|
|
f"Metadata-Version: 2.3\nName: example\nVersion: 0.1.0\nRequires-Dist: {requirement}\n",
|
|
)
|
|
archive.writestr(
|
|
"example-0.1.0.dist-info/entry_points.txt",
|
|
f"[deerflow.extensions]\n{entry}\n",
|
|
)
|
|
archive.writestr(
|
|
"example/__init__.py",
|
|
"raise RuntimeError('The checker must not import plugin code')\n",
|
|
)
|
|
for name, content in (files or {}).items():
|
|
archive.writestr(name, content)
|
|
return path
|
|
|
|
|
|
def test_packaged_entry_is_checked_without_importing_plugin(tmp_path):
|
|
checker.check_wheel(tmp_path, wheel(tmp_path))
|
|
|
|
|
|
def test_backend_only_wheel_skips_extraction(tmp_path, monkeypatch):
|
|
def fail_if_extracted(*args, **kwargs):
|
|
pytest.fail("Backend-only wheels must not be extracted")
|
|
|
|
monkeypatch.setattr(ZipFile, "extractall", fail_if_extracted)
|
|
checker.check_wheel(tmp_path, wheel(tmp_path))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("entry", "message"),
|
|
[
|
|
("example = missing:install", "entry module"),
|
|
("example = example", "entry point target"),
|
|
("example = example:factory.install", "entry point target"),
|
|
("example = example:install\nsecond = example:install", "exactly one"),
|
|
],
|
|
)
|
|
def test_invalid_packaged_entry_is_rejected(tmp_path, entry, message):
|
|
with pytest.raises(ValueError, match=message):
|
|
checker.check_wheel(tmp_path, wheel(tmp_path, entry=entry))
|
|
|
|
|
|
def test_dotted_entry_module_is_accepted_without_importing_plugin(tmp_path):
|
|
checker.check_wheel(
|
|
tmp_path,
|
|
wheel(tmp_path, entry="example = example.nested:install", files={"example/nested.py": "raise RuntimeError('The checker must not import plugin code')\n"}),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"requirement",
|
|
[
|
|
"unrelated>=1",
|
|
"deerflow-extension-api>=99",
|
|
'deerflow-extension-api>=0.2; extra == "optional"',
|
|
],
|
|
)
|
|
def test_missing_or_incompatible_api_dependency_is_rejected(tmp_path, requirement):
|
|
with pytest.raises(ValueError, match="extension-api"):
|
|
checker.check_wheel(tmp_path, wheel(tmp_path, requirement=requirement))
|
|
|
|
|
|
def test_dropped_manifest_is_detected_from_src_layout(tmp_path):
|
|
package = tmp_path / "src" / "example"
|
|
package.mkdir(parents=True)
|
|
(package / "ui_manifest.json").write_text("{}", encoding="utf-8")
|
|
with pytest.raises(ValueError, match="manifest.*not packaged"):
|
|
checker.check_wheel(tmp_path, wheel(tmp_path))
|
|
|
|
|
|
def test_packaged_assets_use_host_manifest_validation(tmp_path):
|
|
manifest = json.dumps({"schema_version": 1, "entry": "main.mjs", "files": ["main.mjs", "styles.css"]})
|
|
files = {
|
|
"example/ui_manifest.json": manifest,
|
|
"example/main.mjs": "export default {};",
|
|
"example/styles.css": "body {}",
|
|
}
|
|
checker.check_wheel(tmp_path, wheel(tmp_path, files=files))
|
|
del files["example/styles.css"]
|
|
with pytest.raises(FileNotFoundError):
|
|
checker.check_wheel(tmp_path, wheel(tmp_path, files=files))
|
|
|
|
|
|
def test_custom_manifest_can_be_checked_explicitly(tmp_path):
|
|
path = "example/resources.json"
|
|
manifest = json.dumps({"schema_version": 1, "entry": "main.mjs", "files": ["main.mjs"]})
|
|
checker.check_wheel(
|
|
tmp_path,
|
|
wheel(tmp_path, files={path: manifest, "example/main.mjs": "export default {};"}),
|
|
manifests=[path],
|
|
)
|
|
|
|
|
|
def test_wheel_root_manifest_is_also_validated(tmp_path):
|
|
with pytest.raises(ValueError, match="Unsupported browser asset manifest"):
|
|
checker.check_wheel(tmp_path, wheel(tmp_path, files={"ui_manifest.json": "{}"}))
|
|
|
|
|
|
def test_single_module_source_cannot_drop_its_manifest(tmp_path):
|
|
(tmp_path / "example.py").write_text("", encoding="utf-8")
|
|
(tmp_path / "ui_manifest.json").write_text("{}", encoding="utf-8")
|
|
with pytest.raises(ValueError, match="manifest.*not packaged"):
|
|
checker.check_wheel(tmp_path, wheel(tmp_path))
|
|
|
|
|
|
@pytest.mark.parametrize("name", ["../escaped", "/escaped", r"example\escaped", "C:/ui_manifest.json", "C:ui_manifest.json", "C:/example/ui_manifest.json", "C:example/ui_manifest.json"])
|
|
def test_wheel_paths_cannot_escape_temporary_directory(tmp_path, monkeypatch, name):
|
|
def fail_if_extracted(*args, **kwargs):
|
|
pytest.fail("Unsafe wheel paths reached extraction")
|
|
|
|
# zipfile normalizes os.sep in entry names on Windows, so a wheel built
|
|
# through writestr never carries a verbatim backslash name there. Inject
|
|
# the hostile name into namelist unless the archive already stores it, so
|
|
# the guard contract is exercised with identical bytes on every host.
|
|
real_namelist = ZipFile.namelist
|
|
|
|
def namelist_with_hostile_entry(self):
|
|
stored = real_namelist(self)
|
|
return stored if name in stored else [*stored, name]
|
|
|
|
monkeypatch.setattr(ZipFile, "extractall", fail_if_extracted)
|
|
monkeypatch.setattr(ZipFile, "namelist", namelist_with_hostile_entry)
|
|
with pytest.raises(ValueError, match="wheel path"):
|
|
checker.check_wheel(tmp_path, wheel(tmp_path, files={name: "no"}))
|
|
|
|
|
|
def test_invalid_packaged_python_is_rejected(tmp_path):
|
|
with pytest.raises(SyntaxError):
|
|
checker.check_wheel(tmp_path, wheel(tmp_path, files={"example/broken.py": "def broken(:\n"}))
|