Files
orca/src/shared/ipc-invoke-envelope-single-source.test.ts
T
Brennan Benson b2eb055a9c refactor(ipc): make the canonical envelope stripper the only one
#17230 added stripIpcInvokeEnvelope as a canonical home for Electron's IPC
wrapper. It was the sixth implementation, not the first: nine other files
carried ten hand-rolled copies, and they disagreed.

Measured against a shared corpus, the copies split four ways. An envelope whose
tail has no "Error: " prefix — Electron builds that tail from the main side's
error.toString(), so a rejected non-Error produces one — was left fully visible
by quick-open. A message-less handler failure rendered the bare word "Error" in
the Linux recovery card, the voice download toast and the AI Vault scan row; an
empty tail rendered an empty string in all three. Every copy was anchored at ^,
so an envelope a caller had prefixed with its own context stayed on screen. The
two AccountsPane copies were scoped to their own channel, so a rejection from
any other channel kept its wrapper.

All ten now route through one stripper, moved to src/shared because
ai-vault-scan-error-message is imported by main and cannot reach a renderer-only
module. The canonical regex additionally covers "Error occurred in handler for",
which AccountsPane stripped and the canonical one did not, and a separate
stripErrorClassPrefix keeps the bare "Error: " trim that three sites had — that
prefix is Error.prototype.toString(), not the envelope, and an existing Linux
card test caught its loss.

Nothing is swallowed. The three sites that gained a null fallback now
console.warn the original rejection, which none of them logged before, and
Electron still logs the handler's original error with its stack in main.

extractIpcErrorMessage keeps its own fail-open regex and its 18 call sites are
untouched: it returns the tail verbatim and never returns null.

A census test pins the envelope to the two files that own it, so a seventh copy
fails CI. It lists nine offenders against the pre-fix tree.
2026-08-29 20:12:30 -07:00

53 lines
2.4 KiB
TypeScript

import { readdirSync, readFileSync } from 'node:fs'
import { join, relative, sep } from 'node:path'
import { describe, expect, it } from 'vitest'
// Why: the envelope is one wire format, and every hand-rolled copy of it is a chance to
// disagree. Six independent copies had already drifted — some kept a trailing `Error:` class
// name, some scoped themselves to a single channel, and three rendered "Error" or an empty
// string when the envelope carried no reason. This census is what stops a seventh appearing.
const ENVELOPE_OWNERS = [
// The canonical stripper. The only place the wire format is described.
'src/shared/ipc-invoke-envelope.ts',
// `extractIpcErrorMessage` keeps its own fail-open regex on purpose: it returns the tail
// verbatim (class name included) and never returns null, which 18 call sites rely on.
'src/renderer/src/lib/ipc-error.ts'
]
const REPO_ROOT = join(__dirname, '..', '..')
function listSourceFiles(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const fullPath = join(dir, entry.name)
if (entry.isDirectory()) {
return listSourceFiles(fullPath)
}
if (!/\.(ts|tsx)$/.test(entry.name) || /\.test\.(ts|tsx)$/.test(entry.name)) {
return []
}
return [fullPath]
})
}
// Why: strip comments first, so a file that only *documents* the envelope (the terminal toast
// explains that its markers arrive IPC-wrapped) is not counted as a second implementation.
function stripComments(source: string): string {
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
}
// Why: match the bare Electron names rather than a quoted-channel shape — the canonical file
// spells them as an alternation, and a future copy could too. Prose lives in comments, which
// are stripped above, so any surviving mention is code that knows the wire format.
const ENVELOPE_MATCHER = /Error invoking remote method|Error occurred in handler for/
describe('IPC invoke envelope has a single source of truth', () => {
it('is described in exactly the files that own it', () => {
const offenders = listSourceFiles(join(REPO_ROOT, 'src'))
.filter((file) => ENVELOPE_MATCHER.test(stripComments(readFileSync(file, 'utf8'))))
.map((file) => relative(REPO_ROOT, file).split(sep).join('/'))
.sort()
expect(offenders).toEqual([...ENVELOPE_OWNERS].sort())
})
})