Skip to content

fix: match plugin tool module prefixes#8604

Open
he-yufeng wants to merge 1 commit into
AstrBotDevs:masterfrom
he-yufeng:fix/plugin-toggle-tool-path
Open

fix: match plugin tool module prefixes#8604
he-yufeng wants to merge 1 commit into
AstrBotDevs:masterfrom
he-yufeng:fix/plugin-toggle-tool-path

Conversation

@he-yufeng
Copy link
Copy Markdown
Contributor

@he-yufeng he-yufeng commented Jun 5, 2026

Summary

  • fix the startswith direction used by turn_off_plugin and turn_on_plugin
  • keep the plugin tool matching semantics aligned with _unbind_plugin
  • add regression coverage for disabling and re-enabling tools whose handler module is under the plugin module path

Verification

  • python -m py_compile astrbot\core\star\star_manager.py tests\test_plugin_manager.py
  • python -m ruff check astrbot\core\star\star_manager.py tests\test_plugin_manager.py
  • git diff --check
  • direct async behavior assertion for turn_off_plugin / turn_on_plugin with a plugin tool and an unrelated tool

I also tried the targeted pytest command, but this local Python environment has another editable project exposing a top-level tests package (C:\dev\GITHUB-clean\AnyCoder\tests), so AstrBot's tests.conftest imports tests.fixtures from the wrong package before collection. The direct assertion above validates the changed behavior without that global test-package collision.

Fixes #8583

Summary by Sourcery

Align plugin tool activation toggling with plugin module path matching semantics and add regression tests for plugin tool activation under plugin modules.

Bug Fixes:

  • Correct plugin tool module path prefix matching when turning plugins off and on so that tools under the plugin module are toggled correctly.

Tests:

  • Add async tests verifying that turning a plugin off deactivates only its tools and that turning it back on reactivates those tools and updates stored preferences accordingly.

@dosubot dosubot Bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label Jun 5, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modifies the plugin activation and deactivation logic in star_manager.py to check if the tool's module path starts with the plugin's module path, and adds corresponding unit tests. However, using mp.startswith(plugin.module_path) can lead to false positives where sibling plugins with matching prefixes (e.g., demo and demo_sibling) are incorrectly toggled. It is recommended to use the helper method self._is_plugin_module_path instead, and to update the new unit tests to include sibling plugin scenarios to prevent regressions.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 1731 to 1736
if (
plugin.module_path
and mp
and plugin.module_path.startswith(mp)
and mp.startswith(plugin.module_path)
and not mp.endswith(("astrbot.builtin_stars", "data.plugins"))
):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using mp.startswith(plugin.module_path) can lead to incorrect matches for sibling plugins that share a common prefix (e.g., a plugin with module path data.plugins.demo would match a tool with module path data.plugins.demo_sibling). To prevent this, we should use the existing helper method self._is_plugin_module_path(mp, plugin.module_path), which correctly checks for exact matches or child module boundaries.

                if (
                    plugin.module_path
                    and self._is_plugin_module_path(mp, plugin.module_path)
                    and not mp.endswith(("astrbot.builtin_stars", "data.plugins"))
                ):

Comment on lines 1806 to 1812
if (
plugin.module_path
and mp
and plugin.module_path.startswith(mp)
and mp.startswith(plugin.module_path)
and not mp.endswith(("astrbot.builtin_stars", "data.plugins"))
and func_tool.name in inactivated_llm_tools
):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to turn_off_plugin, using mp.startswith(plugin.module_path) can cause sibling plugins with matching prefixes to be incorrectly enabled. We should use self._is_plugin_module_path(mp, plugin.module_path) to ensure correct matching.

            if (
                plugin.module_path
                and self._is_plugin_module_path(mp, plugin.module_path)
                and not mp.endswith(("astrbot.builtin_stars", "data.plugins"))
                and func_tool.name in inactivated_llm_tools
            ):

Comment on lines +209 to +241
async def test_turn_off_plugin_deactivates_tools_under_plugin_module(
monkeypatch,
plugin_toggle_preferences,
):
plugin = star_manager_module.StarMetadata(
name="demo",
module_path="data.plugins.demo.main",
activated=True,
)
plugin_tool = _tool(
"demo_tool",
"data.plugins.demo.main.tools",
)
other_tool = _tool(
"other_tool",
"data.plugins.other.main.tools",
)
monkeypatch.setattr(
star_manager_module.llm_tools,
"func_list",
[plugin_tool, other_tool],
)
manager = _plugin_toggle_manager(plugin)

await manager.turn_off_plugin("demo")

