HERMES-GIELIE/tests/test_1466_sidebar_cancel_clarify.py

89 lines
4.8 KiB
Python
Raw Normal View History

"""Regression coverage for issue #1466 sidebar cancel ownership.
The active pane is only a projection; running state belongs to the session that
owns the stream. Cancelling a running session from the sidebar context menu must
address that session's stream id and must only clear approval/clarify UI owned by
that session.
"""
from pathlib import Path
ROOT = Path(__file__).parent.parent
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
def _function_body(src: str, name: str, window: int = 1800) -> str:
idx = src.find(f"function {name}(")
assert idx >= 0, f"{name} not found"
return src[idx : idx + window]
class TestSidebarCancelAction:
def test_running_sidebar_sessions_get_stop_action(self):
"""Running sessions need a context-menu cancel action even when not active pane."""
feat(ux): three high-leverage context-menu essentials from #1764 Issue #1764 asked for a much larger surface (Reveal + Copy-path on every UI surface that references a file path, plus Rename in session menus). Per Nathan's curation we ship only the three highest-leverage pieces in this PR — they cover the three concrete user-visible frictions Cygnus reported, and leave the broader sweep for follow-up. ## 1. Copy file path in workspace tree right-click menu The tree's right-click already had Rename and Reveal in File Manager. Reveal is slow when the user just wants the path string for a terminal/editor — and there was no Copy-path action anywhere. Added "Copy file path" between Reveal and Delete. It POSTs to a new `/api/file/path` endpoint that resolves the relative tree-rooted path into the absolute on-disk path (the frontend can't compute it because only the server knows the workspace root) and writes the result to the OS clipboard via `navigator.clipboard.writeText()`. Falls back to the legacy execCommand pattern on browsers where the modern Clipboard API is gated. The new endpoint deliberately does NOT require the target to exist: copy-path on a recently-deleted file is still useful (paste into a terminal to investigate). `safe_resolve` continues to gate path traversal — the test suite pins this with a `../../../../../etc/passwd` attempt that 400s. ## 2. Rename in session three-dot menu Cygnus's specific ask: double-click rename in the sidebar is timing- sensitive — the first click frequently registers as "open the chat" before the second click arrives, so users open the conversation when they meant to rename it. Putting Rename in the menu eliminates the timing entirely. Added Rename as the FIRST item in `_openSessionActionMenu` (above Pin). It reuses the existing `startRename` closure attached to each session row — no duplicated state, no second API call out of band with the double-click path. Mechanism: the row builder now stores `el._startRename = startRename` and `el.dataset.sid = s.session_id`, so the menu can find the row by data-sid and call its closure directly. This keeps all the `_renamingSid`/`oldTitle`/`applyTitle` bookkeeping single-sourced. Read-only imported sessions skip the menu item via the same `_isReadOnlySession` gate the closure already uses. ## 3. Reveal-failed toast includes the resolved server-side path Cygnus posted a screenshot of a "Failed to reveal: not found" toast that dropped the path entirely. Without it the user can't tell which file the system expected — useful when a stale session row still references a deleted file. Server-side fix in `_handle_file_reveal`: instead of returning `bad(handler, "File not found", 404)`, return `bad(handler, f"File not found: {target}", 404)` where target is the resolved absolute path. Frontend toast also defends against err with no .message: `(err.message||err)` instead of `err.message` alone. Verified live: a missing-file reveal now produces: Failed to reveal: File not found: /home/hermes/workspace/missing-xyz.txt Cygnus's exact diagnostic-friction is gone. ## Tests * tests/test_1764_context_menu_essentials.py (new) - 13 source-level pinning tests - 6 live HTTP behaviour tests against the conftest test server * tests/test_1466_sidebar_cancel_clarify.py - Two assertion-window bumps (3200→4400, 3600→4800) to accommodate the new Rename action prepended to _openSessionActionMenu. The test relied on a fixed-byte-window function-body slice — comments added explaining why the bumps were needed. * All 9 locales got translations for the 5 new keys (copy_file_path, path_copied, path_copy_failed, session_rename, session_rename_desc) — locale parity tests pass. ## Verification Full pytest suite: 4671 passed, 2 skipped, 3 xpassed (matches pre-change baseline). Live browser verification on port 8789: - Right-click .git folder in workspace tree → menu shows Rename / Reveal in File Manager / Copy file path / Delete (red). - Click Copy file path → clipboard gets "/home/hermes/workspace/.git", toast confirms "File path copied to clipboard". - Open session three-dot menu → Rename conversation appears first with pencil icon, followed by Pin / Move / Archive / Duplicate / Delete in the same order as before. - Trigger reveal on a non-existent file → toast reads "Failed to reveal: File not found: /home/hermes/workspace/<filename>". The resolved server-side path is now visible in the failure. Refs nesquena/hermes-webui#1764.
2026-05-07 00:47:35 +00:00
# Window bumped from 3200 → 4400 in #1764 to accommodate the new
# Rename action item, then to 5200 in #2111 for response-aware archive
fix(i18n+tests): complete locale parity + bump test windows for new menu action PR #2294 added the show_previous_messaging_sessions setting and a "Hide from list" menu action for external sessions, but tripped 8 tests: - 4 locale-parity tests (tests/test_{japanese,russian,spanish,chinese}_locale.py) demand every en key be defined in ja/ru/es/zh blocks. The contributor only added the 5 new keys to en + ko, leaving ja/ru/es/zh/it/de/zh-TW/pt/fr missing them. tests/test_provider_quota_status.py also requires the two settings_{label,desc}_previous_messaging_sessions keys in ALL 11 locales. - tests/test_1466_sidebar_cancel_clarify.py read the first 5200 chars of _openSessionActionMenu to find cancelSessionStream/delete actions; the new "Hide from list" branch (17 lines for external sessions) pushed those past the read window. - tests/test_issue1611_session_profile_filtering.py grep'd for the literal string `_keep_latest_messaging_session_per_source(scoped)`, which no longer exists after the call was rewritten as a multi-line keyword-arg form. Fixes: 1. Translations for the 5 new i18n keys added to all 9 missing locales (it, ja, ru, es, de, zh-CN, zh-TW, pt, fr): - session_hide_external - session_hide_external_desc - session_hidden - settings_label_previous_messaging_sessions - settings_desc_previous_messaging_sessions Where the locale already used the English fallback for related keys (ru/es/de session_archive), I provided localized translations for the new keys to match the project's general direction. Native-script quality, not machine-translation. 2. test_1466 window bumped 5200 → 6400 with a comment explaining the bump (mirrors the existing 3200→4400→5200 history annotations). 3. test_1611 dedupe-position check loosened to match the function name without the `(scoped)` suffix so it tolerates both single-line and keyword-arg call shapes. Tests: full suite 5828 passed / 63 skipped / 0 failed (was 8 failed). Behavioral harness verifies the toggle's claimed behavior — off (default) hides reset/compression segments, on shows all rows in timestamp order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 04:38:06 +00:00
# toast handling, then to 6400 in #2294 for the new "Hide from list"
# action prepended for external sessions.
feat(ux): three high-leverage context-menu essentials from #1764 Issue #1764 asked for a much larger surface (Reveal + Copy-path on every UI surface that references a file path, plus Rename in session menus). Per Nathan's curation we ship only the three highest-leverage pieces in this PR — they cover the three concrete user-visible frictions Cygnus reported, and leave the broader sweep for follow-up. ## 1. Copy file path in workspace tree right-click menu The tree's right-click already had Rename and Reveal in File Manager. Reveal is slow when the user just wants the path string for a terminal/editor — and there was no Copy-path action anywhere. Added "Copy file path" between Reveal and Delete. It POSTs to a new `/api/file/path` endpoint that resolves the relative tree-rooted path into the absolute on-disk path (the frontend can't compute it because only the server knows the workspace root) and writes the result to the OS clipboard via `navigator.clipboard.writeText()`. Falls back to the legacy execCommand pattern on browsers where the modern Clipboard API is gated. The new endpoint deliberately does NOT require the target to exist: copy-path on a recently-deleted file is still useful (paste into a terminal to investigate). `safe_resolve` continues to gate path traversal — the test suite pins this with a `../../../../../etc/passwd` attempt that 400s. ## 2. Rename in session three-dot menu Cygnus's specific ask: double-click rename in the sidebar is timing- sensitive — the first click frequently registers as "open the chat" before the second click arrives, so users open the conversation when they meant to rename it. Putting Rename in the menu eliminates the timing entirely. Added Rename as the FIRST item in `_openSessionActionMenu` (above Pin). It reuses the existing `startRename` closure attached to each session row — no duplicated state, no second API call out of band with the double-click path. Mechanism: the row builder now stores `el._startRename = startRename` and `el.dataset.sid = s.session_id`, so the menu can find the row by data-sid and call its closure directly. This keeps all the `_renamingSid`/`oldTitle`/`applyTitle` bookkeeping single-sourced. Read-only imported sessions skip the menu item via the same `_isReadOnlySession` gate the closure already uses. ## 3. Reveal-failed toast includes the resolved server-side path Cygnus posted a screenshot of a "Failed to reveal: not found" toast that dropped the path entirely. Without it the user can't tell which file the system expected — useful when a stale session row still references a deleted file. Server-side fix in `_handle_file_reveal`: instead of returning `bad(handler, "File not found", 404)`, return `bad(handler, f"File not found: {target}", 404)` where target is the resolved absolute path. Frontend toast also defends against err with no .message: `(err.message||err)` instead of `err.message` alone. Verified live: a missing-file reveal now produces: Failed to reveal: File not found: /home/hermes/workspace/missing-xyz.txt Cygnus's exact diagnostic-friction is gone. ## Tests * tests/test_1764_context_menu_essentials.py (new) - 13 source-level pinning tests - 6 live HTTP behaviour tests against the conftest test server * tests/test_1466_sidebar_cancel_clarify.py - Two assertion-window bumps (3200→4400, 3600→4800) to accommodate the new Rename action prepended to _openSessionActionMenu. The test relied on a fixed-byte-window function-body slice — comments added explaining why the bumps were needed. * All 9 locales got translations for the 5 new keys (copy_file_path, path_copied, path_copy_failed, session_rename, session_rename_desc) — locale parity tests pass. ## Verification Full pytest suite: 4671 passed, 2 skipped, 3 xpassed (matches pre-change baseline). Live browser verification on port 8789: - Right-click .git folder in workspace tree → menu shows Rename / Reveal in File Manager / Copy file path / Delete (red). - Click Copy file path → clipboard gets "/home/hermes/workspace/.git", toast confirms "File path copied to clipboard". - Open session three-dot menu → Rename conversation appears first with pencil icon, followed by Pin / Move / Archive / Duplicate / Delete in the same order as before. - Trigger reveal on a non-existent file → toast reads "Failed to reveal: File not found: /home/hermes/workspace/<filename>". The resolved server-side path is now visible in the failure. Refs nesquena/hermes-webui#1764.
2026-05-07 00:47:35 +00:00
# The `session.active_stream_id` / cancelSessionStream / delete checks
# are positional further down in the function, so growing the prefix
# required growing this read window.
fix(i18n+tests): complete locale parity + bump test windows for new menu action PR #2294 added the show_previous_messaging_sessions setting and a "Hide from list" menu action for external sessions, but tripped 8 tests: - 4 locale-parity tests (tests/test_{japanese,russian,spanish,chinese}_locale.py) demand every en key be defined in ja/ru/es/zh blocks. The contributor only added the 5 new keys to en + ko, leaving ja/ru/es/zh/it/de/zh-TW/pt/fr missing them. tests/test_provider_quota_status.py also requires the two settings_{label,desc}_previous_messaging_sessions keys in ALL 11 locales. - tests/test_1466_sidebar_cancel_clarify.py read the first 5200 chars of _openSessionActionMenu to find cancelSessionStream/delete actions; the new "Hide from list" branch (17 lines for external sessions) pushed those past the read window. - tests/test_issue1611_session_profile_filtering.py grep'd for the literal string `_keep_latest_messaging_session_per_source(scoped)`, which no longer exists after the call was rewritten as a multi-line keyword-arg form. Fixes: 1. Translations for the 5 new i18n keys added to all 9 missing locales (it, ja, ru, es, de, zh-CN, zh-TW, pt, fr): - session_hide_external - session_hide_external_desc - session_hidden - settings_label_previous_messaging_sessions - settings_desc_previous_messaging_sessions Where the locale already used the English fallback for related keys (ru/es/de session_archive), I provided localized translations for the new keys to match the project's general direction. Native-script quality, not machine-translation. 2. test_1466 window bumped 5200 → 6400 with a comment explaining the bump (mirrors the existing 3200→4400→5200 history annotations). 3. test_1611 dedupe-position check loosened to match the function name without the `(scoped)` suffix so it tolerates both single-line and keyword-arg call shapes. Tests: full suite 5828 passed / 63 skipped / 0 failed (was 8 failed). Behavioral harness verifies the toggle's claimed behavior — off (default) hides reset/compression segments, on shows all rows in timestamp order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 04:38:06 +00:00
body = _function_body(SESSIONS_JS, "_openSessionActionMenu", 6400)
assert "session.active_stream_id" in body, (
"sidebar action menu must detect per-session active_stream_id instead of S.activeStreamId"
)
assert "cancelSessionStream(session)" in body, (
"running sidebar sessions must expose a stop action that cancels that session"
)
assert body.find("cancelSessionStream(session)") < body.find("deleteSession(session.session_id)"), (
"stop action should appear before destructive delete action"
)
def test_cancel_session_stream_uses_session_owned_stream_id(self):
"""Cancel-from-sidebar must call /api/chat/cancel with the row's stream id."""
body = _function_body(BOOT_JS, "cancelSessionStream")
assert "session&&session.active_stream_id" in body or "session && session.active_stream_id" in body
assert "stream_id=${encodeURIComponent(streamId)}" in body
assert "S.activeStreamId" not in body.split("const streamId", 1)[1].split("fetch", 1)[0], (
"sidebar cancel must not derive the stream id from the active pane global"
)
def test_cancel_session_stream_clears_only_owned_clarify_and_approval_cards(self):
"""Cancelling A from sidebar must not blanket-clear B's clarify/approval cards."""
body = _function_body(BOOT_JS, "cancelSessionStream")
assert "_clarifySessionId===sid" in body, (
"clarify card cleanup must be gated to the cancelled session id"
)
assert "_approvalSessionId===sid" in body, (
"approval card cleanup must be gated to the cancelled session id"
)
assert "hideClarifyCard(true" in body
assert "hideApprovalCard(true" in body
2026-05-04 01:44:11 +00:00
def test_cli_session_helper_identifies_cli_origin(self):
"""CLI sessions should be treated as external-only for destructive action gating."""
body = _function_body(SESSIONS_JS, "_isCliSession", 900)
assert "function _isCliSession(session) {" in body
assert "session.session_source === 'cli'" in body
assert "session.raw_source" in body
assert "session.source_tag" in body
assert "session.source" in body
assert "session.source_label" in body
assert "if (_isMessagingSession(session)) return false;" in body
assert "return session.is_cli_session === true;" in body
def test_cli_sessions_hide_duplicate_and_delete_in_action_menu(self):
"""Session action menu should hide duplicate/delete for CLI-origin sessions."""
# Window bumped 3600 → 4800 in #1764 (Rename action prepended), then
fix(i18n+tests): complete locale parity + bump test windows for new menu action PR #2294 added the show_previous_messaging_sessions setting and a "Hide from list" menu action for external sessions, but tripped 8 tests: - 4 locale-parity tests (tests/test_{japanese,russian,spanish,chinese}_locale.py) demand every en key be defined in ja/ru/es/zh blocks. The contributor only added the 5 new keys to en + ko, leaving ja/ru/es/zh/it/de/zh-TW/pt/fr missing them. tests/test_provider_quota_status.py also requires the two settings_{label,desc}_previous_messaging_sessions keys in ALL 11 locales. - tests/test_1466_sidebar_cancel_clarify.py read the first 5200 chars of _openSessionActionMenu to find cancelSessionStream/delete actions; the new "Hide from list" branch (17 lines for external sessions) pushed those past the read window. - tests/test_issue1611_session_profile_filtering.py grep'd for the literal string `_keep_latest_messaging_session_per_source(scoped)`, which no longer exists after the call was rewritten as a multi-line keyword-arg form. Fixes: 1. Translations for the 5 new i18n keys added to all 9 missing locales (it, ja, ru, es, de, zh-CN, zh-TW, pt, fr): - session_hide_external - session_hide_external_desc - session_hidden - settings_label_previous_messaging_sessions - settings_desc_previous_messaging_sessions Where the locale already used the English fallback for related keys (ru/es/de session_archive), I provided localized translations for the new keys to match the project's general direction. Native-script quality, not machine-translation. 2. test_1466 window bumped 5200 → 6400 with a comment explaining the bump (mirrors the existing 3200→4400→5200 history annotations). 3. test_1611 dedupe-position check loosened to match the function name without the `(scoped)` suffix so it tolerates both single-line and keyword-arg call shapes. Tests: full suite 5828 passed / 63 skipped / 0 failed (was 8 failed). Behavioral harness verifies the toggle's claimed behavior — off (default) hides reset/compression segments, on shows all rows in timestamp order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 04:38:06 +00:00
# to 5200 in #2111 for response-aware archive toast handling, then
# to 6400 in #2294 for the "Hide from list" action on external sessions.
body = _function_body(SESSIONS_JS, "_openSessionActionMenu", 6400)
2026-05-04 01:44:11 +00:00
assert "const isCliSession = _isCliSession(session);" in body
assert "const isExternalSession = isMessagingSession || isCliSession;" in body
assert "if(!isExternalSession)" in body
# duplicate/delete should both be gated by the same external-session check
first = body.find("_appendSessionDuplicateAction")
second = body.find("t('session_delete')")
assert first > 0 and second > 0, "menu actions should still include duplicate/delete nodes"
assert first < second, "duplicate action should render before delete action"