Files
hermes-webui/tests/test_workspace_preview_preserved_on_stream_done.py
Pamnard ee414144d3 Reload open workspace preview when agent mutates that file
Track write/edit tool paths per turn, refresh the open preview on
tool_complete and after preservePreview loadDir on stream done, without
closing preview for unrelated responses or wiping unsaved local edits.
2026-05-31 16:54:22 +00:00

86 lines
3.5 KiB
Python

"""Regression: workspace file preview must survive background file-tree refresh on chat done."""
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
WORKSPACE_JS = (REPO / "static" / "workspace.js").read_text(encoding="utf-8")
def _function_block(src: str, name: str) -> str:
marker = f"function {name}("
start = src.find(marker)
assert start != -1, f"{name}() not found"
params_end = src.find("){", start)
assert params_end != -1, f"{name}() body not found"
brace = params_end + 1
depth = 0
for idx in range(brace, len(src)):
ch = src[idx]
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return src[start : idx + 1]
raise AssertionError(f"{name}() body did not close")
def _done_block() -> str:
start = MESSAGES_JS.find("source.addEventListener('done'")
assert start != -1, "done handler not found in messages.js"
end = MESSAGES_JS.find("source.addEventListener('stream_end'", start)
assert end != -1, "stream_end handler not found after done handler"
return MESSAGES_JS[start:end]
def test_stream_done_refreshes_workspace_without_clearing_preview():
"""Chat completion should refresh the tree but not exit an open file preview."""
done_block = _done_block()
assert "preservePreview:true" in done_block.replace(" ", ""), (
"The done handler must refresh the workspace file tree without calling the "
"directory-navigation clearPreview path in loadDir()."
)
def test_load_dir_supports_preserve_preview_option():
block = _function_block(WORKSPACE_JS, "loadDir")
assert "preservePreview" in block, "loadDir() must accept a preservePreview option"
assert "if(!preservePreview&&typeofclearPreview" in block.replace(" ", ""), (
"loadDir() should skip clearPreview() when preservePreview is requested"
)
assert "awaitrefreshOpenPreviewIfMutated()" in block.replace(" ", ""), (
"Background refresh must reload the open preview when a mutation tool touched it"
)
def test_load_dir_still_clears_preview_for_directory_navigation():
"""#1785: explicit directory navigation must still switch preview back to browse mode."""
block = _function_block(WORKSPACE_JS, "loadDir")
assert "clearPreview({keepPanelOpen:true})" in block.replace(" ", ""), (
"Directory navigation must still clear previews when preservePreview is not set"
)
def test_turn_mutation_tracking_reloads_open_preview():
block = _function_block(WORKSPACE_JS, "refreshOpenPreviewIfMutated")
assert "openFile(_previewCurrentPath" in block.replace(" ", ""), (
"Mutated open previews must reload through openFile()"
)
assert "_previewDirty" in block, "Reload must be skipped while the preview has unsaved edits"
def test_tool_complete_tracks_workspace_mutations_for_preview_reload():
tool_complete_idx = MESSAGES_JS.find("source.addEventListener('tool_complete'")
assert tool_complete_idx != -1
end = MESSAGES_JS.find("source.addEventListener('approval'", tool_complete_idx)
block = MESSAGES_JS[tool_complete_idx:end]
assert "noteWorkspaceMutationsFromToolCall" in block
assert "refreshOpenPreviewIfMutated" in block
def test_stream_start_resets_turn_mutation_tracking():
block = _function_block(MESSAGES_JS, "attachLiveStream")
assert "resetTurnWorkspaceMutations" in block