Skip to content

W4-1: bound speech drafts to the server's 2000 code point limit in the UI - #88

Merged
vladmesh merged 1 commit into
mainfrom
claude/w4-ui-mj8f3g
Sep 29, 2026
Merged

vladmesh merged 1 commit into
mainfrom
claude/w4-ui-mj8f3g

Conversation

@vladmesh

@vladmesh vladmesh commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

The server (service/action_parsing.py) rejects any string param longer than 2000 Unicode code points, measured with Python len. The three frontend speech entry points had no bound, so an overlong draft was sent, cleared and then rejected by the server.

  • components/game/speechLimit.ts holds the shared check. checkSpeech(draft) trims the draft and counts the code points of the trimmed payload with Array.from. It returns the payload, its length, tooLong and canSend. An astral emoji counts as one code point and a combining mark counts as its own code point, the same as Python len. The check does not use JS .length or HTML maxLength, which both count UTF-16 units.

  • components/game/SpeechLengthError.tsx shows a localized message such as "Too long: 2001 of 2000 characters…" with role="alert". It renders nothing while the draft is sendable.

  • All three entry points now use the shared check and send only when canSend is true:

    • action-bar/SayAction.tsx
    • Perception.tsx (talk)
    • NpcInspectModal.tsx (talk)

    On an overlong draft, both the Enter key and the send button do nothing. The draft is kept, the input stays open and the modal stays open. The send button is disabled. The input gets aria-invalid and aria-describedby, which points to the alert.

  • New i18n key game:speech_too_long in en and ru.

The server is unchanged. Valid speech behaves as before and sends the same trimmed payload.

Tests

components/game/__tests__/speechLimit.test.tsx has 20 tests:

  • Boundary tests for the pure check: exactly 2000 code points passes, 2001 fails. Covered with plain text, astral emoji (2000 emoji = 4000 UTF-16 units, allowed) and combining sequences. The length is measured on the trimmed payload.
  • Component tests for each of the three entry points, with plain, astral and combining overlong drafts. In each case the button is disabled, and both the button click and Enter send nothing. The draft is not cleared, the modal is not closed, and the alert text, aria-invalid and aria-describedby are asserted.
  • Astral and combining drafts of exactly 2000 code points are sent in full, through Enter and through the button.
  • One test checks the Russian message.

Against main's components, 10 of these 20 tests fail; with this change all pass.

make check exits 0:

  • ruff: All checks passed
  • mypy: no issues in 186 files
  • pytest: 3046 passed
  • eslint: 0 errors, 1 warning that was already there (EventLog.tsx)
  • tsc: clean
  • vitest: 47 files, 387 tests passed

All three speech entry points (action-bar Say, Perception talk, NPC inspect
talk) now validate the trimmed payload with one shared code-point check,
matching Python len on the server. An overlong draft is not sent, not cleared
and not closed; the send button is disabled and a localized role=alert message
is linked via aria-describedby. Adds boundary and component regression tests
covering plain, astral and combining text on both the button and Enter paths.
@vladmesh

Copy link
Copy Markdown
Owner Author

Independent review: PR #88 — PASS

Reviewed head: efc09457bc355c6af6eb02d5a14c753a813468fb.
Reviewer: Hermes coordinator, independent of implementing Claude cloud session.

Full diff reviewed: three speech inputs share code-point validation of the actual trimmed payload. Exactly 2000 astral characters pass; 2001 fail. Combining marks count separately. Both Enter and click refuse overlong text without clearing or closing it. RU/EN feedback, role=alert and aria-describedby are wired. No backend or unrelated changes.

Independent detached worktree make check exit 0: 3046 pytest, 387 Vitest across 47 files; ruff and mypy pass, tsc clean. One pre-existing EventLog eslint warning (0 errors).
Regression proof: copied the new tests and helper into a separate base-main worktree; 10 component tests fail and 10 pass. New implementation passes all 20. See regression-on-base.log and check.log.

GitHub readback: check, frontend, integration, detect-changes all success on the exact reviewed head. No blocking findings. Generated attribution footer removed and read back; commit has none.

@vladmesh
vladmesh merged commit 4e84905 into main Sep 29, 2026
4 checks passed
@vladmesh
vladmesh deleted the claude/w4-ui-mj8f3g branch September 29, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants