test_present_files_tool.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. """Integration tests for PresentFiles tool."""
  2. import importlib
  3. import os
  4. from pathlib import Path
  5. import pytest
  6. from agency_swarm import Agent
  7. from agency_swarm.tools.built_in import PresentFiles
  8. def _expected_mnt_path(source_path: Path, mnt_dir: Path) -> Path:
  9. cwd = Path.cwd().resolve()
  10. resolved = Path(os.path.abspath(source_path.expanduser()))
  11. try:
  12. relative = resolved.relative_to(cwd)
  13. return mnt_dir / relative
  14. except ValueError:
  15. anchor = resolved.anchor.strip("\\/").replace(":", "")
  16. if not anchor:
  17. anchor = "abs"
  18. anchor = anchor.replace("\\", "_").replace("/", "_")
  19. return mnt_dir / anchor / Path(*resolved.parts[1:])
  20. @pytest.fixture
  21. def agent_with_present_files():
  22. """Create an agent with PresentFiles tool."""
  23. return Agent(
  24. name="PresentFilesAgent",
  25. description="Test agent with file preview capability",
  26. instructions="Present files when requested",
  27. tools=[PresentFiles],
  28. )
  29. class TestPresentFilesBasics:
  30. """Test basic file preview functionality."""
  31. def test_moves_common_file_types_to_mnt(self, agent_with_present_files, tmp_path, monkeypatch):
  32. mnt_dir = tmp_path / "mnt"
  33. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  34. sample_files = {
  35. "example.txt": b"sample",
  36. "example.csv": b"col1,col2\n1,2\n",
  37. "example.md": b"# Example\n",
  38. "example.pdf": b"%PDF-1.4\n%%EOF",
  39. "example.docx": b"PK\x03\x04",
  40. "example.png": b"\x89PNG\r\n\x1a\n",
  41. "example.jpg": b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xd9",
  42. "example.jpeg": b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xd9",
  43. "example.gif": b"GIF89a",
  44. "example.svg": b"<?xml version='1.0' encoding='UTF-8'?><svg xmlns='http://www.w3.org/2000/svg'/>",
  45. "example.mp3": b"ID3",
  46. "example.mp4": b"\x00\x00\x00\x18ftypmp42",
  47. "example.wav": b"RIFF",
  48. "example.zip": b"PK\x03\x04",
  49. "example.tar": b"ustar",
  50. "example.pptx": b"PK\x03\x04",
  51. "example.py": b"print('hello')\n",
  52. "example.js": b"console.log('hello');\n",
  53. "example.ts": b"console.log('hello');\n",
  54. }
  55. file_paths = []
  56. expected_sizes = {}
  57. for name, payload in sample_files.items():
  58. sample_file = tmp_path / name
  59. sample_file.write_bytes(payload)
  60. file_paths.append(str(sample_file))
  61. expected_sizes[name] = sample_file.stat().st_size
  62. tool = PresentFiles(files=file_paths)
  63. tool._caller_agent = agent_with_present_files
  64. result = tool.run()
  65. assert result.get("errors") == []
  66. returned_files = result.get("files", [])
  67. assert len(returned_files) == len(sample_files)
  68. returned_names = {entry["name"] for entry in returned_files}
  69. assert returned_names == set(sample_files.keys())
  70. for entry in returned_files:
  71. assert isinstance(entry["mime_type"], str)
  72. assert entry["mime_type"]
  73. assert entry["size_bytes"] == expected_sizes[entry["name"]]
  74. assert Path(entry["path"]).is_relative_to(mnt_dir)
  75. assert Path(entry["path"]).name == entry["name"]
  76. def test_moves_file_to_mnt(self, agent_with_present_files, tmp_path, monkeypatch):
  77. src_file = tmp_path / "report.pdf"
  78. src_file.write_text("%PDF-1.4\n%%EOF")
  79. expected_size = src_file.stat().st_size
  80. mnt_dir = tmp_path / "mnt"
  81. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  82. tool = PresentFiles(files=[str(src_file)])
  83. tool._caller_agent = agent_with_present_files
  84. result = tool.run()
  85. assert isinstance(result, dict)
  86. assert result.get("errors") == []
  87. assert len(result.get("files", [])) == 1
  88. file_entry = result["files"][0]
  89. assert file_entry["name"] == "report.pdf"
  90. assert file_entry["mime_type"] == "application/pdf"
  91. assert file_entry["size_bytes"] == expected_size
  92. dest_path = Path(file_entry["path"])
  93. assert dest_path.exists()
  94. assert dest_path.is_relative_to(mnt_dir)
  95. assert not src_file.exists()
  96. def test_keeps_file_already_in_mnt(self, agent_with_present_files, tmp_path, monkeypatch):
  97. mnt_dir = tmp_path / "mnt"
  98. mnt_dir.mkdir()
  99. existing_file = mnt_dir / "chart.png"
  100. existing_file.write_bytes(b"\x89PNG\r\n\x1a\n")
  101. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  102. tool = PresentFiles(files=[str(existing_file)])
  103. tool._caller_agent = agent_with_present_files
  104. result = tool.run()
  105. assert result.get("errors") == []
  106. assert len(result.get("files", [])) == 1
  107. file_entry = result["files"][0]
  108. assert Path(file_entry["path"]).resolve() == existing_file.resolve()
  109. assert existing_file.exists()
  110. def test_overwrites_existing_file_in_mnt(self, agent_with_present_files, tmp_path, monkeypatch):
  111. mnt_dir = tmp_path / "mnt"
  112. mnt_dir.mkdir()
  113. src_file = tmp_path / "report.pdf"
  114. src_file.write_text("new")
  115. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  116. existing_file = _expected_mnt_path(src_file, mnt_dir)
  117. existing_file.parent.mkdir(parents=True, exist_ok=True)
  118. existing_file.write_text("old")
  119. tool = PresentFiles(files=[str(src_file)])
  120. tool._caller_agent = agent_with_present_files
  121. result = tool.run()
  122. assert result.get("errors") == []
  123. assert len(result.get("files", [])) == 1
  124. file_entry = result["files"][0]
  125. assert Path(file_entry["path"]).resolve() == existing_file.resolve()
  126. assert existing_file.read_text() == "new"
  127. assert not src_file.exists()
  128. def test_preserves_structure_for_same_basename(self, agent_with_present_files, tmp_path, monkeypatch):
  129. mnt_dir = tmp_path / "mnt"
  130. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  131. dir_a = tmp_path / "a"
  132. dir_b = tmp_path / "b"
  133. dir_a.mkdir()
  134. dir_b.mkdir()
  135. file_a = dir_a / "report.pdf"
  136. file_b = dir_b / "report.pdf"
  137. file_a.write_text("alpha")
  138. file_b.write_text("beta")
  139. tool = PresentFiles(files=[str(file_a), str(file_b)])
  140. tool._caller_agent = agent_with_present_files
  141. result = tool.run()
  142. assert result.get("errors") == []
  143. returned_files = result.get("files", [])
  144. assert len(returned_files) == 2
  145. paths = {Path(entry["path"]).resolve() for entry in returned_files}
  146. assert len(paths) == 2
  147. assert _expected_mnt_path(file_a, mnt_dir) in paths
  148. assert _expected_mnt_path(file_b, mnt_dir) in paths
  149. def test_moves_symlink_without_moving_target(self, agent_with_present_files, tmp_path, monkeypatch):
  150. mnt_dir = tmp_path / "mnt"
  151. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  152. target_dir = tmp_path / "targets"
  153. target_dir.mkdir()
  154. target_file = target_dir / "report.pdf"
  155. target_file.write_text("linked content")
  156. links_dir = tmp_path / "links"
  157. links_dir.mkdir()
  158. symlink_file = links_dir / "report-link.pdf"
  159. try:
  160. symlink_file.symlink_to(target_file)
  161. except OSError as exc:
  162. pytest.skip(f"Symlink creation is not supported in this environment: {exc}")
  163. tool = PresentFiles(files=[str(symlink_file)])
  164. tool._caller_agent = agent_with_present_files
  165. result = tool.run()
  166. assert result.get("errors") == []
  167. assert len(result.get("files", [])) == 1
  168. returned_path = Path(result["files"][0]["path"])
  169. expected_path = _expected_mnt_path(symlink_file, mnt_dir)
  170. assert returned_path == expected_path
  171. assert returned_path.is_symlink()
  172. assert returned_path.resolve() == target_file.resolve()
  173. assert target_file.exists()
  174. assert not symlink_file.exists()
  175. def test_rewrites_relative_symlink_target_after_move(self, agent_with_present_files, tmp_path, monkeypatch):
  176. mnt_dir = tmp_path / "mnt"
  177. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  178. target_dir = tmp_path / "targets"
  179. target_dir.mkdir()
  180. target_file = target_dir / "report.pdf"
  181. target_file.write_text("linked content")
  182. links_dir = tmp_path / "links"
  183. links_dir.mkdir()
  184. symlink_file = links_dir / "report-link.pdf"
  185. relative_target = os.path.relpath(target_file, start=links_dir)
  186. try:
  187. symlink_file.symlink_to(relative_target)
  188. except OSError as exc:
  189. pytest.skip(f"Symlink creation is not supported in this environment: {exc}")
  190. tool = PresentFiles(files=[str(symlink_file)])
  191. tool._caller_agent = agent_with_present_files
  192. result = tool.run()
  193. assert result.get("errors") == []
  194. assert len(result.get("files", [])) == 1
  195. returned_path = Path(result["files"][0]["path"])
  196. assert returned_path.is_symlink()
  197. assert returned_path.resolve() == target_file.resolve()
  198. assert returned_path.read_text() == "linked content"
  199. def test_unknown_extension_uses_octet_stream(self, agent_with_present_files, tmp_path, monkeypatch):
  200. mnt_dir = tmp_path / "mnt"
  201. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  202. src_file = tmp_path / "artifact.unknownext"
  203. src_file.write_text("payload")
  204. tool = PresentFiles(files=[str(src_file)])
  205. tool._caller_agent = agent_with_present_files
  206. result = tool.run()
  207. assert result.get("errors") == []
  208. assert len(result.get("files", [])) == 1
  209. file_entry = result["files"][0]
  210. assert file_entry["mime_type"] == "application/octet-stream"
  211. class TestPresentFilesErrorHandling:
  212. """Test error handling and validations."""
  213. def test_directory_path_reports_error(self, agent_with_present_files, tmp_path, monkeypatch):
  214. mnt_dir = tmp_path / "mnt"
  215. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  216. tool = PresentFiles(files=[str(tmp_path)])
  217. tool._caller_agent = agent_with_present_files
  218. result = tool.run()
  219. assert result.get("files") == []
  220. assert len(result.get("errors", [])) == 1
  221. assert "directory" in result["errors"][0].lower()
  222. def test_missing_file_reports_error(self, agent_with_present_files, tmp_path, monkeypatch):
  223. missing_file = tmp_path / "missing.txt"
  224. mnt_dir = tmp_path / "mnt"
  225. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  226. tool = PresentFiles(files=[str(missing_file)])
  227. tool._caller_agent = agent_with_present_files
  228. result = tool.run()
  229. assert result.get("files") == []
  230. assert len(result.get("errors", [])) == 1
  231. assert "not found" in result["errors"][0].lower()
  232. def test_rejects_large_file(self, agent_with_present_files, tmp_path, monkeypatch):
  233. large_file = tmp_path / "large.bin"
  234. large_file.write_bytes(b"x" * 20)
  235. mnt_dir = tmp_path / "mnt"
  236. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  237. monkeypatch.setenv("FILE_PREVIEW_MAX_BYTES", "10")
  238. tool = PresentFiles(files=[str(large_file)])
  239. tool._caller_agent = agent_with_present_files
  240. result = tool.run()
  241. assert result.get("files") == []
  242. assert len(result.get("errors", [])) == 1
  243. assert "exceeds" in result["errors"][0].lower()
  244. moved_file = _expected_mnt_path(large_file, mnt_dir)
  245. assert moved_file.exists()
  246. assert not large_file.exists()
  247. def test_rejects_large_file_already_in_mnt(self, agent_with_present_files, tmp_path, monkeypatch):
  248. mnt_dir = tmp_path / "mnt"
  249. mnt_dir.mkdir()
  250. large_file = mnt_dir / "large.bin"
  251. large_file.write_bytes(b"x" * 20)
  252. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  253. monkeypatch.setenv("FILE_PREVIEW_MAX_BYTES", "10")
  254. tool = PresentFiles(files=[str(large_file)])
  255. tool._caller_agent = agent_with_present_files
  256. result = tool.run()
  257. assert result.get("files") == []
  258. assert len(result.get("errors", [])) == 1
  259. assert "exceeds" in result["errors"][0].lower()
  260. assert large_file.exists()
  261. def test_move_failure_does_not_delete_existing_destination(self, agent_with_present_files, tmp_path, monkeypatch):
  262. mnt_dir = tmp_path / "mnt"
  263. mnt_dir.mkdir()
  264. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  265. src_file = tmp_path / "report.pdf"
  266. src_file.write_text("new")
  267. destination = _expected_mnt_path(src_file, mnt_dir)
  268. destination.parent.mkdir(parents=True, exist_ok=True)
  269. destination.write_text("old")
  270. def _always_fail_move(source: str, destination_path: str):
  271. raise OSError("simulated move failure")
  272. present_files_module = importlib.import_module("agency_swarm.tools.built_in.PresentFiles")
  273. monkeypatch.setattr(present_files_module.shutil, "move", _always_fail_move)
  274. tool = PresentFiles(files=[str(src_file)])
  275. tool._caller_agent = agent_with_present_files
  276. result = tool.run()
  277. assert result.get("files") == []
  278. assert len(result.get("errors", [])) == 1
  279. assert "unable to move file to mnt directory" in result["errors"][0].lower()
  280. assert destination.exists()
  281. assert destination.read_text() == "old"
  282. assert src_file.exists()
  283. def test_dangling_symlink_destination_respects_overwrite_false(
  284. self, agent_with_present_files, tmp_path, monkeypatch
  285. ):
  286. mnt_dir = tmp_path / "mnt"
  287. mnt_dir.mkdir()
  288. monkeypatch.setenv("MNT_DIR", str(mnt_dir))
  289. src_file = tmp_path / "report.pdf"
  290. src_file.write_text("new")
  291. destination = _expected_mnt_path(src_file, mnt_dir)
  292. destination.parent.mkdir(parents=True, exist_ok=True)
  293. try:
  294. destination.symlink_to(destination.parent / "missing-target.pdf")
  295. except OSError as exc:
  296. pytest.skip(f"Symlink creation is not supported in this environment: {exc}")
  297. tool = PresentFiles(files=[str(src_file)], overwrite=False)
  298. tool._caller_agent = agent_with_present_files
  299. result = tool.run()
  300. assert result.get("files") == []
  301. assert len(result.get("errors", [])) == 1
  302. assert "destination already exists and overwrite is disabled" in result["errors"][0].lower()
  303. assert destination.is_symlink()
  304. assert src_file.exists()