mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-01 18:52:21 +01:00
Merge remote-tracking branch 'origin/dev' into fix/editor-followups-20261030
Reconcile branch with current dev (bebf53be).
Resolve conflicts in static/sw.js (bump cache version and preserve panel precache)
and src/ai_interaction.py (preserve upstream quality handling with per-user settings).
Remediate PR #6992 findings:
- A (CI & Playwright): Pin playwright==1.62.0 in requirements-dev and CI workflow; guard browser test imports against pytest runner collection failure.
- B (Per-User Image Model Settings): Support per-user image_model and image_edit_model in editor_image_models() and ai_interaction; enable non-admin preference persistence via /api/prefs fallback.
- C (CodeQL Regex Alert 1212): Replace polynomial-backtracking regex in routes/editor_agent_routes.py with linear token-based intent matcher.
- D (Undo/Redo rembg layer): Clear stale rembgLiveLayer and rembgLiveSnap references in _restoreState() to prevent edge tuner from corrupting detached layers.
This commit is contained in:
@@ -119,3 +119,177 @@ def test_edit_default_uses_generation_only_when_edit_setting_is_empty(monkeypatc
|
||||
from src import editor_image_settings
|
||||
monkeypatch.setattr(editor_image_settings, 'load_settings', lambda: {'image_model':'generator','image_edit_model':''})
|
||||
assert editor_image_settings.editor_image_models() == {'generation':'generator','edit':'generator'}
|
||||
|
||||
|
||||
def test_editor_image_models_per_user_isolation_and_fallback(tmp_path, monkeypatch):
|
||||
from routes import prefs_routes
|
||||
from src import editor_image_settings, settings
|
||||
|
||||
prefs_file = str(tmp_path / "user_prefs.json")
|
||||
monkeypatch.setattr(prefs_routes, "PREFS_FILE", prefs_file)
|
||||
|
||||
global_config = {"image_model": "global-gen", "image_edit_model": "global-edit"}
|
||||
monkeypatch.setattr(settings, "load_settings", lambda: global_config)
|
||||
monkeypatch.setattr(editor_image_settings, "load_settings", lambda: global_config)
|
||||
|
||||
# Save per-user preferences
|
||||
prefs_routes._save_for_user("alice", {"image_model": "alice-gen", "image_edit_model": "alice-edit"})
|
||||
prefs_routes._save_for_user("bob", {"image_model": "bob-gen"}) # no edit model
|
||||
|
||||
# Alice gets Alice's distinct models
|
||||
alice_models = editor_image_settings.editor_image_models(owner="alice")
|
||||
assert alice_models == {"generation": "alice-gen", "edit": "alice-edit"}
|
||||
|
||||
# Bob gets Bob's generation model, and edit model falls back to global edit model
|
||||
bob_models = editor_image_settings.editor_image_models(owner="bob")
|
||||
assert bob_models == {"generation": "bob-gen", "edit": "global-edit"}
|
||||
|
||||
# When global edit model is empty, edit model falls back to Bob's generation model
|
||||
empty_edit = {"image_model": "global-gen", "image_edit_model": ""}
|
||||
monkeypatch.setattr(settings, "load_settings", lambda: empty_edit)
|
||||
monkeypatch.setattr(editor_image_settings, "load_settings", lambda: empty_edit)
|
||||
bob_fallback_models = editor_image_settings.editor_image_models(owner="bob")
|
||||
assert bob_fallback_models == {"generation": "bob-gen", "edit": "bob-gen"}
|
||||
|
||||
# Restore global settings with global-edit
|
||||
monkeypatch.setattr(settings, "load_settings", lambda: global_config)
|
||||
monkeypatch.setattr(editor_image_settings, "load_settings", lambda: global_config)
|
||||
|
||||
# Charlie (no preferences) falls back to global admin settings
|
||||
charlie_models = editor_image_settings.editor_image_models(owner="charlie")
|
||||
assert charlie_models == {"generation": "global-gen", "edit": "global-edit"}
|
||||
|
||||
# Anonymous/unauthenticated owner falls back to global admin settings
|
||||
anon_models = editor_image_settings.editor_image_models(owner="")
|
||||
assert anon_models == {"generation": "global-gen", "edit": "global-edit"}
|
||||
|
||||
|
||||
def test_non_admin_user_prefs_api_routing_and_isolation(tmp_path, monkeypatch):
|
||||
import time
|
||||
from routes import editor_agent_routes, prefs_routes
|
||||
from src import editor_image_settings, settings
|
||||
|
||||
prefs_file = str(tmp_path / "user_prefs.json")
|
||||
monkeypatch.setattr(prefs_routes, "PREFS_FILE", prefs_file)
|
||||
|
||||
global_config = {"image_model": "admin-global-gen", "image_edit_model": "admin-global-edit"}
|
||||
monkeypatch.setattr(settings, "load_settings", lambda: global_config)
|
||||
monkeypatch.setattr(editor_image_settings, "load_settings", lambda: global_config)
|
||||
|
||||
current_user = "alice"
|
||||
app = FastAPI()
|
||||
app.include_router(prefs_routes.setup_prefs_routes())
|
||||
app.include_router(editor_agent_routes.setup_editor_agent_routes())
|
||||
|
||||
monkeypatch.setattr("src.auth_helpers.get_current_user", lambda request: current_user)
|
||||
monkeypatch.setattr(prefs_routes, "get_current_user", lambda request: current_user)
|
||||
monkeypatch.setattr(editor_agent_routes, "_user", lambda request: current_user)
|
||||
monkeypatch.setattr(editor_agent_routes.editor_agent_model, "startup_probe", lambda: None)
|
||||
monkeypatch.setattr(editor_agent_routes.style_guide, "strengthen", lambda prompt, steps: steps)
|
||||
monkeypatch.setattr(editor_agent_routes.style_guide, "enforce_guards", lambda steps: steps)
|
||||
monkeypatch.setattr(editor_agent_routes, "_image_provider_available", lambda owner: True)
|
||||
monkeypatch.setattr(editor_agent_routes, "_plan_special", lambda *args: None)
|
||||
monkeypatch.setattr(editor_agent_routes, "_ensure_generative", lambda prompt, steps: steps)
|
||||
monkeypatch.setattr(editor_agent_routes, "_unsure_plan", lambda prompt, plan: plan)
|
||||
|
||||
seen = []
|
||||
|
||||
async def execute_gen(self, content, ctx):
|
||||
seen.append(("gen", json.loads(content)))
|
||||
return "stub", {"exit_code": 1, "error": "stop"}
|
||||
|
||||
async def execute_edit(self, content, ctx):
|
||||
seen.append(("edit", json.loads(content)))
|
||||
return "stub", {"exit_code": 1, "error": "stop"}
|
||||
|
||||
monkeypatch.setattr(editor_tools.GenerateImageLayerTool, "execute", execute_gen)
|
||||
monkeypatch.setattr(editor_tools.AiEditImageTool, "execute", execute_edit)
|
||||
|
||||
editor_agent_routes.set_editor_planner(lambda messages, owner: json.dumps({
|
||||
"reply": "Gen", "steps": [
|
||||
{"op": "generate_image", "params": {"prompt": "cat", "model": "stale-planner", "size": "512x512"}}
|
||||
]
|
||||
}))
|
||||
|
||||
try:
|
||||
with TestClient(app) as client:
|
||||
# Alice configures her preferences via PUT /api/prefs/{key}
|
||||
put_gen = client.put("/api/prefs/image_model", json={"value": "alice-fav-gen"})
|
||||
assert put_gen.status_code == 200
|
||||
put_edit = client.put("/api/prefs/image_edit_model", json={"value": "alice-fav-edit"})
|
||||
assert put_edit.status_code == 200
|
||||
|
||||
# Verify GET /api/prefs reflects Alice's choices
|
||||
get_prefs = client.get("/api/prefs")
|
||||
assert get_prefs.status_code == 200
|
||||
assert get_prefs.json()["image_model"] == "alice-fav-gen"
|
||||
assert get_prefs.json()["image_edit_model"] == "alice-fav-edit"
|
||||
|
||||
# Alice invokes quick edit with _use_image_settings: True
|
||||
res = client.post("/api/editor/agent", json={
|
||||
"prompt": "generate a cat",
|
||||
"_use_image_settings": True,
|
||||
"model": "stale-browser-edit",
|
||||
"generation_model": "stale-browser-gen",
|
||||
})
|
||||
assert res.status_code == 200
|
||||
assert len(seen) == 1
|
||||
# Authoritative Alice preference overrides stale planner and browser models
|
||||
assert seen[0][1]["model"] == "alice-fav-gen"
|
||||
|
||||
# Now test Bob (who has no preferences) - should receive global admin settings
|
||||
current_user = "bob"
|
||||
seen.clear()
|
||||
res_bob = client.post("/api/editor/agent", json={
|
||||
"prompt": "generate a dog",
|
||||
"_use_image_settings": True,
|
||||
"model": "stale-browser-edit",
|
||||
"generation_model": "stale-browser-gen",
|
||||
})
|
||||
assert res_bob.status_code == 200
|
||||
assert len(seen) == 1
|
||||
# Bob falls back to global default, not Alice's preference
|
||||
assert seen[0][1]["model"] == "admin-global-gen"
|
||||
finally:
|
||||
editor_agent_routes.set_editor_planner(None)
|
||||
|
||||
|
||||
def test_is_background_removal_intent_grammar_and_dos_safety():
|
||||
from routes.editor_agent_routes import _is_background_removal_intent
|
||||
import time
|
||||
|
||||
# Valid commands across all verbs and targets
|
||||
assert _is_background_removal_intent("remove background")
|
||||
assert _is_background_removal_intent("please remove the background")
|
||||
assert _is_background_removal_intent("erase bg")
|
||||
assert _is_background_removal_intent("delete backdrop")
|
||||
assert _is_background_removal_intent("clear the bg from the active layer")
|
||||
assert _is_background_removal_intent("strip background of current layer!")
|
||||
assert _is_background_removal_intent("erase backdrop from selected layer...")
|
||||
assert _is_background_removal_intent(" PLEASE REMOVE THE BG FROM THE SELECTED LAYER . ! ")
|
||||
|
||||
# Invalid commands rejected
|
||||
assert not _is_background_removal_intent("")
|
||||
assert not _is_background_removal_intent("remove foreground")
|
||||
assert not _is_background_removal_intent("please blur the background")
|
||||
assert not _is_background_removal_intent("remove background and make it dark")
|
||||
assert not _is_background_removal_intent("remove")
|
||||
assert not _is_background_removal_intent("background")
|
||||
assert not _is_background_removal_intent("please remove")
|
||||
assert not _is_background_removal_intent("remove the")
|
||||
assert not _is_background_removal_intent("remove background from layer") # missing active/current/selected
|
||||
assert not _is_background_removal_intent("remove background?") # unallowed punctuation
|
||||
|
||||
# ReDoS safety test: large repeated whitespace must run linearly in < 50ms
|
||||
evil_padding = " \t " * 10000
|
||||
evil_prompt = evil_padding + "please" + evil_padding + "remove" + evil_padding + "the" + evil_padding + "bg" + evil_padding + "!"
|
||||
start = time.perf_counter()
|
||||
assert _is_background_removal_intent(evil_prompt)
|
||||
elapsed = time.perf_counter() - start
|
||||
assert elapsed < 0.1, f"Intent parsing took too long: {elapsed:.4f}s (polynomial backtracking risk)"
|
||||
|
||||
evil_invalid = evil_padding + "remove" + evil_padding + "everything"
|
||||
start = time.perf_counter()
|
||||
assert not _is_background_removal_intent(evil_invalid)
|
||||
elapsed_invalid = time.perf_counter() - start
|
||||
assert elapsed_invalid < 0.1, f"Invalid intent parsing took too long: {elapsed_invalid:.4f}s"
|
||||
|
||||
Reference in New Issue
Block a user