mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
test(terminal): pin the CJK repaint decision against the reporter's own output
#12164 comment 1 and #5921 report agent output with double-width glyphs
rendering duplicated character-by-character while ASCII in the same line
stays clean. No IME, no composition, no keystroke — the user never types
the CJK.
Segmenting all three verbatim samples into maximal same-risk-class runs
gives 33 runs and zero violations of "this run is corrupted iff the
production detector flags it": 17 wide runs all corrupted, 16 narrow runs
all byte-identical. The paired negative is co-located in the same line
rather than in a separate run — the reporter supplied it without knowing.
Doubling is asserted as present, not uniform: 자바스크립트 and 시스템 each
leave a jamo undoubled, which is a repaint-region boundary artifact rather
than a per-character transform.
The discriminating arm is in the test rather than a source mutation:
be3f30e2f8 (#6890) elects a repaint for all 17 corrupted runs when the
agent types nothing, and reverting its disjunct elects none. Both
predicates agree once the user has recently typed, which is the pre-#6890
condition.
Samples inlined with per-sample SHA-256 because .tmp is gitignored and
cannot back a landed test.
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
terminalOutputContainsEastAsianRendererRisk,
|
||||
windowsEastAsianOutputPrefersRenderRefresh
|
||||
} from './terminal-complex-script'
|
||||
|
||||
/**
|
||||
* #12164 comment 1 / #5921: agent output containing double-width glyphs renders
|
||||
* duplicated character-by-character, while ASCII in the same line stays clean.
|
||||
* No IME, no composition, no keystroke — the user never types the CJK; Codex (and
|
||||
* Antigravity CLI) emit it. The reporter characterised the dynamic as incremental
|
||||
* repaint: a block flashes duplicated, then normalises once the next repaint moves on.
|
||||
*
|
||||
* `be3f30e2f8d` ("Fix Windows CJK terminal repaint and ConPTY wrap markers", PR #6890)
|
||||
* is the named owner. Its whole delta on this path is a second, independent disjunct:
|
||||
* before it, a wide-glyph chunk elected a repaint only when the user had recently
|
||||
* typed; after it, native ConPTY agent output elects one with no keystroke at all.
|
||||
*
|
||||
* Recorded output, transcribed verbatim from the issue threads (SHA-256 of the exact
|
||||
* UTF-8 literal above each pair). `.tmp/` is gitignored, so these are inlined rather
|
||||
* than loaded; they are the *actual*, matched here against the production detector.
|
||||
*
|
||||
* Split from `terminal-complex-script.test.ts` (406 lines against an 800-line cap) for
|
||||
* cohesion, not to stay under `max-lines`.
|
||||
*/
|
||||
|
||||
// #5921, StillHanWind, 2026-06-20, Orca v1.4.83, Windows/PowerShell, Codex CLI.
|
||||
// clean b6b001338d44efe028ef8253fdeec4da0e87fce52cfbc224ebd07cceed9b4ffb
|
||||
// actual d5c3ec6a9f54e48674d4c45defc7a327db2cf65f07e6b3be877e6f61b12aa014
|
||||
const CHINESE_CLEAN = '如果还在主 checkout,请创建前端/设计专用 worktree:'
|
||||
const CHINESE_CORRUPTED =
|
||||
'如如果果还还在在主主 checkout,,请请创创建建前前端端/设设计计专专用用 worktree::'
|
||||
|
||||
// #5921 reopen request, pobbye-star, 2026-07-25, Windows 11 build 26100 — i.e. after
|
||||
// #6890 shipped. Same shape in Korean.
|
||||
// clean 017d41fc33b39d24d73d0178eb93277cfa3528fe810ee23df5a3793a92d41cb4
|
||||
// actual 06fba393f10c436c0e84f658e85453732c982efc0e894c161b4a4cbe56f34984
|
||||
const KOREAN_HEADING_CLEAN = '### 1. 매뉴얼 분석 결과 [확정]'
|
||||
const KOREAN_HEADING_CORRUPTED = '### 1. 매매뉴뉴얼얼 분분석석 결결과과 [확확정정]'
|
||||
|
||||
// clean 04c3a0660e8921ce4202af3803eecbc9093a8e78863cd19fb960c3ea28af707f
|
||||
// actual 580a5fc229ea2d5b9879ee03430a53691c17948d679b5aa8cce67696279191cd
|
||||
const KOREAN_BODY_CLEAN =
|
||||
'제시해 주신 매뉴얼은 예스스트레이더의 자바스크립트(JavaScript) 기반 시스템 트레이딩/자동매매'
|
||||
const KOREAN_BODY_CORRUPTED =
|
||||
'제제시시해해 주주신신 매매뉴뉴얼얼은은 예예스스스스트트레레이이더더의의 자자바바스크크립립트트(JavaScript) 기기반반 시시스템템 트트레레이이딩딩/자자동동매매매매'
|
||||
|
||||
const RECORDED_OUTPUT = [
|
||||
{ issue: '#5921 zh-CN', clean: CHINESE_CLEAN, corrupted: CHINESE_CORRUPTED },
|
||||
{ issue: '#5921 ko heading', clean: KOREAN_HEADING_CLEAN, corrupted: KOREAN_HEADING_CORRUPTED },
|
||||
{ issue: '#5921 ko body', clean: KOREAN_BODY_CLEAN, corrupted: KOREAN_BODY_CORRUPTED }
|
||||
]
|
||||
|
||||
const MAX_INTERACTIVE_REDRAW_CHARS = 128 * 1024
|
||||
|
||||
/** Agent streaming its own output: native ConPTY, user has not touched the keyboard. */
|
||||
const UNTYPED_AGENT_OUTPUT = {
|
||||
isWindowsClient: true,
|
||||
isNativeWindowsConpty: true,
|
||||
hadRecentInput: false,
|
||||
maxInteractiveRedrawChars: MAX_INTERACTIVE_REDRAW_CHARS
|
||||
}
|
||||
|
||||
const TYPED_INPUT_ECHO = { ...UNTYPED_AGENT_OUTPUT, hadRecentInput: true }
|
||||
|
||||
/**
|
||||
* The predicate as it stood immediately before #6890, transcribed from
|
||||
* `git show be3f30e2f8d^:src/renderer/src/components/terminal-pane/pty-connection.ts`
|
||||
* lines 3091-3094. This is the mutation arm — reverting #6890 on this path is exactly
|
||||
* dropping the `isNativeWindowsConpty` disjunct.
|
||||
*/
|
||||
function pre6890PrefersRenderRefresh(data: string, state: typeof UNTYPED_AGENT_OUTPUT): boolean {
|
||||
return (
|
||||
state.isWindowsClient &&
|
||||
state.hadRecentInput &&
|
||||
data.length <= state.maxInteractiveRedrawChars &&
|
||||
terminalOutputContainsEastAsianRendererRisk(data)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a line into the maximal runs the PTY repaints as units, classified by the
|
||||
* production detector itself rather than by a width table of our own.
|
||||
*/
|
||||
function segmentByRendererRiskClass(line: string): { risky: boolean; text: string }[] {
|
||||
const runs: { risky: boolean; text: string }[] = []
|
||||
for (const character of line) {
|
||||
const risky = terminalOutputContainsEastAsianRendererRisk(character)
|
||||
const open = runs.at(-1)
|
||||
if (open && open.risky === risky) {
|
||||
open.text += character
|
||||
} else {
|
||||
runs.push({ risky, text: character })
|
||||
}
|
||||
}
|
||||
return runs
|
||||
}
|
||||
|
||||
describe('recorded CJK agent output vs the repaint detector', () => {
|
||||
it('corrupts a run if and only if the detector flags it, across all recorded output', () => {
|
||||
const verdicts: { corrupted: boolean; risky: boolean }[] = []
|
||||
for (const { issue, clean, corrupted } of RECORDED_OUTPUT) {
|
||||
const cleanRuns = segmentByRendererRiskClass(clean)
|
||||
const corruptedRuns = segmentByRendererRiskClass(corrupted)
|
||||
// Corruption never moves a run across the class boundary, so the two segmentations align.
|
||||
expect(
|
||||
corruptedRuns.map((run) => run.risky),
|
||||
issue
|
||||
).toEqual(cleanRuns.map((run) => run.risky))
|
||||
cleanRuns.forEach((run, index) => {
|
||||
verdicts.push({ corrupted: run.text !== corruptedRuns[index]?.text, risky: run.risky })
|
||||
})
|
||||
}
|
||||
|
||||
// Assert the precondition before the correspondence: both classes are actually present,
|
||||
// so neither half of the biconditional is vacuously satisfied.
|
||||
expect(verdicts.filter((verdict) => verdict.risky)).toHaveLength(17)
|
||||
expect(verdicts.filter((verdict) => !verdict.risky)).toHaveLength(16)
|
||||
|
||||
expect(verdicts.filter((verdict) => verdict.corrupted)).toHaveLength(17)
|
||||
for (const verdict of verdicts) {
|
||||
expect(verdict.corrupted).toBe(verdict.risky)
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves the ASCII in the same line byte-identical', () => {
|
||||
// The reporter's own negative: "English text such as `checkout` is not duplicated."
|
||||
// It is co-located with the corrupted CJK, not a separate run.
|
||||
const narrowRuns = RECORDED_OUTPUT.flatMap(({ clean, corrupted }) => {
|
||||
const corruptedRuns = segmentByRendererRiskClass(corrupted)
|
||||
return segmentByRendererRiskClass(clean)
|
||||
.map((run, index) => ({ ...run, after: corruptedRuns[index]?.text }))
|
||||
.filter((run) => !run.risky)
|
||||
})
|
||||
|
||||
expect(narrowRuns.map((run) => run.text)).toContain(' checkout')
|
||||
expect(narrowRuns.map((run) => run.text)).toContain('(JavaScript) ')
|
||||
for (const run of narrowRuns) {
|
||||
expect(run.after).toBe(run.text)
|
||||
expect(windowsEastAsianOutputPrefersRenderRefresh(run.text, UNTYPED_AGENT_OUTPUT)).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('does not double every character in a corrupted run', () => {
|
||||
// The doubling tracks the repaint region, not the character: `자바스크립트` loses one
|
||||
// `스` and `시스템` loses another. A test asserting uniform doubling would assert
|
||||
// something this evidence contradicts.
|
||||
const nonUniform = RECORDED_OUTPUT.flatMap(({ clean, corrupted }) => {
|
||||
const corruptedRuns = segmentByRendererRiskClass(corrupted)
|
||||
return segmentByRendererRiskClass(clean)
|
||||
.map((run, index) => ({ run, after: corruptedRuns[index]?.text ?? '' }))
|
||||
.filter(({ run, after }) => run.risky && after.length !== run.text.length * 2)
|
||||
})
|
||||
|
||||
expect(nonUniform.map(({ run }) => run.text)).toEqual(['자바스크립트', '시스템'])
|
||||
expect(nonUniform.map(({ after }) => after)).toEqual(['자자바바스크크립립트트', '시시스템템'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#6890 as the discriminating mutation', () => {
|
||||
const riskyRuns = (): { risky: boolean; text: string }[] =>
|
||||
RECORDED_OUTPUT.flatMap(({ clean }) =>
|
||||
segmentByRendererRiskClass(clean).filter((run) => run.risky)
|
||||
)
|
||||
|
||||
it('elects a repaint for every corrupted run when the agent types nothing', () => {
|
||||
const refreshed = riskyRuns().filter((run) =>
|
||||
windowsEastAsianOutputPrefersRenderRefresh(run.text, UNTYPED_AGENT_OUTPUT)
|
||||
)
|
||||
expect(refreshed).toHaveLength(17)
|
||||
})
|
||||
|
||||
it('elects none of them once #6890 is reverted', () => {
|
||||
const refreshed = riskyRuns().filter((run) =>
|
||||
pre6890PrefersRenderRefresh(run.text, UNTYPED_AGENT_OUTPUT)
|
||||
)
|
||||
// 17 -> 0. Untyped wide-glyph output was never repainted before #6890, which is the
|
||||
// condition #12164 comment 1 reports and the only condition the two arms disagree on.
|
||||
expect(refreshed).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('agrees with the reverted predicate whenever the user has recently typed', () => {
|
||||
for (const run of riskyRuns()) {
|
||||
expect(windowsEastAsianOutputPrefersRenderRefresh(run.text, TYPED_INPUT_ECHO)).toBe(
|
||||
pre6890PrefersRenderRefresh(run.text, TYPED_INPUT_ECHO)
|
||||
)
|
||||
}
|
||||
const refreshed = riskyRuns().filter((run) =>
|
||||
pre6890PrefersRenderRefresh(run.text, TYPED_INPUT_ECHO)
|
||||
)
|
||||
expect(refreshed).toHaveLength(17)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user