Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

322 lines
10 KiB
Python

"""
FileManager / FileSystem state accessors.
"""
from __future__ import annotations
import posixpath
from typing import Any
from bench_env.task.base import BaseApp
def _sort_nodes(nodes: list[dict[str, Any]]) -> list[dict[str, Any]]:
return sorted(
nodes,
key=lambda node: (
0 if node["type"] == "directory" else 1,
str(node["name"]),
str(node["path"]),
),
)
class FileSystem:
"""OS-level file system state accessor."""
def __init__(self, fs_state: dict[str, Any], init: dict[str, Any] | None = None):
self._nodes: list[dict[str, Any]] = list(fs_state["nodes"])
self._by_path: dict[str, dict[str, Any]] = {
str(node["path"]): node for node in self._nodes
}
self._children_by_parent_id: dict[str | None, list[dict[str, Any]]] = {}
for node in self._nodes:
self._children_by_parent_id.setdefault(node["parentId"], []).append(node)
self._init_nodes: list[dict[str, Any]] | None = None
self._init_by_path: dict[str, dict[str, Any]] | None = None
if init is not None:
self._init_nodes = list(init["nodes"])
self._init_by_path = {
str(node["path"]): node for node in self._init_nodes
}
@property
def nodes(self) -> list[dict[str, Any]]:
return self._nodes
@property
def init_nodes(self) -> list[dict[str, Any]]:
if self._init_nodes is None:
raise ValueError("No init file system state provided")
return self._init_nodes
@property
def has_init(self) -> bool:
return self._init_nodes is not None
def find_by_path(self, path: str) -> dict[str, Any] | None:
return self._by_path.get(path)
def exists(self, path: str) -> bool:
return path in self._by_path
def list_dir(self, dir_path: str) -> list[dict[str, Any]]:
directory = self.find_by_path(dir_path)
if not directory or directory["type"] != "directory":
return []
return _sort_nodes(
list(self._children_by_parent_id.get(directory["id"], []))
)
def files_in_dir(self, dir_path: str) -> list[dict[str, Any]]:
return [node for node in self.list_dir(dir_path) if node["type"] == "file"]
def find_by_name(self, name: str) -> list[dict[str, Any]]:
return _sort_nodes(
[node for node in self._nodes if str(node["name"]) == name]
)
def find_by_mime(self, mime_prefix: str) -> list[dict[str, Any]]:
return _sort_nodes(
[
node
for node in self._nodes
if node["type"] == "file"
and str(node.get("mimeType") or "").startswith(mime_prefix)
]
)
def check_path_exists(self, path: str, *, field: str) -> dict[str, Any]:
node = self.find_by_path(path)
return {
"field": field,
"expected": {"exists": True, "path": path},
"actual": node,
"passed": node is not None,
}
def check_path_not_exists(self, path: str, *, field: str) -> dict[str, Any]:
node = self.find_by_path(path)
return {
"field": field,
"expected": {"exists": False, "path": path},
"actual": node,
"passed": node is None,
}
def check_directory_created(
self,
path: str,
*,
field: str = "file_system.directory_created",
) -> dict[str, Any]:
"""验证目录是本次操作新增的,而不是初始状态已有。"""
init_node = self._init_by_path.get(path)
curr_node = self.find_by_path(path)
passed = (
init_node is None
and curr_node is not None
and curr_node.get("type") == "directory"
)
return {
"field": field,
"expected": {"created_directory": path},
"actual": curr_node,
"passed": passed,
}
def check_paths_deleted(
self,
paths: list[str],
*,
field: str = "file_system.paths_deleted",
) -> dict[str, Any]:
"""检查 init 中指定路径均已从 current 中消失。"""
assert self._init_by_path is not None, "check_paths_deleted requires init state"
missing_from_init = [
path for path in paths if self._init_by_path.get(path) is None
]
assert not missing_from_init, (
"Upstream bug: delete targets not in init: "
+ ", ".join(missing_from_init)
)
remaining = [path for path in paths if self.find_by_path(path) is not None]
return {
"field": field,
"expected": {"deleted": paths},
"actual": {"remaining": remaining},
"passed": not remaining,
}
def check_paths_preserved(
self,
paths: list[str],
*,
field: str = "file_system.paths_preserved",
) -> dict[str, Any]:
"""检查 init 中指定路径仍在 current 中,防止误删/误移动。"""
assert self._init_by_path is not None, "check_paths_preserved requires init state"
missing_from_init = [
path for path in paths if self._init_by_path.get(path) is None
]
assert not missing_from_init, (
"Upstream bug: preserve targets not in init: "
+ ", ".join(missing_from_init)
)
missing = [path for path in paths if self.find_by_path(path) is None]
return {
"field": field,
"expected": {"preserved": paths},
"actual": {"missing": missing},
"passed": not missing,
}
def check_files_renamed(
self,
rename_map: dict[str, str],
*,
field: str = "file_system.files_renamed",
) -> dict[str, Any]:
"""验证一组文件按路径改名;目标节点必须继承源文件 id。"""
assert self._init_by_path is not None, "check_files_renamed requires init state"
missing_from_init = [
src for src in rename_map if self._init_by_path.get(src) is None
]
assert not missing_from_init, (
"Upstream bug: rename sources not in init: "
+ ", ".join(missing_from_init)
)
failures: list[dict[str, Any]] = []
for src, dst in rename_map.items():
init_node = self._init_by_path[src]
curr_src = self.find_by_path(src)
curr_dst = self.find_by_path(dst)
ok = (
curr_src is None
and curr_dst is not None
and str(curr_dst.get("id") or "") == str(init_node.get("id") or "")
)
if not ok:
failures.append(
{
"from": src,
"to": dst,
"srcStillExists": curr_src is not None,
"dst": curr_dst,
}
)
return {
"field": field,
"expected": rename_map,
"actual": failures,
"passed": not failures,
}
def check_file_moved(self, src: str, dst: str, *, field: str) -> dict[str, Any]:
src_init = self._init_by_path.get(src)
src_curr = self.find_by_path(src)
dst_curr = self.find_by_path(dst)
return {
"field": field,
"expected": {"moved_from": src, "moved_to": dst},
"actual": {
"src_init": src_init,
"src_curr": src_curr,
"dst_curr": dst_curr,
},
"passed": (
src_init is not None
and src_curr is None
and dst_curr is not None
),
}
def check_files_moved(
self,
move_map: dict[str, str],
*,
field: str = "file_system.files_moved",
) -> dict[str, Any]:
"""验证一组文件被移动到新路径;目标节点必须继承源文件 id。"""
assert self._init_by_path is not None, "check_files_moved requires init state"
missing_from_init = [
src for src in move_map if self._init_by_path.get(src) is None
]
assert not missing_from_init, (
"Upstream bug: move sources not in init: "
+ ", ".join(missing_from_init)
)
failures: list[dict[str, Any]] = []
for src, dst in move_map.items():
init_node = self._init_by_path[src]
curr_src = self.find_by_path(src)
curr_dst = self.find_by_path(dst)
ok = (
curr_src is None
and curr_dst is not None
and str(curr_dst.get("id") or "") == str(init_node.get("id") or "")
)
if not ok:
failures.append(
{
"from": src,
"to": dst,
"srcStillExists": curr_src is not None,
"dst": curr_dst,
}
)
return {
"field": field,
"expected": move_map,
"actual": failures,
"passed": not failures,
}
def check_directory_file_names_exact(
self,
dir_path: str,
expected_names: list[str],
*,
field: str = "file_system.directory_files",
) -> dict[str, Any]:
"""验证目录内文件名集合与期望完全一致。"""
actual_names = [str(node["name"]) for node in self.files_in_dir(dir_path)]
return {
"field": field,
"expected": sorted(expected_names),
"actual": sorted(actual_names),
"passed": sorted(actual_names) == sorted(expected_names),
}
def check_file_renamed(
self,
old_name: str,
new_name: str,
dir_path: str,
*,
field: str,
) -> dict[str, Any]:
old_path = posixpath.join(dir_path, old_name)
new_path = posixpath.join(dir_path, new_name)
return self.check_file_moved(old_path, new_path, field=field)
class FileManager(BaseApp):
"""FileManager app state accessor (clipboard only)."""
@property
def clipboard_items(self) -> list[dict[str, Any]]:
return self.get_list("clipboardItems")
@property
def clipboard_operation(self) -> str | None:
value = self.get("clipboardOperation")
return str(value) if value is not None else None