test_files.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. """Tests for agency_swarm.utils.files module.
  2. Tests the get_external_caller_directory() function which resolves the directory
  3. of the first caller outside the agency_swarm package. This is used to resolve
  4. relative paths like "./instructions.md" or "./tools" against the user's module file.
  5. """
  6. import os
  7. import subprocess
  8. import sys
  9. import textwrap
  10. from pathlib import Path
  11. from agency_swarm.utils.files import _get_package_root, get_external_caller_directory
  12. class TestGetExternalCallerDirectory:
  13. """Tests for get_external_caller_directory()."""
  14. def test_returns_directory_when_called_from_external_file(self, tmp_path: Path):
  15. """When called from user code, returns the directory of that user file."""
  16. # Create a user script that imports and calls get_external_caller_directory
  17. user_dir = tmp_path / "my_agency"
  18. user_dir.mkdir()
  19. user_script = user_dir / "create_agent.py"
  20. user_script.write_text(
  21. textwrap.dedent("""
  22. from agency_swarm.utils.files import get_external_caller_directory
  23. result = get_external_caller_directory()
  24. print(result)
  25. """)
  26. )
  27. # Execute the user script and capture output
  28. result = subprocess.run(
  29. [sys.executable, str(user_script)],
  30. capture_output=True,
  31. text=True,
  32. cwd=str(tmp_path),
  33. )
  34. assert result.returncode == 0, f"Script failed: {result.stderr}"
  35. output_path = result.stdout.strip()
  36. assert output_path == str(user_dir)
  37. def test_agent_instructions_resolve_relative_to_agent_file(self, tmp_path: Path):
  38. """
  39. Real use case: Agent created with instructions="./instructions.md"
  40. should load instructions from the agent's directory, not CWD.
  41. """
  42. # Create directory structure mimicking user's project
  43. agent_dir = tmp_path / "agents" / "ceo"
  44. agent_dir.mkdir(parents=True)
  45. # Create instructions file in agent directory
  46. instructions_file = agent_dir / "instructions.md"
  47. instructions_file.write_text("You are the CEO agent. Lead with vision.")
  48. # Create user's agent file
  49. agent_script = agent_dir / "ceo_agent.py"
  50. agent_script.write_text(
  51. textwrap.dedent("""
  52. import sys
  53. # Add src to path for agency_swarm import
  54. sys.path.insert(0, sys.argv[1])
  55. from agency_swarm import Agent
  56. agent = Agent(
  57. name="CEO",
  58. instructions="./instructions.md",
  59. model="gpt-5.4-mini",
  60. )
  61. print(agent.instructions)
  62. """)
  63. )
  64. # Execute from a DIFFERENT directory (not the agent's directory)
  65. # This proves paths resolve relative to file location, not CWD
  66. src_path = str(Path(__file__).parent.parent.parent / "src")
  67. result = subprocess.run(
  68. [sys.executable, str(agent_script), src_path],
  69. capture_output=True,
  70. text=True,
  71. cwd=str(tmp_path), # CWD is parent, not agent_dir
  72. )
  73. assert result.returncode == 0, f"Script failed: {result.stderr}"
  74. assert "You are the CEO agent" in result.stdout
  75. def test_agent_tools_folder_resolves_relative_to_agent_file(self, tmp_path: Path):
  76. """
  77. Real use case: Agent with tools_folder="./tools" should load tools
  78. from the agent's tools subdirectory.
  79. """
  80. # Create directory structure
  81. agent_dir = tmp_path / "agents" / "researcher"
  82. tools_dir = agent_dir / "tools"
  83. tools_dir.mkdir(parents=True)
  84. # Create a tool file
  85. tool_file = tools_dir / "search_tool.py"
  86. tool_file.write_text(
  87. textwrap.dedent("""
  88. from agents import function_tool
  89. @function_tool
  90. def search_web(query: str) -> str:
  91. \"\"\"Search the web for information.\"\"\"
  92. return f"Results for: {query}"
  93. """)
  94. )
  95. # Create user's agent file
  96. agent_script = agent_dir / "researcher.py"
  97. agent_script.write_text(
  98. textwrap.dedent("""
  99. import sys
  100. sys.path.insert(0, sys.argv[1])
  101. from agency_swarm import Agent
  102. agent = Agent(
  103. name="Researcher",
  104. instructions="Research things",
  105. tools_folder="./tools",
  106. model="gpt-5.4-mini",
  107. )
  108. tool_names = [t.name for t in agent.tools]
  109. print(",".join(tool_names))
  110. """)
  111. )
  112. src_path = str(Path(__file__).parent.parent.parent / "src")
  113. result = subprocess.run(
  114. [sys.executable, str(agent_script), src_path],
  115. capture_output=True,
  116. text=True,
  117. cwd=str(tmp_path), # Different CWD
  118. )
  119. assert result.returncode == 0, f"Script failed: {result.stderr}"
  120. assert "search_web" in result.stdout
  121. def test_agency_shared_instructions_resolve_relative_to_agency_file(self, tmp_path: Path):
  122. """
  123. Real use case: Agency with shared_instructions="agency_manifesto.md"
  124. should load from the agency's directory.
  125. """
  126. agency_dir = tmp_path / "my_agency"
  127. agency_dir.mkdir()
  128. # Create manifesto file
  129. manifesto_file = agency_dir / "agency_manifesto.md"
  130. manifesto_file.write_text("Our mission: Be helpful and accurate.")
  131. # Create agency script
  132. agency_script = agency_dir / "agency.py"
  133. agency_script.write_text(
  134. textwrap.dedent("""
  135. import sys
  136. sys.path.insert(0, sys.argv[1])
  137. from agency_swarm import Agency, Agent
  138. ceo = Agent(name="CEO", instructions="Lead", model="gpt-5.4-mini")
  139. agency = Agency(ceo, shared_instructions="agency_manifesto.md")
  140. print(agency.shared_instructions)
  141. """)
  142. )
  143. src_path = str(Path(__file__).parent.parent.parent / "src")
  144. result = subprocess.run(
  145. [sys.executable, str(agency_script), src_path],
  146. capture_output=True,
  147. text=True,
  148. cwd=str(tmp_path),
  149. )
  150. assert result.returncode == 0, f"Script failed: {result.stderr}"
  151. assert "Our mission:" in result.stdout
  152. def test_fallback_to_cwd_when_no_external_caller(self):
  153. """When no file-backed external caller exists, returns os.getcwd()."""
  154. # This tests the fallback when called from within the package
  155. # or when the stack has no external callers
  156. result = get_external_caller_directory()
  157. # Since this test file IS outside agency_swarm package,
  158. # it should return THIS file's directory
  159. expected = str(Path(__file__).parent)
  160. assert result == expected
  161. def test_fallback_to_cwd_when_package_root_not_found(self):
  162. """When internal_package doesn't exist, returns os.getcwd()."""
  163. result = get_external_caller_directory(internal_package="nonexistent_package_xyz")
  164. assert result == os.getcwd()
  165. class TestGetPackageRoot:
  166. """Tests for _get_package_root() helper function."""
  167. def test_returns_path_for_valid_package(self):
  168. """Returns the package root path for a valid package."""
  169. result = _get_package_root("agency_swarm")
  170. assert result is not None
  171. assert result.name == "agency_swarm"
  172. assert result.is_dir()
  173. def test_returns_none_for_invalid_package(self):
  174. """Returns None for a package that doesn't exist."""
  175. result = _get_package_root("this_package_does_not_exist_xyz")
  176. assert result is None
  177. def test_returns_none_for_builtin_module(self):
  178. """Returns None for built-in modules without __file__."""
  179. # sys is a built-in module that might not have __file__
  180. # We test by providing a package name that imports but has no file
  181. result = _get_package_root("builtins")
  182. assert result is None
  183. def test_caches_results(self):
  184. """Results are cached via lru_cache."""
  185. # Clear the cache first
  186. _get_package_root.cache_clear()
  187. # Call twice
  188. result1 = _get_package_root("agency_swarm")
  189. result2 = _get_package_root("agency_swarm")
  190. # Results should be identical (same object due to cache)
  191. assert result1 is result2
  192. # Check cache info shows hit
  193. cache_info = _get_package_root.cache_info()
  194. assert cache_info.hits >= 1
  195. class TestSpecialFilenameFiltering:
  196. """Tests for filtering special Python filenames like <stdin>, <string>."""
  197. def test_code_from_exec_falls_back_to_cwd(self, tmp_path: Path):
  198. """
  199. Code executed via exec() has filename '<string>' and should be filtered.
  200. The function should skip such frames and find the real caller or fall back.
  201. """
  202. # Create a script that uses exec() to call get_external_caller_directory
  203. test_script = tmp_path / "exec_test.py"
  204. script_content = """\
  205. import sys
  206. sys.path.insert(0, sys.argv[1])
  207. from agency_swarm.utils.files import get_external_caller_directory
  208. # This code will be executed with filename='<string>'
  209. code = '''
  210. result = get_external_caller_directory()
  211. print(result)
  212. '''
  213. exec(code)
  214. """
  215. test_script.write_text(script_content)
  216. src_path = str(Path(__file__).parent.parent.parent / "src")
  217. result = subprocess.run(
  218. [sys.executable, str(test_script), src_path],
  219. capture_output=True,
  220. text=True,
  221. cwd=str(tmp_path),
  222. )
  223. assert result.returncode == 0, f"Script failed: {result.stderr}"
  224. output_path = result.stdout.strip()
  225. # Should return the outer script's directory (tmp_path), not error out
  226. # because the <string> frame is skipped and the outer exec_test.py is found
  227. assert output_path == str(tmp_path)
  228. def test_handles_mixed_stack_with_special_frames(self, tmp_path: Path):
  229. """
  230. When the call stack has both special frames and real file frames,
  231. the function should skip special frames and find the first real external caller.
  232. """
  233. # Create nested structure where inner code uses eval/exec
  234. outer_dir = tmp_path / "outer"
  235. outer_dir.mkdir()
  236. outer_script = outer_dir / "outer.py"
  237. outer_script.write_text(
  238. textwrap.dedent("""
  239. import sys
  240. sys.path.insert(0, sys.argv[1])
  241. def call_via_exec():
  242. from agency_swarm.utils.files import get_external_caller_directory
  243. # This exec adds a <string> frame to the stack
  244. code = "result = get_external_caller_directory(); print(result)"
  245. exec(code)
  246. call_via_exec()
  247. """)
  248. )
  249. src_path = str(Path(__file__).parent.parent.parent / "src")
  250. result = subprocess.run(
  251. [sys.executable, str(outer_script), src_path],
  252. capture_output=True,
  253. text=True,
  254. cwd=str(tmp_path),
  255. )
  256. assert result.returncode == 0, f"Script failed: {result.stderr}"
  257. output_path = result.stdout.strip()
  258. # Should find outer.py as the external caller (skipping <string>)
  259. assert output_path == str(outer_dir)