mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 08:04:05 +00:00
* fix(mobile): bundle terminal engine and show load errors instead of a blank pane The mobile terminal WebView loaded xterm.js from cdn.jsdelivr.net at runtime; old WebViews (< Chrome 85) fail to parse the modern bundle and blocked-CDN networks fail to fetch it, and the resulting error was silently dropped, leaving the pane permanently blank (#7030). Bundle the engine into the app via exact-pinned npm deps + a postinstall esbuild step (chrome74 target, guarded WeakRef/structuredClone/ replaceChildren shims) emitting a gitignored generated module, inline it into the terminal document, and surface fatal engine failures as a visible overlay with diagnostics and a Reload wired into the existing resubscribe path. Non-fatal errors log without covering a live terminal. Co-authored-by: Orca <help@stably.ai> * fix(mobile): add a native watchdog so a dead terminal document can't stay silently blank CodeRabbit round: if the webview document dies before the glue can post anything (or the RN message bridge never comes up), no error message and no native handler fires. Arm a 15s foreground-gated watchdog per document generation that paints the fatal overlay when web-ready never arrives; first fatal diagnostics win over later cascades. Extract the watchdog and the public contract types to keep TerminalWebView under the line cap, and document the SVG xmlns percent-encoding transform. Co-authored-by: Orca <help@stably.ai> * test(mobile): unmount TerminalWebView renderers so watchdog timers can't leak across tests Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
110 lines
4.7 KiB
TypeScript
110 lines
4.7 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { Script } from 'node:vm'
|
|
import { parse } from 'acorn'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { XTERM_ENGINE_CSS, XTERM_ENGINE_JS } from './terminal-webview-engine.generated'
|
|
import { XTERM_HTML } from './terminal-webview-html'
|
|
|
|
const terminalHtmlSource = readFileSync(
|
|
new URL('./terminal-webview-html.ts', import.meta.url),
|
|
'utf8'
|
|
)
|
|
|
|
describe('terminal WebView bundled engine', () => {
|
|
it('keeps the assembled terminal HTML free of external engine URLs', () => {
|
|
expect(XTERM_HTML).not.toMatch(/\bhttps?:\/\//)
|
|
expect(XTERM_HTML).not.toContain('cdn.jsdelivr.net')
|
|
expect(XTERM_HTML).not.toContain('<script src=')
|
|
expect(XTERM_HTML).not.toContain('rel="stylesheet" href=')
|
|
})
|
|
|
|
it('parses the bundled engine at the Chrome 74 syntax floor', () => {
|
|
expect(() => parse(XTERM_ENGINE_JS, { ecmaVersion: 2019 })).not.toThrow()
|
|
})
|
|
|
|
// Why: the context deliberately omits WeakRef (Chrome 84+) / structuredClone
|
|
// (Chrome 98+) and supplies an Element without replaceChildren (Chrome 86+) —
|
|
// the engine must evaluate on older WebViews via its own guarded runtime shims,
|
|
// which are the linchpin of the old-WebView support (esbuild lowers syntax only).
|
|
it('exposes the xterm globals and installs the old-WebView runtime shims', () => {
|
|
const window: Record<string, unknown> = {}
|
|
class ElementStub {}
|
|
const context = {
|
|
window,
|
|
self: window,
|
|
document: {},
|
|
Element: ElementStub,
|
|
navigator: {
|
|
platform: 'Linux armv8l',
|
|
userAgent: 'Mozilla/5.0 Chrome/74.0.3729.157'
|
|
},
|
|
console,
|
|
setTimeout,
|
|
clearTimeout,
|
|
queueMicrotask,
|
|
URL
|
|
}
|
|
|
|
new Script(XTERM_ENGINE_JS).runInNewContext(context)
|
|
|
|
expect(window).toMatchObject({
|
|
Terminal: expect.any(Function),
|
|
Unicode11Addon: { Unicode11Addon: expect.any(Function) },
|
|
WebglAddon: { WebglAddon: expect.any(Function) }
|
|
})
|
|
|
|
const weakRef = window.WeakRef as (new (target: unknown) => { deref(): unknown }) | undefined
|
|
expect(typeof weakRef).toBe('function')
|
|
const token = {}
|
|
expect(new weakRef!(token).deref()).toBe(token)
|
|
expect(typeof window.structuredClone).toBe('function')
|
|
expect(typeof (ElementStub.prototype as { replaceChildren?: unknown }).replaceChildren).toBe(
|
|
'function'
|
|
)
|
|
})
|
|
|
|
it('keeps the bundled engine from breaking out of its inline script/style tags', () => {
|
|
// Why: the engine JS/CSS are inlined into <script>/<style> blocks. </script
|
|
// and </style are neutralized at build time; the tokenizer-escape openers that
|
|
// could swallow the rest of the document must also be absent from the bundle.
|
|
expect(XTERM_ENGINE_JS).not.toMatch(/<\/script/i)
|
|
expect(XTERM_ENGINE_JS).not.toMatch(/<script/i)
|
|
expect(XTERM_ENGINE_JS).not.toContain('<!--')
|
|
expect(XTERM_ENGINE_CSS).not.toMatch(/<\/style/i)
|
|
})
|
|
|
|
it('reports WebView message handler failures instead of swallowing them', () => {
|
|
const start = terminalHtmlSource.indexOf('function handleIncomingMessage')
|
|
const end = terminalHtmlSource.indexOf("window.addEventListener('resize'", start)
|
|
expect(start).toBeGreaterThanOrEqual(0)
|
|
expect(end).toBeGreaterThan(start)
|
|
const handlerSource = terminalHtmlSource.slice(start, end)
|
|
|
|
expect(handlerSource).toContain('reportEngineError(')
|
|
expect(handlerSource).toContain("'terminal init failed'")
|
|
expect(handlerSource).toContain("'terminal message failed'")
|
|
expect(handlerSource).not.toContain('catch(ex) {}')
|
|
})
|
|
|
|
it('classifies runtime errors by a document-scoped ever-ready latch', () => {
|
|
// Why: init() flips `ready` false on every re-init (live width reflow keeps the
|
|
// old surface visible meanwhile), so the fatal default and the init-catch must
|
|
// key off `everReady` — otherwise a transient reflow error blanks a live
|
|
// terminal behind the fatal overlay. The latch stays set for the document.
|
|
expect(terminalHtmlSource).toContain('var everReady = false;')
|
|
expect(terminalHtmlSource).toContain('everReady = true;')
|
|
expect(terminalHtmlSource).toContain('fatal === undefined ? !everReady : !!fatal')
|
|
expect(terminalHtmlSource).toContain("msg.type === 'init' && !everReady")
|
|
expect(terminalHtmlSource).not.toMatch(/fatal === undefined \? !ready\b/)
|
|
})
|
|
|
|
it('bounds error capture and non-fatal reporting on a degraded engine', () => {
|
|
// Why: a constructed-but-broken engine can throw per render frame; both
|
|
// onerror capture sites must cap the buffer and non-fatal notifies must
|
|
// stop flooding RN while fatal reports always emit.
|
|
const capSites = terminalHtmlSource.match(/__engineErrors\.length < 20/g) ?? []
|
|
expect(capSites.length).toBe(2)
|
|
expect(terminalHtmlSource).toContain('nonFatalErrorNotifies > 5')
|
|
})
|
|
})
|