main
py 216 lines 7.21 KB
Raw
1 import sys
2 from pathlib import Path
3 from types import SimpleNamespace
4
5 import pytest
6
7 PROJECT_ROOT = Path(__file__).resolve().parents[1]
8 if str(PROJECT_ROOT) not in sys.path:
9 sys.path.insert(0, str(PROJECT_ROOT))
10
11 from api.plugins import Plugins
12 from helpers import files, plugins
13
14
15 def test_plugins_list_uses_binary_toggle_instead_of_advanced_dropdown():
16 html = (PROJECT_ROOT / "webui/components/plugins/list/plugin-list.html").read_text(
17 encoding="utf-8"
18 )
19 store = (
20 PROJECT_ROOT / "webui/components/plugins/list/pluginListStore.js"
21 ).read_text(encoding="utf-8")
22
23 assert "plugin-status-toggle" in html
24 assert "plugin-status-select" not in html
25 assert "Open Advanced" not in html
26 assert 'value="advanced"' not in html
27 assert "openPluginAdvancedToggle" not in store
28 assert "plugin-toggle-advanced.html" not in store
29
30
31 def test_advanced_plugin_toggle_modal_was_removed():
32 assert not (
33 PROJECT_ROOT / "webui/components/plugins/toggle/plugin-toggle-advanced.html"
34 ).exists()
35
36
37 def test_list_toggle_state_is_global_even_when_scoped_rules_exist(monkeypatch):
38 monkeypatch.setattr(
39 plugins,
40 "get_plugin_meta",
41 lambda _plugin_name: SimpleNamespace(always_enabled=False),
42 )
43 monkeypatch.setattr(
44 plugins,
45 "get_plugin_roots",
46 lambda _plugin_name: ["plugins/example", "usr/plugins/example"],
47 )
48 monkeypatch.setattr(
49 plugins,
50 "determined_toggle_from_paths",
51 lambda _default, _paths: False,
52 )
53
54 def fail_on_scoped_lookup(*_args, **_kwargs):
55 raise AssertionError("Plugin list toggle state should not inspect scoped rules")
56
57 monkeypatch.setattr(plugins, "find_plugin_assets", fail_on_scoped_lookup)
58
59 assert plugins.get_toggle_state("example") == "disabled"
60
61
62 def test_always_enabled_plugin_ignores_disable_files_at_runtime(monkeypatch):
63 monkeypatch.setattr(plugins.cache, "get", lambda *args, **kwargs: None)
64 monkeypatch.setattr(plugins.cache, "add", lambda *args, **kwargs: None)
65 monkeypatch.setattr(plugins, "get_plugins_list", lambda: ["_required"])
66 monkeypatch.setattr(
67 plugins,
68 "get_plugin_meta",
69 lambda _plugin_name: SimpleNamespace(always_enabled=True),
70 )
71
72 def fail_on_toggle_lookup(*_args, **_kwargs):
73 raise AssertionError("always-enabled plugins must ignore toggle files")
74
75 monkeypatch.setattr(plugins, "determined_toggle_from_paths", fail_on_toggle_lookup)
76
77 assert plugins.get_enabled_plugins(None) == ["_required"]
78
79
80 def test_always_enabled_plugin_rejects_disable_attempt(monkeypatch):
81 monkeypatch.setattr(
82 plugins,
83 "get_plugin_meta",
84 lambda _plugin_name: SimpleNamespace(always_enabled=True),
85 )
86
87 with pytest.raises(ValueError, match="always enabled"):
88 plugins.toggle_plugin.__wrapped__("_required", False)
89
90
91 def test_plugin_api_returns_bad_request_for_rejected_disable(monkeypatch):
92 def reject(*_args, **_kwargs):
93 raise ValueError('Plugin "_required" is always enabled.')
94
95 monkeypatch.setattr(plugins, "toggle_plugin", reject)
96
97 response = Plugins._toggle_plugin.__wrapped__(
98 object.__new__(Plugins),
99 {"plugin_name": "_required", "enabled": False},
100 )
101
102 assert response.status_code == 400
103 assert "always enabled" in response.get_data(as_text=True)
104
105
106 def test_config_scope_activation_toggle_saves_immediately_for_selected_scope():
107 html = (PROJECT_ROOT / "webui/components/plugins/plugin-settings.html").read_text(
108 encoding="utf-8"
109 )
110 settings_store = (
111 PROJECT_ROOT / "webui/components/plugins/plugin-settings-store.js"
112 ).read_text(encoding="utf-8")
113 toggle_store = (
114 PROJECT_ROOT / "webui/components/plugins/toggle/plugin-toggle-store.js"
115 ).read_text(encoding="utf-8")
116
117 assert "@change=\"context.setPluginEnabled($event.target.checked)\"" in html
118 assert "projectName: this.projectName || \"\"" in settings_store
119 assert "agentProfileKey: this.agentProfileKey || \"\"" in settings_store
120 assert (
121 "async setEnabled(enabled, { projectName = this.projectName, "
122 "agentProfileKey = this.agentProfileKey } = {})"
123 ) in toggle_store
124 assert "action: \"toggle_plugin\"" in toggle_store
125
126
127 def test_scoped_plugin_without_settings_form_exposes_configuration_index():
128 list_html = (
129 PROJECT_ROOT / "webui/components/plugins/list/plugin-list.html"
130 ).read_text(encoding="utf-8")
131 list_store = (
132 PROJECT_ROOT / "webui/components/plugins/list/pluginListStore.js"
133 ).read_text(encoding="utf-8")
134 settings_html = (
135 PROJECT_ROOT / "webui/components/plugins/plugin-settings.html"
136 ).read_text(encoding="utf-8")
137 settings_store = (
138 PROJECT_ROOT / "webui/components/plugins/plugin-settings-store.js"
139 ).read_text(encoding="utf-8")
140
141 assert "canOpenPluginConfig(plugin)" in list_html
142 assert "plugin?.per_project_config" in list_store
143 assert "plugin?.per_agent_config" in list_store
144 assert "!pluginMeta.per_project_config" in settings_store
145 assert "!pluginMeta.per_agent_config" in settings_store
146 assert "Scoped configurations" in settings_html
147 assert "context.pluginMeta?.has_config_screen" in settings_html
148
149
150 def test_scoped_plugin_watchdogs_skip_frontend_reload(monkeypatch):
151 handlers = {}
152 changes = []
153 monkeypatch.setattr(
154 plugins.watchdog,
155 "add_watchdog",
156 lambda **kwargs: handlers.setdefault(kwargs["id"], kwargs["handler"]),
157 )
158 monkeypatch.setattr(
159 plugins,
160 "after_plugin_change",
161 lambda names=None, python_change=False, frontend_reload=True: changes.append(
162 (names, python_change, frontend_reload)
163 ),
164 )
165
166 plugins.register_watchdogs()
167 handlers["plugins_agents"](
168 [["/tmp/usr/agents/custom/plugins/_code_execution/.toggle-0", "delete"]]
169 )
170 handlers["plugins_roots"](
171 [["/tmp/plugins/_code_execution/.toggle-0", "delete"]]
172 )
173
174 assert changes == [
175 (["_code_execution"], False, False),
176 (["_code_execution"], False, True),
177 ]
178
179
180 def test_toggle_plugin_writes_project_scope_file_immediately(tmp_path, monkeypatch):
181 monkeypatch.setattr(files, "_base_dir", str(tmp_path))
182 changes = []
183 monkeypatch.setattr(
184 plugins,
185 "after_plugin_change",
186 lambda names, **kwargs: changes.append((names, kwargs)),
187 )
188 monkeypatch.setitem(
189 sys.modules,
190 "helpers.projects",
191 SimpleNamespace(
192 get_project_meta=lambda name, *sub_dirs: files.get_abs_path(
193 "usr/projects",
194 name,
195 ".a0proj",
196 *sub_dirs,
197 )
198 ),
199 )
200
201 plugins.toggle_plugin.__wrapped__("example", False, project_name="alpha")
202
203 scoped_plugin_dir = (
204 tmp_path / "usr/projects/alpha/.a0proj/plugins/example"
205 )
206 assert (scoped_plugin_dir / ".toggle-0").exists()
207 assert not (scoped_plugin_dir / ".toggle-1").exists()
208
209 plugins.toggle_plugin.__wrapped__("example", True, project_name="alpha")
210
211 assert (scoped_plugin_dir / ".toggle-1").exists()
212 assert not (scoped_plugin_dir / ".toggle-0").exists()
213 assert changes == [
214 (["example"], {"frontend_reload": False}),
215 (["example"], {"frontend_reload": False}),
216 ]