File tree Expand file tree Collapse file tree 4 files changed +56
-6
lines changed
Expand file tree Collapse file tree 4 files changed +56
-6
lines changed Original file line number Diff line number Diff line change 11"""FastAPI application for basic-memory knowledge graph API."""
22
33import asyncio
4+ import os
45from contextlib import asynccontextmanager
56
67from fastapi import FastAPI , HTTPException
@@ -54,12 +55,23 @@ async def lifespan(app: FastAPI): # pragma: no cover
5455 logger .info ("Database connections cached in app state" )
5556
5657 # Start file sync if enabled
57- if app_config .sync_changes :
58+ is_test_env = (
59+ app_config .env == "test"
60+ or os .getenv ("BASIC_MEMORY_ENV" , "" ).lower () == "test"
61+ or os .getenv ("PYTEST_CURRENT_TEST" ) is not None
62+ )
63+ if app_config .sync_changes and not is_test_env :
5864 logger .info (f"Sync changes enabled: { app_config .sync_changes } " )
5965 # start file sync task in background
60- app .state .sync_task = asyncio .create_task (initialize_file_sync (app_config ))
66+ async def _file_sync_runner () -> None :
67+ await initialize_file_sync (app_config )
68+
69+ app .state .sync_task = asyncio .create_task (_file_sync_runner ())
6170 else :
62- logger .info ("Sync changes disabled. Skipping file sync service." )
71+ if is_test_env :
72+ logger .info ("Test environment detected. Skipping file sync service." )
73+ else :
74+ logger .info ("Sync changes disabled. Skipping file sync service." )
6375 app .state .sync_task = None
6476
6577 # proceed with startup
Original file line number Diff line number Diff line change 11import asyncio
22import os
3+ import sys
34from contextlib import asynccontextmanager
45from enum import Enum , auto
56from pathlib import Path
2324from basic_memory .repository .postgres_search_repository import PostgresSearchRepository
2425from basic_memory .repository .sqlite_search_repository import SQLiteSearchRepository
2526
27+ # -----------------------------------------------------------------------------
28+ # Windows event loop policy
29+ # -----------------------------------------------------------------------------
30+ # On Windows, the default ProactorEventLoop has known rough edges with aiosqlite
31+ # during shutdown/teardown (threads posting results to a loop that's closing),
32+ # which can manifest as:
33+ # - "RuntimeError: Event loop is closed"
34+ # - "IndexError: pop from an empty deque"
35+ #
36+ # We don't rely on Proactor-only features (like subprocess pipes), so use the
37+ # more stable SelectorEventLoopPolicy everywhere on Windows.
38+ if sys .platform == "win32" : # pragma: no cover
39+ asyncio .set_event_loop_policy (asyncio .WindowsSelectorEventLoopPolicy ())
40+
2641# Module level state
2742_engine : Optional [AsyncEngine ] = None
2843_session_maker : Optional [async_sessionmaker [AsyncSession ]] = None
Original file line number Diff line number Diff line change 33"""
44
55import asyncio
6+ import os
67from contextlib import asynccontextmanager
78
89from fastmcp import FastMCP
@@ -30,9 +31,19 @@ async def lifespan(app: FastMCP):
3031
3132 # Start file sync as background task (if enabled and not in cloud mode)
3233 sync_task = None
33- if app_config .sync_changes and not app_config .cloud_mode_enabled :
34+ is_test_env = (
35+ app_config .env == "test"
36+ or os .getenv ("BASIC_MEMORY_ENV" , "" ).lower () == "test"
37+ or os .getenv ("PYTEST_CURRENT_TEST" ) is not None
38+ )
39+ if is_test_env :
40+ logger .info ("Test environment detected - skipping local file sync" )
41+ elif app_config .sync_changes and not app_config .cloud_mode_enabled :
3442 logger .info ("Starting file sync in background" )
35- sync_task = asyncio .create_task (initialize_file_sync (app_config ))
43+ async def _file_sync_runner () -> None :
44+ await initialize_file_sync (app_config )
45+
46+ sync_task = asyncio .create_task (_file_sync_runner ())
3647 elif app_config .cloud_mode_enabled :
3748 logger .info ("Cloud mode enabled - skipping local file sync" )
3849 else :
Original file line number Diff line number Diff line change @@ -71,7 +71,7 @@ async def reconcile_projects_with_config(app_config: BasicMemoryConfig):
7171
7272async def initialize_file_sync (
7373 app_config : BasicMemoryConfig ,
74- ):
74+ ) -> None :
7575 """Initialize file synchronization services. This function starts the watch service and does not return
7676
7777 Args:
@@ -80,6 +80,18 @@ async def initialize_file_sync(
8080 Returns:
8181 The watch service task that's monitoring file changes
8282 """
83+ # Never start file watching during tests. Even "background" watchers add tasks/threads
84+ # and can interact badly with strict asyncio teardown (especially on Windows/aiosqlite).
85+ #
86+ # Note: Some tests patch ConfigManager.config with a minimal BasicMemoryConfig that
87+ # may not set env="test". So also detect pytest via env vars.
88+ if (
89+ app_config .env == "test"
90+ or os .getenv ("BASIC_MEMORY_ENV" , "" ).lower () == "test"
91+ or os .getenv ("PYTEST_CURRENT_TEST" ) is not None
92+ ):
93+ logger .info ("Test environment detected - skipping file sync initialization" )
94+ return None
8395
8496 # delay import
8597 from basic_memory .sync import WatchService
You can’t perform that action at this time.
0 commit comments