fix(agents): fold non-Latin scripts + close echo-verify test holes

Echo fold kept only ASCII a-z/0-9, so a prompt in Cyrillic/CJK/Arabic/etc.
folded to nothing, hit the unmatchable branch, and fired echo on any render
— a blind submit onto a swallow screen for non-English users, the same
#7466 failure. Fold on the Unicode letter/number classes instead; box
borders and punctuation still drop, so Latin folding is unchanged.

Also strengthen two vacuous FIX-2 guards proven by a revert experiment:
the snapshot-exclusion assertion matched only the first callback arg (the
watcher receives a second meta arg, so not.toHaveBeenCalledWith never
matched), and the echo watch's includeSnapshot:false wiring was unasserted.
Both now fail when the fix is reverted.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-07-08 16:35:42 -07:00
co-authored by Orca
parent 9b8f5d6001
commit e7acc6db10
4 changed files with 29 additions and 15 deletions
@@ -107,6 +107,15 @@ describe('watchForPromptEcho', () => {
)
const watch = watchForPromptEcho('remote-pty', PROMPT, 10_000, {})
await flushMicrotasks()
// The echo watch must opt out of the pre-paste buffer snapshot (#7466),
// or a swallow screen already on the terminal could falsely verify.
expect(mocks.subscribeToRuntimeTerminalData).toHaveBeenCalledWith(
expect.anything(),
'remote-pty',
expect.any(String),
expect.any(Function),
{ includeSnapshot: false }
)
remoteObserver?.(ECHO)
await expect(watch.result).resolves.toBe(true)
expect(remoteUnsubscribe).toHaveBeenCalledTimes(1)
@@ -189,8 +189,10 @@ describe('remote runtime terminal data subscriptions', () => {
// Live output rendered after the subscription still reaches the watcher.
deliverOutput(streamId, 'live')
expect(watcher).not.toHaveBeenCalledWith('PRE-PASTE SCREEN')
expect(watcher).toHaveBeenCalledWith('live')
// onSnapshot passes a second meta arg, so match on the first arg only —
// a bare not.toHaveBeenCalledWith(...) would vacuously pass either way.
expect(watcher.mock.calls.map((call) => call[0])).not.toContain('PRE-PASTE SCREEN')
expect(watcher.mock.calls.map((call) => call[0])).toContain('live')
})
it('keeps the shared terminal multiplexer until the last watcher closes', async () => {
+9
View File
@@ -103,4 +103,13 @@ describe('createPromptEchoScanner', () => {
const scanner = createPromptEchoScanner('fix ci')
expect(scanner.observe('\x1b[11;3Hfix\x1b[11;7Hci')).toBe(true)
})
it('verifies a non-Latin-script prompt instead of failing open on any render', () => {
// #7466: Cyrillic folds to letters (not away), so it matches on its render
// and correctly withholds the submit on a swallow screen.
const swallowed = createPromptEchoScanner('Разреши конфликты слияния')
expect(swallowed.observe(CODEX_TRUST_SWALLOW_REDRAW)).toBe(false)
const rendered = createPromptEchoScanner('Разреши конфликты слияния')
expect(rendered.observe('\x1b[11;3HРазреши\x1b[11;15Hконфликты\x1b[11;25Hслияния')).toBe(true)
})
})
+7 -13
View File
@@ -7,9 +7,12 @@
/**
* Fold terminal output down to comparable text:
* - strip OSC (title/hyperlink) and CSI/charset escape sequences
* - drop every non-alphanumeric character (TUIs repaint with cursor moves
* instead of literal spaces, wrap lines at pane width, and draw box
* borders through the text, so only the letters/digits survive intact)
* - drop every non-letter/non-number character (TUIs repaint with cursor
* moves instead of literal spaces, wrap lines at pane width, and draw box
* borders through the text, so only the letters/digits survive intact).
* Uses the Unicode letter/number classes rather than ASCII so non-Latin
* prompts (Cyrillic, CJK, Arabic, ...) still fold to matchable content
* instead of vanishing to nothing.
* - lowercase for case-stable matching
*/
export function foldTerminalTextForEchoMatch(raw: string): string {
@@ -23,16 +26,7 @@ export function foldTerminalTextForEchoMatch(raw: string): string {
.replace(/\x1b[()#][0-9A-Za-z]/g, '')
.replace(/\x1b[@-_]/g, '')
/* oxlint-enable no-control-regex */
let folded = ''
for (const ch of withoutEscapes.toLowerCase()) {
const code = ch.codePointAt(0) ?? 0
const isDigit = code >= 48 && code <= 57
const isLower = code >= 97 && code <= 122
if (isDigit || isLower) {
folded += ch
}
}
return folded
return withoutEscapes.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '')
}
// Why: large pastes never render verbatim — codex collapses to