test(harness): isolate imports and stabilize browser and DNS fixtures

This commit is contained in:
Alexandre Teixeira
2026-09-22 23:27:08 +01:00
parent 4520b4f6f0
commit 9bc1cdcab0
15 changed files with 31 additions and 164 deletions
+5 -7
View File
@@ -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
+1 -1
View File
@@ -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());}`,
@@ -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):
@@ -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(
+1 -35
View File
@@ -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)
+1 -35
View File
@@ -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<invoke name="foo-bar">\n<parameter name="x">1</parameter>\n</invoke>\n```'
+1
View File
@@ -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(
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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(
+3 -1
View File
@@ -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))
+9 -9
View File
@@ -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';",
),
)
+2 -1
View File
@@ -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',
+1 -1
View File
@@ -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"]
+1 -34
View File
@@ -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 = (
+2 -36
View File
@@ -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(