234 lines
7.8 KiB
Python
234 lines
7.8 KiB
Python
"""Dream linkage when enabling router self-learning via config RPCs.
|
|
|
|
Self-learning's training trigger rides the post-dream hook, so switching it on
|
|
must atomically pull the dream chain (enabled + auto_schedule) up with it —
|
|
while disabling self-learning must never touch dream, and explicit dream
|
|
values in the same edit must win over the linkage. Offline and synthetic.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import opensquilla.gateway.rpc_config # noqa: F401 ensures registration
|
|
from opensquilla.gateway.auth import Principal
|
|
from opensquilla.gateway.config import GatewayConfig
|
|
from opensquilla.gateway.rpc import RpcContext
|
|
from opensquilla.gateway.rpc_config import _handle_config_patch, _handle_config_set
|
|
|
|
|
|
def _ctx(tmp_path, **config_kw) -> RpcContext:
|
|
cfg = GatewayConfig(config_path=str(tmp_path / "config.toml"), **config_kw)
|
|
return RpcContext(
|
|
conn_id="t",
|
|
config=cfg,
|
|
principal=Principal(
|
|
role="operator",
|
|
scopes=frozenset({"operator.admin"}),
|
|
is_owner=True,
|
|
authenticated=True,
|
|
),
|
|
)
|
|
|
|
|
|
async def test_set_enable_selflearning_links_dream(tmp_path) -> None:
|
|
ctx = _ctx(tmp_path)
|
|
assert ctx.config.memory.dream.enabled is False
|
|
|
|
res = await _handle_config_set(
|
|
{"path": "squilla_router.self_learning.enabled", "value": True}, ctx
|
|
)
|
|
|
|
assert ctx.config.squilla_router.self_learning.enabled is True
|
|
assert ctx.config.memory.dream.enabled is True
|
|
assert ctx.config.memory.dream.auto_schedule is True
|
|
assert sorted(res["linked"]) == [
|
|
"memory.dream.auto_schedule",
|
|
"memory.dream.enabled",
|
|
]
|
|
|
|
|
|
async def test_patch_enable_selflearning_links_dream(tmp_path) -> None:
|
|
ctx = _ctx(tmp_path)
|
|
|
|
res = await _handle_config_patch(
|
|
{"patches": {"squilla_router.self_learning.enabled": True}}, ctx
|
|
)
|
|
|
|
assert ctx.config.memory.dream.enabled is True
|
|
assert ctx.config.memory.dream.auto_schedule is True
|
|
assert sorted(res["linked"]) == [
|
|
"memory.dream.auto_schedule",
|
|
"memory.dream.enabled",
|
|
]
|
|
|
|
|
|
async def test_disable_selflearning_never_touches_dream(tmp_path) -> None:
|
|
ctx = _ctx(
|
|
tmp_path,
|
|
memory={"dream": {"enabled": True, "auto_schedule": True}},
|
|
squilla_router={"self_learning": {"enabled": True}},
|
|
)
|
|
|
|
res = await _handle_config_set(
|
|
{"path": "squilla_router.self_learning.enabled", "value": False}, ctx
|
|
)
|
|
|
|
assert ctx.config.squilla_router.self_learning.enabled is False
|
|
assert ctx.config.memory.dream.enabled is True # untouched
|
|
assert ctx.config.memory.dream.auto_schedule is True
|
|
assert "linked" not in res
|
|
|
|
|
|
async def test_explicit_dream_value_wins_over_linkage(tmp_path) -> None:
|
|
"""An operator explicitly keeping dream off in the same patch is obeyed."""
|
|
ctx = _ctx(tmp_path)
|
|
|
|
res = await _handle_config_patch(
|
|
{
|
|
"patches": {
|
|
"squilla_router.self_learning.enabled": True,
|
|
"memory.dream.enabled": False,
|
|
}
|
|
},
|
|
ctx,
|
|
)
|
|
|
|
assert ctx.config.squilla_router.self_learning.enabled is True
|
|
assert ctx.config.memory.dream.enabled is False # explicit value respected
|
|
# auto_schedule alone is still linked (it wasn't explicit), but with
|
|
# enabled=False the chain stays inert; the boot warning covers this state.
|
|
assert "memory.dream.enabled" not in res.get("linked", [])
|
|
|
|
|
|
async def test_already_enabled_selflearning_does_not_relink(tmp_path) -> None:
|
|
"""Re-asserting enabled=true must not resurrect a since-disabled dream."""
|
|
ctx = _ctx(
|
|
tmp_path,
|
|
squilla_router={"self_learning": {"enabled": True}},
|
|
)
|
|
assert ctx.config.memory.dream.enabled is False
|
|
|
|
res = await _handle_config_set(
|
|
{"path": "squilla_router.self_learning.enabled", "value": True}, ctx
|
|
)
|
|
|
|
assert ctx.config.memory.dream.enabled is False # no off->on transition
|
|
assert "linked" not in res
|
|
|
|
|
|
async def test_unrelated_patch_never_links(tmp_path) -> None:
|
|
ctx = _ctx(tmp_path)
|
|
|
|
res = await _handle_config_patch(
|
|
{"patches": {"squilla_router.confidence_threshold": 0.6}}, ctx
|
|
)
|
|
|
|
assert ctx.config.memory.dream.enabled is False
|
|
assert "linked" not in res
|
|
|
|
|
|
async def test_linked_dream_keys_persist_to_toml(tmp_path) -> None:
|
|
"""Linkage must survive a restart: linked keys land in the config file."""
|
|
import tomllib
|
|
|
|
ctx = _ctx(tmp_path)
|
|
await _handle_config_set(
|
|
{"path": "squilla_router.self_learning.enabled", "value": True}, ctx
|
|
)
|
|
|
|
persisted = tomllib.loads((tmp_path / "config.toml").read_text(encoding="utf-8"))
|
|
assert persisted["squilla_router"]["self_learning"]["enabled"] is True
|
|
assert persisted["memory"]["dream"]["enabled"] is True
|
|
assert persisted["memory"]["dream"]["auto_schedule"] is True
|
|
|
|
|
|
async def test_nested_dict_set_triggers_linkage(tmp_path) -> None:
|
|
"""config.set with a dict value on the parent path must link too."""
|
|
ctx = _ctx(tmp_path)
|
|
|
|
res = await _handle_config_set(
|
|
{"path": "squilla_router.self_learning", "value": {"enabled": True}}, ctx
|
|
)
|
|
|
|
assert ctx.config.memory.dream.enabled is True
|
|
assert "memory.dream.enabled" in res["linked"]
|
|
|
|
|
|
async def test_merge_patch_triggers_linkage(tmp_path) -> None:
|
|
"""config.patch merge form (nested dict) must link too."""
|
|
ctx = _ctx(tmp_path)
|
|
|
|
res = await _handle_config_patch(
|
|
{"patch": {"squilla_router": {"self_learning": {"enabled": True}}}}, ctx
|
|
)
|
|
|
|
assert ctx.config.memory.dream.enabled is True
|
|
assert "memory.dream.enabled" in res["linked"]
|
|
|
|
|
|
async def test_linkage_reports_live_reconcile_state(tmp_path) -> None:
|
|
"""Without a registered reconciler the response must flag the restart."""
|
|
from opensquilla.gateway.dream_bridge import (
|
|
register_dream_reconciler,
|
|
reset_dream_reconciler,
|
|
)
|
|
|
|
reset_dream_reconciler()
|
|
ctx = _ctx(tmp_path)
|
|
res = await _handle_config_set(
|
|
{"path": "squilla_router.self_learning.enabled", "value": True}, ctx
|
|
)
|
|
assert res["linkedLive"] is False
|
|
assert res["restartRequired"] is True
|
|
|
|
# With a reconciler wired (as boot does), the linkage goes live in-process.
|
|
calls = {"n": 0}
|
|
|
|
async def fake_reconciler() -> None:
|
|
calls["n"] += 1
|
|
|
|
register_dream_reconciler(fake_reconciler)
|
|
try:
|
|
ctx2 = _ctx(tmp_path)
|
|
res2 = await _handle_config_set(
|
|
{"path": "squilla_router.self_learning.enabled", "value": True}, ctx2
|
|
)
|
|
assert res2["linkedLive"] is True
|
|
assert calls["n"] == 1
|
|
finally:
|
|
reset_dream_reconciler()
|
|
|
|
|
|
async def test_patch_safe_allows_memory_learning_toggles(tmp_path) -> None:
|
|
"""The Settings > Advanced group writes through config.patch.safe
|
|
(operator.write): the three boolean opt-ins must be allowlisted, and the
|
|
safe path must still run the dream linkage (it delegates to the full
|
|
patch handler)."""
|
|
|
|
from opensquilla.gateway.rpc_config import (
|
|
_SAFE_WRITE_PATCH_PATHS,
|
|
_handle_config_patch_safe,
|
|
)
|
|
|
|
assert "squilla_router.self_learning.enabled" in _SAFE_WRITE_PATCH_PATHS
|
|
assert "memory.dream.enabled" in _SAFE_WRITE_PATCH_PATHS
|
|
assert "memory.dream.auto_schedule" in _SAFE_WRITE_PATCH_PATHS
|
|
# Thresholds/schedules stay admin-only.
|
|
assert "squilla_router.self_learning.train_min_samples" not in _SAFE_WRITE_PATCH_PATHS
|
|
|
|
ctx = _ctx(tmp_path)
|
|
res = await _handle_config_patch_safe(
|
|
{"patches": {"squilla_router.self_learning.enabled": True}}, ctx
|
|
)
|
|
assert ctx.config.squilla_router.self_learning.enabled is True
|
|
assert ctx.config.memory.dream.enabled is True
|
|
assert sorted(res["linked"]) == [
|
|
"memory.dream.auto_schedule",
|
|
"memory.dream.enabled",
|
|
]
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
pytest.main([__file__, "-q"])
|