assert plugin_tool.active is False
assert other_tool.active is True
assert plugin_toggle_preferences["inactivated_plugins"] == [
"data.plugins.demo.main"
]
assert plugin_toggle_preferences["inactivated_llm_tools"] == ["demo_tool"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure that sibling plugins with matching prefixes are not incorrectly deactivated, we should add a sibling tool to the test case and assert that it remains active.

@pytest.mark.asyncio
async def test_turn_off_plugin_deactivates_tools_under_plugin_module(
    monkeypatch,
    plugin_toggle_preferences,
):
    plugin = star_manager_module.StarMetadata(
        name="demo",
        module_path="data.plugins.demo.main",
        activated=True,
    )
    plugin_tool = _tool(
        "demo_tool",
        "data.plugins.demo.main.tools",
    )
    other_tool = _tool(
        "other_tool",
        "data.plugins.other.main.tools",
    )
    sibling_tool = _tool(
        "sibling_tool",
        "data.plugins.demo_sibling.main.tools",
    )
    monkeypatch.setattr(
        star_manager_module.llm_tools,
        "func_list",
        [plugin_tool, other_tool, sibling_tool],
    )
    manager = _plugin_toggle_manager(plugin)

    await manager.turn_off_plugin("demo")

    assert plugin_tool.active is False
    assert other_tool.active is True
    assert sibling_tool.active is True
    assert plugin_toggle_preferences["inactivated_plugins"] == [
        "data.plugins.demo.main"
    ]
    assert plugin_toggle_preferences["inactivated_llm_tools"] == ["demo_tool"]

Comment on lines +244 to +279
async def test_turn_on_plugin_reactivates_tools_under_plugin_module(
monkeypatch,
plugin_toggle_preferences,
):
plugin = star_manager_module.StarMetadata(
name="demo",
module_path="data.plugins.demo.main",
activated=False,
)
plugin_toggle_preferences["inactivated_plugins"] = ["data.plugins.demo.main"]
plugin_toggle_preferences["inactivated_llm_tools"] = ["demo_tool", "manual_tool"]
plugin_tool = _tool(
"demo_tool",
"data.plugins.demo.main.tools",
active=False,
)
manual_tool = _tool(
"manual_tool",
"data.plugins.other.main.tools",
active=False,
)
monkeypatch.setattr(
star_manager_module.llm_tools,
"func_list",
[plugin_tool, manual_tool],
)
manager = _plugin_toggle_manager(plugin)

await manager.turn_on_plugin("demo")

assert plugin_tool.active is True
assert manual_tool.active is False
assert plugin_toggle_preferences["inactivated_plugins"] == []
assert plugin_toggle_preferences["inactivated_llm_tools"] == ["manual_tool"]
manager.reload.assert_awaited_once_with("demo")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure that sibling plugins with matching prefixes are not incorrectly activated, we should add a sibling tool to the test case and assert that it remains inactive.

@pytest.mark.asyncio
async def test_turn_on_plugin_reactivates_tools_under_plugin_module(
    monkeypatch,
    plugin_toggle_preferences,
):
    plugin = star_manager_module.StarMetadata(
        name="demo",
        module_path="data.plugins.demo.main",
        activated=False,
    )
    plugin_toggle_preferences["inactivated_plugins"] = ["data.plugins.demo.main"]
    plugin_toggle_preferences["inactivated_llm_tools"] = ["demo_tool", "manual_tool"]
    plugin_tool = _tool(
        "demo_tool",
        "data.plugins.demo.main.tools",
        active=False,
    )
    manual_tool = _tool(
        "manual_tool",
        "data.plugins.other.main.tools",
        active=False,
    )
    sibling_tool = _tool(
        "sibling_tool",
        "data.plugins.demo_sibling.main.tools",
        active=False,
    )
    monkeypatch.setattr(
        star_manager_module.llm_tools,
        "func_list",
        [plugin_tool, manual_tool, sibling_tool],
    )
    manager = _plugin_toggle_manager(plugin)

    await manager.turn_on_plugin("demo")

    assert plugin_tool.active is True
    assert manual_tool.active is False
    assert sibling_tool.active is False
    assert plugin_toggle_preferences["inactivated_plugins"] == []
    assert plugin_toggle_preferences["inactivated_llm_tools"] == ["manual_tool"]
    manager.reload.assert_awaited_once_with("demo")

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@he-yufeng he-yufeng force-pushed the fix/plugin-toggle-tool-path branch from c70323a to 8b1d19e Compare June 5, 2026 13:33
@he-yufeng
Copy link
Copy Markdown
Contributor Author

Addressed the module-prefix review feedback in the latest commit:

  • use _is_plugin_module_path for plugin tool activation/deactivation checks instead of a raw startswith
  • add sibling plugin coverage so data.plugins.demo.main_extra.* is not toggled when data.plugins.demo.main is disabled or re-enabled

Local validation:

  • python -m py_compile astrbot\core\star\star_manager.py tests\test_plugin_manager.py
  • ruff check astrbot\core\star\star_manager.py tests\test_plugin_manager.py
  • git diff --check upstream/master...HEAD

Targeted pytest is still blocked by the local Python environment before it reaches this test file: global pytest plugin loading is missing werkzeug, and isolated plugin loading then misses pytest_asyncio. The PR CI should provide the clean project environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] turn_off_plugin / turn_on_plugin 的 startswith 方向写反

1 participant