Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
"""End-to-end coverage for the profile-scoped plugin config/state bridge (#64227)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
|
||||
from hermes_cli.plugins import PluginContext, PluginManager, PluginManifest
|
||||
|
||||
|
||||
def _context(
|
||||
*, name: str = "fixture-plugin", key: str = "", namespace: str = ""
|
||||
) -> PluginContext:
|
||||
return PluginContext(
|
||||
PluginManifest(name=name, key=key, skill_namespace=namespace),
|
||||
PluginManager(),
|
||||
)
|
||||
|
||||
|
||||
def _in_home(home: Path, fn, *args):
|
||||
token = set_hermes_home_override(home)
|
||||
try:
|
||||
return fn(*args)
|
||||
finally:
|
||||
reset_hermes_home_override(token)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated_home(tmp_path: Path):
|
||||
home = tmp_path / "profile"
|
||||
token = set_hermes_home_override(home)
|
||||
try:
|
||||
yield home
|
||||
finally:
|
||||
reset_hermes_home_override(token)
|
||||
|
||||
|
||||
def test_config_round_trip_uses_canonical_settings_namespace(
|
||||
isolated_home: Path,
|
||||
) -> None:
|
||||
ctx = _context(key="category/fixture-plugin")
|
||||
|
||||
assert ctx.get_config("api_url", default="unset") == "unset"
|
||||
ctx.set_config("api_url", r"C:\Users\Owner\Hermes 🚀")
|
||||
ctx.set_config("retry.policy", {"attempts": 3, "enabled": True})
|
||||
|
||||
raw = yaml.safe_load((isolated_home / "config.yaml").read_text(encoding="utf-8"))
|
||||
settings = raw["plugins"]["entries"]["category/fixture-plugin"]["settings"]
|
||||
assert settings == {
|
||||
"api_url": r"C:\Users\Owner\Hermes 🚀",
|
||||
"retry": {"policy": {"attempts": 3, "enabled": True}},
|
||||
}
|
||||
assert ctx.get_config("api_url") == r"C:\Users\Owner\Hermes 🚀"
|
||||
assert ctx.get_config("retry.policy") == {"attempts": 3, "enabled": True}
|
||||
|
||||
|
||||
def test_config_reads_legacy_config_namespace_until_canonical_value_is_set(
|
||||
isolated_home: Path,
|
||||
) -> None:
|
||||
path = isolated_home / "config.yaml"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(
|
||||
yaml.safe_dump({
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"fixture-plugin": {
|
||||
"config": {"endpoint": "legacy", "legacy_only": 7},
|
||||
"settings": {"endpoint": "canonical"},
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
ctx = _context()
|
||||
|
||||
assert ctx.get_config("endpoint") == "canonical"
|
||||
assert ctx.get_config("legacy_only") == 7
|
||||
|
||||
ctx.set_config("legacy_only", 8)
|
||||
assert ctx.get_config("legacy_only") == 8
|
||||
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
entry = raw["plugins"]["entries"]["fixture-plugin"]
|
||||
assert entry["config"]["legacy_only"] == 7
|
||||
assert entry["settings"]["legacy_only"] == 8
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"key",
|
||||
[
|
||||
"../../security.approval_mode",
|
||||
r"..\..\model.provider",
|
||||
"security.approval_mode",
|
||||
"model.provider",
|
||||
"plugins.entries.other.settings.token",
|
||||
"settings.endpoint",
|
||||
"",
|
||||
],
|
||||
)
|
||||
def test_config_rejects_global_cross_plugin_and_traversal_paths(
|
||||
isolated_home: Path, key: str, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
ctx = _context()
|
||||
|
||||
with pytest.raises(ValueError, match="plugin-relative config key"):
|
||||
ctx.set_config(key, "blocked")
|
||||
|
||||
assert not (isolated_home / "config.yaml").exists()
|
||||
assert "Rejected config path" in caplog.text
|
||||
|
||||
|
||||
def test_config_read_rejects_escape_instead_of_exposing_global_config(
|
||||
isolated_home: Path,
|
||||
) -> None:
|
||||
path = isolated_home / "config.yaml"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text("security:\n approval_mode: always_allow\n", encoding="utf-8")
|
||||
ctx = _context()
|
||||
|
||||
with pytest.raises(ValueError, match="plugin-relative config key"):
|
||||
ctx.get_config("security.approval_mode")
|
||||
|
||||
|
||||
def test_config_parse_failure_is_not_silently_overwritten(isolated_home: Path) -> None:
|
||||
path = isolated_home / "config.yaml"
|
||||
path.parent.mkdir(parents=True)
|
||||
broken = "plugins:\n entries: [unterminated\n"
|
||||
path.write_text(broken, encoding="utf-8")
|
||||
|
||||
with pytest.raises(Exception, match="while parsing|expected"):
|
||||
_context().set_config("endpoint", "safe")
|
||||
|
||||
assert path.read_text(encoding="utf-8") == broken
|
||||
|
||||
|
||||
def test_concurrent_config_writes_do_not_drop_sibling_settings(
|
||||
isolated_home: Path,
|
||||
) -> None:
|
||||
ctx = _context()
|
||||
|
||||
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||
list(
|
||||
pool.map(
|
||||
lambda i: _in_home(isolated_home, ctx.set_config, f"worker_{i}", i),
|
||||
range(24),
|
||||
)
|
||||
)
|
||||
|
||||
assert {f"worker_{i}": ctx.get_config(f"worker_{i}") for i in range(24)} == {
|
||||
f"worker_{i}": i for i in range(24)
|
||||
}
|
||||
|
||||
|
||||
def test_config_cross_process_lock_preserves_every_setting(isolated_home: Path) -> None:
|
||||
script = """
|
||||
import sys
|
||||
from hermes_cli.plugins import PluginContext, PluginManager, PluginManifest
|
||||
ctx = PluginContext(PluginManifest(name='fixture-plugin'), PluginManager())
|
||||
for i in range(int(sys.argv[1]), int(sys.argv[2])):
|
||||
ctx.set_config(f'process_{i}', i)
|
||||
"""
|
||||
env = dict(os.environ, HERMES_HOME=str(isolated_home))
|
||||
processes = [
|
||||
subprocess.Popen(
|
||||
[sys.executable, "-c", script, str(start), str(start + 20)],
|
||||
cwd=Path(__file__).resolve().parents[2],
|
||||
env=env,
|
||||
)
|
||||
for start in (0, 20)
|
||||
]
|
||||
assert [process.wait(timeout=30) for process in processes] == [0, 0]
|
||||
|
||||
ctx = _context()
|
||||
assert {f"process_{i}": ctx.get_config(f"process_{i}") for i in range(40)} == {
|
||||
f"process_{i}": i for i in range(40)
|
||||
}
|
||||
|
||||
|
||||
def test_state_round_trip_is_atomic_and_aligned_with_plugin_data(
|
||||
isolated_home: Path,
|
||||
) -> None:
|
||||
ctx = _context(namespace="agent-plugin-fixture-a1b2c3d4")
|
||||
|
||||
assert ctx.state.get("cursor", default="start") == "start"
|
||||
ctx.state.set("cursor", {"page": 2, "path": r"C:\Users\Owner"})
|
||||
|
||||
state_path = (
|
||||
isolated_home / "plugin-data" / "agent-plugin-fixture-a1b2c3d4" / "state.json"
|
||||
)
|
||||
assert ctx.state.data_dir == state_path.parent
|
||||
assert json.loads(state_path.read_text(encoding="utf-8")) == {
|
||||
"cursor": {"page": 2, "path": r"C:\Users\Owner"}
|
||||
}
|
||||
assert ctx.state.get("cursor") == {"page": 2, "path": r"C:\Users\Owner"}
|
||||
assert not list(state_path.parent.glob("*.tmp"))
|
||||
|
||||
|
||||
def test_native_state_namespace_is_windows_safe_and_cannot_traverse(
|
||||
isolated_home: Path,
|
||||
) -> None:
|
||||
contexts = [_context(name="CON"), _context(key="../../other-plugin")]
|
||||
|
||||
for index, ctx in enumerate(contexts):
|
||||
ctx.state.set("value", index)
|
||||
assert ctx.state.data_dir.parent == isolated_home / "plugin-data"
|
||||
assert ctx.state.data_dir.name.startswith("agent-plugin-")
|
||||
assert ctx.state.data_dir.name.upper() not in {"CON", "NUL", "COM1", "LPT1"}
|
||||
|
||||
|
||||
def test_concurrent_state_updates_do_not_drop_keys(isolated_home: Path) -> None:
|
||||
ctx = _context()
|
||||
|
||||
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||
list(
|
||||
pool.map(
|
||||
lambda i: _in_home(isolated_home, ctx.state.set, f"cursor_{i}", i),
|
||||
range(40),
|
||||
)
|
||||
)
|
||||
|
||||
state = json.loads(ctx.state.path.read_text(encoding="utf-8"))
|
||||
assert state == {f"cursor_{i}": i for i in range(40)}
|
||||
|
||||
|
||||
def test_state_cross_process_lock_preserves_every_update(isolated_home: Path) -> None:
|
||||
script = """
|
||||
import sys
|
||||
from hermes_cli.plugins import PluginContext, PluginManager, PluginManifest
|
||||
ctx = PluginContext(PluginManifest(name='fixture-plugin'), PluginManager())
|
||||
for i in range(int(sys.argv[1]), int(sys.argv[2])):
|
||||
ctx.state.set(f'process_{i}', i)
|
||||
"""
|
||||
env = dict(os.environ, HERMES_HOME=str(isolated_home))
|
||||
processes = [
|
||||
subprocess.Popen(
|
||||
[sys.executable, "-c", script, str(start), str(start + 20)],
|
||||
cwd=Path(__file__).resolve().parents[2],
|
||||
env=env,
|
||||
)
|
||||
for start in (0, 20)
|
||||
]
|
||||
assert [process.wait(timeout=30) for process in processes] == [0, 0]
|
||||
|
||||
state = json.loads(_context().state.path.read_text(encoding="utf-8"))
|
||||
assert state == {f"process_{i}": i for i in range(40)}
|
||||
|
||||
|
||||
def test_state_quota_failure_preserves_previous_file(isolated_home: Path) -> None:
|
||||
ctx = _context()
|
||||
ctx.state.set("cursor", "safe")
|
||||
before = ctx.state.path.read_bytes()
|
||||
|
||||
with pytest.raises(ValueError, match="quota"):
|
||||
ctx.state.set("oversized", "x" * (ctx.state.quota_bytes + 1))
|
||||
|
||||
assert ctx.state.path.read_bytes() == before
|
||||
assert ctx.state.get("cursor") == "safe"
|
||||
|
||||
|
||||
def test_corrupt_state_is_not_silently_overwritten(isolated_home: Path) -> None:
|
||||
ctx = _context()
|
||||
ctx.state.data_dir.mkdir(parents=True)
|
||||
ctx.state.path.write_text('{"cursor":', encoding="utf-8")
|
||||
|
||||
with pytest.raises(RuntimeError, match="Cannot parse plugin state"):
|
||||
ctx.state.set("cursor", "replacement")
|
||||
|
||||
assert ctx.state.path.read_text(encoding="utf-8") == '{"cursor":'
|
||||
|
||||
|
||||
def test_config_and_state_follow_context_local_profile_scope(tmp_path: Path) -> None:
|
||||
homes = [tmp_path / "profiles" / "alpha", tmp_path / "profiles" / "beta"]
|
||||
ctx = _context(namespace="fixture-plugin-data")
|
||||
|
||||
for index, home in enumerate(homes):
|
||||
home.mkdir(parents=True)
|
||||
token = set_hermes_home_override(home)
|
||||
try:
|
||||
ctx.set_config("profile_value", index)
|
||||
ctx.state.set("profile_value", index)
|
||||
finally:
|
||||
reset_hermes_home_override(token)
|
||||
|
||||
# One globally-loaded plugin context follows each multiplexed turn's
|
||||
# context-local profile instead of pinning the startup profile.
|
||||
for index, home in enumerate(homes):
|
||||
token = set_hermes_home_override(home)
|
||||
try:
|
||||
assert ctx.get_config("profile_value") == index
|
||||
assert ctx.state.get("profile_value") == index
|
||||
finally:
|
||||
reset_hermes_home_override(token)
|
||||
|
||||
|
||||
def test_fixture_plugin_round_trips_bridge_during_real_discovery(
|
||||
isolated_home: Path,
|
||||
) -> None:
|
||||
plugin_dir = isolated_home / "plugins" / "bridge-fixture"
|
||||
plugin_dir.mkdir(parents=True)
|
||||
(plugin_dir / "plugin.yaml").write_text(
|
||||
"name: bridge-fixture\nversion: 1.0.0\n", encoding="utf-8"
|
||||
)
|
||||
(plugin_dir / "__init__.py").write_text(
|
||||
"def register(ctx):\n"
|
||||
" old = ctx.get_config('loads', default=0)\n"
|
||||
" ctx.set_config('loads', old + 1)\n"
|
||||
" ctx.state.set('registered', {'profile': ctx.profile_name})\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(isolated_home / "config.yaml").write_text(
|
||||
"plugins:\n enabled:\n - bridge-fixture\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
manager = PluginManager()
|
||||
manager.discover_and_load()
|
||||
|
||||
assert manager._plugins["bridge-fixture"].enabled is True
|
||||
ctx = _context(name="bridge-fixture")
|
||||
assert ctx.get_config("loads") == 1
|
||||
assert ctx.state.get("registered")["profile"] in {"custom", "default"}
|
||||
Reference in New Issue
Block a user