ReMe/tests/unit/test_component_registry.py
jinliyl 618e8cec66
feat: add entry-point plugin system and extract Auto Fin (#459)
* feat: add entry-point plugin system

* fix: harden plugin config and client loading

* docs(workflow): add detailed manual for publishing reme-auto-fin to PyPI

- Provide step-by-step instructions for updating project.version and merging branches
- Explain dependency verification for reme-ai on PyPI during build
- Specify requirements for GitHub Actions secret configuration and version uniqueness
- Describe manual workflow triggering and input of version number
- Recommend publishing order for related projects
- Clarify that only manual dispatch triggers publishing, no automatic triggers on push or tag

* feat: support plugin-defined component types

* refactor: simplify plugin configuration

* fix: isolate plugin loading and defer client fallback

* refactor: freeze built-in component registry

* fix: isolate config entry point loading

* fix: complete auto-fin package metadata
2026-08-19 17:23:23 +08:00

181 lines
5.3 KiB
Python

"""Tests for ComponentRegistry."""
# pylint: disable=missing-function-docstring,missing-class-docstring,protected-access,unused-argument
import pytest
from reme.components.base_component import BaseComponent
from reme.components.component_registry import ComponentRegistry, R, create_application_registry
from reme.enumeration import ComponentEnum
class _DummyComponent(BaseComponent):
component_type = ComponentEnum.FILE_CHUNKER
class _AnotherComponent(BaseComponent):
component_type = ComponentEnum.KEYWORD_INDEX
class _NoComponentType:
pass
class _BaseComponentType(BaseComponent):
component_type = ComponentEnum.BASE
# -- register & get -----------------------------------------------------------
def test_register_direct_with_explicit_name():
reg = ComponentRegistry()
reg.register(_DummyComponent, "my_parser")
assert reg.get(ComponentEnum.FILE_CHUNKER, "my_parser") is _DummyComponent
def test_register_direct_defaults_to_class_name():
reg = ComponentRegistry()
reg.register(_DummyComponent)
assert reg.get(ComponentEnum.FILE_CHUNKER, "_DummyComponent") is _DummyComponent
def test_register_decorator():
reg = ComponentRegistry()
@reg.register("alias")
class MyParser(BaseComponent):
component_type = ComponentEnum.FILE_CHUNKER
assert reg.get(ComponentEnum.FILE_CHUNKER, "alias") is MyParser
def test_register_rejects_missing_component_type():
reg = ComponentRegistry()
with pytest.raises(TypeError, match="component_type"):
reg.register(_NoComponentType, "bad")
def test_register_plugin_defined_component_type():
reg = ComponentRegistry()
class PluginComponent(BaseComponent):
component_type = "example.reranker"
reg.register(PluginComponent, "cross_encoder")
assert reg.get("example.reranker", "cross_encoder") is PluginComponent
assert reg.get_all("example.reranker") == {"cross_encoder": PluginComponent}
def test_register_rejects_unsafe_plugin_component_type():
reg = ComponentRegistry()
class UnsafePluginComponent(BaseComponent):
component_type = "../outside"
with pytest.raises(TypeError, match="component_type"):
reg.register(UnsafePluginComponent, "unsafe")
def test_register_rejects_empty_name():
reg = ComponentRegistry()
with pytest.raises(ValueError, match="empty"):
reg._do_register(_DummyComponent, "")
def test_register_rejects_non_class_non_string():
reg = ComponentRegistry()
with pytest.raises(TypeError, match="Expected a class or string"):
reg.register(42)
# -- get_all ------------------------------------------------------------------
def test_get_all_returns_copy():
reg = ComponentRegistry()
reg.register(_DummyComponent, "a")
reg.register(_AnotherComponent, "b")
parsers = reg.get_all(ComponentEnum.FILE_CHUNKER)
assert parsers == {"a": _DummyComponent}
indexes = reg.get_all(ComponentEnum.KEYWORD_INDEX)
assert indexes == {"b": _AnotherComponent}
# Mutating the copy doesn't affect the registry.
parsers["hacked"] = _DummyComponent
assert "hacked" not in reg.get_all(ComponentEnum.FILE_CHUNKER)
def test_get_all_unknown_type_returns_empty():
reg = ComponentRegistry()
assert not reg.get_all(ComponentEnum.AS_LLM)
# -- get (miss) ---------------------------------------------------------------
def test_get_nonexistent_returns_none():
reg = ComponentRegistry()
assert reg.get(ComponentEnum.FILE_CHUNKER, "nope") is None
# -- unregister ---------------------------------------------------------------
def test_unregister_existing():
reg = ComponentRegistry()
reg.register(_DummyComponent, "x")
assert reg.unregister(ComponentEnum.FILE_CHUNKER, "x") is True
assert reg.get(ComponentEnum.FILE_CHUNKER, "x") is None
def test_unregister_missing_returns_false():
reg = ComponentRegistry()
assert reg.unregister(ComponentEnum.FILE_CHUNKER, "nope") is False
# -- clear --------------------------------------------------------------------
def test_clear():
reg = ComponentRegistry()
reg.register(_DummyComponent, "a")
reg.register(_AnotherComponent, "b")
reg.clear()
assert not reg.get_all(ComponentEnum.FILE_CHUNKER)
assert not reg.get_all(ComponentEnum.KEYWORD_INDEX)
def test_builtin_registry_is_frozen_after_package_bootstrap():
assert R.frozen is True
with pytest.raises(RuntimeError, match="frozen"):
R.register(_DummyComponent, "runtime-mutation")
def test_application_registry_copy_remains_mutable():
reg = create_application_registry()
assert reg.frozen is False
reg.register(_DummyComponent, "runtime-component")
assert reg.get(ComponentEnum.FILE_CHUNKER, "runtime-component") is _DummyComponent
assert R.get(ComponentEnum.FILE_CHUNKER, "runtime-component") is None
if __name__ == "__main__":
print("\n=== ComponentRegistry Tests ===")
test_register_direct_with_explicit_name()
test_register_direct_defaults_to_class_name()
test_register_decorator()
test_register_rejects_missing_component_type()
test_register_rejects_empty_name()
test_register_rejects_non_class_non_string()
test_get_all_returns_copy()
test_get_all_unknown_type_returns_empty()
test_get_nonexistent_returns_none()
test_unregister_existing()
test_unregister_missing_returns_false()
test_clear()
print("\n所有测试通过!")