Skip to content

Commit 12bfba3

Browse files
DeepfreezechillBrian KrafftCopilot
authored
chore(P3): eliminate all actionable warnings + thread-safe close() (#27)
Warning cleanup (54 → 44, remaining are P4 architecture file-size): - DeprecationWarning: wrap set_schema_version tests with deprecated_call() - ResourceWarning: atexit cleanup for mcp_server file handles - RuntimeWarning: MagicMock for sync is_initialized() (was AsyncMock) Thread safety: - MigrationManager.close(): _closed + _conn.close() both under mutex - Eliminates TOCTOU race window Test hygiene: - Architecture boundary allowlist updated for mcp_server.py line shifts - 31 skipped tests audited: 8 Windows/symlink, 23 missing deps (all valid) Verified by 3-agent adversarial review (OOP v4.2.0): - /8eyes (Opus 4.6): SHIP 0.95 — 5/5 attacks repelled - /collab (GPT-5.4): SHIP after close() TOCTOU fix - code-review (Sonnet): APPROVED 1,351 tests passing, 44 warnings (all P4 architecture scope). Co-authored-by: Brian Krafft <bkrafft@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a50bfca commit 12bfba3

5 files changed

Lines changed: 71 additions & 39 deletions

File tree

openspace/mcp_server.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from __future__ import annotations
1818

1919
import asyncio
20+
import atexit
2021
import inspect
2122
import json
2223
import logging
@@ -88,19 +89,36 @@ def __getattr__(self, name):
8889
# drain stderr. Heavy log/print output during execute_task fills the stderr
8990
# pipe buffer, blocking this process on write() → deadlock → timeout.
9091
# Redirect stderr to a log file on Windows to prevent this.
92+
_stderr_file = None
9193
if os.name == "nt":
9294
_stderr_file = open(_LOG_DIR / "mcp_stderr.log", "a", encoding="utf-8", buffering=1)
9395
sys.stderr = _stderr_file
9496

9597
sys.stdout = _MCPSafeStdout(_real_stdout, sys.stderr)
9698

99+
# Set up file handler for logging
100+
_log_file_handler = logging.FileHandler(_LOG_DIR / "mcp_server.log")
97101
logging.basicConfig(
98102
level=logging.INFO,
99103
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
100-
handlers=[logging.FileHandler(_LOG_DIR / "mcp_server.log")],
104+
handlers=[_log_file_handler],
101105
)
102106
logger = logging.getLogger("openspace.mcp_server")
103107

108+
109+
def _cleanup_file_handles():
110+
"""Clean up file handles on exit."""
111+
global _stderr_file, _log_file_handler
112+
if _stderr_file:
113+
_stderr_file.close()
114+
_stderr_file = None
115+
if _log_file_handler:
116+
_log_file_handler.close()
117+
118+
119+
# Register cleanup handler
120+
atexit.register(_cleanup_file_handles)
121+
104122
from mcp.server.fastmcp import FastMCP
105123

106124
_fastmcp_kwargs: dict = {}

openspace/skill_engine/migration_manager.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,16 @@ def _ensure_open(self) -> None:
226226

227227
def close(self) -> None:
228228
"""Close the connection if we own it."""
229-
if self._closed:
230-
return
231-
232-
if self._owns_conn:
233-
self._closed = True
234-
try:
235-
self._conn.close()
236-
except Exception:
237-
pass
229+
with self._mu:
230+
if self._closed:
231+
return
232+
# Only set _closed and close conn if we own the connection
233+
if self._owns_conn:
234+
self._closed = True
235+
try:
236+
self._conn.close()
237+
except Exception:
238+
pass
238239

239240
logger.debug("MigrationManager closed")
240241

tests/test_architecture_boundaries.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -268,15 +268,15 @@ def test_mcp_handler_files_exist(self) -> None:
268268
# public property accessors as delegation is fully wired in Phase 4).
269269
_KNOWN_PRIVATE_ACCESS: frozenset[tuple[str, int, str]] = frozenset(
270270
{
271-
("openspace/mcp_server.py", 185, "_skill_store"),
272-
("openspace/mcp_server.py", 203, "_grounding_config"),
273-
("openspace/mcp_server.py", 296, "_skill_registry"),
274-
("openspace/mcp_server.py", 430, "_skill_registry"),
275-
("openspace/mcp_server.py", 734, "_skill_registry"),
276-
("openspace/mcp_server.py", 635, "_skill_registry"),
277-
("openspace/mcp_server.py", 753, "_skill_evolver"),
278-
("openspace/mcp_server.py", 737, "_skill_evolver"),
279-
("openspace/mcp_server.py", 419, "_grounding_config"),
271+
("openspace/mcp_server.py", 203, "_skill_store"),
272+
("openspace/mcp_server.py", 221, "_grounding_config"),
273+
("openspace/mcp_server.py", 314, "_skill_registry"),
274+
("openspace/mcp_server.py", 448, "_skill_registry"),
275+
("openspace/mcp_server.py", 752, "_skill_registry"),
276+
("openspace/mcp_server.py", 653, "_skill_registry"),
277+
("openspace/mcp_server.py", 771, "_skill_evolver"),
278+
("openspace/mcp_server.py", 755, "_skill_evolver"),
279+
("openspace/mcp_server.py", 437, "_grounding_config"),
280280
}
281281
)
282282

tests/test_migration_manager.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -189,25 +189,29 @@ def test_initial_schema_version_is_zero(self, migration_manager):
189189

190190
def test_set_and_get_schema_version(self, migration_manager):
191191
"""Test setting and retrieving schema versions."""
192-
migration_manager.set_schema_version(5)
192+
with pytest.deprecated_call():
193+
migration_manager.set_schema_version(5)
193194
version = migration_manager.get_schema_version()
194195
assert version == 5
195196

196-
migration_manager.set_schema_version(10)
197+
with pytest.deprecated_call():
198+
migration_manager.set_schema_version(10)
197199
version = migration_manager.get_schema_version()
198200
assert version == 10
199201

200202
def test_set_negative_version_fails(self, migration_manager):
201203
"""Test that setting negative version raises error."""
202204
with pytest.raises(ValueError, match="version must be non-negative"):
203-
migration_manager.set_schema_version(-1)
205+
with pytest.deprecated_call():
206+
migration_manager.set_schema_version(-1)
204207

205208
def test_version_persists_across_connections(self, temp_db_path):
206209
"""Test that schema version persists when reopening database."""
207210
# Set version in first instance
208211
manager1 = MigrationManager(db_path=temp_db_path)
209212
manager1.initialize_schema() # Create tables first
210-
manager1.set_schema_version(7)
213+
with pytest.deprecated_call():
214+
manager1.set_schema_version(7)
211215
manager1.close()
212216

213217
# Verify version persists in second instance
@@ -240,22 +244,25 @@ def test_migrate_from_zero_to_one(self, migration_manager):
240244

241245
def test_migrate_same_version_is_noop(self, migration_manager):
242246
"""Test that migrating to current version is no-op."""
243-
migration_manager.set_schema_version(3)
247+
with pytest.deprecated_call():
248+
migration_manager.set_schema_version(3)
244249

245250
# Should not raise error and version should remain unchanged
246251
migration_manager.migrate_to_version(3)
247252
assert migration_manager.get_schema_version() == 3
248253

249254
def test_migrate_to_lower_version_fails(self, migration_manager):
250255
"""Test that downgrading version is not allowed."""
251-
migration_manager.set_schema_version(5)
256+
with pytest.deprecated_call():
257+
migration_manager.set_schema_version(5)
252258

253259
with pytest.raises(ValueError, match="Cannot downgrade from version 5 to 3"):
254260
migration_manager.migrate_to_version(3)
255261

256262
def test_unsupported_migration_path_fails(self, migration_manager):
257263
"""Test that unsupported migration paths raise errors."""
258-
migration_manager.set_schema_version(2)
264+
with pytest.deprecated_call():
265+
migration_manager.set_schema_version(2)
259266

260267
with pytest.raises(RuntimeError, match="Migration from version 2 to 5 not supported"):
261268
migration_manager.migrate_to_version(5)
@@ -272,7 +279,8 @@ def test_ensure_current_schema_migrates_when_needed(self, migration_manager):
272279

273280
def test_ensure_current_schema_handles_newer_version(self, migration_manager):
274281
"""Test warning when database is newer than expected."""
275-
migration_manager.set_schema_version(5)
282+
with pytest.deprecated_call():
283+
migration_manager.set_schema_version(5)
276284

277285
# Should log warning but not fail
278286
with patch('openspace.skill_engine.migration_manager.logger') as mock_logger:
@@ -340,17 +348,22 @@ def test_operations_fail_when_closed(self, migration_manager):
340348
"""Test that operations fail after manager is closed."""
341349
migration_manager.close()
342350

343-
operations = [
344-
lambda: migration_manager.initialize_schema(),
345-
lambda: migration_manager.get_schema_version(),
346-
lambda: migration_manager.set_schema_version(1),
347-
lambda: migration_manager.migrate_to_version(1),
348-
lambda: migration_manager.ensure_current_schema(1),
349-
]
350-
351-
for operation in operations:
352-
with pytest.raises(RuntimeError, match="MigrationManager is closed"):
353-
operation()
351+
# Test each operation individually
352+
with pytest.raises(RuntimeError, match="MigrationManager is closed"):
353+
migration_manager.initialize_schema()
354+
355+
with pytest.raises(RuntimeError, match="MigrationManager is closed"):
356+
migration_manager.get_schema_version()
357+
358+
with pytest.raises(RuntimeError, match="MigrationManager is closed"):
359+
with pytest.deprecated_call():
360+
migration_manager.set_schema_version(1)
361+
362+
with pytest.raises(RuntimeError, match="MigrationManager is closed"):
363+
migration_manager.migrate_to_version(1)
364+
365+
with pytest.raises(RuntimeError, match="MigrationManager is closed"):
366+
migration_manager.ensure_current_schema(1)
354367

355368

356369
class TestSkillStoreFacadeIntegration:

tests/test_traceback_safety.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def test_correlation_id_in_log_and_response(self):
218218

219219
def _make_openspace_mock():
220220
"""Create a mock OpenSpace instance sufficient for mcp_server imports."""
221-
mock = AsyncMock()
221+
mock = MagicMock()
222222
mock.is_initialized.return_value = True
223223
mock._skill_registry = MagicMock()
224224
mock._skill_evolver = MagicMock()

0 commit comments

Comments
 (0)