mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 16:38:49 +00:00
145 lines
4.8 KiB
Python
145 lines
4.8 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from nanobot.extensions.store import (
|
|
ExtensionSourceKind,
|
|
ExtensionStore,
|
|
InstalledExtension,
|
|
)
|
|
|
|
|
|
def _package(root: Path, *, version: str = "1.0.0") -> Path:
|
|
root.mkdir()
|
|
(root / "extension.py").write_text("def register(api):\n pass\n")
|
|
(root / "nanobot.extension.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"id": "store.test",
|
|
"name": "Store test",
|
|
"version": version,
|
|
"entry": "extension:register",
|
|
"permissions": [
|
|
{"name": "network", "reason": "Fetch selected sources."}
|
|
],
|
|
}
|
|
)
|
|
)
|
|
return root
|
|
|
|
|
|
def test_local_install_is_atomic_and_untrusted_by_default(tmp_path: Path) -> None:
|
|
store = ExtensionStore(tmp_path / "installed")
|
|
|
|
result = store.install_local(_package(tmp_path / "source"))
|
|
|
|
assert result.record.source is ExtensionSourceKind.LOCAL
|
|
assert result.record.integrity.startswith("sha256:")
|
|
assert result.manifest.id == "store.test"
|
|
assert not store.discover().candidates[0].trusted
|
|
|
|
|
|
def test_policy_updates_survive_discovery(tmp_path: Path) -> None:
|
|
store = ExtensionStore(tmp_path / "installed")
|
|
store.install_local(_package(tmp_path / "source"))
|
|
|
|
store.set_trusted("store.test", True)
|
|
store.set_enabled("store.test", False)
|
|
store.set_permissions("store.test", {"network"})
|
|
|
|
candidate = store.discover().candidates[0]
|
|
assert candidate.trusted
|
|
assert not candidate.enabled
|
|
assert candidate.granted_permissions == frozenset({"network"})
|
|
|
|
|
|
def test_unknown_permission_cannot_be_granted(tmp_path: Path) -> None:
|
|
store = ExtensionStore(tmp_path / "installed")
|
|
store.install_local(_package(tmp_path / "source"))
|
|
|
|
with pytest.raises(ValueError, match="not requested"):
|
|
store.set_permissions("store.test", {"filesystem.write"})
|
|
|
|
|
|
def test_modified_install_loses_effective_trust(tmp_path: Path) -> None:
|
|
store = ExtensionStore(tmp_path / "installed")
|
|
store.install_local(_package(tmp_path / "source"), trusted=True)
|
|
(store.root / "store.test" / "extension.py").write_text("changed = True\n")
|
|
|
|
discovery = store.discover()
|
|
|
|
assert not discovery.candidates[0].trusted
|
|
assert not discovery.candidates[0].integrity_valid
|
|
assert discovery.diagnostics[0].code == "extension_integrity_mismatch"
|
|
|
|
|
|
def test_changed_reinstall_revokes_trust_but_identical_reinstall_preserves_it(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
source = _package(tmp_path / "source")
|
|
store = ExtensionStore(tmp_path / "installed")
|
|
store.install_local(source, trusted=True)
|
|
|
|
identical = store.install_local(source)
|
|
assert identical.record.trusted
|
|
|
|
(source / "extension.py").write_text("changed = True\n")
|
|
changed = store.install_local(source)
|
|
assert not changed.record.trusted
|
|
|
|
|
|
def test_install_rejects_missing_manifest_and_symlinks(tmp_path: Path) -> None:
|
|
store = ExtensionStore(tmp_path / "installed")
|
|
empty = tmp_path / "empty"
|
|
empty.mkdir()
|
|
with pytest.raises(ValueError, match="cannot read extension manifest"):
|
|
store.install_local(empty)
|
|
|
|
source = _package(tmp_path / "source")
|
|
(source / "outside").symlink_to(tmp_path)
|
|
with pytest.raises(ValueError, match="symlink"):
|
|
store.install_local(source)
|
|
|
|
|
|
def test_install_rejects_source_containing_store(tmp_path: Path) -> None:
|
|
source = _package(tmp_path / "source")
|
|
store = ExtensionStore(source / "installed")
|
|
|
|
with pytest.raises(ValueError, match="cannot contain the extension store"):
|
|
store.install_local(source)
|
|
|
|
|
|
def test_registry_validation_and_uninstall(tmp_path: Path) -> None:
|
|
with pytest.raises(ValueError, match="sha256 digest"):
|
|
InstalledExtension.model_validate(
|
|
{
|
|
"id": "sample",
|
|
"version": "1.0.0",
|
|
"source": "local",
|
|
"source_ref": "/tmp/sample",
|
|
"integrity": "invalid",
|
|
"installed_at": "2026-01-01T00:00:00Z",
|
|
}
|
|
)
|
|
|
|
store = ExtensionStore(tmp_path / "installed")
|
|
store.install_local(_package(tmp_path / "source"))
|
|
store.uninstall("store.test")
|
|
|
|
assert store.records() == {}
|
|
assert not (store.root / "store.test").exists()
|
|
|
|
|
|
def test_corrupt_registry_is_diagnostic_and_not_overwritten(tmp_path: Path) -> None:
|
|
store = ExtensionStore(tmp_path / "installed")
|
|
store.install_local(_package(tmp_path / "source"))
|
|
store.registry_path.write_text("{broken")
|
|
|
|
discovery = store.discover()
|
|
|
|
assert discovery.diagnostics[0].code == "invalid_extension_registry"
|
|
with pytest.raises(ValueError, match="invalid extension registry"):
|
|
store.set_trusted("store.test", True)
|
|
assert store.registry_path.read_text() == "{broken"
|