* Reduce and tighten comments and docstrings in tests Shorten verbose comments and docstrings across the test suite without changing any test logic. Remove narration that restates the next line, collapse long module and test docstrings to a single line, and drop banner separators. Keep regression context (issue and PR references, run ids), skip reasons, mocking and timing rationale, license headers, lint and type directives, and commented-out code. Comments and docstrings only: an AST signature check confirms no code, assertions, or string literals changed, and the suite byte-compiles cleanly. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
201 lines
8 KiB
Python
201 lines
8 KiB
Python
"""RTL bidi contract on chat composers: all three need dir="auto", and the IME
|
|
smoke must drop the dead STUDIO_OLD_PW env var."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
THREAD_TSX = REPO / "studio/frontend/src/components/assistant-ui/thread.tsx"
|
|
SHARED_TSX = REPO / "studio/frontend/src/features/chat/shared-composer.tsx"
|
|
WORKFLOW_YML = REPO / ".github/workflows/studio-ui-smoke.yml"
|
|
IME_PY = REPO / "tests/studio/playwright_chat_ime_i18n.py"
|
|
|
|
|
|
def _block_around(
|
|
src: str,
|
|
anchor: str,
|
|
radius: int = 600,
|
|
) -> str:
|
|
idx = src.find(anchor)
|
|
assert idx != -1, f"anchor {anchor!r} not found"
|
|
return src[max(idx - radius, 0) : idx + radius]
|
|
|
|
|
|
def test_main_composer_has_dir_auto():
|
|
# PR #5784 turned the attribute into a JSX conditional; anchor on the inner
|
|
# "Message input" literal, which survives both spellings.
|
|
block = _block_around(THREAD_TSX.read_text(), '"Message input"')
|
|
assert 'dir="auto"' in block, 'main composer is missing dir="auto"'
|
|
|
|
|
|
def test_edit_composer_has_dir_auto():
|
|
block = _block_around(THREAD_TSX.read_text(), "aui-edit-composer-input")
|
|
assert 'dir="auto"' in block, 'edit composer is missing dir="auto"'
|
|
|
|
|
|
def test_compare_composer_has_dir_auto():
|
|
block = _block_around(SHARED_TSX.read_text(), "Send to both models")
|
|
assert 'dir="auto"' in block, 'compare composer is missing dir="auto"'
|
|
|
|
|
|
def test_ime_workflow_step_does_not_set_studio_old_pw():
|
|
yml = WORKFLOW_YML.read_text()
|
|
drive_idx = yml.find("Drive IME + multilingual paste regression")
|
|
assert drive_idx != -1, "IME drive step not found in workflow"
|
|
next_step_idx = yml.find("- name:", drive_idx + 1)
|
|
drive_block = yml[drive_idx : next_step_idx if next_step_idx != -1 else None]
|
|
assert (
|
|
"STUDIO_OLD_PW" not in drive_block
|
|
), "IME drive step still passes dead STUDIO_OLD_PW env var"
|
|
assert "STUDIO_NEW_PW" in drive_block, "IME drive step missing STUDIO_NEW_PW"
|
|
|
|
|
|
def test_ime_pass_password_step_does_not_export_old_pw():
|
|
yml = WORKFLOW_YML.read_text()
|
|
pass_idx = yml.find("Pass bootstrap pw for IME / i18n test")
|
|
assert pass_idx != -1, "IME password setup step not found"
|
|
next_step_idx = yml.find("- name:", pass_idx + 1)
|
|
pass_block = yml[pass_idx : next_step_idx if next_step_idx != -1 else None]
|
|
assert (
|
|
"STUDIO_IME_OLD_PW" not in pass_block
|
|
), "IME password setup still exports dead STUDIO_IME_OLD_PW"
|
|
assert "STUDIO_IME_NEW_PW" in pass_block
|
|
|
|
|
|
def test_ime_playwright_script_does_not_read_studio_old_pw():
|
|
src = IME_PY.read_text()
|
|
code_only = re.sub(r'""".*?"""', "", src, flags = re.DOTALL)
|
|
assert (
|
|
"STUDIO_OLD_PW" not in code_only
|
|
), "IME Playwright script still references dead STUDIO_OLD_PW env var"
|
|
assert 'os.environ["STUDIO_NEW_PW"]' in code_only
|
|
|
|
|
|
def test_main_composer_has_stuck_compositionend_watchdog():
|
|
"""Issue #5546: WSL Chrome never emits compositionend after IME commit, so the
|
|
composer needs a watchdog releasing the composing flag or Send stays disabled."""
|
|
src = THREAD_TSX.read_text()
|
|
assert (
|
|
"IME_STUCK_TIMEOUT_MS" in src
|
|
), "main composer is missing the stuck-compositionend watchdog (issue #5546)"
|
|
assert "onCompositionUpdate" in src, (
|
|
"main composer is missing onCompositionUpdate wiring; the "
|
|
"watchdog only resets while the IME is actively emitting events"
|
|
)
|
|
|
|
|
|
def test_compare_composer_has_stuck_compositionend_watchdog():
|
|
src = SHARED_TSX.read_text()
|
|
assert (
|
|
"IME_STUCK_TIMEOUT_MS" in src
|
|
), "compare composer is missing the stuck-compositionend watchdog (issue #5546)"
|
|
assert "onCompositionUpdate" in src, "compare composer is missing onCompositionUpdate wiring"
|
|
|
|
|
|
def test_main_composer_keydown_repins_composing_during_ime():
|
|
"""Issue #5546: the keydown IME gate must re-pin composingRef so a follow-up
|
|
Enter does not submit preedit text after the watchdog clears it."""
|
|
src = THREAD_TSX.read_text()
|
|
assert "onKeyDown" in src, "main composer is missing onKeyDown IME gate"
|
|
assert "e.nativeEvent.isComposing" in src and "keyCode === 229" in src, (
|
|
"main composer keydown gate must check both nativeEvent.isComposing "
|
|
"and the IME keyCode 229 sentinel"
|
|
)
|
|
|
|
|
|
def test_compare_composer_keydown_repins_composing_during_ime():
|
|
"""Compare composer onKeyDown re-pins composingRef on IME keypress so a
|
|
follow-up click-Send during the watchdog window does not slip preedit text."""
|
|
src = SHARED_TSX.read_text()
|
|
assert "composingRef.current = true" in src, (
|
|
"compare composer keydown gate must re-pin composingRef when the "
|
|
"browser still considers the IME active"
|
|
)
|
|
|
|
|
|
def _extract_block(
|
|
src: str,
|
|
anchor: str,
|
|
opener: str = "(",
|
|
closer: str = ")",
|
|
) -> str:
|
|
"""Source within the first balanced opener/closer after `anchor`, scoping
|
|
assertions to one handler."""
|
|
start = src.find(anchor)
|
|
assert start != -1, f"anchor {anchor!r} not found"
|
|
open_idx = src.find(opener, start)
|
|
assert open_idx != -1, f"opener {opener!r} after {anchor!r} not found"
|
|
depth = 0
|
|
for i in range(open_idx, len(src)):
|
|
c = src[i]
|
|
if c == opener:
|
|
depth += 1
|
|
elif c == closer:
|
|
depth -= 1
|
|
if depth == 0:
|
|
return src[start : i + 1]
|
|
raise AssertionError(f"unbalanced {opener!r}/{closer!r} after {anchor!r}")
|
|
|
|
|
|
def test_main_composer_keydown_rearms_watchdog():
|
|
"""After keydown re-pins composingRef the watchdog must re-arm, else the
|
|
WSL+Chrome no-compositionend path locks Send after any IME keypress (#5546)."""
|
|
src = THREAD_TSX.read_text()
|
|
block = _extract_block(src, "const onKeyDown = useCallback")
|
|
assert "refreshStuckTimer" in block, (
|
|
"main composer keydown gate must call refreshStuckTimer after "
|
|
"re-pinning composingRef so the watchdog runs again on the "
|
|
"stuck-compositionend path"
|
|
)
|
|
assert "clearStuckTimer();" not in block.replace("clearStuckTimer\n", "").replace(
|
|
"clearStuckTimer,", ""
|
|
), (
|
|
"main composer keydown gate must not leave the watchdog only "
|
|
"cleared; that would regress the stuck-compositionend path"
|
|
)
|
|
|
|
|
|
def test_compare_composer_keydown_rearms_watchdog():
|
|
"""Same re-arm contract for the compare-mode composer."""
|
|
src = SHARED_TSX.read_text()
|
|
block = _extract_block(src, "function onKeyDown", opener = "{", closer = "}")
|
|
assert "refreshStuckImeTimer" in block, (
|
|
"compare composer keydown gate must call refreshStuckImeTimer "
|
|
"after re-pinning composingRef"
|
|
)
|
|
|
|
|
|
def _assert_enter_guard_before_immediate_recovery(block: str, refresh_call: str) -> None:
|
|
enter_idx = block.find('e.key === "Enter"')
|
|
recovery_idx = block.find("setCompositionState(false)")
|
|
assert enter_idx != -1, "keydown handler is missing an Enter guard"
|
|
assert recovery_idx != -1, "keydown handler is missing immediate recovery"
|
|
assert enter_idx < recovery_idx, (
|
|
"stuck-composition recovery must guard Enter before clearing "
|
|
"composingRef; candidate-confirming Enter must not submit"
|
|
)
|
|
guard_block = block[enter_idx:recovery_idx]
|
|
assert "preventDefault()" in guard_block, (
|
|
"Enter while composingRef is stuck must prevent the same key from "
|
|
"falling through to submit"
|
|
)
|
|
assert (
|
|
refresh_call in guard_block
|
|
), "Enter while composingRef is stuck must keep the watchdog armed"
|
|
assert (
|
|
"return;" in guard_block
|
|
), "Enter while composingRef is stuck must not reach immediate recovery"
|
|
|
|
|
|
def test_main_composer_stuck_enter_does_not_clear_before_submit():
|
|
src = THREAD_TSX.read_text()
|
|
block = _extract_block(src, "const onKeyDown = useCallback")
|
|
_assert_enter_guard_before_immediate_recovery(block, "refreshStuckTimer")
|
|
|
|
|
|
def test_compare_composer_stuck_enter_does_not_clear_before_submit():
|
|
src = SHARED_TSX.read_text()
|
|
block = _extract_block(src, "function onKeyDown", opener = "{", closer = "}")
|
|
_assert_enter_guard_before_immediate_recovery(block, "refreshStuckImeTimer")
|