Skip to content

test(js): read the cursor next to the state it is compared against - #99

Open
Ayman Bagabas (aymanbagabas) wants to merge 1 commit into
mainfrom
fix/js-cursor-race
Open

test(js): read the cursor next to the state it is compared against#99
Ayman Bagabas (aymanbagabas) wants to merge 1 commit into
mainfrom
fix/js-cursor-race

Conversation

@aymanbagabas

Copy link
Copy Markdown
Member

echo roundtrip drives a real session fails intermittently on CI, most often on macOS:

Expected values to be strictly deep-equal:
+ actual - expected

  {
+   x: 2,
-   x: 0,
    y: 2
  }

It compared getCursor() against a state.cursor captured five calls earlier:

const state = await su.state();          // reads the cursor here
assert.ok(state.cols > 0);
assert.match(await su.text(), /hello-sdk/);
assert.match(await su.getCommand(), /echo hello-sdk/);
assert.match(await su.getOutput(), /hello-sdk/);
assert.equal(await su.getExitCode(), 0);
assert.equal(typeof (await su.getCwd()), "string");
assert.deepEqual(await su.getCursor(), state.cursor);   // ...and again here

The shell draws its next prompt after the command finishes, and that moves the cursor. A prompt landing between the two reads makes them differ by exactly its width, which is what x: 0 against x: 2 is.

Reading them next to each other narrows the window about sixfold, from the five intervening round trips to one:

window with the five calls between: 0.29 ms
window when read adjacently:        0.05 ms

This narrows the window rather than closing it

Worth being plain about, since the title says "race".

The window exists because wait command falls back to "the prompt came back and the screen is idle" when a session has no shell integration, and an idle screen cannot be told apart from a prompt that has not started yet.

My first attempt was await su.waitIdle() before the snapshot, which looks like the right barrier and is the idiom already used by the snapshot test in this file. I measured it against a shell with a deliberately slow prompt (PROMPT_COMMAND='sleep 0.4') before trusting it:

cursor right after 'wait command': {"x":0,"y":2}
cursor after 'wait idle':          {"x":0,"y":2}    <- no barrier at all
cursor 1s later (settled):         {"x":2,"y":2}

waitIdle returned with the cursor still at column 0, because the screen was quiet precisely because the prompt had not begun. So I dropped it rather than ship a barrier that does nothing, and left a comment so the next person does not reach for it either.

With shell integration working, wait command leaves the cursor already settled and there is no window to speak of — so the underlying question is why integration was not in play on the CI runner that failed. That, and two sibling flakes in the Rust lifecycle suite, are tracked in #98.

Testing

43/43 JS binding tests, three consecutive runs. The change is test-only.

`echo roundtrip drives a real session` compared `getCursor()` against a
`state.cursor` captured five calls earlier. The shell draws its next
prompt after the command finishes, which moves the cursor, so a prompt
landing between the two reads made them differ by its width: macOS CI saw
`{x: 0}` against `{x: 2}`.

Reading them next to each other narrows that window about sixfold, from
the five intervening round trips to one.

It narrows rather than closes it. The reason a window exists at all is
that `wait command` falls back to "the prompt came back and the screen is
idle" when a session has no shell integration, and an idle screen is
indistinguishable from a prompt that has not started. `waitIdle` is no
help for the same reason: measured against a shell with a deliberately
slow prompt, it returned with the cursor still at column 0, because the
screen was quiet precisely because the prompt had not begun. That root
cause is tracked in #98.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reduces flakiness in the JS bindings integration test by comparing the cursor position against the captured state.cursor immediately after the snapshot is taken, minimizing the chance that the shell prompt redraw moves the cursor between reads.

Changes:

  • Move getCursor() vs state.cursor comparison to be adjacent to su.state() in echo roundtrip drives a real session.
  • Add an explanatory comment documenting the underlying prompt redraw timing/race and why waitIdle() is not a reliable barrier in this scenario.
Show a summary per file
File Description
bindings/js/test/integration.test.mjs Reorders cursor assertions to reduce prompt redraw timing window and documents the rationale.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 1/1 changed files
  • Comments generated: 0
  • Review effort level: Lite

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