2121
2222import pytest
2323
24+ import specsmith .mcp_server as mcp_mod
25+
2426
2527# ---------------------------------------------------------------------------
2628# Autouse: suppress auto-update noise
@@ -48,9 +50,7 @@ def _make_rpc(method: str, params: dict[str, Any] | None = None, req_id: int = 1
4850
4951def _send_rpc (msg : dict [str , Any ]) -> dict [str , Any ]:
5052 """Feed one JSON-RPC message to _handle_request and return its response."""
51- from specsmith .mcp_server import _handle_request
52-
53- resp = _handle_request (msg )
53+ resp = mcp_mod ._handle_request (msg )
5454 assert resp is not None , "Expected a response but got None"
5555 return resp
5656
@@ -100,11 +100,9 @@ def test_each_tool_has_input_schema(self) -> None:
100100 assert tool ["inputSchema" ]["type" ] == "object"
101101
102102 def test_notification_returns_none (self ) -> None :
103- from specsmith .mcp_server import _handle_request
104-
105103 # Notifications have no id — no response expected
106104 msg = {"jsonrpc" : "2.0" , "method" : "notifications/initialized" , "params" : {}}
107- assert _handle_request (msg ) is None
105+ assert mcp_mod . _handle_request (msg ) is None
108106
109107 def test_unknown_method_returns_error (self ) -> None :
110108 resp = _send_rpc (_make_rpc ("no_such_method" , {}, req_id = 99 ))
@@ -126,10 +124,10 @@ def test_ping_returns_ok(self) -> None:
126124 assert resp ["result" ] == {}
127125
128126 def test_parse_error_on_bad_json (self ) -> None :
129- from specsmith .mcp_server import _handle_request
130-
131127 # _handle_request only processes dicts; parse errors are handled in run_server
132- resp = _handle_request ({"jsonrpc" : "2.0" , "method" : "initialize" , "id" : 10 , "params" : {}})
128+ resp = mcp_mod ._handle_request (
129+ {"jsonrpc" : "2.0" , "method" : "initialize" , "id" : 10 , "params" : {}}
130+ )
133131 assert resp is not None
134132 assert "result" in resp
135133
@@ -141,15 +139,11 @@ def test_parse_error_on_bad_json(self) -> None:
141139
142140class TestGovernanceAudit :
143141 def test_returns_healthy_key (self , tmp_path : Path ) -> None :
144- from specsmith .mcp_server import _handle_governance_audit
145-
146- result = _handle_governance_audit ({"project_dir" : str (tmp_path )})
142+ result = mcp_mod ._handle_governance_audit ({"project_dir" : str (tmp_path )})
147143 assert "healthy" in result
148144
149145 def test_returns_checks_list (self , tmp_path : Path ) -> None :
150- from specsmith .mcp_server import _handle_governance_audit
151-
152- result = _handle_governance_audit ({"project_dir" : str (tmp_path )})
146+ result = mcp_mod ._handle_governance_audit ({"project_dir" : str (tmp_path )})
153147 assert isinstance (result .get ("checks" ), list )
154148
155149 def test_via_tools_call (self , tmp_path : Path ) -> None :
@@ -167,16 +161,12 @@ def test_via_tools_call(self, tmp_path: Path) -> None:
167161
168162class TestGovernanceCheckpoint :
169163 def test_returns_anchor_key (self , tmp_path : Path ) -> None :
170- from specsmith .mcp_server import _handle_governance_checkpoint
171-
172- result = _handle_governance_checkpoint ({"project_dir" : str (tmp_path )})
164+ result = mcp_mod ._handle_governance_checkpoint ({"project_dir" : str (tmp_path )})
173165 assert "anchor" in result
174166 assert result ["anchor" ].startswith ("SPECSMITH-ANCHOR-" )
175167
176168 def test_returns_phase_and_health (self , tmp_path : Path ) -> None :
177- from specsmith .mcp_server import _handle_governance_checkpoint
178-
179- result = _handle_governance_checkpoint ({"project_dir" : str (tmp_path )})
169+ result = mcp_mod ._handle_governance_checkpoint ({"project_dir" : str (tmp_path )})
180170 assert "health" in result
181171 assert "phase" in result
182172 assert "req_count" in result
@@ -196,15 +186,11 @@ def test_via_tools_call(self, tmp_path: Path) -> None:
196186
197187class TestGovernancePreflight :
198188 def test_empty_intent_returns_rejected (self , tmp_path : Path ) -> None :
199- from specsmith .mcp_server import _handle_governance_preflight
200-
201- result = _handle_governance_preflight ({"intent" : "" , "project_dir" : str (tmp_path )})
189+ result = mcp_mod ._handle_governance_preflight ({"intent" : "" , "project_dir" : str (tmp_path )})
202190 assert result ["decision" ] == "rejected"
203191
204192 def test_returns_decision_key (self , tmp_path : Path ) -> None :
205- from specsmith .mcp_server import _handle_governance_preflight
206-
207- result = _handle_governance_preflight (
193+ result = mcp_mod ._handle_governance_preflight (
208194 {
209195 "intent" : "read governance health status" ,
210196 "project_dir" : str (tmp_path ),
@@ -233,9 +219,7 @@ def test_via_tools_call(self, tmp_path: Path) -> None:
233219
234220class TestGovernancePhase :
235221 def test_returns_phase_key (self , tmp_path : Path ) -> None :
236- from specsmith .mcp_server import _handle_governance_phase
237-
238- result = _handle_governance_phase ({"project_dir" : str (tmp_path )})
222+ result = mcp_mod ._handle_governance_phase ({"project_dir" : str (tmp_path )})
239223 assert "phase" in result
240224
241225 def test_via_tools_call (self , tmp_path : Path ) -> None :
@@ -253,9 +237,7 @@ def test_via_tools_call(self, tmp_path: Path) -> None:
253237
254238class TestGovernanceReqList :
255239 def test_missing_requirements_json_returns_error (self , tmp_path : Path ) -> None :
256- from specsmith .mcp_server import _handle_governance_req_list
257-
258- result = _handle_governance_req_list ({"project_dir" : str (tmp_path )})
240+ result = mcp_mod ._handle_governance_req_list ({"project_dir" : str (tmp_path )})
259241 assert "error" in result
260242
261243 def test_with_requirements_json (self , tmp_path : Path ) -> None :
@@ -270,9 +252,7 @@ def test_with_requirements_json(self, tmp_path: Path) -> None:
270252 json .dumps ([{"id" : "TEST-001" , "covers" : "REQ-001" }]), encoding = "utf-8"
271253 )
272254
273- from specsmith .mcp_server import _handle_governance_req_list
274-
275- result = _handle_governance_req_list ({"project_dir" : str (tmp_path )})
255+ result = mcp_mod ._handle_governance_req_list ({"project_dir" : str (tmp_path )})
276256 assert result ["total" ] == 2
277257 assert result ["covered" ] == 1
278258 req_ids = [r ["id" ] for r in result ["reqs" ]]
@@ -290,9 +270,7 @@ def test_status_filter(self, tmp_path: Path) -> None:
290270 ]
291271 (spec_dir / "requirements.json" ).write_text (json .dumps (reqs ), encoding = "utf-8" )
292272
293- from specsmith .mcp_server import _handle_governance_req_list
294-
295- result = _handle_governance_req_list (
273+ result = mcp_mod ._handle_governance_req_list (
296274 {
297275 "project_dir" : str (tmp_path ),
298276 "status_filter" : "planned" ,
@@ -318,68 +296,56 @@ class TestRegistryFunctions:
318296 def test_read_registry_missing_file_returns_empty ( # noqa: E501
319297 self , tmp_path : Path , monkeypatch : pytest .MonkeyPatch
320298 ) -> None :
321- import specsmith .mcp_server as _mod
322-
323299 monkeypatch .setenv ("SPECSMITH_HOME" , str (tmp_path ))
324300 # No file created — should return empty list
325- result = _mod .read_registry ()
301+ result = mcp_mod .read_registry ()
326302 assert result == []
327303
328304 def test_write_then_read_roundtrip (
329305 self , tmp_path : Path , monkeypatch : pytest .MonkeyPatch
330306 ) -> None :
331- import specsmith .mcp_server as _mod
332-
333307 monkeypatch .setenv ("SPECSMITH_HOME" , str (tmp_path ))
334308 paths = ["/path/to/proj1" , "/path/to/proj2" ]
335- _mod .write_registry (paths )
336- assert _mod .read_registry () == paths
309+ mcp_mod .write_registry (paths )
310+ assert mcp_mod .read_registry () == paths
337311
338312 def test_register_project_adds_new_entry (
339313 self , tmp_path : Path , monkeypatch : pytest .MonkeyPatch
340314 ) -> None :
341- import specsmith .mcp_server as _mod
342-
343315 monkeypatch .setenv ("SPECSMITH_HOME" , str (tmp_path ))
344316 proj = tmp_path / "myproject"
345317 proj .mkdir ()
346- added = _mod .register_project (str (proj ))
318+ added = mcp_mod .register_project (str (proj ))
347319 assert added is True
348- assert str (proj ) in _mod .read_registry ()
320+ assert str (proj ) in mcp_mod .read_registry ()
349321
350322 def test_register_project_idempotent (
351323 self , tmp_path : Path , monkeypatch : pytest .MonkeyPatch
352324 ) -> None :
353- import specsmith .mcp_server as _mod
354-
355325 monkeypatch .setenv ("SPECSMITH_HOME" , str (tmp_path ))
356326 proj = tmp_path / "myproject"
357327 proj .mkdir ()
358- _mod .register_project (str (proj ))
359- added_again = _mod .register_project (str (proj ))
328+ mcp_mod .register_project (str (proj ))
329+ added_again = mcp_mod .register_project (str (proj ))
360330 assert added_again is False
361- assert _mod .read_registry ().count (str (proj )) == 1
331+ assert mcp_mod .read_registry ().count (str (proj )) == 1
362332
363333 def test_unregister_project_removes_entry (
364334 self , tmp_path : Path , monkeypatch : pytest .MonkeyPatch
365335 ) -> None :
366- import specsmith .mcp_server as _mod
367-
368336 monkeypatch .setenv ("SPECSMITH_HOME" , str (tmp_path ))
369337 proj = tmp_path / "myproject"
370338 proj .mkdir ()
371- _mod .register_project (str (proj ))
372- removed = _mod .unregister_project (str (proj ))
339+ mcp_mod .register_project (str (proj ))
340+ removed = mcp_mod .unregister_project (str (proj ))
373341 assert removed is True
374- assert str (proj ) not in _mod .read_registry ()
342+ assert str (proj ) not in mcp_mod .read_registry ()
375343
376344 def test_unregister_project_returns_false_if_not_registered (
377345 self , tmp_path : Path , monkeypatch : pytest .MonkeyPatch
378346 ) -> None :
379- import specsmith .mcp_server as _mod
380-
381347 monkeypatch .setenv ("SPECSMITH_HOME" , str (tmp_path ))
382- result = _mod .unregister_project ("/nonexistent/path" )
348+ result = mcp_mod .unregister_project ("/nonexistent/path" )
383349 assert result is False
384350
385351 def test_registry_based_server_startup (
@@ -388,50 +354,40 @@ def test_registry_based_server_startup(
388354 """run_server with no args should pick up projects from the registry."""
389355 import io
390356
391- import specsmith .mcp_server as _mod
392- from specsmith .mcp_server import run_server
393-
394357 monkeypatch .setenv ("SPECSMITH_HOME" , str (tmp_path ))
395358 proj_a = tmp_path / "alpha"
396359 proj_a .mkdir ()
397360 proj_b = tmp_path / "beta"
398361 proj_b .mkdir ()
399- _mod .register_project (str (proj_a ))
400- _mod .register_project (str (proj_b ))
362+ mcp_mod .register_project (str (proj_a ))
363+ mcp_mod .register_project (str (proj_b ))
401364
402365 old_stdin = sys .stdin
403366 sys .stdin = io .StringIO ("" )
404367 try :
405- run_server () # no project_dir arg — should use registry
368+ mcp_mod . run_server () # no project_dir arg — should use registry
406369 finally :
407370 sys .stdin = old_stdin
408371
409372 # First registered project is the default
410- assert str (proj_a ) == _mod ._DEFAULT_PROJECT_DIR
411- assert str (proj_b ) in _mod ._REGISTERED_PROJECTS
373+ assert str (proj_a ) == mcp_mod ._DEFAULT_PROJECT_DIR
374+ assert str (proj_b ) in mcp_mod ._REGISTERED_PROJECTS
412375
413376
414377class TestGovernanceProjectList :
415378 def test_returns_default_project (self , tmp_path : Path ) -> None :
416- from specsmith .mcp_server import _handle_governance_project_list , run_server
417-
418379 # Prime server state with a known directory
419- run_server . __func__ if hasattr ( run_server , "__func__" ) else run_server # noqa: B018
420- import specsmith . mcp_server as _mod
380+ mcp_mod . _DEFAULT_PROJECT_DIR = str ( tmp_path )
381+ mcp_mod . _REGISTERED_PROJECTS = [ str ( tmp_path )]
421382
422- _mod ._DEFAULT_PROJECT_DIR = str (tmp_path )
423- _mod ._REGISTERED_PROJECTS = [str (tmp_path )]
424-
425- result = _handle_governance_project_list ({})
383+ result = mcp_mod ._handle_governance_project_list ({})
426384 assert result ["default_project" ] == str (tmp_path )
427385 assert str (tmp_path ) in result ["projects" ]
428386 assert result ["count" ] >= 1
429387
430388 def test_via_tools_call (self , tmp_path : Path ) -> None :
431- import specsmith .mcp_server as _mod
432-
433- _mod ._DEFAULT_PROJECT_DIR = str (tmp_path )
434- _mod ._REGISTERED_PROJECTS = [str (tmp_path )]
389+ mcp_mod ._DEFAULT_PROJECT_DIR = str (tmp_path )
390+ mcp_mod ._REGISTERED_PROJECTS = [str (tmp_path )]
435391
436392 resp = _send_rpc (
437393 _make_rpc (
@@ -449,36 +405,31 @@ def test_extra_projects_registered(self, tmp_path: Path) -> None:
449405 """run_server registers extra_project_dirs and they appear in project_list."""
450406 import io
451407
452- import specsmith .mcp_server as _mod
453- from specsmith .mcp_server import run_server
454-
455408 extra = tmp_path / "extra"
456409 extra .mkdir ()
457410
458411 # Patch stdin to EOF immediately so run_server exits
459412 old_stdin = sys .stdin
460413 sys .stdin = io .StringIO ("" )
461414 try :
462- run_server (project_dir = str (tmp_path ), extra_project_dirs = [str (extra )])
415+ mcp_mod . run_server (project_dir = str (tmp_path ), extra_project_dirs = [str (extra )])
463416 finally :
464417 sys .stdin = old_stdin
465418
466- assert str (tmp_path .resolve ()) == _mod ._DEFAULT_PROJECT_DIR
467- assert str (extra .resolve ()) in _mod ._REGISTERED_PROJECTS
468- assert _mod ._REGISTERED_PROJECTS [0 ] == str (tmp_path .resolve ())
419+ assert str (tmp_path .resolve ()) == mcp_mod ._DEFAULT_PROJECT_DIR
420+ assert str (extra .resolve ()) in mcp_mod ._REGISTERED_PROJECTS
421+ assert mcp_mod ._REGISTERED_PROJECTS [0 ] == str (tmp_path .resolve ())
469422
470423 def test_no_chdir_called (self , tmp_path : Path , monkeypatch : pytest .MonkeyPatch ) -> None :
471424 """run_server must NOT call os.chdir — paths are resolved absolutely."""
472425 import io
473426
474- from specsmith .mcp_server import run_server
475-
476427 chdir_calls : list [str ] = []
477428 monkeypatch .setattr ("os.chdir" , lambda p : chdir_calls .append (str (p )))
478429 old_stdin = sys .stdin
479430 sys .stdin = io .StringIO ("" )
480431 try :
481- run_server (project_dir = str (tmp_path ))
432+ mcp_mod . run_server (project_dir = str (tmp_path ))
482433 finally :
483434 sys .stdin = old_stdin
484435
@@ -487,9 +438,7 @@ def test_no_chdir_called(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch)
487438
488439class TestGovernanceTraceSeal :
489440 def test_missing_description_returns_error (self , tmp_path : Path ) -> None :
490- from specsmith .mcp_server import _handle_governance_trace_seal
491-
492- result = _handle_governance_trace_seal (
441+ result = mcp_mod ._handle_governance_trace_seal (
493442 {
494443 "seal_type" : "milestone" ,
495444 "description" : "" ,
@@ -503,9 +452,7 @@ def test_seals_milestone(self, tmp_path: Path) -> None:
503452 spec_dir = tmp_path / ".specsmith"
504453 spec_dir .mkdir ()
505454
506- from specsmith .mcp_server import _handle_governance_trace_seal
507-
508- result = _handle_governance_trace_seal (
455+ result = mcp_mod ._handle_governance_trace_seal (
509456 {
510457 "seal_type" : "milestone" ,
511458 "description" : "test seal for pytest" ,
@@ -520,12 +467,10 @@ def test_second_seal_chains_to_first(self, tmp_path: Path) -> None:
520467 spec_dir = tmp_path / ".specsmith"
521468 spec_dir .mkdir ()
522469
523- from specsmith .mcp_server import _handle_governance_trace_seal
524-
525- _handle_governance_trace_seal (
470+ mcp_mod ._handle_governance_trace_seal (
526471 {"seal_type" : "decision" , "description" : "first" , "project_dir" : str (tmp_path )}
527472 )
528- result2 = _handle_governance_trace_seal (
473+ result2 = mcp_mod . _handle_governance_trace_seal (
529474 {"seal_type" : "milestone" , "description" : "second" , "project_dir" : str (tmp_path )}
530475 )
531476 assert result2 ["seal_id" ] == "SEAL-0002"
@@ -586,8 +531,9 @@ def test_initialize_via_subprocess(self) -> None:
586531 timeout = 15 ,
587532 env = env ,
588533 )
589- except subprocess .TimeoutExpired :
534+ except subprocess .TimeoutExpired : # pragma: no cover
590535 pytest .skip ("mcp serve subprocess timed out (slow CI env)" )
536+ return # never reached; satisfies flow analysis
591537
592538 assert proc .stdout , f"No output from mcp serve (stderr: { proc .stderr [:500 ]} )"
593539 response = json .loads (proc .stdout .strip ().splitlines ()[0 ])
@@ -623,8 +569,9 @@ def test_tools_list_via_subprocess(self) -> None:
623569 timeout = 15 ,
624570 env = env ,
625571 )
626- except subprocess .TimeoutExpired :
572+ except subprocess .TimeoutExpired : # pragma: no cover
627573 pytest .skip ("mcp serve subprocess timed out (slow CI env)" )
574+ return # never reached; satisfies flow analysis
628575
629576 lines = [ln for ln in proc .stdout .strip ().splitlines () if ln .strip ()]
630577 assert len (lines ) >= 2 , f"Expected ≥2 responses, got: { lines } "
0 commit comments