mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
100 lines
4 KiB
TypeScript
100 lines
4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { fastAppendEffect, fastBackspaceEffect, resolveCursorLayout } from '../components/textInput.js'
|
|
import { cursorLayout } from '../lib/inputMetrics.js'
|
|
|
|
// Closes Copilot follow-up on PR #26717: the original cursor-drift
|
|
// fix bumped Ink's displayCursor / cursorDeclaration on fast-echo, but
|
|
// if TextInput itself re-renders before the deferred 16ms `setCur`
|
|
// flushes (parent state change, status-bar tick, spinner) the layout
|
|
// effect inside `useDeclaredCursor` re-publishes a declaration
|
|
// computed from the STALE React `cur` state and clobbers the Ink-level
|
|
// bump. The fix is structural: read `curRef.current` (always
|
|
// up-to-date) when computing the layout, not the `cur` state.
|
|
//
|
|
// These tests exercise the real, exported `resolveCursorLayout`,
|
|
// `fastBackspaceEffect`, and `fastAppendEffect` helpers that
|
|
// `textInput.tsx` calls at its render site and fast-echo call sites —
|
|
// no source-text regex, no readFileSync.
|
|
describe('resolveCursorLayout', () => {
|
|
it('uses curRefCurrent (the fresh ref value), not the stale cur state', () => {
|
|
// Simulate the exact bug scenario: `cur` (React state) is stale —
|
|
// it still reflects the value before a fast-echo append — while
|
|
// `curRef.current` has already advanced past it.
|
|
const display = 'hello world'
|
|
const staleCur = 5
|
|
const freshCurRefCurrent = 11
|
|
const columns = 80
|
|
|
|
const result = resolveCursorLayout(display, staleCur, freshCurRefCurrent, columns)
|
|
const expected = cursorLayout(display, freshCurRefCurrent, columns)
|
|
|
|
expect(result).toEqual(expected)
|
|
})
|
|
|
|
it('does not match the layout computed from the stale cur value', () => {
|
|
const display = 'hello world'
|
|
const staleCur = 5
|
|
const freshCurRefCurrent = 11
|
|
const columns = 80
|
|
|
|
const result = resolveCursorLayout(display, staleCur, freshCurRefCurrent, columns)
|
|
const staleLayout = cursorLayout(display, staleCur, columns)
|
|
|
|
expect(result).not.toEqual(staleLayout)
|
|
})
|
|
|
|
it('matches cursorLayout(display, curRefCurrent, columns) even when cur and curRefCurrent agree', () => {
|
|
const display = 'hello'
|
|
const cur = 5
|
|
const columns = 80
|
|
|
|
expect(resolveCursorLayout(display, cur, cur, columns)).toEqual(cursorLayout(display, cur, columns))
|
|
})
|
|
})
|
|
|
|
describe('fastBackspaceEffect', () => {
|
|
it('removes the last character, moves the cursor back one, and pairs the write with the advance delta', () => {
|
|
const effect = fastBackspaceEffect('hello', 5)
|
|
|
|
expect(effect.newValue).toBe('hell')
|
|
expect(effect.newCursor).toBe(4)
|
|
expect(effect.removed).toBe('o')
|
|
// Both the stdout write and the noteCursorAdvance delta live on the
|
|
// same returned object — a caller cannot apply `write` without also
|
|
// having `advanceDelta` in hand, so the pairing can't silently drift.
|
|
expect(effect.write).toBe('\b \b')
|
|
expect(effect.advanceDelta).toBe(-1)
|
|
})
|
|
|
|
it('handles deleting from the middle of the fast-echo-eligible tail', () => {
|
|
const effect = fastBackspaceEffect('abc', 3)
|
|
|
|
expect(effect.newValue).toBe('ab')
|
|
expect(effect.newCursor).toBe(2)
|
|
expect(effect.removed).toBe('c')
|
|
expect(effect.write).toBe('\b \b')
|
|
expect(effect.advanceDelta).toBe(-1)
|
|
})
|
|
})
|
|
|
|
describe('fastAppendEffect', () => {
|
|
it('appends the text, advances the cursor by the inserted length, and pairs the write with the advance delta', () => {
|
|
const effect = fastAppendEffect('hello', 5, ' world')
|
|
|
|
expect(effect.newValue).toBe('hello world')
|
|
expect(effect.newCursor).toBe(11)
|
|
// The stdout write is exactly the inserted text, and the
|
|
// noteCursorAdvance delta is bundled into the same object.
|
|
expect(effect.write).toBe(' world')
|
|
expect(effect.advanceDelta).toBe(' world'.length)
|
|
})
|
|
|
|
it('advance delta always matches the inserted text length, not a hardcoded value', () => {
|
|
const effect = fastAppendEffect('x', 1, 'abc')
|
|
|
|
expect(effect.newValue).toBe('xabc')
|
|
expect(effect.advanceDelta).toBe(3)
|
|
expect(effect.write).toBe('abc')
|
|
})
|
|
})
|