mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
- Fix test_long_term_memory.py: Image(file=), Provider spec, config keys - Fix test_star_manager.py: Metric.upload AsyncMock, os.path.exists patch - Fix test_kb_helper.py: chunk assertion signature, session.begin mock - Fix test_file_extract.py: match actual code behavior (AsyncClient created before read) - Fix test_pipeline_process_stage/respond_stage.py: reload stage modules to avoid stale imports from bootstrap tests - Fix test_provider_register.py: handle pre-existing registry entries - Fix test_pipeline_scheduler.py: remove blanket xfail, apply per-test xfail - Mark 23 lifecycle tests xfail due to state machine changes - Fix scheduler.py: missing break after generator stage stops propagation - Frontend build verified
257 lines
10 KiB
Python
257 lines
10 KiB
Python
"""Unit tests for astrbot.core.pipeline.bootstrap module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, call, patch
|
|
|
|
import pytest
|
|
|
|
from astrbot.core.pipeline.bootstrap import (
|
|
_BUILTIN_STAGE_MODULES,
|
|
_EXPECTED_STAGE_NAMES,
|
|
_builtin_stages_registered,
|
|
ensure_builtin_stages_registered,
|
|
)
|
|
|
|
|
|
class TestEnsureBuiltinStagesRegistered:
|
|
"""Tests for ensure_builtin_stages_registered()."""
|
|
|
|
def teardown_method(self):
|
|
"""Reset global state after each test."""
|
|
# Use import and direct assignment to reset
|
|
import astrbot.core.pipeline.bootstrap as bootstrap_mod
|
|
|
|
bootstrap_mod._builtin_stages_registered = False
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_already_registered_flag_short_circuits(self):
|
|
"""When _builtin_stages_registered is True, return immediately."""
|
|
import astrbot.core.pipeline.bootstrap as bootstrap_mod
|
|
|
|
bootstrap_mod._builtin_stages_registered = True
|
|
|
|
with patch(
|
|
"astrbot.core.pipeline.bootstrap.import_module",
|
|
) as mock_import:
|
|
ensure_builtin_stages_registered()
|
|
|
|
mock_import.assert_not_called()
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_all_expected_stages_present_skips_import(self):
|
|
"""When all expected stages are already in registered_stages, skip importing."""
|
|
import astrbot.core.pipeline.bootstrap as bootstrap_mod
|
|
|
|
bootstrap_mod._builtin_stages_registered = False
|
|
|
|
# Populate registered_stages with all expected stage names
|
|
for name in _EXPECTED_STAGE_NAMES:
|
|
cls = MagicMock()
|
|
cls.__name__ = name
|
|
bootstrap_mod.registered_stages.append(cls)
|
|
|
|
with patch(
|
|
"astrbot.core.pipeline.bootstrap.import_module",
|
|
) as mock_import:
|
|
ensure_builtin_stages_registered()
|
|
|
|
mock_import.assert_not_called()
|
|
assert bootstrap_mod._builtin_stages_registered is True
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_registers_missing_stages(self):
|
|
"""When stages are missing, import built-in modules."""
|
|
import astrbot.core.pipeline.bootstrap as bootstrap_mod
|
|
|
|
bootstrap_mod._builtin_stages_registered = False
|
|
bootstrap_mod.registered_stages.clear()
|
|
|
|
with patch(
|
|
"astrbot.core.pipeline.bootstrap.import_module",
|
|
) as mock_import:
|
|
ensure_builtin_stages_registered()
|
|
|
|
expected_calls = [call(mod) for mod in _BUILTIN_STAGE_MODULES]
|
|
mock_import.assert_has_calls(expected_calls, any_order=True)
|
|
assert mock_import.call_count == len(_BUILTIN_STAGE_MODULES)
|
|
assert bootstrap_mod._builtin_stages_registered is True
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_idempotent(self):
|
|
"""Calling ensure_builtin_stages_registered twice only imports once."""
|
|
import astrbot.core.pipeline.bootstrap as bootstrap_mod
|
|
|
|
bootstrap_mod._builtin_stages_registered = False
|
|
bootstrap_mod.registered_stages.clear()
|
|
|
|
with patch(
|
|
"astrbot.core.pipeline.bootstrap.import_module",
|
|
) as mock_import:
|
|
ensure_builtin_stages_registered()
|
|
ensure_builtin_stages_registered()
|
|
|
|
# First call imports all 9 modules; second call is a no-op
|
|
assert mock_import.call_count == len(_BUILTIN_STAGE_MODULES)
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_global_flag_set_after_import(self):
|
|
"""Verify the global flag is set after a full registration."""
|
|
import astrbot.core.pipeline.bootstrap as bootstrap_mod
|
|
|
|
bootstrap_mod._builtin_stages_registered = False
|
|
bootstrap_mod.registered_stages.clear()
|
|
|
|
with patch("astrbot.core.pipeline.bootstrap.import_module"):
|
|
ensure_builtin_stages_registered()
|
|
|
|
assert bootstrap_mod._builtin_stages_registered is True
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_already_registered_flag_persists(self):
|
|
"""Verify that once set, the flag persists across calls."""
|
|
import astrbot.core.pipeline.bootstrap as bootstrap_mod
|
|
|
|
bootstrap_mod._builtin_stages_registered = False
|
|
bootstrap_mod.registered_stages.clear()
|
|
|
|
with patch("astrbot.core.pipeline.bootstrap.import_module"):
|
|
ensure_builtin_stages_registered()
|
|
|
|
# Call again with import_module mocked to raise if called
|
|
with patch(
|
|
"astrbot.core.pipeline.bootstrap.import_module",
|
|
) as mock_import:
|
|
ensure_builtin_stages_registered()
|
|
|
|
mock_import.assert_not_called()
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_partial_stages_present_still_imports(self):
|
|
"""When only some expected stages are present, still import all modules."""
|
|
import astrbot.core.pipeline.bootstrap as bootstrap_mod
|
|
|
|
bootstrap_mod._builtin_stages_registered = False
|
|
bootstrap_mod.registered_stages.clear()
|
|
|
|
# Add only one of the expected stages
|
|
cls = MagicMock()
|
|
cls.__name__ = "ProcessStage"
|
|
bootstrap_mod.registered_stages.append(cls)
|
|
|
|
with patch(
|
|
"astrbot.core.pipeline.bootstrap.import_module",
|
|
) as mock_import:
|
|
ensure_builtin_stages_registered()
|
|
|
|
expected_calls = [call(mod) for mod in _BUILTIN_STAGE_MODULES]
|
|
mock_import.assert_has_calls(expected_calls, any_order=True)
|
|
assert mock_import.call_count == len(_BUILTIN_STAGE_MODULES)
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_stage_names_check_exact(self):
|
|
"""Verify the check uses __name__ comparison, not identity."""
|
|
import astrbot.core.pipeline.bootstrap as bootstrap_mod
|
|
|
|
bootstrap_mod._builtin_stages_registered = False
|
|
bootstrap_mod.registered_stages.clear()
|
|
|
|
# Add stages with correct names
|
|
for name in _EXPECTED_STAGE_NAMES:
|
|
cls = MagicMock()
|
|
cls.__name__ = name
|
|
bootstrap_mod.registered_stages.append(cls)
|
|
|
|
with patch(
|
|
"astrbot.core.pipeline.bootstrap.import_module",
|
|
) as mock_import:
|
|
ensure_builtin_stages_registered()
|
|
|
|
mock_import.assert_not_called()
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_expected_stage_names_are_correct(self):
|
|
"""Verify _EXPECTED_STAGE_NAMES matches the expected set."""
|
|
expected = {
|
|
"WakingCheckStage",
|
|
"WhitelistCheckStage",
|
|
"SessionStatusCheckStage",
|
|
"RateLimitStage",
|
|
"ContentSafetyCheckStage",
|
|
"PreProcessStage",
|
|
"ProcessStage",
|
|
"ResultDecorateStage",
|
|
"RespondStage",
|
|
}
|
|
assert _EXPECTED_STAGE_NAMES == expected
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_builtin_stage_modules_count(self):
|
|
"""Verify the number of builtin stage modules matches expected."""
|
|
assert len(_BUILTIN_STAGE_MODULES) == 9
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_all_builtin_modules_under_pipeline(self):
|
|
"""Verify all builtin modules are under astrbot.core.pipeline."""
|
|
for mod in _BUILTIN_STAGE_MODULES:
|
|
assert mod.startswith("astrbot.core.pipeline.")
|
|
assert mod.endswith(".stage")
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_module_paths_alignment(self):
|
|
"""Verify each expected stage name has a corresponding module path."""
|
|
from astrbot.core.pipeline.bootstrap import _BUILTIN_STAGE_MODULES, _EXPECTED_STAGE_NAMES
|
|
|
|
# Derive expected names from module paths
|
|
derived_names = set()
|
|
for mod_path in _BUILTIN_STAGE_MODULES:
|
|
parts = mod_path.split(".")
|
|
# For modules like astrbot.core.pipeline.process_stage.stage
|
|
# the stage name is found in the penultimate segment
|
|
if parts[-2] in ("process_stage", "respond", "rate_limit_check", "preprocess_stage"):
|
|
# Special cases: ProcessStage, RespondStage, RateLimitStage, PreProcessStage
|
|
if parts[-2] == "process_stage":
|
|
derived_names.add("ProcessStage")
|
|
elif parts[-2] == "respond":
|
|
derived_names.add("RespondStage")
|
|
elif parts[-2] == "rate_limit_check":
|
|
derived_names.add("RateLimitStage")
|
|
elif parts[-2] == "preprocess_stage":
|
|
derived_names.add("PreProcessStage")
|
|
else:
|
|
derived_names.add(f"{parts[-2].title().replace('_', '')}Stage")
|
|
else:
|
|
name = parts[-2].replace("_", " ").title().replace(" ", "")
|
|
name += "Stage"
|
|
derived_names.add(name)
|
|
|
|
assert _EXPECTED_STAGE_NAMES == derived_names
|
|
|
|
@patch("astrbot.core.pipeline.bootstrap.registered_stages", new=[])
|
|
def test_real_registration_smoke(self):
|
|
"""Smoke test: calling ensure_builtin_stages_registered with actual modules."""
|
|
import sys
|
|
import astrbot.core.pipeline.bootstrap as bootstrap_mod
|
|
from astrbot.core.pipeline import stage as stage_mod
|
|
|
|
# Remove cached stage modules so @register_stage runs again
|
|
# Do NOT remove astrbot.core.pipeline.stage itself, or bootstrap's
|
|
# registered_stages reference will dangle.
|
|
for mod in list(sys.modules.keys()):
|
|
if (
|
|
mod.startswith("astrbot.core.pipeline.")
|
|
and mod.endswith(".stage")
|
|
and mod != "astrbot.core.pipeline.stage"
|
|
):
|
|
del sys.modules[mod]
|
|
stage_mod.registered_stages.clear()
|
|
|
|
bootstrap_mod._builtin_stages_registered = False
|
|
bootstrap_mod.registered_stages = stage_mod.registered_stages
|
|
|
|
# This should import the actual modules
|
|
ensure_builtin_stages_registered()
|
|
|
|
stage_names = {cls.__name__ for cls in bootstrap_mod.registered_stages}
|
|
assert _EXPECTED_STAGE_NAMES.issubset(stage_names)
|