feat(lora): the LoRA save takes the settings revision
POST /prompt_manager/lora/enable accepts the optional expected_revision that GET /settings returns, so the System Save row refuses to write over a config.json changed elsewhere (settings-changed, nothing written). The legacy page sends none and still merges into what is on disk.
This commit is contained in:
@@ -168,6 +168,9 @@ class LoraIntegrationMixin:
|
||||
async def lora_enable(self, request):
|
||||
"""Enable or disable LoraManager integration and save it to config.json.
|
||||
|
||||
``expected_revision`` (optional, GET /settings' ``revision``) refuses
|
||||
a save over a config.json changed meanwhile (``settings-changed``).
|
||||
|
||||
The values are committed through the config store first (#220) and
|
||||
published at runtime only once config.json holds them; a save that
|
||||
cannot be written reports ``persisted: false`` and changes nothing.
|
||||
@@ -180,6 +183,11 @@ class LoraIntegrationMixin:
|
||||
for flag in _LORA_FLAGS:
|
||||
if flag in data and not isinstance(data[flag], bool):
|
||||
return self._bad_lora_request(f"{flag} must be true or false")
|
||||
# The System Save row sends the revision it read (#220); the
|
||||
# legacy page sends none and merges into what is on disk.
|
||||
expected_revision = data.get("expected_revision")
|
||||
if expected_revision is not None and not isinstance(expected_revision, str):
|
||||
return self._bad_lora_request("expected_revision must be a string")
|
||||
enabled = data.get("enabled", False)
|
||||
path = data.get("path", "")
|
||||
trigger_words = data.get("trigger_words_enabled", False)
|
||||
@@ -226,7 +234,7 @@ class LoraIntegrationMixin:
|
||||
_saved, revision = await self._run_in_executor(
|
||||
get_config_store().commit,
|
||||
_lora_settings_mutation(values),
|
||||
expected_revision=None,
|
||||
expected_revision=expected_revision,
|
||||
publish=publish,
|
||||
)
|
||||
except (ConfigUnreadable, RevisionConflict, OSError) as exc:
|
||||
|
||||
@@ -528,6 +528,48 @@ class TestLoraEnableRoute(LoraAPITestCase):
|
||||
self.assertEqual(self.config_file.read_bytes(), b"{not json")
|
||||
self.assertFalse(IntegrationConfig.LORA_TRIGGER_WORDS_ENABLED)
|
||||
|
||||
async def test_enable_with_a_stale_revision_changes_nothing(self):
|
||||
# System S10: the Save row sends the revision it read; a file changed
|
||||
# since is a settings-changed conflict and nothing is written.
|
||||
self.config_file.write_text(json.dumps({"web_ui": {"result_timeout": 9}}))
|
||||
resp = await self.client.request("GET", "/prompt_manager/settings")
|
||||
revision = (await resp.json())["revision"]
|
||||
self.config_file.write_text(json.dumps({"web_ui": {"result_timeout": 10}}))
|
||||
before = self.config_file.read_bytes()
|
||||
|
||||
resp = await self._post_json(
|
||||
"/prompt_manager/lora/enable",
|
||||
{
|
||||
"enabled": False,
|
||||
"trigger_words_enabled": True,
|
||||
"expected_revision": revision,
|
||||
},
|
||||
)
|
||||
body = await resp.json()
|
||||
|
||||
self.assertEqual((resp.status, body["code"]), (409, "settings-changed"))
|
||||
self.assertEqual(self.config_file.read_bytes(), before)
|
||||
self.assertFalse(IntegrationConfig.LORA_TRIGGER_WORDS_ENABLED)
|
||||
|
||||
resp = await self._post_json(
|
||||
"/prompt_manager/lora/enable",
|
||||
{
|
||||
"enabled": False,
|
||||
"trigger_words_enabled": True,
|
||||
"expected_revision": body["revision"],
|
||||
},
|
||||
)
|
||||
saved = await resp.json()
|
||||
self.assertEqual(resp.status, 200)
|
||||
self.assertTrue(saved["revision"].startswith("sha256:"))
|
||||
self.assertTrue(IntegrationConfig.LORA_TRIGGER_WORDS_ENABLED)
|
||||
|
||||
resp = await self._post_json(
|
||||
"/prompt_manager/lora/enable",
|
||||
{"enabled": False, "expected_revision": 7},
|
||||
)
|
||||
self.assertEqual(resp.status, 400)
|
||||
|
||||
async def test_enable_requires_boolean_flags_and_body_cap(self):
|
||||
for body in (
|
||||
{"enabled": "yes"},
|
||||
|
||||
Reference in New Issue
Block a user