Repository navigation
fix(src): read each encodeMouse field once - #41
Merged
Merged
Conversation
A getter can return one value to the TypeScript checks and another to the addon. This test fails until the normalizers read each field once. Signed-off-by: Thomas Kosiewski <tk@coder.com> --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `medium`_ Change-Id: Id19dd86ea228c68d85b420216eaa2f4cb43d1621
The normalizers destructure each caller field into a local, then validate and forward those locals. Error types, messages and check order stay the same. Signed-off-by: Thomas Kosiewski <tk@coder.com> --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `medium`_ Change-Id: Ia074be12cc33f616d1762b28f4e8d514e0652039
Member
Author
|
Verdict: PASS+NOTES Parent:
Note (non-blocking):
Generated with |
Merged
ThomasK33
added a commit
that referenced
this pull request
Oct 30, 2026
encodeMouse first ships in this release, so #41 and #42 did not fix a released bug. Their behavior now reads as part of the new API. _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_ Change-Id: Ic6de3b1b21ac87d8274619c74fd8121ef0e333e0 Signed-off-by: Thomas Kosiewski <tk@coder.com>
ThomasK33
added a commit
that referenced
this pull request
Oct 30, 2026
## Why The goal is to handle and land the items still open after the #12 follow-ups, one PR each. This PR adds the test coverage for the read-once contract that #41 added. `src/README.md` promises that `encodeMouse` reads each caller field once. The #41 test checks only `x` and `geometry.screenWidth`. A later change that reads `y` or `cellWidth` twice still passes it. ## What changed `test/terminal.test.ts` has one new native test, "reads every event, modifier, option, and geometry field once", right after the #41 test. 1. The test puts a counting getter on all 19 caller fields. Every optional field is set to a valid value, so each one is present. 2. It calls `encodeMouse` once and checks the bytes: `ESC[<12;4;6M`. The SGR rule gives this value: left button 0, plus 4 for shift, plus 8 for alt. Column and row are 1-based (x 3 → 4, y 5 → 6). 3. It checks the read counts as one equality on a map of field name to count. A failure names each field read more than once. ## Scope Test only. No change to `src/` or `native/`. ## Blast Radius One test in the native suite. It skips with the rest of the suite when the addon is not built. ## Verification I proved the test can fail. I put the parent of #41's fix (`git show 30242de^:src/index.ts`) into `src/index.ts`, rebuilt, and ran the test. It fails and names the 10 fields read more than once: ``` $ npx vitest run test/terminal.test.ts -t "reads every" FAIL test/terminal.test.ts > GhosttyVtTerminal > reads every event, modifier, option, and geometry field once AssertionError: expected { screenWidth: 3, …(18) } to deeply equal { geometry: 1, …(18) } - "action": 1, + "action": 3, - "button": 1, - "cellHeight": 1, - "cellWidth": 1, + "button": 5, + "cellHeight": 3, + "cellWidth": 3, - "geometry": 1, - "modifiers": 1, + "geometry": 2, + "modifiers": 5, - "screenHeight": 1, - "screenWidth": 1, + "screenHeight": 3, + "screenWidth": 3, - "x": 1, - "y": 1, + "x": 3, + "y": 3, Tests 1 failed | 33 skipped (34) ``` The other 9 fields showed 1 read each. Then I restored `src/index.ts`, rebuilt, and ran the test again: ``` $ npx vitest run test/terminal.test.ts -t "reads every" Test Files 1 passed (1) Tests 1 passed | 33 skipped (34) ``` Gates on the final commit. Each one exited 0: 1. `npm ci && LIBGHOSTTY_VT_PREFIX=<ReleaseFast> npm run build:native && npm run build && npm run verify`. 78 tests passed. 2. The same with the ReleaseSafe prefix. 78 tests passed. 3. UBSan: `rm -rf dist && LIBGHOSTTY_VT_PREFIX=<ReleaseFast> CXXFLAGS="-fsanitize=undefined,float-cast-overflow -fno-sanitize-recover=all" LDFLAGS="-fsanitize=undefined,float-cast-overflow" npm run build:native && npm test && npm run smoke`. 78 tests passed, smoke passed. --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `medium`_
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The user wants four small leftovers from the mouse-encoding PR (#12) fixed and landed on main. This PR fixes one of them.
normalizeMouseOptionsandnormalizeMouseEventinsrc/index.tsread some caller properties more than once: once to validate, again to copy. A getter can pass the TypeScript checks with one value and hand the addon another. The addon checks again, so memory stays safe. But the TypeScript layer then validates a value it does not forward.On main with a ReleaseSafe addon,
xis read twice andgeometry.screenWidththree times. A getter that returns 3 and then 1e30 forxthrows aRangeErrorfor a value the caller's first read never produced. AscreenWidthgetter that returns 80 and then 0 passes the TypeScript check and fails in the addon.What changed
options.geometry, every geometry field,anyButtonPressed,trackLastCell,event.action,button,x,y,modifiersand itsshift,ctrl,alt. They validate and forward only those locals.src/README.mdnow states that each caller field is read once.Scope
Only the double read in the two TypeScript normalizers. The other three #12 leftovers are separate PRs.
native/is unchanged.Blast Radius
Callers with plain data objects see no change. Callers whose getters return different values on each read now get the first value checked and encoded. A getter that throws now throws when the normalizer destructures it, before any later field check. That changes which error such a caller sees, not the message of any check.
Verification
npx vitest run test/terminal.test.ts -t "getters change"failed withRangeError: mouse x is outside Ghostty's cell or pixel coordinate rangeatnormalizeMouseEvent src/index.ts:130.node repro.mjs). The R1 lines belong to another leftover and are the same as on main:x reads: 2andscreenWidth reads: 3and threw both errors.npm ci && build:native (ReleaseFast) && npm run build && npm run verify: exit 0, 68 tests passed.rm -rf dist,CXXFLAGS=-fsanitize=undefined,float-cast-overflow -fno-sanitize-recover=all,npm test && npm run smoke): exit 0, 68 tests passed..agents/skills/verify-native-lifecycle/run.sh:OK: every result matches EXPECT.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:medium