merge: bring the element-style work onto main's api_v3 package split

main moved 16 commits while this branch was open, and one of them split
api_v3.py into a package. A rebase would have conflicted on that file in
each of the 15 commits that touch it; merging resolves it once.

Reapplied into the package, unchanged in substance:

- _schema_type_is / _schema_allows_null and the nullable-field handling
  (api_v3/__init__.py, where the form parser now lives)
- the indexed-array recombiner's union-type fix and its all-blank-means-null
  case (api_v3/plugins.py)
- scalable / native_size on the font catalog, and the removal of the three
  font-override stubs (api_v3/fonts.py)

Two places took main's version rather than mine, deliberately:

- pages_v3 now reads the schema through SchemaManager *and* keeps
  resolve_under for the raw-read fallback, so the containment guard main
  added in the CodeQL triage survives.
- serve_plugin_widget moves off os.path.basename onto safe_path_component
  and resolve_under. main dropped `import os` from pages_v3 in favour of
  those helpers, and they reject a bad component rather than mangling it
  into a valid-looking one.

The url-map snapshot loses exactly the three font-override routes. Edited
rather than regenerated: that test exists to catch a route lost by accident
during a move, and regenerating it wholesale is how such a loss would be
papered over.

Verified against a fresh run of current main: same failure set, 4692
passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-11 19:27:06 -04:00
co-authored by Claude Opus 5
101 changed files with 16851 additions and 10432 deletions
+29 -4
View File
@@ -127,6 +127,31 @@ class TestConfigAPI:
assert response.status_code == 200
mock_config_manager.save_config_atomic.assert_called_once()
def test_save_main_config_fails_closed_when_schema_path_is_unresolvable(
self, client, mock_config_manager, mock_plugin_manager
):
"""A plugin id whose schema path fails safe-resolution must not have
its config saved with secret_fields left empty.
Regression test for the CodeQL/CodeRabbit finding on
web_interface/blueprints/api_v3/config.py: previously, when
resolve_under() returned None (e.g. a plugin directory reached via a
symlink), the code fell through to `secret_fields = set()` and saved
the plugin's submitted config -- credentials included -- as
ordinary, unencrypted configuration instead of refusing the request.
"""
mock_plugin_manager.plugin_manifests = {'evil': {}}
with patch('web_interface.blueprints.api_v3.config.resolve_under', return_value=None):
response = client.post(
'/api/v3/config/main',
data=json.dumps({'evil': {'api_key': 'super-secret'}}),
content_type='application/json'
)
assert response.status_code == 400
mock_config_manager.save_config_atomic.assert_not_called()
def test_save_main_config_validation_error(self, client, mock_config_manager):
"""Test saving config with validation error."""
invalid_config = {'invalid': 'data'}
@@ -390,7 +415,7 @@ class TestConfigAPI:
class TestSystemAPI:
"""Test system API endpoints."""
@patch('web_interface.blueprints.api_v3.subprocess')
@patch('web_interface.blueprints.api_v3.system.subprocess')
def test_get_system_status(self, mock_subprocess, client):
"""Test getting system status."""
# The endpoint returns 503 without psutil, which is an optional
@@ -407,7 +432,7 @@ class TestSystemAPI:
data = json.loads(response.data)
assert 'service' in data or 'status' in data or 'active' in data
@patch('web_interface.blueprints.api_v3.subprocess')
@patch('web_interface.blueprints.api_v3.system.subprocess')
def test_get_system_version(self, mock_subprocess, client):
"""Test getting system version."""
mock_result = MagicMock()
@@ -421,7 +446,7 @@ class TestSystemAPI:
data = json.loads(response.data)
assert 'version' in data.get('data', {}) or 'version' in data
@patch('web_interface.blueprints.api_v3.subprocess')
@patch('web_interface.blueprints.api_v3.system.subprocess')
def test_execute_system_action(self, mock_subprocess, client):
"""Test executing system action."""
mock_result = MagicMock()
@@ -502,7 +527,7 @@ class TestDisplayAPI:
if response.status_code in [200, 201]:
assert api_v3.cache_manager.set.called
@patch('web_interface.blueprints.api_v3._ensure_cache_manager')
@patch('web_interface.blueprints.api_v3.display._ensure_cache_manager')
def test_stop_on_demand_display(self, mock_ensure_cache, client):
"""Test stopping on-demand display."""