@@ -770,3 +770,49 @@ async def test_ordinary_range_is_unaffected(self, server, mcp_repo):
770770 {"repo_path" : str (mcp_repo .path ), "diff_range" : "HEAD~1..HEAD" },
771771 )
772772 assert "calc.py" in _get_text (result )
773+
774+
775+ class TestFileContextHonoursTheIgnoreContract :
776+ """`get_file_context` accepts `**/*`, so it is the widest read surface the
777+ MCP server exposes. It used to glob without the ignore specs that
778+ `get_tree_map` and diff mode both apply, which made `.diffctx/ignore` — a
779+ security contract per QA.md — advisory for anyone who asked directly."""
780+
781+ @staticmethod
782+ def _repo (tmp_path ):
783+ root = tmp_path / "ignored_repo"
784+ (root / ".diffctx" ).mkdir (parents = True )
785+ (root / ".diffctx" / "ignore" ).write_text ("secrets/\n *.key\n " )
786+ (root / "secrets" ).mkdir ()
787+ (root / "secrets" / "prod.env" ).write_text ("API_TOKEN=must-not-surface\n " )
788+ (root / "deploy.key" ).write_text ("PRIVATE-KEY-must-not-surface\n " )
789+ (root / "app.py" ).write_text ("def run():\n return 1\n " )
790+ return root
791+
792+ @pytest .mark .asyncio
793+ async def test_a_declared_exclusion_is_not_readable_through_a_glob (self , server , tmp_path ):
794+ root = self ._repo (tmp_path )
795+ result = await server .call_tool (
796+ "get_file_context" ,
797+ {"repo_path" : str (root ), "patterns" : ["**/*" ]},
798+ )
799+ text = _get_text (result )
800+
801+ assert "must-not-surface" not in text , "an ignored file's contents were returned"
802+ assert "prod.env" not in text
803+ assert "deploy.key" not in text
804+ assert "app.py" in text , "the fix must not stop ordinary files being read"
805+
806+ @pytest .mark .asyncio
807+ async def test_an_excluded_file_is_not_counted_in_the_truncation_notice (self , server , tmp_path ):
808+ """Reporting "N more files" for withheld files would leak that they
809+ exist, which is most of what the exclusion is protecting."""
810+ root = self ._repo (tmp_path )
811+ result = await server .call_tool (
812+ "get_file_context" ,
813+ {"repo_path" : str (root ), "patterns" : ["**/*" ], "dry_run" : True },
814+ )
815+ text = _get_text (result )
816+
817+ assert "secrets" not in text
818+ assert "deploy.key" not in text
0 commit comments