Files
KiloStar/tests/unit/test_gsm_snapshot.py
T

274 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""GSM 配置快照(进程内读路径)相关测试。
去 Ray 后,快照不再进 Ray Object Store``_publish_snapshot`` 直接在进程内构建
并持有一个不可变 ``GSMSnapshot`` 对象,``current_config_ref`` 返回 ``(version, snapshot)``。
读端 ``fetch_snapshot`` 用版本号做进程内缓存失效。
主要验证:
- ``_build_snapshot`` 正确从 6 类内存状态打包配置
- ``_publish_snapshot`` 让 version 单调递增并刷新快照对象
- 写入路径(add_individual / add_provider_wrap / 等)会自动发布新快照
- ``fetch_snapshot`` 客户端:版本号一致时走本地缓存,不一致时重拉
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from kilostar.core.global_state_machine.gsm_snapshot import (
GSMSnapshot,
fetch_snapshot,
reset_local_cache,
)
# ─── GSM 集成(直接构造 plain 对象) ────────────────────────────────
@pytest.fixture
def gsm_instance():
from kilostar.core.global_state_machine.global_state_machine import (
GlobalStateMachine,
)
from kilostar.core.global_state_machine.individual_manager import (
GlobalIndividualManager,
)
from kilostar.core.global_state_machine.provider_manager import ProviderManager
from kilostar.core.global_state_machine.skill_manager import GlobalSkillManager
from kilostar.core.global_state_machine.tool_manager import GlobalToolManager
obj = GlobalStateMachine.__new__(GlobalStateMachine)
obj._global_provider_manager = ProviderManager(postgres=None)
obj._global_tool_manager = GlobalToolManager()
obj._global_skill_manager = GlobalSkillManager()
obj._global_individual_manager = GlobalIndividualManager()
obj._mcp_servers = {}
obj._tool_configs = {}
obj._custom_toolsets = {}
obj._config_version = 0
obj._current_ref = None
obj.postgres_database = MagicMock()
return obj
def test_build_snapshot_picks_up_all_six_categories(gsm_instance):
"""_build_snapshot 应正确从 GSM 内存的 6 类数据打包。"""
from kilostar.core.global_state_machine.model_provider.base_provider import (
Provider,
)
gsm_instance._global_provider_manager.provider_register["p1"] = Provider(
provider_title="p1",
provider_url="http://x",
provider_apikey="k",
provider_models=[],
provider_type="openai",
)
gsm_instance._global_individual_manager._individuals["a1"] = {"agent_id": "a1"}
gsm_instance._mcp_servers["s1"] = {"server_id": "s1"}
gsm_instance._tool_configs["t1"] = {"key": "v"}
gsm_instance._custom_toolsets["ts1"] = {"toolset_id": "ts1"}
snap = gsm_instance._build_snapshot()
assert "p1" in snap.providers
assert "a1" in snap.individuals
assert "s1" in snap.mcp_servers
assert "t1" in snap.tool_configs
assert "ts1" in snap.custom_toolsets
def test_build_snapshot_exposes_system_tools_by_scope(gsm_instance):
"""系统工具按 scope 分桶的工具名清单要随快照发布出去(客户端重建 toolset 用)。"""
tm = gsm_instance._global_tool_manager
def _f1():
return "f1"
def _f2():
return "f2"
tm._tool_funcs.clear()
tm._tool_funcs["default"]["file_reader"] = _f1
tm._tool_funcs["control_node"]["approval"] = _f2
snap = gsm_instance._build_snapshot()
assert snap.system_tools_by_scope.get("default") == ["file_reader"]
assert snap.system_tools_by_scope.get("control_node") == ["approval"]
assert set(snap.tool_funcs.keys()) == {"file_reader", "approval"}
def test_publish_snapshot_increments_version(gsm_instance):
assert gsm_instance._config_version == 0
assert gsm_instance._current_ref is None
gsm_instance._publish_snapshot()
v1 = gsm_instance._config_version
ref1 = gsm_instance._current_ref
assert v1 == 1
assert ref1 is not None
gsm_instance._publish_snapshot()
assert gsm_instance._config_version == 2
assert gsm_instance._current_ref is not ref1 # 重新构建应是新对象
@pytest.mark.asyncio
async def test_current_config_ref_lazy_publishes_when_empty(gsm_instance):
"""从未发布过快照时,current_config_ref 应自动发布一次而不是返回 None。"""
version, snap = await gsm_instance.current_config_ref()
assert version == 1
assert snap is not None
@pytest.mark.asyncio
async def test_current_version_is_lightweight(gsm_instance):
gsm_instance._publish_snapshot()
gsm_instance._publish_snapshot()
assert await gsm_instance.current_version() == 2
@pytest.mark.asyncio
async def test_add_individual_publishes_new_snapshot(gsm_instance):
"""写入路径 add_individual 应自动 +1 version。"""
before = gsm_instance._config_version
await gsm_instance.add_individual("agent-x", {"model_id": "gpt-4o"})
after = gsm_instance._config_version
assert after == before + 1
@pytest.mark.asyncio
async def test_add_provider_wrap_publishes_new_snapshot(gsm_instance):
"""add_provider_wrap 即便走 mock 适配器也应该最终发布一次新快照。"""
from kilostar.core.global_state_machine.model_provider.base_provider import (
Provider,
)
fake_provider = Provider(
provider_title="my-openai",
provider_url="http://x",
provider_apikey="k",
provider_models=[],
provider_type="openai",
)
gsm_instance._global_provider_manager.provider_mapper["openai"] = MagicMock()
gsm_instance._global_provider_manager.provider_mapper[
"openai"
].create_provider = AsyncMock(return_value=fake_provider)
gsm_instance.postgres_database.add_provider_db = AsyncMock()
before = gsm_instance._config_version
await gsm_instance.add_provider_wrap(
provider_type="openai",
provider_title="my-openai",
provider_url="http://x",
provider_apikey="k",
provider_owner="alice",
)
after = gsm_instance._config_version
assert after == before + 1
# ─── fetch_snapshot 客户端缓存 ────────────────────────────────────
@pytest.mark.asyncio
async def test_fetch_snapshot_uses_local_cache_when_version_matches():
"""版本号一致时不再调 current_config_ref,直接返回本地缓存快照。"""
reset_local_cache()
snap = GSMSnapshot(version=5, providers={"p": MagicMock()})
fake_gsm = MagicMock()
fake_gsm.current_version = AsyncMock(return_value=5)
fake_gsm.current_config_ref = AsyncMock(
side_effect=AssertionError("不应触发:缓存版本一致时不应调 current_config_ref")
)
from kilostar.core.global_state_machine import gsm_snapshot as snap_mod
snap_mod._local_cache["version"] = 5
snap_mod._local_cache["snapshot"] = snap
result = await fetch_snapshot(gsm_actor=fake_gsm)
assert result is snap
fake_gsm.current_version.assert_awaited_once()
@pytest.mark.asyncio
async def test_fetch_snapshot_refetches_when_version_changes():
"""版本号变了应重新拉取新 snapshot 并更新缓存。"""
reset_local_cache()
new_snap = GSMSnapshot(version=10)
fake_gsm = MagicMock()
fake_gsm.current_version = AsyncMock(return_value=10)
fake_gsm.current_config_ref = AsyncMock(return_value=(10, new_snap))
result = await fetch_snapshot(gsm_actor=fake_gsm)
assert result is new_snap
fake_gsm.current_config_ref.assert_awaited_once()
from kilostar.core.global_state_machine import gsm_snapshot as snap_mod
assert snap_mod._local_cache["version"] == 10
@pytest.mark.asyncio
async def test_fetch_snapshot_use_cache_false_skips_cache():
"""``use_cache=False`` 直接走 current_config_ref,不读本地缓存。"""
reset_local_cache()
fresh = GSMSnapshot(version=1)
fake_gsm = MagicMock()
fake_gsm.current_config_ref = AsyncMock(return_value=(1, fresh))
result = await fetch_snapshot(gsm_actor=fake_gsm, use_cache=False)
assert result is fresh
# ─── build_tools_for_scope 客户端 helper ────────────────────────
def test_build_tools_for_scope_assembles_system_and_custom():
"""客户端按 snapshot 的 custom_toolsets + all_funcs 展开为扁平 callable 列表。"""
from kilostar.core.global_state_machine.gsm_snapshot import (
build_tools_for_scope,
)
def _sys_default():
return "d"
def _sys_scope():
return "s"
def _tp_a():
return "a"
snap = GSMSnapshot(
all_funcs={"sys_default": _sys_default, "sys_scope": _sys_scope, "tp_a": _tp_a},
custom_toolsets={
"system_basic": {
"toolset_id": "system_basic",
"tools": ["sys_default", "sys_scope"],
},
"grp": {"toolset_id": "grp", "tools": ["tp_a"]},
},
)
result = build_tools_for_scope(snap, "control_node")
assert len(result) == 3
assert result == [_sys_default, _sys_scope, _tp_a]
def test_build_tools_for_scope_skips_empty_buckets():
"""没有工具的 scope 返回空列表。"""
from kilostar.core.global_state_machine.gsm_snapshot import (
build_tools_for_scope,
)
snap = GSMSnapshot(all_funcs={}, custom_toolsets={})
assert build_tools_for_scope(snap, "control_node") == []