Files
hermes-webui/tests/test_workspace_symlink_tree_shape.py
T
nesquena-hermes 1e18c336f1 stage #4581 onto master (v0.51.556) — surface escape-target symlinks as display-only workspace rows
claw-io: symlinks whose target resolves outside the workspace root were silently dropped; now emitted
with target_outside_workspace=True (display-only — safe_resolve_ws/open_anchored_fd still block
navigation). Dedup-winner over #4573 (stronger tests). Co-authored-by: claw-io <claw-io@users.noreply.github.com>
2026-06-21 09:15:22 +00:00

115 lines
4.6 KiB
Python

"""Tests for #4226 — symlinks in the workspace file tree must display correctly.
The renderer _renderTreeItems must use isDirLike (type==='dir' || (type==='symlink'
&& is_dir)) for all expand/navigate/delete gates rather than type==='dir' alone.
"""
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
ICONS_JS = (REPO_ROOT / "static" / "icons.js").read_text(encoding="utf-8")
I18N_JS = (REPO_ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
WS_JS = (REPO_ROOT / "static" / "workspace.js").read_text(encoding="utf-8")
def _render_block() -> str:
start = UI_JS.find("function _renderTreeItems(container, entries, depth)")
assert start >= 0, "_renderTreeItems not found in static/ui.js"
# Capture to end of function (next top-level async/function declaration)
end = UI_JS.find("\nasync function deleteWorkspaceDir", start)
assert end >= 0, "end of _renderTreeItems not found"
return UI_JS[start:end]
class TestIsDirLikeLocals:
def test_isLk_local_declared(self):
block = _render_block()
assert "const isLk = item.type === 'symlink';" in block, \
"isLk local must be declared inside the per-item loop"
def test_isExternalLink_local_declared(self):
block = _render_block()
assert "const isExternalLink = isLk && item.target_outside_workspace;" in block, \
"isExternalLink local must be declared for display-only external symlinks"
def test_isDirLike_local_declared(self):
block = _render_block()
assert "const isDirLike = !isExternalLink && (item.type === 'dir' || (isLk && item.is_dir));" in block, \
"isDirLike local must be declared inside the per-item loop"
def test_isFileLike_local_declared(self):
block = _render_block()
assert "const isFileLike = !isExternalLink && !isDirLike;" in block, \
"isFileLike local must be declared for file-like symlink handling"
def test_wsIsDir_dataset_set(self):
block = _render_block()
assert "el.dataset.wsIsDir = String(isDirLike);" in block, \
"data-ws-is-dir must be set on every file-item row"
class TestExpandToggle:
def test_toggle_uses_isDirLike(self):
block = _render_block()
assert "if(isDirLike){" in block, \
"expand toggle must branch on isDirLike, not item.type==='dir'"
def test_recursive_render_uses_isDirLike(self):
block = _render_block()
assert "if(isDirLike&&S._expandedDirs.has(item.path)){" in block, \
"recursive child render must branch on isDirLike"
class TestLinkIcon:
def test_link_icon_path_exists(self):
assert "'link':" in ICONS_JS, \
"'link' key must be added to LI_PATHS in static/icons.js"
def test_icon_dispatch_uses_li_link(self):
block = _render_block()
assert "li('link', 14)" in block, \
"icon dispatch must emit li('link', 14) for symlink rows"
def test_external_link_icon_path_exists(self):
assert "'external-link':" in ICONS_JS, \
"'external-link' key must be in LI_PATHS for display-only external symlinks"
def test_icon_dispatch_uses_li_external_link(self):
block = _render_block()
assert "li('external-link', 14)" in block, \
"icon dispatch must emit li('external-link', 14) for external symlink rows"
class TestSymlinkTooltip:
def test_symlink_link_to_key_in_i18n(self):
count = I18N_JS.count("symlink_link_to:")
assert count >= 5, \
f"symlink_link_to key must appear in multiple locale blocks; found {count}"
def test_elideMiddle_utility_present(self):
assert "function elideMiddle(" in UI_JS, \
"elideMiddle() utility must be defined in static/ui.js"
def test_tooltip_uses_symlink_link_to(self):
block = _render_block()
assert "t('symlink_link_to')" in block, \
"symlink rows must set a tooltip using the symlink_link_to i18n key"
class TestDragDropGuard:
def test_workspace_drag_guard_widened(self):
assert '[data-ws-is-dir="true"]' in WS_JS, \
"workspace.js drag-over guard must also exclude [data-ws-is-dir='true'] rows"
class TestSymlinkFileAffordances:
def test_size_badge_uses_isFileLike(self):
block = _render_block()
assert "if(isFileLike&&item.size){" in block, \
"symlink-to-file rows must render the file-size badge"
def test_delete_button_uses_isFileLike(self):
block = _render_block()
assert "if(isFileLike){" in block, \
"symlink-to-file rows must render the inline delete button"