diff --git a/application/agents/workflows/workflow_engine.py b/application/agents/workflows/workflow_engine.py index 22d918aa..93791526 100644 --- a/application/agents/workflows/workflow_engine.py +++ b/application/agents/workflows/workflow_engine.py @@ -1286,7 +1286,7 @@ class WorkflowEngine: query = self.state.get("query", "") if not query: - return + return [] try: retriever = RetrieverCreator.create_retriever( diff --git a/application/llm/anthropic.py b/application/llm/anthropic.py index 8b670b15..21fc818c 100644 --- a/application/llm/anthropic.py +++ b/application/llm/anthropic.py @@ -384,8 +384,8 @@ class AnthropicLLM(BaseLLM): try: output = int(getattr(usage, "output_tokens", 0) or 0) if output_only: - cached = created = 0 - base_input = 0 + # Input counts come from the message_start record below. + base_input = created = cached = 0 else: base_input = int(getattr(usage, "input_tokens", 0) or 0) created = int(getattr(usage, "cache_creation_input_tokens", 0) or 0) diff --git a/application/vectorstore/pgvector.py b/application/vectorstore/pgvector.py index 40d012d2..2b3640e9 100644 --- a/application/vectorstore/pgvector.py +++ b/application/vectorstore/pgvector.py @@ -212,6 +212,22 @@ class PGVectorStore(BaseVectorStore): ) ] + def _nearest_sql(self) -> str: + """Build the nearest-neighbour SELECT for this store's table. + + Identifiers (table/column names) come from this instance's + construction, never from a request, so they cannot be interpolated by a + caller; the query *values* are always bound parameters. + """ + return ( + f"SELECT {self._text_column}, {self._metadata_column}, " + f"({self._vector_column} <=> %s::vector) AS distance " + f"FROM {self._table_name} " + "WHERE source_id = %s " + f"ORDER BY {self._vector_column} <=> %s::vector " + "LIMIT %s;" + ) + def _exact_search(self, cursor, query_vector, k: int, ann_results: list) -> list: """Redo a short indexed search exactly, when the source has more rows. @@ -237,14 +253,7 @@ class PGVectorStore(BaseVectorStore): cursor.execute("SET LOCAL enable_indexscan = off;") cursor.execute("SET LOCAL enable_bitmapscan = off;") cursor.execute( - f""" - SELECT {self._text_column}, {self._metadata_column}, - ({self._vector_column} <=> %s::vector) as distance - FROM {self._table_name} - WHERE source_id = %s - ORDER BY {self._vector_column} <=> %s::vector - LIMIT %s; - """, + self._nearest_sql(), (query_vector, self._source_id, query_vector, k), ) exact = cursor.fetchall() @@ -264,6 +273,7 @@ class PGVectorStore(BaseVectorStore): try: cursor.connection.rollback() except Exception: + # Connection already gone; nothing left to roll back. pass return ann_results finally: @@ -272,6 +282,7 @@ class PGVectorStore(BaseVectorStore): cursor.execute("RESET enable_indexscan;") cursor.execute("RESET enable_bitmapscan;") except Exception: + # Cursor/transaction already unusable; the settings die with it. pass def search_with_scores( @@ -295,14 +306,7 @@ class PGVectorStore(BaseVectorStore): try: # Use cosine distance for similarity search with proper vector formatting - search_query = f""" - SELECT {self._text_column}, {self._metadata_column}, - ({self._vector_column} <=> %s::vector) as distance - FROM {self._table_name} - WHERE source_id = %s - ORDER BY {self._vector_column} <=> %s::vector - LIMIT %s; - """ + search_query = self._nearest_sql() cursor.execute(search_query, (query_vector, self._source_id, query_vector, k)) results = cursor.fetchall() @@ -337,6 +341,7 @@ class PGVectorStore(BaseVectorStore): try: conn.rollback() except Exception: + # Connection already gone; nothing left to roll back. pass return [] finally: diff --git a/tests/agents/test_default_tools.py b/tests/agents/test_default_tools.py index f6d03aa2..4f1217f1 100644 --- a/tests/agents/test_default_tools.py +++ b/tests/agents/test_default_tools.py @@ -195,7 +195,24 @@ class TestSandboxToolsAreNotShippedDefaults: @pytest.mark.parametrize("name", ["code_executor", "artifact_generator"]) def test_absent_from_shipped_defaults(self, name): - assert name not in default_tools.settings.DEFAULT_CHAT_TOOLS + """Assert the *shipped* default, not this deployment's resolved one. + + Reading ``settings.DEFAULT_CHAT_TOOLS`` here would fail on any install + that legitimately enabled these — which is the documented way to turn + them on when a sandbox runner exists. + """ + from application.core.settings import Settings + + shipped = Settings.model_fields["DEFAULT_CHAT_TOOLS"].default + assert name not in shipped + + @pytest.mark.parametrize("name", ["code_executor", "artifact_generator"]) + def test_not_synthesized_when_not_configured(self, name, monkeypatch): + monkeypatch.setattr( + default_tools.settings, + "DEFAULT_CHAT_TOOLS", + ["memory", "read_webpage", "scheduler"], + ) assert name not in { r["name"] for r in default_tools.synthesized_default_tools(None) } diff --git a/tests/agents/test_workflow_agent_types.py b/tests/agents/test_workflow_agent_types.py index a612634a..9af4e10f 100644 --- a/tests/agents/test_workflow_agent_types.py +++ b/tests/agents/test_workflow_agent_types.py @@ -3,6 +3,8 @@ from types import SimpleNamespace from typing import Any, Dict +from unittest.mock import MagicMock + import pytest from application.agents.agentic_agent import AgenticAgent @@ -503,9 +505,23 @@ class TestWorkflowNodeSourceAuthorization: engine.agent._resolve_owner_id = lambda: owner return engine + @staticmethod + def _stub_db(monkeypatch): + """``_authorized_node_sources`` opens a connection; don't need a real one.""" + import contextlib + + import application.storage.db.session as session + + @contextlib.contextmanager + def _conn(): + yield MagicMock() + + monkeypatch.setattr(session, "db_readonly", _conn) + def test_owner_sources_survive(self, monkeypatch): import application.api.user.team_sharing as ts + self._stub_db(monkeypatch) monkeypatch.setattr(ts, "can_access", lambda *a, **k: True) engine = self._engine("owner") assert engine._authorized_node_sources(["s1", "s2"]) == ["s1", "s2"] @@ -513,6 +529,7 @@ class TestWorkflowNodeSourceAuthorization: def test_foreign_sources_are_dropped(self, monkeypatch): import application.api.user.team_sharing as ts + self._stub_db(monkeypatch) monkeypatch.setattr(ts, "can_access", lambda conn, k, sid, u: sid == "mine") engine = self._engine("owner") assert engine._authorized_node_sources(["mine", "theirs"]) == ["mine"] @@ -530,6 +547,7 @@ class TestWorkflowNodeSourceAuthorization: def _boom(*a, **k): raise RuntimeError("db down") + self._stub_db(monkeypatch) monkeypatch.setattr(ts, "can_access", _boom) engine = self._engine("owner") assert engine._authorized_node_sources(["s1"]) == [] diff --git a/tests/api/answer/services/test_stream_processor_utils.py b/tests/api/answer/services/test_stream_processor_utils.py index c3b71592..629518d0 100644 --- a/tests/api/answer/services/test_stream_processor_utils.py +++ b/tests/api/answer/services/test_stream_processor_utils.py @@ -386,9 +386,7 @@ class TestConfigureSource: def test_request_active_docs_used(self, monkeypatch): import application.api.answer.services.stream_processor as sp_mod - from application.api.answer.services.stream_processor import ( - StreamProcessor, - ) + StreamProcessor = sp_mod.StreamProcessor monkeypatch.setattr(sp_mod, "can_access", lambda *a, **k: True) sp = StreamProcessor({"active_docs": "abc"}, {"sub": "u"}) @@ -402,9 +400,7 @@ class TestConfigureSource: answer, while /api/sources//search correctly refused the same id. """ import application.api.answer.services.stream_processor as sp_mod - from application.api.answer.services.stream_processor import ( - StreamProcessor, - ) + StreamProcessor = sp_mod.StreamProcessor monkeypatch.setattr(sp_mod, "can_access", lambda *a, **k: False) sp = StreamProcessor({"active_docs": "someone-elses-id"}, {"sub": "u"}) @@ -414,9 +410,7 @@ class TestConfigureSource: def test_mixed_access_keeps_only_the_readable_ids(self, monkeypatch): import application.api.answer.services.stream_processor as sp_mod - from application.api.answer.services.stream_processor import ( - StreamProcessor, - ) + StreamProcessor = sp_mod.StreamProcessor monkeypatch.setattr( sp_mod, "can_access", lambda conn, kind, sid, user: sid == "mine" @@ -427,9 +421,7 @@ class TestConfigureSource: def test_access_check_failure_fails_closed(self, monkeypatch): import application.api.answer.services.stream_processor as sp_mod - from application.api.answer.services.stream_processor import ( - StreamProcessor, - ) + StreamProcessor = sp_mod.StreamProcessor def _boom(*a, **k): raise RuntimeError("db down") diff --git a/tests/api/answer/test_source_access_control.py b/tests/api/answer/test_source_access_control.py index dedca608..27426556 100644 --- a/tests/api/answer/test_source_access_control.py +++ b/tests/api/answer/test_source_access_control.py @@ -17,7 +17,8 @@ from __future__ import annotations import pytest import application.api.answer.services.stream_processor as sp_mod -from application.api.answer.services.stream_processor import StreamProcessor + +StreamProcessor = sp_mod.StreamProcessor OWNED = "src-owned" TEAM_SHARED = "src-team-shared"