From 176953c7ac23df2d30804b27c46d98d755306ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Fri, 24 Apr 2026 17:27:26 +0900 Subject: [PATCH 1/3] support validator null stub db and config access --- scripts/validate_plugins/run.py | 15 +++++++++++++++ tests/test_validate_plugins.py | 27 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/scripts/validate_plugins/run.py b/scripts/validate_plugins/run.py index f95b9741..f70c737d 100644 --- a/scripts/validate_plugins/run.py +++ b/scripts/validate_plugins/run.py @@ -630,6 +630,21 @@ async def _return_self(): return _return_self().__await__() + async def __aenter__(self) -> "NullStub": + return self + + async def __aexit__(self, exc_type, exc, tb) -> bool: + del exc_type, exc, tb + return False + + def get(self, key=None, default=None): + del key + return default + + def pop(self, key=None, default=None): + del key + return default + def __iter__(self): return iter(()) diff --git a/tests/test_validate_plugins.py b/tests/test_validate_plugins.py index 5d4df159..9aafb582 100644 --- a/tests/test_validate_plugins.py +++ b/tests/test_validate_plugins.py @@ -385,6 +385,33 @@ def test_load_plugins_index_rejects_non_dict_values(self): os.remove(index_path) +class DummyContextStubTests(unittest.IsolatedAsyncioTestCase): + async def test_null_stub_supports_async_database_context_pattern(self): + module = load_validator_module() + + db = module.DummyContext().get_db() + + async with db.get_db() as session: + self.assertIsInstance(session, module.NullStub) + async with session.begin() as transaction: + self.assertIs(transaction, session) + result = await session.execute("SELECT 1") + + self.assertIs(result, session) + + async def test_null_stub_returns_defaults_for_restart_style_config_access(self): + module = load_validator_module() + + with mock.patch.dict(os.environ, {}, clear=True): + dashboard_config = module.DummyContext().get_config().get("dashboard", {}) + + self.assertEqual(dashboard_config.get("host", "127.0.0.1"), "127.0.0.1") + self.assertEqual( + int(os.environ.get("DASHBOARD_PORT", dashboard_config.get("port", 6185))), + 6185, + ) + + class ValidationProgressTests(unittest.TestCase): def test_build_parser_defaults_max_workers_to_sixteen(self): module = load_validator_module() From 8ba41b6f0b1159d801d7cc2046764ad7d1bf0851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Sat, 25 Apr 2026 01:26:07 +0900 Subject: [PATCH 2/3] add validator dummy context config and data dir --- scripts/validate_plugins/run.py | 57 +++++++++++++++++++++++++++++++++ tests/test_validate_plugins.py | 22 +++++++++++++ 2 files changed, 79 insertions(+) diff --git a/scripts/validate_plugins/run.py b/scripts/validate_plugins/run.py index f70c737d..bd42b823 100644 --- a/scripts/validate_plugins/run.py +++ b/scripts/validate_plugins/run.py @@ -652,9 +652,55 @@ def __bool__(self) -> bool: return False +class DummyConfig(dict): + def __init__(self, initial=None) -> None: + super().__init__() + if initial: + for key, value in initial.items(): + self[key] = value + + @staticmethod + def _wrap(value): + if isinstance(value, dict) and not isinstance(value, DummyConfig): + return DummyConfig(value) + return value + + def __setitem__(self, key, value) -> None: + super().__setitem__(key, self._wrap(value)) + + def __getitem__(self, key): + if key in self: + return super().__getitem__(key) + return NullStub() + + def __getattr__(self, name: str): + return self.get(name) + + class DummyContext: def __init__(self) -> None: self._star_manager = None + self._astrbot_root = Path(os.environ.get("ASTRBOT_ROOT", Path.cwd())).resolve() + self._data_root = self._astrbot_root / "data" + self._plugin_data_dir = self._data_root / "plugin_data" + self._plugin_data_dir.mkdir(parents=True, exist_ok=True) + self._config = DummyConfig( + { + "wake_prefix": [], + "dashboard": {}, + "admins_id": [], + "admin_ids": [], + "platform_settings": { + "aiocqhttp": {}, + "qqofficial": {}, + "telegram": {}, + "gewechat": {}, + "wechatpadpro": {}, + }, + "data_dir": str(self._data_root), + } + ) + self.config = self._config def get_all_stars(self): try: @@ -684,6 +730,17 @@ def register_llm_tool(self, name: str, func_args, desc: str, func_obj) -> None: def unregister_llm_tool(self, name: str) -> None: del name + def get_config(self, umo: str | None = None): + del umo + return self._config + + def get_context_config(self): + return self._config + + def get_data_dir(self) -> str: + self._plugin_data_dir.mkdir(parents=True, exist_ok=True) + return str(self._plugin_data_dir) + def __getattr__(self, name: str) -> NullStub: del name return NullStub() diff --git a/tests/test_validate_plugins.py b/tests/test_validate_plugins.py index 9aafb582..36b580ac 100644 --- a/tests/test_validate_plugins.py +++ b/tests/test_validate_plugins.py @@ -386,6 +386,18 @@ def test_load_plugins_index_rejects_non_dict_values(self): class DummyContextStubTests(unittest.IsolatedAsyncioTestCase): + def test_dummy_context_returns_worker_data_dir_for_plugin_storage(self): + module = load_validator_module() + + with tempfile.TemporaryDirectory() as tmp_dir: + astrbot_root = Path(tmp_dir) / "astrbot-root" + with mock.patch.dict(os.environ, {"ASTRBOT_ROOT": str(astrbot_root)}, clear=True): + data_dir = Path(module.DummyContext().get_data_dir()) + data_dir_exists = data_dir.is_dir() + + self.assertEqual(data_dir.resolve(), (astrbot_root / "data" / "plugin_data").resolve()) + self.assertTrue(data_dir_exists) + async def test_null_stub_supports_async_database_context_pattern(self): module = load_validator_module() @@ -411,6 +423,16 @@ async def test_null_stub_returns_defaults_for_restart_style_config_access(self): 6185, ) + def test_dummy_context_exposes_dict_like_config_defaults(self): + module = load_validator_module() + + with mock.patch.dict(os.environ, {}, clear=True): + context = module.DummyContext() + + self.assertEqual(context.get_config()["wake_prefix"], []) + self.assertEqual(context.get_config()["dashboard"].get("port", 6185), 6185) + self.assertEqual(context._config.get("expire_seconds", 300), 300) + class ValidationProgressTests(unittest.TestCase): def test_build_parser_defaults_max_workers_to_sixteen(self): From 3870231e32cc3304c8ee1e1a099bf5cbd35899da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Sat, 25 Apr 2026 12:19:54 +0900 Subject: [PATCH 3/3] simplify validator dummy config and lazy data dir --- scripts/validate_plugins/run.py | 41 +++++++++++++-------------------- tests/test_validate_plugins.py | 17 ++++++++++++++ 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/scripts/validate_plugins/run.py b/scripts/validate_plugins/run.py index bd42b823..6426c209 100644 --- a/scripts/validate_plugins/run.py +++ b/scripts/validate_plugins/run.py @@ -654,27 +654,14 @@ def __bool__(self) -> bool: class DummyConfig(dict): def __init__(self, initial=None) -> None: - super().__init__() - if initial: - for key, value in initial.items(): - self[key] = value - - @staticmethod - def _wrap(value): - if isinstance(value, dict) and not isinstance(value, DummyConfig): - return DummyConfig(value) - return value - - def __setitem__(self, key, value) -> None: - super().__setitem__(key, self._wrap(value)) - - def __getitem__(self, key): - if key in self: - return super().__getitem__(key) + super().__init__(initial or {}) + + def __missing__(self, key): + del key return NullStub() def __getattr__(self, name: str): - return self.get(name) + return self[name] class DummyContext: @@ -683,25 +670,30 @@ def __init__(self) -> None: self._astrbot_root = Path(os.environ.get("ASTRBOT_ROOT", Path.cwd())).resolve() self._data_root = self._astrbot_root / "data" self._plugin_data_dir = self._data_root / "plugin_data" - self._plugin_data_dir.mkdir(parents=True, exist_ok=True) self._config = DummyConfig( { "wake_prefix": [], - "dashboard": {}, + "dashboard": DummyConfig(), "admins_id": [], "admin_ids": [], - "platform_settings": { + "platform_settings": DummyConfig( + { "aiocqhttp": {}, "qqofficial": {}, "telegram": {}, "gewechat": {}, "wechatpadpro": {}, - }, + } + ), "data_dir": str(self._data_root), } ) self.config = self._config + def _ensure_plugin_data_dir(self) -> Path: + self._plugin_data_dir.mkdir(parents=True, exist_ok=True) + return self._plugin_data_dir + def get_all_stars(self): try: from astrbot.core.star.star import star_registry @@ -735,11 +727,10 @@ def get_config(self, umo: str | None = None): return self._config def get_context_config(self): - return self._config + return self.get_config() def get_data_dir(self) -> str: - self._plugin_data_dir.mkdir(parents=True, exist_ok=True) - return str(self._plugin_data_dir) + return str(self._ensure_plugin_data_dir()) def __getattr__(self, name: str) -> NullStub: del name diff --git a/tests/test_validate_plugins.py b/tests/test_validate_plugins.py index 36b580ac..cc17887e 100644 --- a/tests/test_validate_plugins.py +++ b/tests/test_validate_plugins.py @@ -386,6 +386,23 @@ def test_load_plugins_index_rejects_non_dict_values(self): class DummyContextStubTests(unittest.IsolatedAsyncioTestCase): + def test_dummy_context_defers_plugin_data_dir_creation_until_requested(self): + module = load_validator_module() + + with tempfile.TemporaryDirectory() as tmp_dir: + astrbot_root = Path(tmp_dir) / "astrbot-root" + plugin_data_dir = astrbot_root / "data" / "plugin_data" + + with mock.patch.dict(os.environ, {"ASTRBOT_ROOT": str(astrbot_root)}, clear=True): + context = module.DummyContext() + dir_exists_before = plugin_data_dir.exists() + created_dir = Path(context.get_data_dir()) + dir_exists_after = plugin_data_dir.is_dir() + + self.assertFalse(dir_exists_before) + self.assertEqual(created_dir.resolve(), plugin_data_dir.resolve()) + self.assertTrue(dir_exists_after) + def test_dummy_context_returns_worker_data_dir_for_plugin_storage(self): module = load_validator_module()