mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-08 11:56:59 +00:00
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).
This commit is contained in:
@@ -1306,6 +1306,7 @@ def _diagnose_serve_output(text: str) -> dict | None:
|
|||||||
"SGLang native kernel/runtime is missing or mismatched on this server.",
|
"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": "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"},
|
{"label": "if libnvrtc is still missing, install the matching CUDA/NVRTC runtime on this host", "op": "manual"},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -796,50 +796,6 @@ def _extract_last_user_message(messages: List[Dict]) -> str:
|
|||||||
return ""
|
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:
|
def _user_turn_count(messages: List[Dict]) -> int:
|
||||||
"""Count real user turns in the message list."""
|
"""Count real user turns in the message list."""
|
||||||
count = 0
|
count = 0
|
||||||
@@ -4004,7 +3960,6 @@ async def stream_agent_loop(
|
|||||||
tool_result_texts = [] # plain text for native tool role messages
|
tool_result_texts = [] # plain text for native tool role messages
|
||||||
budget_hit = False
|
budget_hit = False
|
||||||
for i, block in enumerate(tool_blocks):
|
for i, block in enumerate(tool_blocks):
|
||||||
block = _repair_manage_notes_reminder_block(block, _last_user)
|
|
||||||
# --- Tool budget check ---
|
# --- Tool budget check ---
|
||||||
if max_tool_calls > 0 and total_tool_calls >= max_tool_calls:
|
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'
|
yield f'data: {json.dumps({"type": "budget_exceeded", "limit": max_tool_calls, "used": total_tool_calls})}\n\n'
|
||||||
|
|||||||
+1
-1
@@ -8688,7 +8688,7 @@ button.hamburger {
|
|||||||
.attach-crop-overlay {
|
.attach-crop-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 100000;
|
z-index: var(--attach-crop-z, 100001);
|
||||||
background: rgba(0, 0, 0, 0.72);
|
background: rgba(0, 0, 0, 0.72);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -254,6 +254,6 @@ def test_frontend_always_sends_explicit_allow_bash():
|
|||||||
def test_frontend_sends_explicit_allow_web_search_false_in_agent_mode():
|
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."""
|
"""chat.js must send allow_web_search=false when web toggle is off in agent mode."""
|
||||||
source = _CHAT_JS.read_text(encoding="utf-8")
|
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"
|
"Frontend must send explicit allow_web_search=false in agent mode when toggle is off"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ def _add_handler():
|
|||||||
def _stub_store(monkeypatch):
|
def _stub_store(monkeypatch):
|
||||||
created = []
|
created = []
|
||||||
monkeypatch.setattr(cr, "_fetch_contacts", lambda *a, **k: [])
|
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
|
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"))
|
result = asyncio.run(handler({"name": None, "email": "x@y.com"}, _admin="admin"))
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
# name fell back to the email local-part instead of crashing.
|
# 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()
|
handler = _add_handler()
|
||||||
result = asyncio.run(handler({"name": "Bob", "email": None}, _admin="admin"))
|
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"])]
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ def test_background_status_poll_reconciles_into_local_tasks():
|
|||||||
source = _read("static/js/cookbookRunning.js")
|
source = _read("static/js/cookbookRunning.js")
|
||||||
|
|
||||||
assert "const statusById = new Map(tasks.map(t => [t.session_id, t]));" in source
|
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 "? 'done'" in source
|
||||||
assert ": (live.status === 'error'" in source
|
assert ": (live.status === 'error'" in source
|
||||||
assert "? '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."""
|
downgrading the card to crashed."""
|
||||||
source = _read("static/js/cookbookRunning.js")
|
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
|
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())
|
normalized = " ".join(source.split())
|
||||||
assert (
|
assert (
|
||||||
"const downloadDone = task.type === 'download' "
|
"const downloadDone = task.type === 'download' "
|
||||||
"&& String(task.output || '').includes('DOWNLOAD_OK');"
|
"&& String(combinedOutput || '').includes('DOWNLOAD_OK');"
|
||||||
) in normalized
|
) in normalized
|
||||||
|
|
||||||
|
|
||||||
def test_dependency_install_payload_keeps_env_path_for_refresh():
|
def test_dependency_install_payload_keeps_env_path_for_refresh():
|
||||||
source = _read("static/js/cookbook.js")
|
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():
|
def test_local_dependency_probe_refreshes_user_site_visibility():
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def test_diagnose_sglang_native_dependency_errors():
|
|||||||
diagnosis = _diagnose_serve_output(output)
|
diagnosis = _diagnose_serve_output(output)
|
||||||
|
|
||||||
assert diagnosis is not None
|
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"]]
|
labels = [suggestion["label"] for suggestion in diagnosis["suggestions"]]
|
||||||
assert any("libnuma-dev" in label for label in labels)
|
assert any("libnuma-dev" in label for label in labels)
|
||||||
assert any("python3.12-dev" in label for label in labels)
|
assert any("python3.12-dev" in label for label in labels)
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ def test_sglang_native_dependency_diagnosis_is_exposed_to_browser():
|
|||||||
|
|
||||||
assert r"Python\.h" in source
|
assert r"Python\.h" in source
|
||||||
assert r"libnuma\.so\.1" 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 "libnuma-dev python3.12-dev build-essential" in source
|
||||||
assert "sglang-kernel" in source
|
assert "sglang-kernel" in source
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ def test_selected_server_helpers_prefer_profile_key_before_host_fallback():
|
|||||||
|
|
||||||
def test_cookbook_submodules_resolve_visible_profile_selection():
|
def test_cookbook_submodules_resolve_visible_profile_selection():
|
||||||
assert "_serverByVal?.(_ssv)" in DOWNLOAD
|
assert "_serverByVal?.(_ssv)" in DOWNLOAD
|
||||||
assert "_serverByVal?.(_envState.remoteServerKey || host)" in DOWNLOAD
|
assert "_serverByVal?.(_envState.remoteServerKey || _envState.remoteHost || '')" in DOWNLOAD
|
||||||
assert "_serverByVal?.(_envState.remoteServerKey || _zh)" in DOWNLOAD
|
assert "_serverByVal?.(zombieCandidate.remoteServerKey || zombieCandidate.payload?.remote_server_key || _zh)" in DOWNLOAD
|
||||||
assert "_serverByVal(_envState.remoteServerKey || remoteHost)" in HWFIT
|
assert "_serverByVal(_envState.remoteServerKey || remoteHost)" in HWFIT
|
||||||
assert "hk: _currentServerValue()" in HWFIT
|
assert "hk: _currentServerValue()" in HWFIT
|
||||||
assert "sel.value = _currentServerValue();" in HWFIT
|
assert "sel.value = _currentServerValue();" in HWFIT
|
||||||
|
|||||||
+11
-11
@@ -43,13 +43,12 @@ def _fake_sysctl(brand="Apple M2 Pro", memsize_gb=32, wired_mb=None, display_jso
|
|||||||
return run
|
return run
|
||||||
|
|
||||||
|
|
||||||
def test_mlx_models_hidden_on_metal():
|
def test_mlx_models_visible_on_metal():
|
||||||
"""MLX-quantized models can't be served by llama.cpp or Ollama (the only
|
"""Apple Silicon can serve MLX models through the MLX engine, so Cookbook
|
||||||
Metal-capable engines Odysseus generates), so they must never be recommended
|
should surface MLX rows on Metal while still hiding them on CUDA."""
|
||||||
on Apple Silicon — even though the catalog tags them as Apple-only."""
|
|
||||||
results = rank_models(_metal_system(), limit=900)
|
results = rank_models(_metal_system(), limit=900)
|
||||||
mlx = [m for m in results if str(m.get("quant", "")).startswith("mlx-")]
|
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():
|
def _cuda_system():
|
||||||
@@ -65,17 +64,18 @@ def test_mlx_hidden_on_cuda_backend_unchanged():
|
|||||||
assert mlx == []
|
assert mlx == []
|
||||||
|
|
||||||
|
|
||||||
def test_only_gguf_models_recommended_on_metal():
|
def test_only_gguf_or_mlx_models_recommended_on_metal():
|
||||||
"""llama.cpp and Ollama (the only Metal engines) need GGUF. Safetensors-only
|
"""Metal recommendations must be servable locally: either GGUF for
|
||||||
repos — incl. vLLM-only AWQ/GPTQ/FP8 — can't be served on Metal, so every
|
llama.cpp/Ollama or MLX for the MLX engine."""
|
||||||
model recommended on Apple Silicon must ship a servable GGUF."""
|
|
||||||
catalog = {m["name"]: m for m in get_models()}
|
catalog = {m["name"]: m for m in get_models()}
|
||||||
unservable = [
|
unservable = [
|
||||||
r["name"] for r in rank_models(_metal_system(), limit=900)
|
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"))
|
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():
|
def test_qwen_catalog_entries_point_at_verified_gguf_repos():
|
||||||
|
|||||||
@@ -67,11 +67,9 @@ def test_manual_ram_mode_wipes_gpu_and_unified_flag():
|
|||||||
assert "unified_memory" not in s
|
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 —
|
"""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
|
only locally servable GGUF or MLX models survive."""
|
||||||
'metal' was accepted, this box ranked as CUDA and surfaced safetensors-only
|
|
||||||
repos the Mac can't serve."""
|
|
||||||
system = _apply_manual_hardware(
|
system = _apply_manual_hardware(
|
||||||
{"backend": "cuda", "available_ram_gb": 32.0, "total_ram_gb": 64.0},
|
{"backend": "cuda", "available_ram_gb": 32.0, "total_ram_gb": 64.0},
|
||||||
manual_mode="gpu", manual_vram_gb="48", manual_backend="metal",
|
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()}
|
catalog = {m["name"]: m for m in get_models()}
|
||||||
unservable = [
|
unservable = [
|
||||||
r["name"] for r in rank_models(system, limit=900)
|
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"))
|
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]}"
|
||||||
|
|||||||
@@ -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():
|
def test_desktop_new_chat_actions_use_shared_preference_helper():
|
||||||
source = APP_JS.read_text(encoding="utf-8")
|
source = APP_JS.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
shared_handler = _slice(
|
||||||
|
source,
|
||||||
|
"async function _handleNewChatAction",
|
||||||
|
"// New session button on icon rail",
|
||||||
|
)
|
||||||
rail_handler = _slice(
|
rail_handler = _slice(
|
||||||
source,
|
source,
|
||||||
"// New session button on icon rail",
|
"// 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');",
|
"const sidebarNewChatBtn = el('sidebar-new-chat-btn');",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "if (await _createDirectChatFromPreferredModel()) return;" in rail_handler
|
assert "if (preferModel && await _createDirectChatFromPreferredModel()) return;" in shared_handler
|
||||||
assert "if (await _createDirectChatFromPreferredModel()) return;" in brand_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 rail_handler
|
||||||
assert "const dc = await _refreshDefaultChat();" not in brand_handler
|
assert "const dc = await _refreshDefaultChat();" not in brand_handler
|
||||||
|
|||||||
Reference in New Issue
Block a user