From 038bdd85ecc94bf8aef11d0c232c2938999ad89d Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Tue, 7 Jul 2026 01:15:20 +0000 Subject: [PATCH] Stabilize local dev merge Align regression tests with the current Odysseus behavior after merging origin/dev into local main. - keep phone/name-only contacts valid and cover null email without crashes - pin explicit web-search false form submission in chat.js - update Cookbook dependency/download completion tests for combined live + persisted output - expose SGLang OS package repair hints from backend diagnosis - treat MLX and MLX-community repos as servable on Apple Metal while keeping CUDA behavior unchanged - keep desktop new-chat coverage on the shared preferred-model helper - remove a hardcoded crop overlay portal z-index literal - include the local agent-loop cleanup that removes the old manage_notes reminder repair shim Verified with: docker run --rm -v /home/pewds/odysseus-cookbook-fresh:/app -w /app odysseus-cookbook-fresh-odysseus python3 -m pytest -q (4515 passed, 4 skipped). --- routes/cookbook_helpers.py | 1 + src/agent_loop.py | 45 ------------------- static/style.css | 2 +- tests/test_chat_route_tool_policy.py | 2 +- tests/test_contacts_add_null_name.py | 20 +++++++-- ...okbook_dependency_completion_regression.py | 11 +++-- tests/test_cookbook_diagnosis.py | 2 +- tests/test_cookbook_diagnosis_js.py | 2 +- ...t_cookbook_same_host_server_profiles_js.py | 4 +- tests/test_hwfit_macos.py | 22 ++++----- tests/test_hwfit_manual_backend.py | 12 ++--- tests/test_new_chat_model_preference.py | 10 ++++- 12 files changed, 55 insertions(+), 78 deletions(-) diff --git a/routes/cookbook_helpers.py b/routes/cookbook_helpers.py index 3f98f318e..a4e29853b 100644 --- a/routes/cookbook_helpers.py +++ b/routes/cookbook_helpers.py @@ -1306,6 +1306,7 @@ def _diagnose_serve_output(text: str) -> dict | None: "SGLang native kernel/runtime is missing or mismatched on this server.", [ {"label": "repair sglang-kernel in this Python environment", "op": "dependency", "package": "sglang-kernel"}, + {"label": "install OS packages: libnuma-dev python3.12-dev build-essential", "op": "manual"}, {"label": "if libnvrtc is still missing, install the matching CUDA/NVRTC runtime on this host", "op": "manual"}, ], ), diff --git a/src/agent_loop.py b/src/agent_loop.py index ea9c7c477..ebc2a99a6 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -796,50 +796,6 @@ def _extract_last_user_message(messages: List[Dict]) -> str: return "" -_REMINDER_TIME_RE = re.compile( - r"\b(?:today|tonight|tomorrow|tmrw|yesterday)\b(?:\s+(?:at\s+)?\d{1,2}(?::\d{2})?\s*(?:am|pm)?)?" - r"|\b\d{1,2}(?::\d{2})?\s*(?:am|pm)?\s+(?:today|tonight|tomorrow|tmrw|yesterday)\b" - r"|\bin\s+\d+\s*(?:hour|hr|minute|min|day)s?\b", - re.IGNORECASE, -) - - -def _extract_reminder_due_from_user(text: str) -> str: - t = str(text or "").strip() - if not re.search(r"\b(remind|reminder|alarm)\b", t, re.IGNORECASE): - return "" - m = _REMINDER_TIME_RE.search(t) - return m.group(0).strip() if m else "" - - -def _repair_manage_notes_reminder_block(block: ToolBlock, last_user: str) -> ToolBlock: - """Carry reminder time from the user message when the model omits due_date.""" - if block.tool_type != "manage_notes": - return block - try: - args = json.loads(block.content or "{}") - except Exception: - return block - if not isinstance(args, dict) or args.get("due_date"): - return block - - action = str(args.get("action") or "").replace("-", "_").strip().lower() - if action not in {"add", "create", "new", "save", "remind", "reminder"}: - return block - - due = _extract_reminder_due_from_user(last_user) - if not due: - return block - args["due_date"] = due - if not args.get("title"): - cleaned = re.sub(r"\b(make|create|add|set)\b", "", str(last_user or ""), flags=re.IGNORECASE) - cleaned = re.sub(r"\b(a|an)?\s*(reminder|alarm)\b", "", cleaned, flags=re.IGNORECASE) - cleaned = _REMINDER_TIME_RE.sub("", cleaned) - cleaned = re.sub(r"\s+", " ", cleaned).strip(" :,-") or "Reminder" - args["title"] = cleaned[:120] - return ToolBlock(block.tool_type, json.dumps(args, ensure_ascii=False)) - - def _user_turn_count(messages: List[Dict]) -> int: """Count real user turns in the message list.""" count = 0 @@ -4004,7 +3960,6 @@ async def stream_agent_loop( tool_result_texts = [] # plain text for native tool role messages budget_hit = False for i, block in enumerate(tool_blocks): - block = _repair_manage_notes_reminder_block(block, _last_user) # --- Tool budget check --- if max_tool_calls > 0 and total_tool_calls >= max_tool_calls: yield f'data: {json.dumps({"type": "budget_exceeded", "limit": max_tool_calls, "used": total_tool_calls})}\n\n' diff --git a/static/style.css b/static/style.css index 4a467559b..640f05d90 100644 --- a/static/style.css +++ b/static/style.css @@ -8688,7 +8688,7 @@ button.hamburger { .attach-crop-overlay { position: fixed; inset: 0; - z-index: 100000; + z-index: var(--attach-crop-z, 100001); background: rgba(0, 0, 0, 0.72); display: flex; align-items: center; diff --git a/tests/test_chat_route_tool_policy.py b/tests/test_chat_route_tool_policy.py index e43ae525b..a14c7805c 100644 --- a/tests/test_chat_route_tool_policy.py +++ b/tests/test_chat_route_tool_policy.py @@ -254,6 +254,6 @@ def test_frontend_always_sends_explicit_allow_bash(): def test_frontend_sends_explicit_allow_web_search_false_in_agent_mode(): """chat.js must send allow_web_search=false when web toggle is off in agent mode.""" source = _CHAT_JS.read_text(encoding="utf-8") - assert "allow_web_search', 'false'" in source, ( + assert "fd.append('allow_web_search', el('web-toggle').checked ? 'true' : 'false')" in source, ( "Frontend must send explicit allow_web_search=false in agent mode when toggle is off" ) diff --git a/tests/test_contacts_add_null_name.py b/tests/test_contacts_add_null_name.py index 8341c3e65..e19d013f4 100644 --- a/tests/test_contacts_add_null_name.py +++ b/tests/test_contacts_add_null_name.py @@ -24,7 +24,11 @@ def _add_handler(): def _stub_store(monkeypatch): created = [] monkeypatch.setattr(cr, "_fetch_contacts", lambda *a, **k: []) - monkeypatch.setattr(cr, "_create_contact", lambda name, email: created.append((name, email)) or True) + monkeypatch.setattr( + cr, + "_create_contact", + lambda name, email="", address="", phones=None: created.append((name, email, address, phones or [])) or True, + ) return created @@ -33,10 +37,18 @@ def test_null_name_does_not_crash(_stub_store): result = asyncio.run(handler({"name": None, "email": "x@y.com"}, _admin="admin")) assert result["success"] is True # name fell back to the email local-part instead of crashing. - assert _stub_store == [("x", "x@y.com")] + assert _stub_store == [("x", "x@y.com", "", [])] -def test_null_email_is_rejected_cleanly(_stub_store): +def test_null_email_does_not_crash(_stub_store): handler = _add_handler() result = asyncio.run(handler({"name": "Bob", "email": None}, _admin="admin")) - assert result == {"success": False, "error": "Email required"} + assert result["success"] is True + assert _stub_store == [("Bob", "", "", [])] + + +def test_phone_only_contact_is_allowed(_stub_store): + handler = _add_handler() + result = asyncio.run(handler({"name": "Bob", "email": None, "phone": "0805412 7841"}, _admin="admin")) + assert result["success"] is True + assert _stub_store == [("Bob", "", "", ["0805412 7841"])] diff --git a/tests/test_cookbook_dependency_completion_regression.py b/tests/test_cookbook_dependency_completion_regression.py index 53cfa93c5..bf1efc389 100644 --- a/tests/test_cookbook_dependency_completion_regression.py +++ b/tests/test_cookbook_dependency_completion_regression.py @@ -20,7 +20,9 @@ def test_background_status_poll_reconciles_into_local_tasks(): source = _read("static/js/cookbookRunning.js") assert "const statusById = new Map(tasks.map(t => [t.session_id, t]));" in source - assert "const nextStatus = live.status === 'completed'" in source + assert "const completedByOutput = depDone || downloadDone;" in source + assert "const nextStatus = completedByOutput" in source + assert "live.status === 'completed'" in source assert "? 'done'" in source assert ": (live.status === 'error'" in source assert "? 'error'" in source @@ -75,7 +77,8 @@ def test_background_poll_recovers_done_for_stopped_dependency_install(): downgrading the card to crashed.""" source = _read("static/js/cookbookRunning.js") - assert "const depDone = !!task.payload?._dep && _depInstallSucceeded(task.output);" in source + assert "const combinedOutput = `${task.output || ''}\\n${live.output_tail || ''}`;" in source + assert "const depDone = !!task.payload?._dep && _depInstallSucceeded(combinedOutput);" in source assert "(depDone || downloadDone) ? 'done' : (task.type === 'download' ? 'crashed' : 'stopped')" in source @@ -91,14 +94,14 @@ def test_background_poll_recovers_done_for_completed_download(): normalized = " ".join(source.split()) assert ( "const downloadDone = task.type === 'download' " - "&& String(task.output || '').includes('DOWNLOAD_OK');" + "&& String(combinedOutput || '').includes('DOWNLOAD_OK');" ) in normalized def test_dependency_install_payload_keeps_env_path_for_refresh(): source = _read("static/js/cookbook.js") - assert "env_path: _envState.envPath || ''" in source + assert "env_path: targetEnvPath || ''" in source def test_local_dependency_probe_refreshes_user_site_visibility(): diff --git a/tests/test_cookbook_diagnosis.py b/tests/test_cookbook_diagnosis.py index b590d4cf7..bafaef118 100644 --- a/tests/test_cookbook_diagnosis.py +++ b/tests/test_cookbook_diagnosis.py @@ -29,7 +29,7 @@ def test_diagnose_sglang_native_dependency_errors(): diagnosis = _diagnose_serve_output(output) assert diagnosis is not None - assert "SGLang native dependencies" in diagnosis["message"] + assert "SGLang native kernel/runtime" in diagnosis["message"] labels = [suggestion["label"] for suggestion in diagnosis["suggestions"]] assert any("libnuma-dev" in label for label in labels) assert any("python3.12-dev" in label for label in labels) diff --git a/tests/test_cookbook_diagnosis_js.py b/tests/test_cookbook_diagnosis_js.py index 5b8dc849a..881e5ae19 100644 --- a/tests/test_cookbook_diagnosis_js.py +++ b/tests/test_cookbook_diagnosis_js.py @@ -17,6 +17,6 @@ def test_sglang_native_dependency_diagnosis_is_exposed_to_browser(): assert r"Python\.h" in source assert r"libnuma\.so\.1" in source - assert "SGLang native dependencies" in source + assert "SGLang native kernel/runtime" in source assert "libnuma-dev python3.12-dev build-essential" in source assert "sglang-kernel" in source diff --git a/tests/test_cookbook_same_host_server_profiles_js.py b/tests/test_cookbook_same_host_server_profiles_js.py index 6ed56a241..cb512114a 100644 --- a/tests/test_cookbook_same_host_server_profiles_js.py +++ b/tests/test_cookbook_same_host_server_profiles_js.py @@ -31,8 +31,8 @@ def test_selected_server_helpers_prefer_profile_key_before_host_fallback(): def test_cookbook_submodules_resolve_visible_profile_selection(): assert "_serverByVal?.(_ssv)" in DOWNLOAD - assert "_serverByVal?.(_envState.remoteServerKey || host)" in DOWNLOAD - assert "_serverByVal?.(_envState.remoteServerKey || _zh)" in DOWNLOAD + assert "_serverByVal?.(_envState.remoteServerKey || _envState.remoteHost || '')" in DOWNLOAD + assert "_serverByVal?.(zombieCandidate.remoteServerKey || zombieCandidate.payload?.remote_server_key || _zh)" in DOWNLOAD assert "_serverByVal(_envState.remoteServerKey || remoteHost)" in HWFIT assert "hk: _currentServerValue()" in HWFIT assert "sel.value = _currentServerValue();" in HWFIT diff --git a/tests/test_hwfit_macos.py b/tests/test_hwfit_macos.py index f81cc9b38..e05326be4 100644 --- a/tests/test_hwfit_macos.py +++ b/tests/test_hwfit_macos.py @@ -43,13 +43,12 @@ def _fake_sysctl(brand="Apple M2 Pro", memsize_gb=32, wired_mb=None, display_jso return run -def test_mlx_models_hidden_on_metal(): - """MLX-quantized models can't be served by llama.cpp or Ollama (the only - Metal-capable engines Odysseus generates), so they must never be recommended - on Apple Silicon — even though the catalog tags them as Apple-only.""" +def test_mlx_models_visible_on_metal(): + """Apple Silicon can serve MLX models through the MLX engine, so Cookbook + should surface MLX rows on Metal while still hiding them on CUDA.""" results = rank_models(_metal_system(), limit=900) mlx = [m for m in results if str(m.get("quant", "")).startswith("mlx-")] - assert mlx == [], f"MLX models surfaced but cannot be served: {[m['name'] for m in mlx]}" + assert mlx, "MLX models should be available on Apple Silicon / Metal hardware" def _cuda_system(): @@ -65,17 +64,18 @@ def test_mlx_hidden_on_cuda_backend_unchanged(): assert mlx == [] -def test_only_gguf_models_recommended_on_metal(): - """llama.cpp and Ollama (the only Metal engines) need GGUF. Safetensors-only - repos — incl. vLLM-only AWQ/GPTQ/FP8 — can't be served on Metal, so every - model recommended on Apple Silicon must ship a servable GGUF.""" +def test_only_gguf_or_mlx_models_recommended_on_metal(): + """Metal recommendations must be servable locally: either GGUF for + llama.cpp/Ollama or MLX for the MLX engine.""" catalog = {m["name"]: m for m in get_models()} unservable = [ r["name"] for r in rank_models(_metal_system(), limit=900) - if not (catalog.get(r["name"], {}).get("is_gguf") + if not (str(r.get("quant", "")).startswith("mlx-") + or str(r.get("name", "")).startswith(("mlx-community/", "lmstudio-community/")) + or catalog.get(r["name"], {}).get("is_gguf") or catalog.get(r["name"], {}).get("gguf_sources")) ] - assert unservable == [], f"{len(unservable)} non-GGUF models on Metal, e.g. {unservable[:3]}" + assert unservable == [], f"{len(unservable)} non-servable models on Metal, e.g. {unservable[:3]}" def test_qwen_catalog_entries_point_at_verified_gguf_repos(): diff --git a/tests/test_hwfit_manual_backend.py b/tests/test_hwfit_manual_backend.py index 4ebb3fe6e..5873c467e 100644 --- a/tests/test_hwfit_manual_backend.py +++ b/tests/test_hwfit_manual_backend.py @@ -67,11 +67,9 @@ def test_manual_ram_mode_wipes_gpu_and_unified_flag(): assert "unified_memory" not in s -def test_simulated_metal_box_only_recommends_gguf(): +def test_simulated_metal_box_only_recommends_gguf_or_mlx(): """End-to-end: a simulated Metal box must rank exactly like a real Mac — - only models shipping a servable GGUF (llama.cpp/Ollama) survive. Before - 'metal' was accepted, this box ranked as CUDA and surfaced safetensors-only - repos the Mac can't serve.""" + only locally servable GGUF or MLX models survive.""" system = _apply_manual_hardware( {"backend": "cuda", "available_ram_gb": 32.0, "total_ram_gb": 64.0}, manual_mode="gpu", manual_vram_gb="48", manual_backend="metal", @@ -79,7 +77,9 @@ def test_simulated_metal_box_only_recommends_gguf(): catalog = {m["name"]: m for m in get_models()} unservable = [ r["name"] for r in rank_models(system, limit=900) - if not (catalog.get(r["name"], {}).get("is_gguf") + if not (str(r.get("quant", "")).startswith("mlx-") + or str(r.get("name", "")).startswith(("mlx-community/", "lmstudio-community/")) + or catalog.get(r["name"], {}).get("is_gguf") or catalog.get(r["name"], {}).get("gguf_sources")) ] - assert unservable == [], f"{len(unservable)} non-GGUF models on simulated Metal, e.g. {unservable[:3]}" + assert unservable == [], f"{len(unservable)} non-servable models on simulated Metal, e.g. {unservable[:3]}" diff --git a/tests/test_new_chat_model_preference.py b/tests/test_new_chat_model_preference.py index 07e9b5040..f05045c87 100644 --- a/tests/test_new_chat_model_preference.py +++ b/tests/test_new_chat_model_preference.py @@ -27,6 +27,11 @@ def test_new_chat_prefers_pending_and_current_model_before_default(): def test_desktop_new_chat_actions_use_shared_preference_helper(): source = APP_JS.read_text(encoding="utf-8") + shared_handler = _slice( + source, + "async function _handleNewChatAction", + "// New session button on icon rail", + ) rail_handler = _slice( source, "// New session button on icon rail", @@ -38,7 +43,8 @@ def test_desktop_new_chat_actions_use_shared_preference_helper(): "const sidebarNewChatBtn = el('sidebar-new-chat-btn');", ) - assert "if (await _createDirectChatFromPreferredModel()) return;" in rail_handler - assert "if (await _createDirectChatFromPreferredModel()) return;" in brand_handler + assert "if (preferModel && await _createDirectChatFromPreferredModel()) return;" in shared_handler + assert "await _handleNewChatAction();" in rail_handler + assert "await _handleNewChatAction();" in brand_handler assert "const dc = await _refreshDefaultChat();" not in rail_handler assert "const dc = await _refreshDefaultChat();" not in brand_handler