From 9bc1cdcab0595d810bcc03730e18229737d9b145 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> Date: Tue, 22 Sep 2026 23:27:08 +0100 Subject: [PATCH] test(harness): isolate imports and stabilize browser and DNS fixtures --- tests/conftest.py | 12 +++--- tests/streaming/markdownHarness.mjs | 2 +- .../test_consolidate_memory_explicit_drops.py | 2 +- tests/test_copy_message_strips_thinking_js.py | 2 +- tests/test_fenced_inline_args.py | 36 +----------------- tests/test_fenced_invoke_no_raw_xml.py | 36 +----------------- tests/test_integrations_url_join.py | 1 + tests/test_markdown_lazy_lib_loading_js.py | 2 +- tests/test_markdown_rendering_js.py | 2 +- tests/test_review_document_conversion.py | 4 +- tests/test_startup_session_bootstrap_js.py | 18 ++++----- tests/test_tile_manager_snap_zones_js.py | 3 +- tests/test_upload_multifile.py | 2 +- tests/test_web_search_raw_json_tool_call.py | 35 +---------------- tests/test_web_search_time_filter.py | 38 +------------------ 15 files changed, 31 insertions(+), 164 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0e8faebde..3dab8dbd1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -112,8 +112,7 @@ def _serve_test_static(): sock.close() if is_bound: - yield - return + raise RuntimeError("port 7011 is already in use; browser tests require this worktree's static server") root_dir = Path(__file__).resolve().parent.parent @@ -134,12 +133,11 @@ def _serve_test_static(): class _Server(socketserver.TCPServer): allow_reuse_address = True + server = _Server(("127.0.0.1", 7011), _Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() try: - server = _Server(("127.0.0.1", 7011), _Handler) - thread = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() yield + finally: server.shutdown() server.server_close() - except Exception: - yield diff --git a/tests/streaming/markdownHarness.mjs b/tests/streaming/markdownHarness.mjs index af34b3372..430599719 100644 --- a/tests/streaming/markdownHarness.mjs +++ b/tests/streaming/markdownHarness.mjs @@ -26,7 +26,7 @@ export async function loadMarkdown() { globalThis.MutationObserver = class { observe() {} }; let src = fs.readFileSync(path.join(REPO, 'static/js/markdown.js'), 'utf8'); - src = src.replace(/import uiModule from ['"]\.\/ui\.js(?:\?[^'"]*)?['"];?/, ''); + src = src.replace(/import uiModule from ['"]\.\/ui\.js(?:[?#][^'"]*)?['"];?/, ''); src = src.replace( /import \{ splitTableRow \} from ['"]\.\/markdown\/tableRow\.js['"];/, () => `function splitTableRow(row){return (row||'').replace(/^\\s*\\|/,'').replace(/\\|\\s*$/,'').split('|').map((c)=>c.trim());}`, diff --git a/tests/test_consolidate_memory_explicit_drops.py b/tests/test_consolidate_memory_explicit_drops.py index b03e651bc..d52fd7580 100644 --- a/tests/test_consolidate_memory_explicit_drops.py +++ b/tests/test_consolidate_memory_explicit_drops.py @@ -36,7 +36,7 @@ def test_omitted_memory_survives_only_explicit_drop(monkeypatch): monkeypatch.setattr(src.memory, "MemoryManager", _FakeMM) monkeypatch.setattr( src.task_endpoint, "resolve_task_candidates", - lambda owner=None: [("http://x/v1", "model", {})], + lambda owner=None, **kwargs: [("http://x/v1", "model", {})], ) async def fake_llm(_candidates, **kwargs): diff --git a/tests/test_copy_message_strips_thinking_js.py b/tests/test_copy_message_strips_thinking_js.py index 74a03f80c..86dea41e0 100644 --- a/tests/test_copy_message_strips_thinking_js.py +++ b/tests/test_copy_message_strips_thinking_js.py @@ -58,7 +58,7 @@ def _extract_thinking_blocks(text: str) -> dict: let source = fs.readFileSync('./static/js/markdown.js', 'utf8'); source = source.replace( - /import uiModule from ['"]\.\/ui\.js(?:\?[^'"]*)?['"];?/, + /import uiModule from ['"]\.\/ui\.js(?:[?#][^'"]*)?['"];?/, '' ); source = source.replace( diff --git a/tests/test_fenced_inline_args.py b/tests/test_fenced_inline_args.py index 5de7d9a93..76d691010 100644 --- a/tests/test_fenced_inline_args.py +++ b/tests/test_fenced_inline_args.py @@ -5,41 +5,7 @@ with the args on the same line as the tag; the parser must execute those. The relaxed tag pattern must NOT prefix-match longer fence tags: ```python3 is a language hint, not a "python" tool call with content "3\n...". """ -import sys -from unittest.mock import MagicMock - -# This module needs the real agent-tool stack; importing it pulls in heavy -# DB/auth deps, so we stub those just long enough to import, then restore them. -# We deliberately do NOT pop src.tool_execution: popping and re-importing it -# rebinds the `src` package's `tool_execution` attribute, so a later -# `import src.tool_execution as te` resolves to a different module object than -# the one its functions live in - which silently breaks tests that monkeypatch -# it (e.g. test_edit_file's admin gate) and breaks request-scoped ContextVars. -_ABSENT = object() -_AGENT_MODULES = ["src.agent_tools", "src.tool_parsing", "src.tool_schemas"] -_STUBBED = [ - "sqlalchemy", "sqlalchemy.orm", "sqlalchemy.ext", "sqlalchemy.ext.declarative", - "sqlalchemy.ext.hybrid", "sqlalchemy.sql", "sqlalchemy.sql.expression", - "src.database", "core.models", "core.database", "core.auth", -] -_saved_stubs = {name: sys.modules.get(name, _ABSENT) for name in _STUBBED} - -for _mod in _AGENT_MODULES: - sys.modules.pop(_mod, None) -for _mod in _STUBBED: - if _mod not in sys.modules: - sys.modules[_mod] = MagicMock() - -import src.agent_tools # noqa: E402, F401 -from src.tool_parsing import parse_tool_blocks, strip_tool_blocks # noqa: E402 - -# Drop the stubs we installed so they do not leak into later tests. -for _name, _original in _saved_stubs.items(): - if _original is _ABSENT: - sys.modules.pop(_name, None) - else: - sys.modules[_name] = _original - +from src.tool_parsing import parse_tool_blocks, strip_tool_blocks def test_inline_args_on_tag_line_parse(): # The original bug: ```list_email_accounts {} (args on the tag line) diff --git a/tests/test_fenced_invoke_no_raw_xml.py b/tests/test_fenced_invoke_no_raw_xml.py index e8a66a76a..e9b6db441 100644 --- a/tests/test_fenced_invoke_no_raw_xml.py +++ b/tests/test_fenced_invoke_no_raw_xml.py @@ -3,41 +3,7 @@ can't be converted (e.g. a hyphenated/namespaced tool name that _XML_INVOKE_RE's \\w+ won't match, or an unknown tool) must NOT fall through and ship the raw XML to the code executor as if it were python/bash. """ -import sys -from unittest.mock import MagicMock - -# This module needs the real agent-tool stack; importing it pulls in heavy -# DB/auth deps, so we stub those just long enough to import, then restore them. -# We deliberately do NOT pop src.tool_execution: popping and re-importing it -# rebinds the `src` package's `tool_execution` attribute, so a later -# `import src.tool_execution as te` resolves to a different module object than -# the one its functions live in - which silently breaks tests that monkeypatch -# it (e.g. test_edit_file's admin gate) and breaks request-scoped ContextVars. -_ABSENT = object() -_AGENT_MODULES = ["src.agent_tools", "src.tool_parsing", "src.tool_schemas"] -_STUBBED = [ - "sqlalchemy", "sqlalchemy.orm", "sqlalchemy.ext", "sqlalchemy.ext.declarative", - "sqlalchemy.ext.hybrid", "sqlalchemy.sql", "sqlalchemy.sql.expression", - "src.database", "core.models", "core.database", "core.auth", -] -_saved_stubs = {name: sys.modules.get(name, _ABSENT) for name in _STUBBED} - -for _mod in _AGENT_MODULES: - sys.modules.pop(_mod, None) -for _mod in _STUBBED: - if _mod not in sys.modules: - sys.modules[_mod] = MagicMock() - -import src.agent_tools # noqa: E402, F401 -from src.tool_parsing import parse_tool_blocks # noqa: E402 - -# Drop the stubs we installed so they do not leak into later tests. -for _name, _original in _saved_stubs.items(): - if _original is _ABSENT: - sys.modules.pop(_name, None) - else: - sys.modules[_name] = _original - +from src.tool_parsing import parse_tool_blocks def test_unconvertible_invoke_in_fence_is_not_executed_as_code(): text = '```python\n\n1\n\n```' diff --git a/tests/test_integrations_url_join.py b/tests/test_integrations_url_join.py index a5e19722e..461b6e380 100644 --- a/tests/test_integrations_url_join.py +++ b/tests/test_integrations_url_join.py @@ -105,6 +105,7 @@ async def test_api_call_root_path_has_no_trailing_slash(): with ( patch.object(integrations, "_find_integration", return_value=DISCORD_INTEGRATION), + patch("src.url_safety._default_resolver", return_value=["93.184.216.34"]), patch("httpx.AsyncClient", return_value=mock_client), ): result = await integrations.execute_api_call( diff --git a/tests/test_markdown_lazy_lib_loading_js.py b/tests/test_markdown_lazy_lib_loading_js.py index 9a3b6e43b..ee334a828 100644 --- a/tests/test_markdown_lazy_lib_loading_js.py +++ b/tests/test_markdown_lazy_lib_loading_js.py @@ -102,7 +102,7 @@ globalThis.document = { globalThis.MutationObserver = class { observe() {} }; let source = fs.readFileSync('./static/js/markdown.js', 'utf8'); -source = source.replace(/import uiModule from ['"]\.\/ui\.js(?:\?[^'"]*)?['"];?/, ''); +source = source.replace(/import uiModule from ['"]\.\/ui\.js(?:[?#][^'"]*)?['"];?/, ''); source = source.replace( /import \{ splitTableRow \} from ['"]\.\/markdown\/tableRow\.js['"];/, `function splitTableRow(row) { diff --git a/tests/test_markdown_rendering_js.py b/tests/test_markdown_rendering_js.py index 2391f98ba..10f70c8e5 100644 --- a/tests/test_markdown_rendering_js.py +++ b/tests/test_markdown_rendering_js.py @@ -53,7 +53,7 @@ def _run_markdown_case(markdown: str, render_expr: str = "mod.mdToHtml(input)", let source = fs.readFileSync('./static/js/markdown.js', 'utf8'); source = source.replace( - /import uiModule from ['"]\.\/ui\.js(?:\?[^'"]*)?['"];?/, + /import uiModule from ['"]\.\/ui\.js(?:[?#][^'"]*)?['"];?/, '' ); source = source.replace( diff --git a/tests/test_review_document_conversion.py b/tests/test_review_document_conversion.py index 10499bbdf..4309b2c0a 100644 --- a/tests/test_review_document_conversion.py +++ b/tests/test_review_document_conversion.py @@ -114,6 +114,7 @@ async def test_docx_preview_runs_off_loop_and_checks_owner(monkeypatch, conversi def test_imported_office_document_is_owned_at_first_commit(monkeypatch, tmp_path): + import importlib from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker import src.database as database @@ -122,7 +123,8 @@ def test_imported_office_document_is_owned_at_first_commit(monkeypatch, tmp_path database.Base.metadata.create_all(engine) factory = sessionmaker(bind=engine) monkeypatch.setattr(database, "SessionLocal", factory) - monkeypatch.setattr("src.agent_tools.document_tools.set_active_document", lambda doc_id: None) + document_tools = importlib.import_module("src.agent_tools.document_tools") + monkeypatch.setattr(document_tools, "set_active_document", lambda doc_id: None) owners_at_commit = [] def inspect_new_rows(session): owners_at_commit.extend(row.owner for row in session.new if isinstance(row, database.Document)) diff --git a/tests/test_startup_session_bootstrap_js.py b/tests/test_startup_session_bootstrap_js.py index 3fe37748b..353646751 100644 --- a/tests/test_startup_session_bootstrap_js.py +++ b/tests/test_startup_session_bootstrap_js.py @@ -302,39 +302,39 @@ def results(tmp_path_factory): source = _SESSIONS.read_text(encoding="utf-8") versioned_rewrites = ( ( - r"import Storage from './storage\.js(?:\?[^']+)?';", + r"import Storage from './storage\.js(?:[?#][^']*)?';", "import Storage from './storage.mjs';", ), ( - r"import uiModule, \{ autoResize, styledPrompt \} from './ui\.js(?:\?[^']+)?';", + r"import uiModule, \{ autoResize, styledPrompt \} from './ui\.js(?:[?#][^']*)?';", "import uiModule, { autoResize, styledPrompt } from './ui.mjs';", ), ( - r"import chatRenderer from './chatRenderer\.js(?:\?[^']+)?';", + r"import chatRenderer from './chatRenderer\.js(?:[?#][^']*)?';", "import chatRenderer from './chatRenderer.mjs';", ), ( - r"import \{ providerLogo \} from './providers\.js(?:\?[^']+)?';", + r"import \{ providerLogo \} from './providers\.js(?:[?#][^']*)?';", "import { providerLogo } from './providers.mjs';", ), ( - r"import \{ initModelPicker, updateModelPicker \} from './modelPicker\.js(?:\?[^']+)?';", + r"import \{ initModelPicker, updateModelPicker \} from './modelPicker\.js(?:[?#][^']*)?';", "import { initModelPicker, updateModelPicker } from './modelPicker.mjs';", ), ( - r"import themeModule from './theme\.js(?:\?[^']+)?';", + r"import themeModule from './theme\.js(?:[?#][^']*)?';", "import themeModule from './theme.mjs';", ), ( - r"import spinnerModule from './spinner\.js(?:\?[^']+)?';", + r"import spinnerModule from './spinner\.js(?:[?#][^']*)?';", "import spinnerModule from './spinner.mjs';", ), ( - r"import \{ actionMenuRank, orderActionMenuItems, SELECT_MENU_ICON \} from './actionMenuOrder\.js(?:\?[^']+)?';", + r"import \{ actionMenuRank, orderActionMenuItems, SELECT_MENU_ICON \} from './actionMenuOrder\.js(?:[?#][^']*)?';", "import { actionMenuRank, orderActionMenuItems, SELECT_MENU_ICON } from './actionMenuOrder.mjs';", ), ( - r"import \{ registerEscapeLayer, bindMenuDismiss \} from './escMenuStack\.js(?:\?[^']+)?';", + r"import \{ registerEscapeLayer, bindMenuDismiss \} from './escMenuStack\.js(?:[?#][^']*)?';", "import { registerEscapeLayer, bindMenuDismiss } from './escMenuStack.mjs';", ), ) diff --git a/tests/test_tile_manager_snap_zones_js.py b/tests/test_tile_manager_snap_zones_js.py index 85f2b8da1..f97c8aa1e 100644 --- a/tests/test_tile_manager_snap_zones_js.py +++ b/tests/test_tile_manager_snap_zones_js.py @@ -20,11 +20,12 @@ def _run_tile_case(): innerWidth: 1200, innerHeight: 800, addEventListener() {{}}, + getComputedStyle() {{ return {{ display: 'block' }}; }}, }}; let sidebarVisible = false; const sidebar = {{ classList: {{ contains(name) {{ return name === 'hidden' ? !sidebarVisible : false; }} }}, - getBoundingClientRect() {{ return {{ right: 240 }}; }}, + getBoundingClientRect() {{ return {{ left: 0, right: 240, width: 240, top: 0, bottom: 800 }}; }}, }}; globalThis.document = {{ readyState: 'loading', diff --git a/tests/test_upload_multifile.py b/tests/test_upload_multifile.py index 2e40948e6..cf692e610 100644 --- a/tests/test_upload_multifile.py +++ b/tests/test_upload_multifile.py @@ -191,7 +191,7 @@ async def test_chat_image_upload_is_added_to_gallery(tmp_path, monkeypatch): up.setup_upload_routes(h) endpoint = _endpoint(up.router) - result = await endpoint(_request(user="alice"), [_image_upload()]) + result = await endpoint(_request(user="alice"), [_image_upload()], gallery_id=None) uploaded = result["files"][0] assert uploaded["gallery_id"] diff --git a/tests/test_web_search_raw_json_tool_call.py b/tests/test_web_search_raw_json_tool_call.py index 57abc47d7..09a5b57ba 100644 --- a/tests/test_web_search_raw_json_tool_call.py +++ b/tests/test_web_search_raw_json_tool_call.py @@ -9,41 +9,8 @@ That is an intended tool call in non-native/textual tool mode, but older parsing only recognized fenced blocks, [TOOL_CALL], XML invoke, and tool_code markup. """ import json -import sys -from unittest.mock import MagicMock - -# This module needs the real agent-tool stack; importing it pulls in heavy -# DB/auth deps, so we stub those just long enough to import, then restore them. -# We deliberately do NOT pop src.tool_execution: popping and re-importing it -# rebinds the `src` package's `tool_execution` attribute, so a later -# `import src.tool_execution as te` resolves to a different module object than -# the one its functions live in - which silently breaks tests that monkeypatch -# it (e.g. test_edit_file's admin gate) and breaks request-scoped ContextVars. -_ABSENT = object() -_AGENT_MODULES = ["src.agent_tools", "src.tool_parsing", "src.tool_schemas"] -_STUBBED = [ - "sqlalchemy", "sqlalchemy.orm", "sqlalchemy.ext", "sqlalchemy.ext.declarative", - "sqlalchemy.ext.hybrid", "sqlalchemy.sql", "sqlalchemy.sql.expression", - "src.database", "core.models", "core.database", "core.auth", -] -_saved_stubs = {name: sys.modules.get(name, _ABSENT) for name in _STUBBED} - -for _mod in _AGENT_MODULES: - sys.modules.pop(_mod, None) -for _mod in _STUBBED: - if _mod not in sys.modules: - sys.modules[_mod] = MagicMock() - -import src.agent_tools # noqa: E402, F401 -from src.tool_parsing import parse_tool_blocks, strip_tool_blocks # noqa: E402 - -# Drop the stubs we installed so they do not leak into later tests. -for _name, _original in _saved_stubs.items(): - if _original is _ABSENT: - sys.modules.pop(_name, None) - else: - sys.modules[_name] = _original +from src.tool_parsing import parse_tool_blocks, strip_tool_blocks def test_raw_json_after_web_search_phrase_runs_as_web_search(): text = ( diff --git a/tests/test_web_search_time_filter.py b/tests/test_web_search_time_filter.py index d54369413..88a42336a 100644 --- a/tests/test_web_search_time_filter.py +++ b/tests/test_web_search_time_filter.py @@ -6,43 +6,9 @@ web_search branch emitted a bare query string and dropped time_filter. These pin that a valid filter is passed through as JSON, while plain/invalid cases stay a bare string (back-compat). """ -import sys -from unittest.mock import MagicMock - -# This module needs the real agent-tool stack; importing it pulls in heavy -# DB/auth deps, so we stub those just long enough to import, then restore them. -# We deliberately do NOT pop src.tool_execution: popping and re-importing it -# rebinds the `src` package's `tool_execution` attribute, so a later -# `import src.tool_execution as te` resolves to a different module object than -# the one its functions live in - which silently breaks tests that monkeypatch -# it (e.g. test_edit_file's admin gate) and breaks request-scoped ContextVars. -_ABSENT = object() -_AGENT_MODULES = ["src.agent_tools", "src.tool_parsing", "src.tool_schemas"] -_STUBBED = [ - "sqlalchemy", "sqlalchemy.orm", "sqlalchemy.ext", "sqlalchemy.ext.declarative", - "sqlalchemy.ext.hybrid", "sqlalchemy.sql", "sqlalchemy.sql.expression", - "src.database", "core.models", "core.database", "core.auth", -] -_saved_stubs = {name: sys.modules.get(name, _ABSENT) for name in _STUBBED} - -for _mod in _AGENT_MODULES: - sys.modules.pop(_mod, None) -for _mod in _STUBBED: - if _mod not in sys.modules: - sys.modules[_mod] = MagicMock() - -import json # noqa: E402 - -import src.agent_tools # noqa: E402, F401 -from src.tool_schemas import function_call_to_tool_block # noqa: E402 - -# Drop the stubs we installed so they do not leak into later tests. -for _name, _original in _saved_stubs.items(): - if _original is _ABSENT: - sys.modules.pop(_name, None) - else: - sys.modules[_name] = _original +import json +from src.tool_schemas import function_call_to_tool_block def test_time_filter_is_preserved_as_json(): block = function_call_to_tool_block(