diff --git a/src/agent_loop.py b/src/agent_loop.py index f753a0b5b..c90168324 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -2005,6 +2005,7 @@ async def stream_agent_loop( and (_casual_low_signal_turn or not active_email) and (_casual_low_signal_turn or not workspace) and not forced_tools + and not relevant_tools ) # Tool retrieval uses the latest message by default. It may inherit recent # user turns only for explicit continuations ("yes", "do it", "1"). diff --git a/tests/test_aux_llm_owner_scope.py b/tests/test_aux_llm_owner_scope.py index 534a2e429..9bd55482d 100644 --- a/tests/test_aux_llm_owner_scope.py +++ b/tests/test_aux_llm_owner_scope.py @@ -43,7 +43,8 @@ def test_background_session_sort_uses_owner_task_endpoint(): def test_scheduler_fallbacks_and_research_headers_are_owner_scoped(): src = _src("src/task_scheduler.py") - assert "resolve_utility_fallback_candidates(owner=task.owner or None)" in src + assert "resolve_task_candidates(" in src + assert "owner=task.owner or None" in src assert 'resolve_endpoint(\n "research",' in src assert "owner=task.owner or None" in src assert "headers_from_resolver = False" in src diff --git a/tests/test_builtin_actions_owner_scope.py b/tests/test_builtin_actions_owner_scope.py index a13f41d24..d14a94462 100644 --- a/tests/test_builtin_actions_owner_scope.py +++ b/tests/test_builtin_actions_owner_scope.py @@ -51,23 +51,19 @@ class _Db: self.closed = True -def _resolver_spy(monkeypatch, utility_result=("", "", {}), default_result=("http://llm", "model", {})): - from src import endpoint_resolver +def _resolver_spy(monkeypatch, candidates=None): + from src import task_endpoint calls = [] - fallback_calls = [] - def fake_resolve(kind, *args, **kwargs): - calls.append((kind, kwargs.get("owner"))) - return utility_result if kind == "utility" else default_result + def fake_candidates(*args, **kwargs): + calls.append(kwargs.get("owner")) + if candidates is None: + return [("http://llm", "model", {})] + return list(candidates) - def fake_fallbacks(*args, **kwargs): - fallback_calls.append(kwargs.get("owner")) - return [] - - monkeypatch.setattr(endpoint_resolver, "resolve_endpoint", fake_resolve) - monkeypatch.setattr(endpoint_resolver, "resolve_utility_fallback_candidates", fake_fallbacks) - return calls, fallback_calls + monkeypatch.setattr(task_endpoint, "resolve_task_candidates", fake_candidates) + return calls @pytest.mark.asyncio @@ -88,7 +84,7 @@ async def test_classify_events_resolves_llm_for_task_owner(monkeypatch): location="", ) db = _Db({FakeCalendarEvent: [event]}) - calls, _fallback_calls = _resolver_spy(monkeypatch, utility_result=("http://llm", "model", {})) + calls = _resolver_spy(monkeypatch) monkeypatch.setattr(database, "CalendarEvent", FakeCalendarEvent) monkeypatch.setattr(database, "SessionLocal", lambda: db) @@ -97,7 +93,7 @@ async def test_classify_events_resolves_llm_for_task_owner(monkeypatch): assert ok is True assert "Scanned 1 upcoming event" in message - assert calls == [("utility", "alice")] + assert calls == ["alice"] assert db.closed is True @@ -122,7 +118,7 @@ async def test_learn_sender_signatures_resolves_llm_for_task_owner(monkeypatch): def logout(self): return None - calls, _fallback_calls = _resolver_spy(monkeypatch, utility_result=("", "", {}), default_result=("", "", {})) + calls = _resolver_spy(monkeypatch, candidates=[]) imap_owners = [] def fake_imap_connect(_account_id=None, owner=""): @@ -135,14 +131,14 @@ async def test_learn_sender_signatures_resolves_llm_for_task_owner(monkeypatch): assert ok is False assert message == "No LLM endpoint available" - assert calls == [("utility", "alice"), ("default", "alice")] + assert calls == ["alice"] assert imap_owners == ["alice"] @pytest.mark.asyncio async def test_learn_sender_signatures_writes_owner_scoped_cache(monkeypatch, tmp_path): from routes import email_helpers - from src import endpoint_resolver, llm_core + from src import llm_core, task_endpoint from src.builtin_actions import action_learn_sender_signatures db_path = tmp_path / "scheduled_emails.db" @@ -205,15 +201,15 @@ async def test_learn_sender_signatures_writes_owner_scoped_cache(monkeypatch, tm monkeypatch.setattr(email_helpers, "_imap_connect", fake_imap_connect) monkeypatch.setattr( - endpoint_resolver, - "resolve_endpoint", - lambda kind, *args, **kwargs: ("http://llm", "alice-model", {}), + task_endpoint, + "resolve_task_candidates", + lambda *args, **kwargs: [("http://llm", "alice-model", {})], ) - async def fake_llm_call_async(**_kwargs): + async def fake_llm_call_async(_candidates, **_kwargs): return "Writer Example\nExample Co.\nwriter@example.com" - monkeypatch.setattr(llm_core, "llm_call_async", fake_llm_call_async) + monkeypatch.setattr(llm_core, "llm_call_async_with_fallback", fake_llm_call_async) message, ok = await action_learn_sender_signatures("alice") @@ -253,7 +249,7 @@ async def test_check_email_urgency_resolves_llm_candidates_for_task_owner(monkey from_address = _Column() db = _Db({FakeEmailAccount: []}) - calls, fallback_calls = _resolver_spy(monkeypatch, utility_result=("http://llm", "model", {})) + calls = _resolver_spy(monkeypatch) monkeypatch.chdir(tmp_path) monkeypatch.setattr(database, "EmailAccount", FakeEmailAccount) @@ -262,6 +258,5 @@ async def test_check_email_urgency_resolves_llm_candidates_for_task_owner(monkey with pytest.raises(TaskNoop, match="no email accounts configured"): await action_check_email_urgency("alice") - assert calls == [("utility", "alice")] - assert fallback_calls == ["alice"] + assert calls == ["alice"] assert db.closed is True diff --git a/tests/test_builtin_memory_consolidation.py b/tests/test_builtin_memory_consolidation.py index bebd43586..0b4b6b49f 100644 --- a/tests/test_builtin_memory_consolidation.py +++ b/tests/test_builtin_memory_consolidation.py @@ -29,8 +29,8 @@ def _read_memories(data_dir): @pytest.mark.asyncio async def test_consolidate_memory_empty_owner_treats_each_owner_separately(monkeypatch, tmp_path): from src import constants - from src import endpoint_resolver from src import llm_core + from src import task_endpoint action_consolidate_memory = _import_consolidate_action() long_alice_text = "Alice private project context. " + ("A" * 2200) @@ -44,11 +44,15 @@ async def test_consolidate_memory_empty_owner_treats_each_owner_separately(monke ], ) monkeypatch.setattr(constants, "DATA_DIR", str(data_dir)) - monkeypatch.setattr(endpoint_resolver, "resolve_endpoint", lambda *args, **kwargs: ("http://llm", "model", {})) + monkeypatch.setattr( + task_endpoint, + "resolve_task_candidates", + lambda *args, **kwargs: [("http://llm", "model", {})], + ) prompts = [] - async def fake_llm_call_async(**kwargs): + async def fake_llm_call_async(_candidates, **kwargs): prompt = kwargs["messages"][0]["content"] prompts.append(prompt) if "alice-long" in prompt: @@ -71,7 +75,7 @@ async def test_consolidate_memory_empty_owner_treats_each_owner_separately(monke } ) - monkeypatch.setattr(llm_core, "llm_call_async", fake_llm_call_async) + monkeypatch.setattr(llm_core, "llm_call_async_with_fallback", fake_llm_call_async) message, ok = await action_consolidate_memory("") diff --git a/tests/test_consolidate_memory_explicit_drops.py b/tests/test_consolidate_memory_explicit_drops.py index ed9bc0234..b03e651bc 100644 --- a/tests/test_consolidate_memory_explicit_drops.py +++ b/tests/test_consolidate_memory_explicit_drops.py @@ -29,24 +29,24 @@ class _FakeMM: def test_omitted_memory_survives_only_explicit_drop(monkeypatch): import src.memory - import src.endpoint_resolver import src.llm_core + import src.task_endpoint _FakeMM.saved = None monkeypatch.setattr(src.memory, "MemoryManager", _FakeMM) monkeypatch.setattr( - src.endpoint_resolver, "resolve_endpoint", - lambda kind, owner=None: ("http://x/v1", "model", {}), + src.task_endpoint, "resolve_task_candidates", + lambda owner=None: [("http://x/v1", "model", {})], ) - async def fake_llm(**kwargs): + async def fake_llm(_candidates, **kwargs): # Model keeps 'a', drops 'b', and OMITS 'c' entirely. return json.dumps({ "keep": [{"id": "a", "text": "Likes dark roast coffee", "category": "preference"}], "drop": [{"id": "b", "reason": "duplicate of a"}], }) - monkeypatch.setattr(src.llm_core, "llm_call_async", fake_llm) + monkeypatch.setattr(src.llm_core, "llm_call_async_with_fallback", fake_llm) msg, ok = asyncio.run(ba.action_consolidate_memory("alice")) diff --git a/tests/test_cookbook_cpu_only_serve.py b/tests/test_cookbook_cpu_only_serve.py index 5c21e5c60..bcb06b098 100644 --- a/tests/test_cookbook_cpu_only_serve.py +++ b/tests/test_cookbook_cpu_only_serve.py @@ -68,18 +68,18 @@ def test_vllm_blank_swap_omits_swap_space_flag(): def test_serve_preflight_uses_selected_server_not_stale_env_host(): text = SERVE_SRC.read_text(encoding="utf-8") - assert "const _selectedServeTarget = (() => {" in text - assert "const _hostStr = _selectedServeTarget.host || '';" in text + assert "function _selectedServeTarget(panel) {" in text + assert "const _hostStr = launchTarget.host || '';" in text assert "(t.remoteHost || '') === _hostStr" in text - assert "const _probeHost = (_selectedServeTarget.host || '').trim();" in text - assert "const _portHost = (_selectedServeTarget.host || '').trim();" in text + assert "const _probeHost = (launchTarget.host || '').trim();" in text + assert "const _portHost = (launchTarget.host || '').trim();" in text def test_vllm_route_strips_swap_space_when_runtime_rejects_it(): text = ROUTES_SRC.read_text(encoding="utf-8") - assert "Removing --swap-space 0; off is represented by omitting the vLLM flag." in text - assert "vLLM serve does not support --swap-space; removing it" in text + assert "Setting vLLM --swap-space 0 so the runtime does not reserve CPU swap per GPU." in text + assert "vLLM serve does not expose --swap-space; removing the flag and patching the runtime default to 0." in text assert "ODYSSEUS_VLLM_HELP_CMD" in text assert "print(shlex.join(parts[:serve_i + 1] + [\"--help\"]))" in text assert "eval \"$ODYSSEUS_VLLM_HELP_CMD\" 2>&1 | grep -q -- \"--swap-space\"" in text diff --git a/tests/test_cookbook_helpers.py b/tests/test_cookbook_helpers.py index a7c3ee017..5d2db5dda 100644 --- a/tests/test_cookbook_helpers.py +++ b/tests/test_cookbook_helpers.py @@ -348,7 +348,7 @@ def test_serve_pip_install_normalizes_llama_cpp_alias_and_adds_wheel_index(): src = (pathlib.Path(__file__).resolve().parent.parent / "routes" / "cookbook_routes.py").read_text(encoding="utf-8") - assert "re.sub(r\"(?/dev/null || [ -d /opt/rocm ] || [ -n "$ROCM_PATH" ] || [ -n "$HIP_PATH" ]' in script assert 'cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_HIP=ON' in script assert 'cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_CUDA=ON' in script @@ -676,7 +676,7 @@ def test_llama_cpp_linux_bootstrap_nvcc_without_cudart_warns_and_falls_back(): # outer else that handles no-GPU-toolchain). Verify it appears at least once # before the outer "no HIP/CUDA toolchain" warning. cpu_cmake = 'cmake -B build -DCMAKE_BUILD_TYPE=Release &&' - no_toolchain_warn = 'WARNING: no HIP/CUDA toolchain found' + no_toolchain_warn = 'WARNING: no HIP/CUDA/Vulkan toolchain found' assert cpu_cmake in script assert script.index(cpu_cmake) < script.index(no_toolchain_warn) @@ -693,8 +693,8 @@ def test_llama_cpp_linux_bootstrap_keeps_cpu_fallback_when_no_gpu_toolchain(): _append_llama_cpp_linux_accel_build_lines(runner_lines) script = "\n".join(runner_lines) - assert 'WARNING: no HIP/CUDA toolchain found — building llama-server for CPU only.' in script - assert 'Install ROCm for AMD GPUs or vLLM/CUDA tooling for NVIDIA' in script + assert 'WARNING: no HIP/CUDA/Vulkan toolchain found — building llama-server for CPU only.' in script + assert 'Install Vulkan (libvulkan-dev) / ROCm for AMD GPUs or CUDA tooling for NVIDIA' in script def test_llama_cpp_rebuild_cmd_clears_cached_build_paths(): diff --git a/tests/test_cookbook_same_host_server_profiles_js.py b/tests/test_cookbook_same_host_server_profiles_js.py index 13f7d2db4..6ed56a241 100644 --- a/tests/test_cookbook_same_host_server_profiles_js.py +++ b/tests/test_cookbook_same_host_server_profiles_js.py @@ -50,14 +50,14 @@ def test_serve_launch_preflights_use_selected_target_and_port(): assert "if (launchTarget.port) _probeParams.set('ssh_port', launchTarget.port);" in SERVE assert "const _portHost = (launchTarget.host || '').trim();" in SERVE assert "StrictHostKeyChecking=no ${_sshPrefix(launchTarget.port)}${_portHost}" in SERVE - assert "let serveHost = launchTarget.host || '';" in SERVE + assert "const serveHost = launchTarget.host || '';" in SERVE assert SERVE.index(launch_target) < SERVE.index("const _runningMod = await import('./cookbookRunning.js');") def test_running_tab_resolves_profile_key_not_first_host(): - assert "_serverByVal(_envState.remoteServerKey || _tHost)" in RUNNING + assert "_serverByVal(_targetKey)" in RUNNING assert "_serverByVal(_envState.remoteServerKey || _host)" in RUNNING - assert "_serverByVal(_envState.remoteServerKey || host)" in RUNNING + assert "_serverByVal(savedKey)" in RUNNING assert "_serverByVal = shared._serverByVal;" in RUNNING assert "_selectedServer = shared._selectedServer;" in RUNNING diff --git a/tests/test_docker_devops_hardening.py b/tests/test_docker_devops_hardening.py index 7d7b6d37c..95185f5f5 100644 --- a/tests/test_docker_devops_hardening.py +++ b/tests/test_docker_devops_hardening.py @@ -59,8 +59,8 @@ def test_docker_entrypoint_does_not_resolve_root_commands_from_app_local_path(): path_export = script.index('export PATH="/app/.local/bin:$PATH"') gosu_capture = script.index('GOSU_BIN="$(command -v gosu)"') python_capture = script.index('PYTHON_BIN="$(command -v python)"') - setup_call = script.index('"$GOSU_BIN" "$PUID:$PGID" "$PYTHON_BIN" /app/setup.py') - final_exec = script.index('exec "$GOSU_BIN" "$PUID:$PGID" "$@"') + setup_call = script.index('"$GOSU_BIN" "$ODY_USER" "$PYTHON_BIN" /app/setup.py') + final_exec = script.index('exec "$GOSU_BIN" "$ODY_USER" "$@"') assert gosu_capture < path_export < setup_call assert python_capture < path_export < setup_call