Files
host ce7fb3afd6 feat: project directory tree + first-round snapshot
- cloud/tree.py: structured worktree tree (dirs first, ignore
  .git/node_modules, symlinks not followed, entry/depth
  truncation flags, text render for injection)
- GET /tasks/{id}/tree with /files auth + worktree resolution,
  max_depth 1..5, max_entries 1..500, escape 400, missing 404,
  cloud/cross-host 409
- MCP get_task_tree translating to the new endpoint
- worker first-round injection of a repo-checkout top-level
  snapshot (2 layers, 120 entries) after repo resolution,
  labelled as checkout snapshot not worktree; skipped for
  coordinator/resume/fork, silent when no host source
- docs/project-tree.md; tests for tree, endpoint, injection,
  MCP translation
2026-10-30 14:46:11 +08:00

112 lines
4.4 KiB
Python

"""cloud/tree.py 单测:结构、忽略、符号链接、截断、渲染。
不联网、不跑模型;只建临时目录。
"""
import os
import sys
import tempfile
import unittest
from pathlib import Path
try:
from cloud.tree import build_tree, render_tree_text
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.tree import build_tree, render_tree_text
class TreeTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
(self.root / "pkg").mkdir()
(self.root / "pkg" / "deep").mkdir()
(self.root / "pkg" / "mod.py").write_text("x", encoding="utf-8")
(self.root / "pkg" / "deep" / "leaf.txt").write_text("y",
encoding="utf-8")
(self.root / "README.md").write_text("# hi\n", encoding="utf-8")
(self.root / ".git").mkdir()
(self.root / ".git" / "config").write_text("x", encoding="utf-8")
(self.root / "node_modules").mkdir()
(self.root / "node_modules" / "pkg").mkdir()
os.symlink("/etc/passwd", self.root / "esc")
def tearDown(self):
self.tmp.cleanup()
def _names(self, tree):
return [(e["name"], e["type"]) for e in tree["entries"]]
def test_root_structure_dirs_first_and_ignore(self):
tree = build_tree(str(self.root))
# 目录在前、同类按名:pkg(目录)在文件之前;.git 与
# node_modules 一律不出现;符号链接列出但不跟进。
self.assertEqual(self._names(tree),
[("pkg", "dir"), ("esc", "symlink"),
("README.md", "file")])
self.assertFalse(tree["truncated"])
pkg = tree["entries"][0]
self.assertEqual([c["name"] for c in pkg["children"]],
["deep", "mod.py"])
self.assertEqual(pkg["children"][0]["children"][0]["name"],
"leaf.txt")
def test_max_depth_cuts_children_with_flag(self):
tree = build_tree(str(self.root), max_depth=1)
pkg = tree["entries"][0]
self.assertEqual(pkg["children"], [])
self.assertTrue(pkg["children_truncated"])
tree2 = build_tree(str(self.root), max_depth=2)
deep = tree2["entries"][0]["children"][0]
self.assertEqual(deep["children"], [])
self.assertTrue(deep["children_truncated"])
def test_max_entries_truncates_deterministically(self):
def total(entries):
return sum(1 + total(e.get("children") or [])
for e in entries)
tree = build_tree(str(self.root), max_entries=2)
self.assertTrue(tree["truncated"])
# 条数按整棵树计(含子层):pkg + deep = 2 条即停,
# pkg 的 children 因此半截且 children_truncated=True。
self.assertEqual(total(tree["entries"]), 2)
self.assertTrue(tree["entries"][0]["children_truncated"])
# 再次跑同一输入,结果必须相同(截断不可随机)。
again = build_tree(str(self.root), max_entries=2)
self.assertEqual(tree, again)
def test_subdir_and_size(self):
tree = build_tree(str(self.root), path="pkg")
self.assertEqual(tree["path"], "pkg")
mod = [e for e in tree["entries"] if e["name"] == "mod.py"][0]
self.assertEqual(mod["path"], "pkg/mod.py")
self.assertEqual(mod["size"], 1)
def test_escape_file_and_missing(self):
with self.assertRaises(ValueError):
build_tree(str(self.root), path="../outside")
with self.assertRaises(ValueError):
build_tree(str(self.root), path="/etc/passwd")
with self.assertRaises(FileNotFoundError):
build_tree(str(self.root), path="nope")
with self.assertRaises(NotADirectoryError):
build_tree(str(self.root), path="README.md")
# 指向 worktree 外的符号链接作 path 时同样逃逸。
with self.assertRaises(ValueError):
build_tree(str(self.root), path="esc")
def test_render_text(self):
tree = build_tree(str(self.root), max_depth=2)
text = render_tree_text(tree)
self.assertIn("pkg/", text)
self.assertIn("mod.py", text)
self.assertIn("esc@", text)
self.assertNotIn(".git", text)
self.assertIn("…", text) # max_depth=2 时 deep 的子层被截断
if __name__ == "__main__":
unittest.main()