fix(extensions): keep settings-only installs canonical (#5094)

This commit is contained in:
Rod Boev 2026-06-28 18:25:47 -04:00
parent 46d91a7c61
commit aef8e524fd
4 changed files with 77 additions and 3 deletions

View file

@ -857,7 +857,10 @@ def _gallery_installed_runtime_manifest(
for key in ("name", "enabled", "scripts", "stylesheets", "sidecar", "permissions", "settings_schema"):
if key in manifest:
top_entry[key] = manifest[key]
if any(key in top_entry for key in ("scripts", "stylesheets", "sidecar")):
if any(
key in top_entry
for key in ("scripts", "stylesheets", "sidecar", "permissions", "settings_schema")
):
entries.append(_copy_manifest_entry_with_asset_base(top_entry, asset_base))
for _source, _index, entry in _manifest_extension_entries(manifest):
copied = _copy_manifest_entry_with_asset_base(entry, asset_base)

View file

@ -210,6 +210,14 @@
return overrides;
}
function readSettingsState(schema,key){
const stored=safeRead(key);
const checked=validate(schema,stored);
const overrides=overridesFromValues(schema,checked.values);
if(JSON.stringify(stored)!==JSON.stringify(overrides)) safeWrite(key,overrides);
return {values:checked.values,overrides};
}
function supportsSettings(meta){
return !!(meta&&meta.storage_owned&&Array.isArray(meta.settings_schema)&&meta.settings_schema.length);
}
@ -220,7 +228,10 @@
const schema=supportsSettings(meta)?meta.settings_schema:[];
const key=settingsKey(clean);
function current(){
return validate(schema,safeRead(key)).values;
return supportsSettings(meta)?readSettingsState(schema,key).values:validate(schema,safeRead(key)).values;
}
function currentOverrides(){
return supportsSettings(meta)?readSettingsState(schema,key).overrides:safeRead(key);
}
function setAll(values){
if(!supportsSettings(meta)) return {ok:false,values:current(),errors:{extension:'unsupported'}};
@ -237,7 +248,7 @@
schema,
defaults:defaultsFor(schema),
get values(){return current();},
get overrides(){return safeRead(key);},
get overrides(){return currentOverrides();},
get(name){return current()[name];},
validate(values){return validate(schema,values);},
set(name,value){

View file

@ -68,6 +68,10 @@ def test_extension_settings_runtime_normalizes_persists_resets_and_clears():
assert.strictEqual(window.hermesExt.settings.forExtension('demo.ext').get('mode'), 'compact');
assert.deepStrictEqual(settings.setAll({{flag: true, mode: 'compact', count: 2}}).values, {{flag: true, mode: 'compact', count: 2}});
assert.deepStrictEqual(JSON.parse(store.get('hermes.ext.settings.demo.ext')), {{flag: true}});
store.set('hermes.ext.settings.demo.ext', JSON.stringify({{flag: false, unknown: 'kept', bad: 'x'}}));
assert.deepStrictEqual(settings.values, {{flag: false, mode: 'compact', count: 2}});
assert.deepStrictEqual(settings.overrides, {{}});
assert.strictEqual(store.has('hermes.ext.settings.demo.ext'), false);
assert.strictEqual(settings.set('mode', 'invalid').ok, false);
assert.deepStrictEqual(settings.reset(), {{flag: false, mode: 'compact', count: 2}});
assert.strictEqual(store.has('hermes.ext.settings.demo.ext'), false);

View file

@ -148,6 +148,62 @@ def test_gallery_installed_extension_becomes_runtime_manifest(monkeypatch, tmp_p
assert status["extensions"][0]["id"] == "my-ext"
def test_gallery_installed_settings_only_manifest_becomes_runtime_entry(monkeypatch, tmp_path):
"""Settings-only gallery installs still surface in status and runtime config."""
ext_dir, state_dir = _setup_ext_env(monkeypatch, tmp_path)
monkeypatch.delenv("HERMES_WEBUI_EXTENSION_MANIFEST", raising=False)
monkeypatch.delenv("HERMES_WEBUI_EXTENSION_SCRIPT_URLS", raising=False)
monkeypatch.delenv("HERMES_WEBUI_EXTENSION_STYLESHEET_URLS", raising=False)
import api.extensions as ext_mod
files = {
"my-ext/manifest.json": json.dumps(
{
"name": "Settings Only",
"version": "1.0.0",
"permissions": {"storage": {"owned": True}},
"settings_schema": [
{"key": "flag", "type": "boolean", "default": True},
],
}
),
}
zip_bytes = _make_zip(files)
sha = hashlib.sha256(zip_bytes).hexdigest()
monkeypatch.setattr(ext_mod, "_safe_download", lambda *a, **kw: zip_bytes)
ext_mod.install_extension(
"my-ext",
"https://hermes-webui.github.io/exts/my-ext.zip",
sha,
)
assert ext_mod.get_extension_config() == {
"enabled": True,
"script_urls": [],
"stylesheet_urls": [],
"extensions": [
{
"id": "my-ext",
"name": "Settings Only",
"storage_owned": True,
"settings_schema": [
{"key": "flag", "type": "boolean", "label": "flag", "description": "", "default": True},
],
}
],
}
status = ext_mod.get_extension_status()
assert status["manifest"]["status"] == "gallery_installed"
assert status["counts"]["manifest_extensions"] == 1
assert status["extensions"][0]["id"] == "my-ext"
assert status["extensions"][0]["storage_owned"] is True
assert status["extensions"][0]["settings_schema"] == [
{"key": "flag", "type": "boolean", "label": "flag", "description": "", "default": True},
]
def test_install_bootstraps_managed_default_root_without_env(monkeypatch, tmp_path):
"""Plug-and-play (#4933): one-click install works with NO env configured.