HERMES-GIELIE/tests/test_1062_busy_input_modes.py

246 lines
12 KiB
Python
Raw Normal View History

v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067) * feat: busy input modes with queue/interrupt/steer slash commands - Add busy_input_mode setting (queue/interrupt/steer) to config defaults - Add /queue, /interrupt, /steer slash commands with handlers - Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior) - Add settings dropdown in settings panel with load/save/apply wiring - Initialize window._busyInputMode at boot and on settings save - Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant) Addresses #720 * test: 17 regression tests for busy_input_mode + slash commands PR description noted manual testing only. Added structural tests matching the pattern used by recent contributor PRs (#1010, #1011, #1018, #1022, #1058) so future refactors don't silently regress the wiring: Backend (api/config.py): - default 'queue' is set in _DEFAULT_SETTINGS - enum validator restricts to {queue, interrupt, steer} Slash commands (static/commands.js): - /queue, /interrupt, /steer all registered with correct fns - /interrupt and /steer set noEcho:true (the queued payload becomes the visible turn, not the slash invocation) - cmdQueue requires S.busy - cmdInterrupt + cmdSteer call queueSessionMessage before cancelStream (otherwise the drain has nothing to pick up) send() busy branch (static/messages.js): - reads window._busyInputMode - calls cancelStream on interrupt/steer - queues before cancelling (ordering invariant) Boot init + panels.js wiring (static/boot.js, static/panels.js): - both success and fallback paths set window._busyInputMode - load/save/apply path threads busy_input_mode through i18n (static/i18n.js): - all 17 new keys present in each of the 6 locale blocks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers 1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text as a user bubble, then the drain would send the queued message, causing a double-bubble in the conversation (#840 pattern). 2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue payload but never cleared S.pendingFiles or called renderTray(). Staged files would remain in the tray and be re-attached on the next send(), duplicating attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge(). 3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only interrupt + steer), now documents that all three must set noEcho:true. 4. test_slash_commands_clear_pending_files: new test that all three handlers clear S.pendingFiles and call renderTray() after enqueuing. Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> * docs: v0.50.214 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
"""Regression tests for busy_input_mode (PR #1062, closes #720).
Pins the wiring for the three modes (queue / interrupt / steer):
- The setting key + default + enum validation in api/config.py
- Three slash commands registered in static/commands.js
- send()'s busy branch reads window._busyInputMode and dispatches
- Boot initializes window._busyInputMode from settings
- 17 new i18n keys present in all 6 locale blocks
Issue: #720 (configurable busy-input behaviour)
"""
from pathlib import Path
ROOT = Path(__file__).parent.parent
CONFIG_PY = (ROOT / "api" / "config.py").read_text(encoding="utf-8")
COMMANDS_JS = (ROOT / "static" / "commands.js").read_text(encoding="utf-8")
MESSAGES_JS = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
# ── Backend: setting registration + enum validation ─────────────────────
class TestBusyInputModeSetting:
"""The new setting key must be registered with a default and enum validator."""
def test_default_is_queue(self):
"""Default value preserves existing queue behaviour for users who don't touch the setting."""
assert '"busy_input_mode": "queue"' in CONFIG_PY, (
"_DEFAULT_SETTINGS must include busy_input_mode='queue' so existing users see no change"
)
def test_enum_validator_present(self):
"""_SETTINGS_ENUM_KEYS must validate busy_input_mode against {queue, interrupt, steer}."""
# Find the entry inside the enum dict (a set literal as the value)
idx = CONFIG_PY.find('"busy_input_mode": {')
assert idx >= 0, "busy_input_mode entry missing from _SETTINGS_ENUM_KEYS"
block = CONFIG_PY[idx:idx + 200]
assert '"queue"' in block and '"interrupt"' in block and '"steer"' in block, (
"busy_input_mode enum must contain {queue, interrupt, steer}"
)
# ── Frontend: slash commands ─────────────────────────────────────────────
class TestSlashCommandRegistration:
"""The three new slash commands must be registered in COMMANDS array."""
def test_queue_command_registered(self):
assert "name:'queue'" in COMMANDS_JS and "fn:cmdQueue" in COMMANDS_JS
def test_interrupt_command_registered(self):
assert "name:'interrupt'" in COMMANDS_JS and "fn:cmdInterrupt" in COMMANDS_JS
def test_steer_command_registered(self):
assert "name:'steer'" in COMMANDS_JS and "fn:cmdSteer" in COMMANDS_JS
def test_all_three_busy_commands_are_no_echo(self):
"""All three busy commands must set noEcho:true so the slash invocation
is not echoed as a visible user bubble. Without noEcho, /queue causes a
double-bubble: the raw slash text appears, then the queued message appears
again when the drain fires.
"""
for name in ("queue", "interrupt", "steer"):
idx = COMMANDS_JS.find(f"name:'{name}'")
assert idx >= 0, f"{name} not registered"
block = COMMANDS_JS[idx:idx + 250]
assert "noEcho:true" in block, (
f"/{name} registration must set noEcho:true — "
"without it the command text is echoed as a user bubble, causing duplicates"
)
class TestSlashCommandHandlers:
"""The three handler functions must guard properly and call cancelStream where appropriate."""
def test_cmd_queue_requires_busy(self):
"""/queue while not busy is a usage error — user should send normally."""
idx = COMMANDS_JS.find("async function cmdQueue(")
assert idx >= 0
body = COMMANDS_JS[idx:idx + 600]
assert "if(!S.busy)" in body, "/queue must check S.busy and reject if idle"
def test_cmd_interrupt_calls_cancel_stream(self):
idx = COMMANDS_JS.find("async function cmdInterrupt(")
assert idx >= 0
body = COMMANDS_JS[idx:idx + 800]
assert "queueSessionMessage" in body, "/interrupt must queue the new message before cancelling"
assert "cancelStream" in body, "/interrupt must call cancelStream() so the drain re-sends"
def test_cmd_steer_delegates_to_try_steer(self):
"""/steer delegates to _trySteer which calls /api/chat/steer with
a queue+cancel fallback. The fallback path is exercised by tests
in test_real_steer.py — this test just pins the delegation."""
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067) * feat: busy input modes with queue/interrupt/steer slash commands - Add busy_input_mode setting (queue/interrupt/steer) to config defaults - Add /queue, /interrupt, /steer slash commands with handlers - Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior) - Add settings dropdown in settings panel with load/save/apply wiring - Initialize window._busyInputMode at boot and on settings save - Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant) Addresses #720 * test: 17 regression tests for busy_input_mode + slash commands PR description noted manual testing only. Added structural tests matching the pattern used by recent contributor PRs (#1010, #1011, #1018, #1022, #1058) so future refactors don't silently regress the wiring: Backend (api/config.py): - default 'queue' is set in _DEFAULT_SETTINGS - enum validator restricts to {queue, interrupt, steer} Slash commands (static/commands.js): - /queue, /interrupt, /steer all registered with correct fns - /interrupt and /steer set noEcho:true (the queued payload becomes the visible turn, not the slash invocation) - cmdQueue requires S.busy - cmdInterrupt + cmdSteer call queueSessionMessage before cancelStream (otherwise the drain has nothing to pick up) send() busy branch (static/messages.js): - reads window._busyInputMode - calls cancelStream on interrupt/steer - queues before cancelling (ordering invariant) Boot init + panels.js wiring (static/boot.js, static/panels.js): - both success and fallback paths set window._busyInputMode - load/save/apply path threads busy_input_mode through i18n (static/i18n.js): - all 17 new keys present in each of the 6 locale blocks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers 1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text as a user bubble, then the drain would send the queued message, causing a double-bubble in the conversation (#840 pattern). 2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue payload but never cleared S.pendingFiles or called renderTray(). Staged files would remain in the tray and be re-attached on the next send(), duplicating attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge(). 3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only interrupt + steer), now documents that all three must set noEcho:true. 4. test_slash_commands_clear_pending_files: new test that all three handlers clear S.pendingFiles and call renderTray() after enqueuing. Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> * docs: v0.50.214 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
idx = COMMANDS_JS.find("async function cmdSteer(")
assert idx >= 0
body = COMMANDS_JS[idx:idx + 800]
# cmdSteer now delegates to _trySteer; the fallback (queueSessionMessage
# + cancelStream) lives inside _trySteer.
assert "_trySteer" in body, "cmdSteer must call _trySteer to use the real /api/chat/steer endpoint"
# The shared helper must contain the fallback path
helper_idx = COMMANDS_JS.find("async function _trySteer(")
assert helper_idx >= 0, "_trySteer helper must exist"
helper_body = COMMANDS_JS[helper_idx:helper_idx + 1500]
assert "queueSessionMessage" in helper_body
assert "cancelStream" in helper_body
# Toast should differ from interrupt to signal it's the steer path
assert "cmd_steer_fallback" in helper_body or "steer_fallback" in helper_body
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067) * feat: busy input modes with queue/interrupt/steer slash commands - Add busy_input_mode setting (queue/interrupt/steer) to config defaults - Add /queue, /interrupt, /steer slash commands with handlers - Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior) - Add settings dropdown in settings panel with load/save/apply wiring - Initialize window._busyInputMode at boot and on settings save - Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant) Addresses #720 * test: 17 regression tests for busy_input_mode + slash commands PR description noted manual testing only. Added structural tests matching the pattern used by recent contributor PRs (#1010, #1011, #1018, #1022, #1058) so future refactors don't silently regress the wiring: Backend (api/config.py): - default 'queue' is set in _DEFAULT_SETTINGS - enum validator restricts to {queue, interrupt, steer} Slash commands (static/commands.js): - /queue, /interrupt, /steer all registered with correct fns - /interrupt and /steer set noEcho:true (the queued payload becomes the visible turn, not the slash invocation) - cmdQueue requires S.busy - cmdInterrupt + cmdSteer call queueSessionMessage before cancelStream (otherwise the drain has nothing to pick up) send() busy branch (static/messages.js): - reads window._busyInputMode - calls cancelStream on interrupt/steer - queues before cancelling (ordering invariant) Boot init + panels.js wiring (static/boot.js, static/panels.js): - both success and fallback paths set window._busyInputMode - load/save/apply path threads busy_input_mode through i18n (static/i18n.js): - all 17 new keys present in each of the 6 locale blocks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers 1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text as a user bubble, then the drain would send the queued message, causing a double-bubble in the conversation (#840 pattern). 2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue payload but never cleared S.pendingFiles or called renderTray(). Staged files would remain in the tray and be re-attached on the next send(), duplicating attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge(). 3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only interrupt + steer), now documents that all three must set noEcho:true. 4. test_slash_commands_clear_pending_files: new test that all three handlers clear S.pendingFiles and call renderTray() after enqueuing. Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> * docs: v0.50.214 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
# ── send() busy branch ───────────────────────────────────────────────────
def test_slash_commands_clear_pending_files(self):
"""All three busy command handlers must clear S.pendingFiles (directly
or via _trySteer) after enqueuing, so staged files are not duplicated.
cmdQueue and cmdInterrupt call queueSessionMessage themselves and clear
S.pendingFiles directly. cmdSteer delegates to _trySteer. The fallback/interrupt path clears
S.pendingFiles inside _trySteer; the success path returns early and
send() handles the post-await clear. Either way files are not
duplicated — we verify by checking _trySteer body for the clearing.
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067) * feat: busy input modes with queue/interrupt/steer slash commands - Add busy_input_mode setting (queue/interrupt/steer) to config defaults - Add /queue, /interrupt, /steer slash commands with handlers - Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior) - Add settings dropdown in settings panel with load/save/apply wiring - Initialize window._busyInputMode at boot and on settings save - Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant) Addresses #720 * test: 17 regression tests for busy_input_mode + slash commands PR description noted manual testing only. Added structural tests matching the pattern used by recent contributor PRs (#1010, #1011, #1018, #1022, #1058) so future refactors don't silently regress the wiring: Backend (api/config.py): - default 'queue' is set in _DEFAULT_SETTINGS - enum validator restricts to {queue, interrupt, steer} Slash commands (static/commands.js): - /queue, /interrupt, /steer all registered with correct fns - /interrupt and /steer set noEcho:true (the queued payload becomes the visible turn, not the slash invocation) - cmdQueue requires S.busy - cmdInterrupt + cmdSteer call queueSessionMessage before cancelStream (otherwise the drain has nothing to pick up) send() busy branch (static/messages.js): - reads window._busyInputMode - calls cancelStream on interrupt/steer - queues before cancelling (ordering invariant) Boot init + panels.js wiring (static/boot.js, static/panels.js): - both success and fallback paths set window._busyInputMode - load/save/apply path threads busy_input_mode through i18n (static/i18n.js): - all 17 new keys present in each of the 6 locale blocks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers 1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text as a user bubble, then the drain would send the queued message, causing a double-bubble in the conversation (#840 pattern). 2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue payload but never cleared S.pendingFiles or called renderTray(). Staged files would remain in the tray and be re-attached on the next send(), duplicating attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge(). 3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only interrupt + steer), now documents that all three must set noEcho:true. 4. test_slash_commands_clear_pending_files: new test that all three handlers clear S.pendingFiles and call renderTray() after enqueuing. Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> * docs: v0.50.214 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
"""
# cmdQueue and cmdInterrupt clear pendingFiles directly
for fn_name in ("cmdQueue", "cmdInterrupt"):
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067) * feat: busy input modes with queue/interrupt/steer slash commands - Add busy_input_mode setting (queue/interrupt/steer) to config defaults - Add /queue, /interrupt, /steer slash commands with handlers - Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior) - Add settings dropdown in settings panel with load/save/apply wiring - Initialize window._busyInputMode at boot and on settings save - Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant) Addresses #720 * test: 17 regression tests for busy_input_mode + slash commands PR description noted manual testing only. Added structural tests matching the pattern used by recent contributor PRs (#1010, #1011, #1018, #1022, #1058) so future refactors don't silently regress the wiring: Backend (api/config.py): - default 'queue' is set in _DEFAULT_SETTINGS - enum validator restricts to {queue, interrupt, steer} Slash commands (static/commands.js): - /queue, /interrupt, /steer all registered with correct fns - /interrupt and /steer set noEcho:true (the queued payload becomes the visible turn, not the slash invocation) - cmdQueue requires S.busy - cmdInterrupt + cmdSteer call queueSessionMessage before cancelStream (otherwise the drain has nothing to pick up) send() busy branch (static/messages.js): - reads window._busyInputMode - calls cancelStream on interrupt/steer - queues before cancelling (ordering invariant) Boot init + panels.js wiring (static/boot.js, static/panels.js): - both success and fallback paths set window._busyInputMode - load/save/apply path threads busy_input_mode through i18n (static/i18n.js): - all 17 new keys present in each of the 6 locale blocks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers 1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text as a user bubble, then the drain would send the queued message, causing a double-bubble in the conversation (#840 pattern). 2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue payload but never cleared S.pendingFiles or called renderTray(). Staged files would remain in the tray and be re-attached on the next send(), duplicating attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge(). 3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only interrupt + steer), now documents that all three must set noEcho:true. 4. test_slash_commands_clear_pending_files: new test that all three handlers clear S.pendingFiles and call renderTray() after enqueuing. Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> * docs: v0.50.214 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
idx = COMMANDS_JS.find(f"function {fn_name}(")
assert idx >= 0, f"{fn_name} not found"
body = COMMANDS_JS[idx:idx + 800]
assert "S.pendingFiles=[]" in body, (
f"{fn_name} must clear S.pendingFiles after queueSessionMessage"
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067) * feat: busy input modes with queue/interrupt/steer slash commands - Add busy_input_mode setting (queue/interrupt/steer) to config defaults - Add /queue, /interrupt, /steer slash commands with handlers - Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior) - Add settings dropdown in settings panel with load/save/apply wiring - Initialize window._busyInputMode at boot and on settings save - Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant) Addresses #720 * test: 17 regression tests for busy_input_mode + slash commands PR description noted manual testing only. Added structural tests matching the pattern used by recent contributor PRs (#1010, #1011, #1018, #1022, #1058) so future refactors don't silently regress the wiring: Backend (api/config.py): - default 'queue' is set in _DEFAULT_SETTINGS - enum validator restricts to {queue, interrupt, steer} Slash commands (static/commands.js): - /queue, /interrupt, /steer all registered with correct fns - /interrupt and /steer set noEcho:true (the queued payload becomes the visible turn, not the slash invocation) - cmdQueue requires S.busy - cmdInterrupt + cmdSteer call queueSessionMessage before cancelStream (otherwise the drain has nothing to pick up) send() busy branch (static/messages.js): - reads window._busyInputMode - calls cancelStream on interrupt/steer - queues before cancelling (ordering invariant) Boot init + panels.js wiring (static/boot.js, static/panels.js): - both success and fallback paths set window._busyInputMode - load/save/apply path threads busy_input_mode through i18n (static/i18n.js): - all 17 new keys present in each of the 6 locale blocks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers 1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text as a user bubble, then the drain would send the queued message, causing a double-bubble in the conversation (#840 pattern). 2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue payload but never cleared S.pendingFiles or called renderTray(). Staged files would remain in the tray and be re-attached on the next send(), duplicating attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge(). 3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only interrupt + steer), now documents that all three must set noEcho:true. 4. test_slash_commands_clear_pending_files: new test that all three handlers clear S.pendingFiles and call renderTray() after enqueuing. Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> * docs: v0.50.214 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
)
assert "renderTray()" in body, (
f"{fn_name} must call renderTray() after clearing pendingFiles"
)
# cmdSteer delegates to _trySteer; that helper clears pendingFiles
idx_try = COMMANDS_JS.find("function _trySteer(")
assert idx_try >= 0, "_trySteer not found"
try_body = COMMANDS_JS[idx_try:idx_try + 1200]
assert "S.pendingFiles=[]" in try_body, (
"_trySteer must clear S.pendingFiles in its fallback path — "
"without this, files are lost on steer→interrupt fallback"
)
assert "renderTray()" in try_body, (
"_trySteer must call renderTray() after clearing pendingFiles"
)
v0.50.214: busy input modes + queue/interrupt/steer slash commands (#1067) * feat: busy input modes with queue/interrupt/steer slash commands - Add busy_input_mode setting (queue/interrupt/steer) to config defaults - Add /queue, /interrupt, /steer slash commands with handlers - Modify send() to respect busy_input_mode (interrupt cancels and resends, steer falls back to interrupt with toast, queue preserves existing behavior) - Add settings dropdown in settings panel with load/save/apply wiring - Initialize window._busyInputMode at boot and on settings save - Add 17 i18n keys across all 6 locale blocks (en/ru/es/de/zh/zh-Hant) Addresses #720 * test: 17 regression tests for busy_input_mode + slash commands PR description noted manual testing only. Added structural tests matching the pattern used by recent contributor PRs (#1010, #1011, #1018, #1022, #1058) so future refactors don't silently regress the wiring: Backend (api/config.py): - default 'queue' is set in _DEFAULT_SETTINGS - enum validator restricts to {queue, interrupt, steer} Slash commands (static/commands.js): - /queue, /interrupt, /steer all registered with correct fns - /interrupt and /steer set noEcho:true (the queued payload becomes the visible turn, not the slash invocation) - cmdQueue requires S.busy - cmdInterrupt + cmdSteer call queueSessionMessage before cancelStream (otherwise the drain has nothing to pick up) send() busy branch (static/messages.js): - reads window._busyInputMode - calls cancelStream on interrupt/steer - queues before cancelling (ordering invariant) Boot init + panels.js wiring (static/boot.js, static/panels.js): - both success and fallback paths set window._busyInputMode - load/save/apply path threads busy_input_mode through i18n (static/i18n.js): - all 17 new keys present in each of the 6 locale blocks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add noEcho:true to /queue; clear pendingFiles in all three slash handlers 1. /queue was missing noEcho:true — the dispatcher would echo the raw slash text as a user bubble, then the drain would send the queued message, causing a double-bubble in the conversation (#840 pattern). 2. cmdQueue, cmdInterrupt, and cmdSteer all captured S.pendingFiles into the queue payload but never cleared S.pendingFiles or called renderTray(). Staged files would remain in the tray and be re-attached on the next send(), duplicating attachments. Fix: add S.pendingFiles=[];renderTray() after updateQueueBadge(). 3. test_all_three_busy_commands_are_no_echo: expanded to cover /queue (was only interrupt + steer), now documents that all three must set noEcho:true. 4. test_slash_commands_clear_pending_files: new test that all three handlers clear S.pendingFiles and call renderTray() after enqueuing. Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> * docs: v0.50.214 release notes and version bump --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-26 01:51:06 +00:00
class TestSendBusyBranchDispatch:
"""send()'s busy block must read window._busyInputMode and branch accordingly."""
def test_send_reads_busy_input_mode(self):
# The send() function should read window._busyInputMode in the busy block
send_idx = MESSAGES_JS.find("async function send(")
assert send_idx >= 0
# Look in the first ~3000 chars of send() for the busy mode read
send_body = MESSAGES_JS[send_idx:send_idx + 3000]
assert "_busyInputMode" in send_body, (
"send() must read window._busyInputMode in the S.busy branch"
)
def test_send_calls_cancel_stream_on_interrupt(self):
send_idx = MESSAGES_JS.find("async function send(")
send_body = MESSAGES_JS[send_idx:send_idx + 3000]
# The interrupt branch must call cancelStream
assert "cancelStream" in send_body
# And queue before cancel (otherwise the drain has nothing to pick up)
# Verify the order textually: queueSessionMessage appears before cancelStream
# within the busy block's interrupt branch
cancel_idx = send_body.find("cancelStream")
queue_idx = send_body.find("queueSessionMessage")
assert queue_idx >= 0 and cancel_idx >= 0
assert queue_idx < cancel_idx, (
"queueSessionMessage must run before cancelStream so the drain "
"after setBusy(false) picks up the queued message"
)
# ── Boot init + settings panel wiring ───────────────────────────────────
class TestBootAndPanelsWiring:
def test_boot_init_default_path(self):
"""Boot success path initialises window._busyInputMode from settings."""
assert "window._busyInputMode=(s.busy_input_mode||'queue')" in BOOT_JS
def test_boot_init_fallback_path(self):
"""Boot fallback path (settings load failed) initialises to safe default."""
# The fallback should set window._busyInputMode='queue'
assert "window._busyInputMode='queue'" in BOOT_JS
def test_panels_load_save_apply(self):
assert "settingsBusyInputMode" in PANELS_JS, "panels.js must load the setting"
assert "body.busy_input_mode" in PANELS_JS, "saveSettings must include busy_input_mode in body"
assert "window._busyInputMode=body.busy_input_mode" in PANELS_JS, (
"_applySavedSettingsUi must propagate busy_input_mode to the global"
)
def test_index_html_dropdown_has_three_options(self):
idx = INDEX_HTML.find('id="settingsBusyInputMode"')
assert idx >= 0
block = INDEX_HTML[idx:idx + 800]
assert 'value="queue"' in block
assert 'value="interrupt"' in block
assert 'value="steer"' in block
# ── i18n locale coverage ─────────────────────────────────────────────────
class TestI18nKeys:
"""All 17 new keys must appear in each of the 6 locale blocks."""
REQUIRED_KEYS = [
"cmd_queue",
"cmd_interrupt",
"cmd_steer",
"cmd_queue_no_msg",
"cmd_queue_not_busy",
"cmd_queue_confirm",
"cmd_interrupt_no_msg",
"cmd_interrupt_confirm",
"cmd_steer_no_msg",
"cmd_steer_fallback",
"busy_steer_fallback",
"busy_interrupt_confirm",
"settings_label_busy_input_mode",
"settings_desc_busy_input_mode",
"settings_busy_input_mode_queue",
"settings_busy_input_mode_interrupt",
"settings_busy_input_mode_steer",
]
def test_each_key_appears_at_least_six_times(self):
"""Each key should appear once per locale (en, ru, es, de, zh, zh-Hant) = 6 occurrences minimum."""
for key in self.REQUIRED_KEYS:
count = I18N_JS.count(f"{key}:")
assert count >= 6, (
f"i18n key {key!r} appears {count} times; expected ≥6 (one per locale block)"
)
def test_key_count_total(self):
"""17 keys × 6 locales = 102 minimum occurrences across the file."""
total = sum(I18N_JS.count(f"{key}:") for key in self.REQUIRED_KEYS)
assert total >= 17 * 6, (
f"Total i18n occurrences = {total}; expected ≥ {17*6}"
)