mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
84 lines
3.0 KiB
Python
84 lines
3.0 KiB
Python
"""PATCH settings merges a diff onto fresh state, so a renderer save can't
|
|
clobber a field it didn't send. This is the structural close on the last
|
|
renderer-vs-agent race: the lost update is now unrepresentable, you can't
|
|
overwrite a field you never put in the body.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
import httpx
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from backend.main import app
|
|
|
|
|
|
def p_auth_headers():
|
|
import backend.auth as auth_mod
|
|
if not auth_mod.TOKEN:
|
|
import secrets
|
|
auth_mod.TOKEN = secrets.token_urlsafe(32)
|
|
return {"Authorization": f"Bearer {auth_mod.TOKEN}"}
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
return TestClient(app, headers=p_auth_headers())
|
|
|
|
|
|
@pytest.fixture
|
|
def reset_settings():
|
|
from backend.apps.settings.settings import load_settings, save_settings
|
|
original = load_settings().model_copy(deep=True)
|
|
yield
|
|
save_settings(original)
|
|
|
|
|
|
def test_patch_changes_only_sent_fields(client, reset_settings):
|
|
from backend.apps.settings.settings import load_settings, save_settings
|
|
s = load_settings()
|
|
s.theme = "dark"
|
|
s.default_mode = "chat" # as if something else had set this
|
|
save_settings(s)
|
|
|
|
r = client.patch("/api/settings", json={"theme": "light"})
|
|
assert r.status_code == 200, r.text
|
|
final = load_settings()
|
|
assert final.theme == "light" # the field we sent changed
|
|
assert final.default_mode == "chat" # the field we DIDN'T send is untouched
|
|
|
|
|
|
def test_patch_ignores_unknown_fields(client, reset_settings):
|
|
r = client.patch("/api/settings", json={"theme": "light", "not_a_field": 123})
|
|
assert r.status_code == 200
|
|
# Unknown key is dropped, not stored; the real field still applied.
|
|
from backend.apps.settings.settings import load_settings
|
|
assert load_settings().theme == "light"
|
|
assert "not_a_field" not in load_settings().model_dump()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_renderer_patch_and_agent_write_both_survive(reset_settings):
|
|
"""The renderer PATCHes one field while an autonomous agent writes another,
|
|
at the same time. Both must land: the renderer never sends the agent's field,
|
|
so it can't clobber it, and both reads happen fresh under the shared lock."""
|
|
from backend.apps.settings.settings import load_settings, save_settings
|
|
base = load_settings()
|
|
base.theme = "dark"
|
|
base.default_mode = "agent"
|
|
save_settings(base)
|
|
|
|
transport = httpx.ASGITransport(app=app)
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test", headers=p_auth_headers()) as client:
|
|
r1, r2 = await asyncio.gather(
|
|
client.patch("/api/settings", json={"theme": "light"}),
|
|
client.post("/api/settings-meta/write", json={"changes": {"default_mode": "chat"}}),
|
|
)
|
|
assert r1.status_code == 200 and r2.status_code == 200
|
|
|
|
final = load_settings()
|
|
assert final.theme == "light", "renderer's change lost"
|
|
assert final.default_mode == "chat", "agent's change clobbered by the renderer PATCH"
|