- 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
112 lines
4.4 KiB
Python
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()
|