From f4b354502522ee8dc12928dcfdee41545eacdd6f Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 1 Aug 2026 03:18:26 -0700 Subject: [PATCH] test(ime): add a local trace recorder for platforms CI cannot cover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CDP-dispatched keystrokes bypass the OS input-method layer, so no amount of page.keyboard.type() produces a real composition. The Linux matrix drives ibus through X11, but there is no macOS or Windows IME CI, and on those platforms a human typing into a page is the only way to get a trace that can honestly be labelled recorded. pnpm ime:record serves a localhost page that captures the same field set as tests/e2e/terminal-ime-boundary-probe.ts — including the selection offsets at each event, which are what distinguish a recorded trace from a reconstructed one — and writes it in the shape the fixture loader already consumes. Nothing leaves the machine. The engine name and the description of what the human typed are left as TODOs in the output, because the page cannot observe either. Co-authored-by: Orca --- config/scripts/record-ime-trace.mjs | 261 ++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 262 insertions(+) create mode 100644 config/scripts/record-ime-trace.mjs diff --git a/config/scripts/record-ime-trace.mjs b/config/scripts/record-ime-trace.mjs new file mode 100644 index 00000000000..32a6fec66f7 --- /dev/null +++ b/config/scripts/record-ime-trace.mjs @@ -0,0 +1,261 @@ +// Captures a real IME composition trace from your own machine and your own input +// method, in the exact JSON shape src/renderer/src/lib/*.test-fixtures.ts consumes. +// +// Why this exists: CDP-dispatched keystrokes bypass the OS input-method layer, so no +// amount of page.keyboard.type() produces a real composition. The Linux CI matrix +// drives ibus through X11 and covers hangul/libpinyin/anthy/unikey, but there is no +// macOS or Windows IME CI. On those platforms a human typing into this page is the +// only way to get a trace that is honestly labelled `recorded`. +// +// pnpm ime:record +// +// Then open the printed URL, switch to the input method you want to characterize, +// type, and press Save. Everything runs on localhost and nothing is uploaded. + +import { createServer } from 'node:http' +import { mkdirSync, writeFileSync } from 'node:fs' +import path from 'node:path' + +const projectDir = path.resolve(import.meta.dirname, '../..') +const outputDir = path.join(projectDir, 'test-results', 'ime-recordings') +const host = '127.0.0.1' +const port = Number(process.env.ORCA_IME_RECORDER_PORT ?? 7331) + +// Kept identical to tests/e2e/terminal-ime-boundary-probe.ts so traces captured here +// and traces captured in CI deserialize through the same loader. +const RECORDED_EVENT_TYPES = [ + 'compositionstart', + 'compositionupdate', + 'compositionend', + 'beforeinput', + 'input', + 'keydown', + 'keypress', + 'keyup' +] + +const PAGE = /* html */ ` + + + +Orca IME trace recorder + + + +

Orca IME trace recorder

+

+ Switch to the input method you want to characterize, then compose in the box below. + Every composition, input and key event is captured together with the textarea's + value and selection offsets at that exact instant — the offsets are what make a + trace recorded rather than reconstructed. +

+

+ Type the sequence you care about, including the committing key and any candidate + navigation. Then name it after the behavior it demonstrates and press Save. +

+ + + +
+ + +
+
+ + + 0 events + +
+ +
+ + + + +` + +function inferEnvironment(userAgent) { + const platform = userAgent.includes('Mac') + ? 'darwin' + : userAgent.includes('Windows') + ? 'win32' + : 'linux' + // Electron reports a Chrome token too, so WebKit and Gecko are checked first. + const browser = userAgent.includes('Firefox') + ? 'gecko' + : userAgent.includes('Chrome') + ? 'chromium' + : 'webkit' + return { browser, platform } +} + +function toTraceFile(payload) { + const { browser, platform } = inferEnvironment(payload.userAgent ?? '') + return { + committed: payload.committed ?? '', + env: { + browser, + // Filled in by hand: the page cannot see which input method produced the events. + engine: 'TODO: name the input method, e.g. macOS Pinyin - Simplified', + platform + }, + events: payload.events, + final: payload.final, + initial: payload.initial, + name: payload.name, + origin: + `Recorded by config/scripts/record-ime-trace.mjs on ${platform}. ` + + `User agent: ${payload.userAgent ?? 'unknown'}. ` + + 'TODO: describe what the human typed and the issue this belongs to.', + provenance: 'recorded' + } +} + +function readBody(request) { + return new Promise((resolve, reject) => { + const chunks = [] + request.on('data', (chunk) => chunks.push(chunk)) + request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) + request.on('error', reject) + }) +} + +const server = createServer(async (request, response) => { + if (request.method === 'GET' && (request.url === '/' || request.url === '/index.html')) { + response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }) + response.end(PAGE) + return + } + if (request.method === 'POST' && request.url === '/trace') { + try { + const payload = JSON.parse(await readBody(request)) + const safeName = payload.name.replaceAll(/[^a-z0-9]+/gi, '-').toLowerCase() + mkdirSync(outputDir, { recursive: true }) + const filePath = path.join(outputDir, `${safeName}.json`) + writeFileSync(filePath, `${JSON.stringify(toTraceFile(payload), null, 2)}\n`) + const relative = path.relative(projectDir, filePath) + console.log(`[ime-recorder] wrote ${relative} (${payload.events.length} events)`) + response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }) + response.end(relative) + } catch (error) { + console.error('[ime-recorder] failed to save trace:', error) + response.writeHead(500) + response.end('save failed') + } + return + } + response.writeHead(404) + response.end() +}) + +server.listen(port, host, () => { + console.log(`[ime-recorder] open http://${host}:${port}/`) + console.log(`[ime-recorder] traces are written to ${path.relative(projectDir, outputDir)}`) + console.log('[ime-recorder] press Ctrl+C when finished') +}) diff --git a/package.json b/package.json index 2a2708c44d7..da00d8f2a68 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "win-crash-survival-e2e": "node tests/tools/win-crash-survival-e2e/run.mjs", "test:e2e:ssh-codex-artifacts-repro": "node config/scripts/run-ssh-codex-artifacts-repro-e2e.mjs", "test:e2e:headful": "pnpm run ensure:electron-runtime && npx playwright test --config tests/playwright.config.ts --project electron-headful", + "ime:record": "node config/scripts/record-ime-trace.mjs", "test:e2e:terminal-ime-native": "node config/scripts/run-terminal-ibus-engine-e2e.mjs", "test:e2e:computer": "vitest run --config tests/e2e/vitest.config.ts", "bench:idle-cpu": "pnpm run ensure:electron-runtime && node config/scripts/run-idle-cpu-benchmark.mjs",