merge main into fix/reveal-structured-chat-from-history

Main landed #18906, which repairs the same epoch-fencing failure from the other
end: when the fence drops a frame it now schedules one bounded authoritative
census, and only that census may revive an epoch. This branch prevents the
wrongful retirement instead — a sentinel or a retraction is no longer recorded
as the current publication.

They compose. Prevention keeps the common case from ever reaching the fence, so
no census round trip and no window where the chat is invisible; the repair lane
still covers a retirement this branch does not prevent, such as a publisher
returning under an epoch it inherited. Kept both sides of the one textual
conflict: the retraction type guard and the census options.
This commit is contained in:
Merge Sim
2026-09-06 00:17:48 -07:00
347 changed files with 21610 additions and 2021 deletions
@@ -0,0 +1,110 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { stripTypeScriptTypes } from 'node:module'
import { performance } from 'node:perf_hooks'
// Run from the worktree root: node config/scripts/benchmark-browser-tunnel-framing.mjs [base-ref]
const path = 'src/shared/browser-network-tunnel-stream-framing.ts'
const baselineRef = process.argv[2] ?? 'HEAD'
const beforeSource = execFileSync('git', ['show', `${baselineRef}:${path}`], {
encoding: 'utf8'
})
const afterSource = readFileSync(path, 'utf8')
const load = (source) =>
import(
`data:text/javascript;base64,${Buffer.from(
stripTypeScriptTypes(source, { mode: 'transform' })
).toString('base64')}`
)
const before = await load(beforeSource)
const after = await load(afterSource)
function measure(module, chunks, payload, repetitions) {
let frameCount = 0
let lastFrame
const onFrame = (frame) => {
frameCount++
lastFrame = frame
}
const onError = (error) => {
throw error
}
const run = () => {
const decoder = new module.BrowserNetworkTunnelStreamFrameDecoder(onFrame, onError)
for (const chunk of chunks) {
decoder.feed(chunk)
}
}
run()
assert.deepEqual(lastFrame, payload)
const samples = []
for (let sample = 0; sample < 5; sample++) {
const start = performance.now()
for (let iteration = 0; iteration < repetitions; iteration++) {
run()
}
samples.push((performance.now() - start) / repetitions)
}
assert.equal(frameCount, 1 + 5 * repetitions)
return samples.sort((a, b) => a - b)[2]
}
function countCopies(module, chunks) {
const originalSet = Uint8Array.prototype.set
const originalSlice = Uint8Array.prototype.slice
let copied = 0
Uint8Array.prototype.set = function (source, offset) {
copied += source.length
return originalSet.call(this, source, offset)
}
Uint8Array.prototype.slice = function (...args) {
const result = originalSlice.apply(this, args)
copied += result.length
return result
}
try {
const decoder = new module.BrowserNetworkTunnelStreamFrameDecoder(
() => {},
(error) => {
throw error
}
)
for (const chunk of chunks) {
decoder.feed(chunk)
}
} finally {
Uint8Array.prototype.set = originalSet
Uint8Array.prototype.slice = originalSlice
}
return copied
}
const rows = []
for (const [payloadBytes, chunkBytes, repetitions] of [
[1, 5, 10000],
[64 * 1024, 65540, 1000],
[64 * 1024, 4096, 100],
[64 * 1024, 256, 25],
[64 * 1024, 16, 5],
[64 * 1024, 1, 1]
]) {
const payload = Uint8Array.from({ length: payloadBytes }, (_, index) => index % 251)
const encoded = before.encodeBrowserNetworkTunnelStreamFrame(payload)
const chunks = []
for (let offset = 0; offset < encoded.length; offset += chunkBytes) {
chunks.push(encoded.subarray(offset, offset + chunkBytes))
}
const beforeMs = measure(before, chunks, payload, repetitions)
const afterMs = measure(after, chunks, payload, repetitions)
rows.push({
payloadBytes,
chunkBytes,
beforeMs: +beforeMs.toFixed(6),
afterMs: +afterMs.toFixed(6),
speedup: +(beforeMs / afterMs).toFixed(2),
beforeCopiedBytes: countCopies(before, chunks),
afterCopiedBytes: countCopies(after, chunks)
})
}
console.log(JSON.stringify({ node: process.version, baselineRef, rows }, null, 2))
@@ -0,0 +1,121 @@
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import { existsSync, realpathSync } from 'node:fs'
import { delimiter, join, resolve } from 'node:path'
// Emit each revision with tsc -p config/tsconfig.cli.json --outDir <dir> --composite false --incremental false.
// Run: node config/scripts/benchmark-cli-error-imports.mjs <before-dir> <after-dir>
const [beforeDir, afterDir] = process.argv.slice(2)
assert.ok(beforeDir && afterDir, 'Pass distinct before and after TypeScript output directories.')
assert.notEqual(
realpathSync(beforeDir),
realpathSync(afterDir),
'Do not compare a build to itself.'
)
const entries = {
before: join(resolve(beforeDir), 'cli', 'index.js'),
after: join(resolve(afterDir), 'cli', 'index.js')
}
for (const entry of Object.values(entries)) {
assert.ok(existsSync(entry), `Missing emitted CLI: ${entry}`)
}
const { runProcessSync } = createRequire(import.meta.url)(
join(resolve(afterDir), 'shared', 'child-process', 'run-process.js')
)
const child = String.raw`
const { performance } = require('node:perf_hooks')
const { writeSync } = require('node:fs')
const { createHash } = require('node:crypto')
const { basename } = require('node:path')
let stdout = '', stderr = ''
process.stdout.write = (text) => { stdout += text; return true }
process.stderr.write = (text) => { stderr += text; return true }
const started = performance.now()
const cli = require(process.argv[1])
const importMs = performance.now() - started
cli.main(JSON.parse(process.argv[2])).then(() => {
const totalMs = performance.now() - started
const modules = Object.keys(require.cache)
writeSync(1, JSON.stringify({
importMs, totalMs, modules: modules.length,
featureFormatters: modules.filter((file) => ['browser', 'terminal', 'project', 'automation', 'workspace', 'computer'].some((name) => basename(file) === name + '-format.js')),
stdout: createHash('sha256').update(stdout).digest('hex'),
stderr: createHash('sha256').update(stderr).digest('hex'),
exitCode: process.exitCode || 0
}))
process.exitCode = 0
}).catch((error) => { writeSync(2, String(error)); process.exitCode = 1 })
`
const cases = [
['--help'],
['help', 'terminal', 'read'],
['does-not-exist'],
['computer', 'click', '--does-not-exist'],
['does-not-exist', '--json']
]
const median = (values) => [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)]
const summarize = (samples) => ({
importMs: median(samples.map((sample) => sample.importMs)),
totalMs: median(samples.map((sample) => sample.totalMs)),
modules: samples[0].modules
})
const rows = []
for (const args of cases) {
const samples = { before: [], after: [] }
let expected
for (let run = 0; run < 22; run++) {
for (const variant of run % 2 ? ['after', 'before'] : ['before', 'after']) {
const result = runProcessSync({
program: process.execPath,
args: ['-e', child, entries[variant], JSON.stringify(args)],
timeoutMs: 30_000,
env: {
...process.env,
NODE_PATH: [resolve('node_modules'), process.env.NODE_PATH]
.filter(Boolean)
.join(delimiter)
}
})
assert.equal(result.timedOut, false, 'CLI child timed out.')
assert.equal(result.code, 0, result.stderr)
const sample = JSON.parse(result.stdout)
const output = { stdout: sample.stdout, stderr: sample.stderr, exitCode: sample.exitCode }
expected ??= output
assert.deepEqual(output, expected, `${variant} output changed for ${args.join(' ')}`)
if (variant === 'after') {
assert.deepEqual(
sample.featureFormatters,
[],
'Help and syntax errors must skip feature formatters.'
)
}
if (run >= 2) {
samples[variant].push(sample)
}
}
}
assert.ok(samples.after[0].modules < samples.before[0].modules, 'Expected fewer loaded modules.')
rows.push({
args,
before: summarize(samples.before),
after: summarize(samples.after),
output: expected,
samples
})
}
console.log(
JSON.stringify(
{
node: process.version,
platform: process.platform,
measurement:
'Fresh-process import + main; excludes process creation; warmed filesystem; 2 warmups and 20 samples per variant, alternating order.',
entries,
rows
},
null,
2
)
)
@@ -0,0 +1,128 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { EventEmitter } from 'node:events'
import { readFileSync } from 'node:fs'
import Module from 'node:module'
import { dirname, resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { build } from 'esbuild'
// Run from the worktree root: node config/scripts/benchmark-cli-response-framing.mjs <base-ref>
const sourcePath = 'src/cli/runtime/transport.ts'
const baselineRef = process.argv[2]
assert.ok(baselineRef, 'Pass the pre-change transport revision as base-ref.')
const beforeSource = execFileSync('git', ['show', `${baselineRef}:${sourcePath}`], {
encoding: 'utf8'
})
let chunks = []
async function loadTransport(source) {
const built = await build({
stdin: { contents: source, loader: 'ts', resolveDir: dirname(resolve(sourcePath)) },
bundle: true,
platform: 'node',
format: 'cjs',
write: false,
logLevel: 'silent'
})
const module = new Module(resolve(sourcePath))
const originalRequire = module.require.bind(module)
module.require = (name) => {
if (name === 'node:crypto') {
return { randomUUID: () => 'benchmark-request' }
}
if (name !== 'node:net') {
return originalRequire(name)
}
return {
createConnection() {
const socket = new EventEmitter()
socket.setEncoding = () => {}
socket.end = () => {}
socket.destroy = () => {}
socket.write = () => {
for (const chunk of chunks) {
socket.emit('data', chunk)
}
}
queueMicrotask(() => socket.emit('connect'))
return socket
}
}
}
module._compile(built.outputFiles[0].text, resolve(sourcePath))
return module.exports.sendRequest
}
const before = await loadTransport(beforeSource)
const after = await loadTransport(readFileSync(sourcePath, 'utf8'))
const metadata = {
runtimeId: 'benchmark-runtime',
authToken: 'benchmark-token',
transports: [{ kind: 'unix', endpoint: 'injected-socket' }]
}
const run = (sendRequest) => sendRequest(metadata, 'terminal.read', {}, 30000)
async function measure(sendRequest, payloadBytes, repetitions) {
const warmup = await run(sendRequest)
assert.equal(warmup.result.data.length, payloadBytes)
const samples = []
for (let sample = 0; sample < 5; sample++) {
const start = performance.now()
for (let iteration = 0; iteration < repetitions; iteration++) {
await run(sendRequest)
}
samples.push((performance.now() - start) / repetitions)
}
return samples.sort((a, b) => a - b)[2]
}
async function searchedCharacters(sendRequest) {
const original = String.prototype.indexOf
let searched = 0
String.prototype.indexOf = function (needle, position) {
if (needle === '\n') {
searched += this.length - (position ?? 0)
}
return original.call(this, needle, position)
}
try {
await run(sendRequest)
} finally {
String.prototype.indexOf = original
}
return searched
}
const rows = []
for (const [payloadBytes, chunkChars, repetitions] of [
[32, 65536, 1000],
[1024 * 1024, 2 * 1024 * 1024, 20],
[1024 * 1024, 65536, 10],
[1024 * 1024, 4096, 5],
[4 * 1024 * 1024, 4096, 2],
[4 * 1024 * 1024, 256, 1]
]) {
const line = `${JSON.stringify({
id: 'benchmark-request',
ok: true,
result: { data: 'x'.repeat(payloadBytes) },
_meta: { runtimeId: 'benchmark-runtime' }
})}\n`
chunks = []
for (let offset = 0; offset < line.length; offset += chunkChars) {
chunks.push(line.slice(offset, offset + chunkChars))
}
const beforeMs = await measure(before, payloadBytes, repetitions)
const afterMs = await measure(after, payloadBytes, repetitions)
rows.push({
payloadBytes,
chunkChars,
beforeMs: +beforeMs.toFixed(6),
afterMs: +afterMs.toFixed(6),
speedup: +(beforeMs / afterMs).toFixed(2),
beforeSearchedCharacters: await searchedCharacters(before),
afterSearchedCharacters: await searchedCharacters(after)
})
}
console.log(JSON.stringify({ node: process.version, baselineRef, rows }, null, 2))
@@ -0,0 +1,165 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import Module from 'node:module'
import { resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { build } from 'esbuild'
// Pass the pre-change file-explorer-entries.ts snapshot as the only argument.
const baselinePath = process.argv[2]
assert.ok(baselinePath, 'Pass a pre-change file-explorer-entries.ts snapshot.')
const entry = 'src/renderer/src/components/right-sidebar/file-explorer-entries.ts'
const baseline = readFileSync(baselinePath, 'utf8')
assert.notEqual(baseline, readFileSync(entry, 'utf8'), 'Do not compare the source to itself.')
async function load(useBaseline) {
const result = await build({
stdin: {
contents: `export { isDotfileRelativePath } from './${entry}';
export { createNameFilteredFileExplorerProjection } from './src/renderer/src/components/right-sidebar/file-explorer-name-filter-projection.ts';`,
resolveDir: process.cwd(),
loader: 'ts'
},
bundle: true,
platform: 'node',
format: 'cjs',
write: false,
logLevel: 'silent',
alias: { '@': resolve('src/renderer/src') },
plugins: useBaseline
? [
{
name: 'baseline-dotfile-predicate',
setup(builder) {
builder.onLoad({ filter: /file-explorer-entries\.ts$/ }, () => ({
contents: baseline,
loader: 'ts'
}))
}
}
]
: []
})
const module = new Module(resolve('dotfile-benchmark.cjs'))
module.paths = Module._nodeModulePaths(process.cwd())
module._compile(result.outputFiles[0].text, module.id)
return module.exports
}
const versions = [await load(true), await load(false)]
let parityCases = 0
function check(path, depth) {
assert.equal(
versions[0].isDotfileRelativePath(path),
versions[1].isDotfileRelativePath(path),
path
)
parityCases++
if (depth > 0) {
for (const character of ['.', '/', '\\', 'a', '\n']) {
check(path + character, depth - 1)
}
}
}
check('', 8)
function measure(functions, iterations = 1) {
let sink = 0
const run = (fn) => {
for (let i = 0; i < iterations; i++) {
sink += Number(fn())
}
}
for (const fn of functions) {
for (let warmup = 0; warmup < 3; warmup++) {
run(fn)
}
}
const samples = [[], []]
for (let round = 0; round < 11; round++) {
for (const variant of round % 2 ? [1, 0] : [0, 1]) {
const start = performance.now()
run(functions[variant])
samples[variant].push(performance.now() - start)
}
}
return {
beforeMs: samples[0].sort((a, b) => a - b)[5],
afterMs: samples[1].sort((a, b) => a - b)[5],
iterations,
sink
}
}
const predicates = []
for (const path of [
'a',
'.env',
'packages/pkg/src/file.tsx',
`a${'.'.repeat(254)}`,
`${'/'.repeat(4096)}.`,
`${'../'.repeat(1000)}file.ts`,
'😀/.你好',
'\n/.\n'
]) {
check(path, 0)
predicates.push({
pathLength: path.length,
prefix: path.slice(0, 40),
...measure(
versions.map((version) => () => version.isDotfileRelativePath(path)),
10_000
)
})
}
const projections = []
for (const count of [1000, 10_000, 100_000]) {
for (const query of ['nonmatching-needle', 'file-42']) {
const args = {
ignoredSet: new Set(['unrelated']),
nameFilter: {
query,
relativePaths: Array.from(
{ length: count },
(_, i) => `packages/package-${i % 50}/src/components/section-${i % 10}/file-${i}.tsx`
)
},
showDotfiles: false,
showGitIgnoredFiles: false,
worktreePath: '/workspace'
}
const functions = versions.map(
(version) => () => version.createNameFilteredFileExplorerProjection(args)
)
const rows = functions.map((fn) => {
const projection = fn()
return Array.from({ length: projection.getVisibleCount() }, (_, i) =>
projection.getRowAtIndex(i)
)
})
assert.deepEqual(rows[0], rows[1])
projections.push({
count,
query,
visibleRows: rows[0].length,
...measure(functions.map((fn) => () => fn().getVisibleCount()))
})
}
}
console.log(
JSON.stringify(
{
node: process.version,
platform: process.platform,
baselinePath: resolve(baselinePath),
parityCases,
samples: 11,
warmups: 3,
predicates,
projections
},
null,
2
)
)
@@ -0,0 +1,72 @@
import { strict as assert } from 'node:assert'
import { EventEmitter } from 'node:events'
import { mkdtemp, rm } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { build } from 'esbuild'
if (!global.gc) {
throw new Error('Run with node --expose-gc')
}
const root = resolve(import.meta.dirname, '../..')
const directory = await mkdtemp(join(tmpdir(), 'orca-sentinel-retention-'))
const output = join(directory, 'sentinel.cjs')
try {
await build({
stdin: {
contents: `export {waitForSentinel} from './src/main/ssh/ssh-relay-deploy-helpers';
export {RELAY_SENTINEL} from './src/main/ssh/relay-protocol';`,
resolveDir: root,
loader: 'ts'
},
bundle: true,
platform: 'node',
format: 'cjs',
packages: 'external',
banner: {
js: `var require = require('node:module').createRequire(${JSON.stringify(join(root, 'package.json'))});`
},
outfile: output
})
const { waitForSentinel, RELAY_SENTINEL } = createRequire(import.meta.url)(output)
const held = []
const banners = []
for (let i = 0; i < 100; i++) {
const channel = Object.assign(new EventEmitter(), {
stderr: new EventEmitter(),
stdin: { write: () => true },
close: () => {}
})
const pending = waitForSentinel(channel)
banners.push(feedBanner(channel))
channel.emit('data', Buffer.from(RELAY_SENTINEL))
const transport = await pending
const received = []
transport.onData((bytes) => received.push(bytes.toString()))
channel.emit('data', Buffer.from('frame'))
assert.deepEqual(received, ['frame'])
held.push({ channel, transport })
}
await new Promise((resolve) => setImmediate(resolve))
for (let i = 0; i < 5; i++) {
global.gc()
}
const retained = banners.filter((reference) => reference.deref() !== undefined).length
console.log(
JSON.stringify({
connections: held.length,
bannerBytes: 65536,
retainedBannerBuffers: retained,
retainedBannerBytes: retained * 65536
})
)
} finally {
await rm(directory, { recursive: true, force: true })
}
function feedBanner(channel) {
const banner = Buffer.alloc(65536, 120)
channel.emit('data', banner)
return new WeakRef(banner.buffer)
}
+122
View File
@@ -0,0 +1,122 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import * as fs from 'node:fs/promises'
import Module from 'node:module'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { build } from 'esbuild'
// Pass a pre-change skill-root-file-walk.ts snapshot as the only argument.
const baselinePath = process.argv[2]
const brokenLinks = process.argv.includes('--broken')
assert.ok(baselinePath, 'Pass a pre-change skill-root-file-walk.ts snapshot.')
const entry = 'src/main/skills/skill-root-file-walk.ts'
const baseline = readFileSync(baselinePath, 'utf8')
assert.notEqual(baseline, readFileSync(entry, 'utf8'), 'Do not compare the source to itself.')
let statCalls = 0
async function load(useBaseline) {
const result = await build({
entryPoints: [entry],
bundle: true,
platform: 'node',
format: 'cjs',
write: false,
logLevel: 'silent',
plugins: useBaseline
? [
{
name: 'baseline-skill-depth',
setup(builder) {
builder.onLoad({ filter: /skill-root-file-walk\.ts$/ }, () => ({
contents: baseline,
loader: 'ts'
}))
}
}
]
: []
})
const module = new Module(resolve('skill-depth-benchmark.cjs'))
module.paths = Module._nodeModulePaths(process.cwd())
const originalRequire = module.require.bind(module)
module.require = (name) =>
name === 'node:fs/promises'
? {
...fs,
stat: (...args) => {
statCalls++
return fs.stat(...args)
}
}
: originalRequire(name)
module._compile(result.outputFiles[0].text, module.id)
return module.exports.findSkillFiles
}
const before = await load(true)
const after = await load(false)
const median = (values) => values.sort((a, b) => a - b)[Math.floor(values.length / 2)]
const temporaryRoot = await fs.mkdtemp(join(tmpdir(), 'orca-skill-depth-benchmark-'))
try {
for (const links of [0, 8, 100, 1000]) {
const root = join(temporaryRoot, String(links))
const edge = join(root, 'a', 'b', 'c', 'd')
const target = join(temporaryRoot, 'target')
await fs.mkdir(edge, { recursive: true })
await fs.mkdir(target, { recursive: true })
await fs.writeFile(join(target, 'SKILL.md'), 'skill')
await fs.writeFile(join(edge, 'SKILL.md'), 'edge')
for (let index = 0; index < links; index++) {
await fs.symlink(
brokenLinks ? join(target, 'missing') : target,
join(edge, `link${index}`),
process.platform === 'win32' ? 'junction' : 'dir'
)
}
for (const depth of [4, 5]) {
const timings = { before: [], after: [] }
const counts = {}
let rows
for (let sample = 0; sample < 13; sample++) {
const versions =
sample % 2
? [
['after', after],
['before', before]
]
: [
['before', before],
['after', after]
]
for (const [name, walk] of versions) {
statCalls = 0
const start = performance.now()
const result = await walk(root, depth)
const elapsed = performance.now() - start
if (rows) {
assert.deepEqual(result, rows)
}
rows = result
counts[name] = statCalls
if (sample >= 2) {
timings[name].push(elapsed)
}
}
}
console.log(
JSON.stringify({
links,
brokenLinks,
depth,
statCalls: counts,
rows: rows.length,
medianMs: { before: median(timings.before), after: median(timings.after) }
})
)
}
}
} finally {
await fs.rm(temporaryRoot, { recursive: true, force: true })
}
@@ -0,0 +1,80 @@
import { strict as assert } from 'node:assert'
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { build } from 'esbuild'
const root = resolve(import.meta.dirname, '../..')
const source = join(root, 'src/renderer/src/store/slices/tab-group-reference-repair.ts')
const directory = await mkdtemp(join(tmpdir(), 'orca-tab-repair-'))
const current = await readFile(source, 'utf8')
const indexed = `const orderedTabIds = new Set(group.tabOrder)
const missingTabIds = ownedTabIds.filter((tabId) => !orderedTabIds.has(tabId))`
assert(current.includes(indexed), 'Expected indexed implementation')
try {
const implementations = []
for (const baseline of [true, false]) {
const outfile = join(directory, baseline ? 'before.cjs' : 'after.cjs')
await build({
stdin: {
contents: baseline
? current.replace(
indexed,
'const missingTabIds = ownedTabIds.filter((tabId) => !group.tabOrder.includes(tabId))'
)
: current,
resolveDir: resolve(source, '..'),
loader: 'ts'
},
bundle: true,
platform: 'node',
format: 'cjs',
outfile,
alias: { '@': join(root, 'src/renderer/src') }
})
implementations.push(createRequire(import.meta.url)(outfile).appendOwnedTabIdsToGroups)
}
const rows = []
for (const count of [1, 10, 100, 1_000, 10_000]) {
for (const missing of [false, true]) {
const ids = Array.from({ length: count }, (_, i) => `tab-${i}`)
const groups = [
{ id: 'group', worktreeId: 'workspace', activeTabId: null, tabOrder: ids, recentTabIds: [] }
]
const owners = new Map(ids.map((id) => [missing ? `missing-${id}` : id, 'group']))
assert.deepEqual(implementations[0](groups, owners), implementations[1](groups, owners))
const iterations = Math.max(1, Math.floor(10_000 / count))
const samples = [[], []]
for (let sample = -3; sample < 11; sample++) {
for (const index of sample % 2 === 0 ? [0, 1] : [1, 0]) {
const start = performance.now()
for (let i = 0; i < iterations; i++) {
implementations[index](groups, owners)
}
const elapsed = (performance.now() - start) / iterations
if (sample >= 0) {
samples[index].push(elapsed)
}
}
}
rows.push({
count,
missing,
iterations,
beforeMs: samples[0].sort((a, b) => a - b)[5],
afterMs: samples[1].sort((a, b) => a - b)[5]
})
}
}
console.log(
JSON.stringify(
{ node: process.version, platform: process.platform, samples: 11, warmups: 3, rows },
null,
2
)
)
} finally {
await rm(directory, { recursive: true, force: true })
}
@@ -0,0 +1,124 @@
import assert from 'node:assert/strict'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import Module from 'node:module'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { build } from 'esbuild'
const entry = 'src/shared/agent-hook-listener/transcript-reader.ts'
assert.ok(process.argv[2], 'Pass a pre-change transcript-reader.ts snapshot.')
const baseline = readFileSync(process.argv[2], 'utf8')
assert.notEqual(baseline, readFileSync(entry, 'utf8'), 'Do not compare the source to itself.')
async function load(useBaseline) {
const result = await build({
stdin: {
contents: `export * from './${entry}';
export { extractAssistantTextFromLine } from './src/shared/agent-hook-listener/transcript-entry-text.ts';`,
resolveDir: process.cwd(),
loader: 'ts'
},
bundle: true,
platform: 'node',
format: 'cjs',
write: false,
logLevel: 'silent',
plugins: useBaseline
? [
{
name: 'baseline-transcript-reader',
setup(builder) {
builder.onLoad({ filter: /transcript-reader\.ts$/ }, () => ({
contents: baseline,
loader: 'ts'
}))
}
}
]
: []
})
const module = new Module(resolve('transcript-benchmark.cjs'))
module.paths = Module._nodeModulePaths(process.cwd())
module._compile(result.outputFiles[0].text, module.id)
return module.exports
}
const versions = [await load(true), await load(false)]
function measure(functions, iterations) {
let sink = 0
const run = (fn) => {
for (let i = 0; i < iterations; i++) {
sink += fn()?.length ?? 0
}
}
for (const fn of functions) {
for (let i = 0; i < 3; i++) {
run(fn)
}
}
const samples = [[], []]
for (let round = 0; round < 11; round++) {
for (const index of round % 2 ? [1, 0] : [0, 1]) {
const start = performance.now()
run(functions[index])
samples[index].push((performance.now() - start) / iterations)
}
}
return {
beforeMs: samples[0].sort((a, b) => a - b)[5],
afterMs: samples[1].sort((a, b) => a - b)[5],
iterations,
sink
}
}
const cases = [
['tiny', `${JSON.stringify({ role: 'assistant', content: 'hello' })}\n`, 10000],
['64KiB line', `${JSON.stringify({ role: 'assistant', content: 'x'.repeat(65500) })}\n`, 100],
[
'4MiB line',
`${JSON.stringify({ role: 'assistant', content: 'x'.repeat(4 * 1024 * 1024 - 40) })}\n`,
10
],
[
'1000 short tool lines',
Array.from({ length: 1000 }, () =>
JSON.stringify({ role: 'tool', content: 'x'.repeat(100) })
).join('\n'),
50
],
[
'Unicode line',
`${JSON.stringify({ role: 'assistant', content: '😀漢字'.repeat(16000) })}\n`,
100
],
[
'leading and trailing blank lines',
`\n\r\n${JSON.stringify({ role: 'assistant', content: 'hello' })}\n\n`,
10000
]
]
const directory = mkdtempSync(join(tmpdir(), 'orca-transcript-benchmark-'))
try {
for (const [name, text, iterations] of cases) {
const file = join(directory, 'transcript.jsonl')
writeFileSync(file, text)
const scanners = versions.map(
(v) => () => v.findLastExtractedTranscriptLineText(text, v.extractAssistantTextFromLine)
)
const readers = versions.map((v) => () => v.readLastAssistantFromTranscriptOnce(file))
assert.equal(scanners[0](), scanners[1](), name)
assert.equal(readers[0](), readers[1](), name)
console.log(
JSON.stringify({
name,
bytes: Buffer.byteLength(text),
scanner: measure(scanners, iterations),
warmFileReader: measure(readers, Math.min(iterations, 100))
})
)
}
} finally {
rmSync(directory, { recursive: true, force: true })
}
@@ -32,6 +32,8 @@ import {
import { join, resolve } from 'node:path'
import { RELAY_WINDOWS_PROCESS_TREE_FILENAME } from '../../src/shared/relay-artifacts.ts'
import {
ensureWindowsProcessTreeCommandLinePatch,
inspectWindowsProcessTreeAddon,
nodeGypRebuildInvocation,
stageWindowsProcessTreeNodeAddonApiHeaders,
WINDOWS_PROCESS_TREE_PACKAGE_DIR as PACKAGE_DIR
@@ -89,6 +91,13 @@ function assertPatchApplied() {
'config/patches/@vscode__windows-process-tree@0.8.0.patch; run pnpm install.'
)
}
if (processCc.includes('OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ')) {
throw new Error(
'src/process.cc still takes PROCESS_VM_READ for memory or CPU counters it never reads ' +
'from the address space. pnpm did not apply ' +
'config/patches/@vscode__windows-process-tree@0.8.0.patch; run pnpm install.'
)
}
}
// pnpm can materialize this CRLF package without applying its patch. Repair the
@@ -123,6 +132,13 @@ function applyWindowsProcessTreeBuildFixes() {
''
)
processCc = processCc.replace(/process_count < 1024 && /, '')
// The memory and CPU readers only ever call GetProcessMemoryInfo/GetProcessTimes,
// which need no more than PROCESS_QUERY_LIMITED_INFORMATION; taking VM_READ is
// what EDR scores.
processCc = processCc.replaceAll(
'OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid)',
'OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid)'
)
if (bindingGyp !== originalBinding) {
writeFileSync(bindingPath, bindingGyp)
@@ -131,7 +147,8 @@ function applyWindowsProcessTreeBuildFixes() {
writeFileSync(processPath, processCc)
}
stageWindowsProcessTreeNodeAddonApiHeaders(PACKAGE_DIR)
if (bindingGyp !== originalBinding || processCc !== originalProcess) {
const repairedCommandLine = ensureWindowsProcessTreeCommandLinePatch(PACKAGE_DIR)
if (bindingGyp !== originalBinding || processCc !== originalProcess || repairedCommandLine) {
console.warn('[windows-process-tree] Repaired un-applied pnpm patch hunks before build.')
}
}
@@ -173,6 +190,14 @@ function main() {
if (!existsSync(built)) {
throw new Error(`node-gyp reported success but ${built} is missing.`)
}
// Why check the artifact and not only the source: the source checks above run
// before node-gyp, and a stale build directory can outlive them.
if (inspectWindowsProcessTreeAddon(built) === 'unpatched') {
throw new Error(
'The built addon still calls ReadProcessMemory, so it did not come from the patched ' +
'command-line reader. A relay would get the primitive MDE scores as credential dumping.'
)
}
const machine = readPeMachine(built)
if (machine !== PE_MACHINE[arch]) {
throw new Error(
@@ -2,7 +2,7 @@
// Equivalence check for deferring the RuntimeClient module graph in the CLI.
//
// Builds the CLI twice with the REAL tsc emit — once from the working tree and
// once with the seven touched files restored from git HEAD~ (the pre-deferral
// once with the touched files restored from git HEAD~ (the pre-deferral
// implementation) — then compares stdout, stderr and exit code BYTE FOR BYTE
// across a matrix of invocations.
//
@@ -13,7 +13,7 @@
//
// Usage: node config/scripts/cli-runtime-client-deferral-equivalence.mjs [--baseline <rev>]
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync } from 'node:fs'
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -21,8 +21,11 @@ const REPO = fileURLToPath(new URL('../..', import.meta.url))
// The files this change touches. Restoring exactly these from the baseline rev
// reconstructs the old implementation without disturbing anything else.
// Files absent at the baseline (e.g. cli-error.ts, split out of format.ts
// later) are removed for the baseline build and put back afterwards.
const TOUCHED = [
'src/cli/args.ts',
'src/cli/cli-error.ts',
'src/cli/dispatch.ts',
'src/cli/flags.ts',
'src/cli/format.ts',
@@ -72,12 +75,16 @@ function buildTree(label, baselineRev) {
if (baselineRev) {
for (const file of TOUCHED) {
const path = join(REPO, file)
restored.push([path, readFileSync(path)])
const old = execFileSync('git', ['show', `${baselineRev}:${file}`], {
restored.push([path, existsSync(path) ? readFileSync(path) : null])
const old = spawnSync('git', ['show', `${baselineRev}:${file}`], {
cwd: REPO,
maxBuffer: 64 * 1024 * 1024
})
writeFileSync(path, old)
if (old.status === 0) {
writeFileSync(path, old.stdout)
} else {
rmSync(path, { force: true })
}
}
}
execFileSync(
@@ -97,7 +104,11 @@ function buildTree(label, baselineRev) {
)
} finally {
for (const [path, contents] of restored) {
writeFileSync(path, contents)
if (contents === null) {
rmSync(path, { force: true })
} else {
writeFileSync(path, contents)
}
}
}
return join(outDir, 'cli/index.js')
+88 -18
View File
@@ -128,10 +128,14 @@ export async function createDraftRelease({
throw new Error('token is required')
}
const previousTag = latestPreviousPublishedDesktopReleaseTag(
await fetchRepoReleases(repo, token, fetchImpl),
tag
)
const releases = await fetchRepoReleases(repo, token, fetchImpl)
const existingRelease = releases.find((release) => release?.tag_name === tag)
if (existingRelease && existingRelease.draft !== true) {
log(`Release ${tag} already exists and is published.`)
return
}
const previousTag = latestPreviousPublishedDesktopReleaseTag(releases, tag)
const generateNotesBody = {
tag_name: tag,
target_commitish: tag,
@@ -156,24 +160,90 @@ export async function createDraftRelease({
typeof releaseNotes.name === 'string' && releaseNotes.name.length > 0 ? releaseNotes.name : tag
const prerelease = tag.includes('-rc.')
// Why: GitHub's generated release notes can exceed the release body API
// limit, so create with a bounded body. Omit target_commitish because the
// release-cut tag already exists and GitHub rejects the tag name there.
await githubJson(fetchImpl, `https://api.github.com/repos/${repo}/releases`, token, {
method: 'POST',
body: JSON.stringify({
tag_name: tag,
name,
body,
draft: true,
prerelease
if (existingRelease) {
if (!Number.isInteger(existingRelease.id)) {
throw new Error(`Draft release ${tag} is missing a GitHub release id`)
}
// Why: the listing is a snapshot; the draft can be published while notes
// generate, and patching then overwrites a live release body.
const currentRelease = await githubJson(
fetchImpl,
`https://api.github.com/repos/${repo}/releases/${existingRelease.id}`,
token
)
if (currentRelease?.draft !== true) {
log(`Release ${tag} was published while notes were generated; leaving it unchanged.`)
return
}
// Why: the PATCH endpoint supports no conditional/versioned update, so the
// GET above cannot close the window. The PATCH response reports the state we
// actually wrote to; if publication won, put the published body back.
const patchedRelease = await githubJson(
fetchImpl,
`https://api.github.com/repos/${repo}/releases/${existingRelease.id}`,
token,
{
method: 'PATCH',
body: JSON.stringify({ body })
}
)
if (patchedRelease?.draft !== true) {
const publishedBody = typeof currentRelease.body === 'string' ? currentRelease.body : ''
if (publishedBody === body) {
log(`Release ${tag} was published while notes were patched; its body is unchanged.`)
return
}
// Why: the rollback must not clobber a body written after our PATCH, so
// restore only while the release still carries exactly what we wrote.
const releaseBeforeRollback = await githubJson(
fetchImpl,
`https://api.github.com/repos/${repo}/releases/${existingRelease.id}`,
token
)
if (releaseBeforeRollback?.body !== body) {
log(
`Release ${tag} was published and its body changed again while notes were patched; leaving the newer body in place.`
)
return
}
await githubJson(
fetchImpl,
`https://api.github.com/repos/${repo}/releases/${existingRelease.id}`,
token,
{
method: 'PATCH',
body: JSON.stringify({ body: publishedBody })
}
)
log(
`Release ${tag} was published while notes were patched; restored its published body and left the generated notes unapplied.`
)
return
}
} else {
// Why: GitHub's generated release notes can exceed the release body API
// limit, so create with a bounded body. Omit target_commitish because the
// release-cut tag already exists and GitHub rejects the tag name there.
await githubJson(fetchImpl, `https://api.github.com/repos/${repo}/releases`, token, {
method: 'POST',
body: JSON.stringify({
tag_name: tag,
name,
body,
draft: true,
prerelease
})
})
})
}
if (generatedBody.length !== body.length) {
log(`Created draft release ${tag} with truncated generated notes (${body.length} chars).`)
log(
`${existingRelease ? 'Updated' : 'Created'} draft release ${tag} with truncated generated notes (${body.length} chars).`
)
} else {
log(`Created draft release ${tag} with generated notes (${body.length} chars).`)
log(
`${existingRelease ? 'Updated' : 'Created'} draft release ${tag} with generated notes (${body.length} chars).`
)
}
}
+129 -3
View File
@@ -132,7 +132,7 @@ describe('createDraftRelease', () => {
it('creates a draft release with bounded generated notes', async () => {
const fetchImpl = vi
.fn()
.mockResolvedValueOnce(jsonResponse([release('v1.4.35'), release('v1.4.36')]))
.mockResolvedValueOnce(jsonResponse([release('v1.4.35')]))
.mockResolvedValueOnce(jsonResponse({ name: 'v1.4.36', body: 'a'.repeat(130_000) }))
.mockResolvedValueOnce(jsonResponse({ tag_name: 'v1.4.36', draft: true }))
@@ -184,7 +184,7 @@ describe('createDraftRelease', () => {
it('marks rc tags as prereleases', async () => {
const fetchImpl = vi
.fn()
.mockResolvedValueOnce(jsonResponse([release('v1.4.36'), release('v1.4.36-rc.1')]))
.mockResolvedValueOnce(jsonResponse([release('v1.4.36')]))
.mockResolvedValueOnce(jsonResponse({ name: 'v1.4.36-rc.1', body: 'notes' }))
.mockResolvedValueOnce(jsonResponse({ tag_name: 'v1.4.36-rc.1', draft: true }))
@@ -200,10 +200,136 @@ describe('createDraftRelease', () => {
expect(createBody.prerelease).toBe(true)
})
it('regenerates notes for an existing draft release', async () => {
const fetchImpl = vi
.fn()
.mockResolvedValueOnce(
jsonResponse([release('v1.4.35'), release('v1.4.36', { draft: true, id: 42 })])
)
.mockResolvedValueOnce(jsonResponse({ name: 'v1.4.36', body: 'notes' }))
.mockResolvedValueOnce(jsonResponse({ id: 42, draft: true, body: 'stale' }))
.mockResolvedValueOnce(jsonResponse({ id: 42, draft: true, body: 'notes' }))
await createDraftRelease({
repo: 'stablyai/orca',
tag: 'v1.4.36',
token: 'token',
fetchImpl,
log: vi.fn()
})
expect(fetchImpl).toHaveBeenNthCalledWith(
3,
'https://api.github.com/repos/stablyai/orca/releases/42',
expect.not.objectContaining({ method: expect.anything() })
)
expect(fetchImpl).toHaveBeenNthCalledWith(
4,
'https://api.github.com/repos/stablyai/orca/releases/42',
expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ body: 'notes' }) })
)
})
it('skips the update when the draft was published while notes were generated', async () => {
const fetchImpl = vi
.fn()
.mockResolvedValueOnce(
jsonResponse([release('v1.4.35'), release('v1.4.36', { draft: true, id: 42 })])
)
.mockResolvedValueOnce(jsonResponse({ name: 'v1.4.36', body: 'notes' }))
.mockResolvedValueOnce(jsonResponse({ id: 42, draft: false }))
await createDraftRelease({
repo: 'stablyai/orca',
tag: 'v1.4.36',
token: 'token',
fetchImpl,
log: vi.fn()
})
expect(fetchImpl).toHaveBeenCalledTimes(3)
expect(fetchImpl).toHaveBeenNthCalledWith(
3,
'https://api.github.com/repos/stablyai/orca/releases/42',
expect.not.objectContaining({ method: expect.anything() })
)
})
it('restores the published body when publication lands between the check and the patch', async () => {
const log = vi.fn()
const fetchImpl = vi
.fn()
.mockResolvedValueOnce(
jsonResponse([release('v1.4.35'), release('v1.4.36', { draft: true, id: 42 })])
)
.mockResolvedValueOnce(jsonResponse({ name: 'v1.4.36', body: 'notes' }))
.mockResolvedValueOnce(jsonResponse({ id: 42, draft: true, body: 'hand-written notes' }))
.mockResolvedValueOnce(jsonResponse({ id: 42, draft: false, body: 'notes' }))
.mockResolvedValueOnce(jsonResponse({ id: 42, draft: false, body: 'notes' }))
.mockResolvedValueOnce(jsonResponse({ id: 42, draft: false, body: 'hand-written notes' }))
await createDraftRelease({
repo: 'stablyai/orca',
tag: 'v1.4.36',
token: 'token',
fetchImpl,
log
})
expect(fetchImpl).toHaveBeenCalledTimes(6)
expect(fetchImpl).toHaveBeenNthCalledWith(
6,
'https://api.github.com/repos/stablyai/orca/releases/42',
expect.objectContaining({
method: 'PATCH',
body: JSON.stringify({ body: 'hand-written notes' })
})
)
expect(log).toHaveBeenCalledWith(expect.stringContaining('restored its published body'))
})
it('leaves a body written after the patch in place instead of rolling it back', async () => {
const log = vi.fn()
const fetchImpl = vi
.fn()
.mockResolvedValueOnce(
jsonResponse([release('v1.4.35'), release('v1.4.36', { draft: true, id: 42 })])
)
.mockResolvedValueOnce(jsonResponse({ name: 'v1.4.36', body: 'notes' }))
.mockResolvedValueOnce(jsonResponse({ id: 42, draft: true, body: 'hand-written notes' }))
.mockResolvedValueOnce(jsonResponse({ id: 42, draft: false, body: 'notes' }))
.mockResolvedValueOnce(jsonResponse({ id: 42, draft: false, body: 'newer published body' }))
await createDraftRelease({
repo: 'stablyai/orca',
tag: 'v1.4.36',
token: 'token',
fetchImpl,
log
})
expect(fetchImpl).toHaveBeenCalledTimes(5)
expect(log).toHaveBeenCalledWith(expect.stringContaining('leaving the newer body in place'))
})
it('preserves notes on an existing published release', async () => {
const fetchImpl = vi.fn().mockResolvedValueOnce(jsonResponse([release('v1.4.36', { id: 42 })]))
await createDraftRelease({
repo: 'stablyai/orca',
tag: 'v1.4.36',
token: 'token',
fetchImpl,
log: vi.fn()
})
expect(fetchImpl).toHaveBeenCalledTimes(1)
})
it('omits previous_tag_name for the first desktop release so notes fall back to the GitHub default', async () => {
const fetchImpl = vi
.fn()
.mockResolvedValueOnce(jsonResponse([release('v1.4.36'), release('mobile-v0.0.12')]))
.mockResolvedValueOnce(jsonResponse([release('mobile-v0.0.12')]))
.mockResolvedValueOnce(jsonResponse({ name: 'v1.4.36', body: 'notes' }))
.mockResolvedValueOnce(jsonResponse({ tag_name: 'v1.4.36', draft: true }))
@@ -103,14 +103,24 @@ describe('electron-builder markdown file associations', () => {
// Why: this include was renamed from daemon-host-uninstall.nsh to carry the markdown
// hooks too. electron-builder allows only one include, so a merge that drops the daemon
// sweep would silently orphan a running orca-terminal-daemon.exe on every uninstall.
// sweep would silently orphan a running daemon host on every uninstall.
//
// Asserted against comment-stripped script, and on the app exe name first: the relocated
// host is a verbatim copy of the app exe (daemonHostExeName, daemon-host-relocation.ts),
// so a macro that kills only orca-terminal-daemon.exe matches no running process. The
// prose above the macro names both, so a toContain over the raw file proves nothing.
it('keeps the daemon-host uninstall sweep across the include rename', async () => {
const hooks = await readInstallerHooks()
const script = stripNsisCommentLines(await readInstallerHooks())
expect(hooks).toContain('orca-terminal-daemon.exe')
expect(hooks).toContain('$LOCALAPPDATA\\Orca\\daemon-host')
expect(script).toMatch(/taskkill[^\n]*\/IM\s+"?\$\{APP_EXECUTABLE_FILENAME\}"?/)
// Legacy name, so hosts left by builds that renamed the copy still get reaped.
expect(script).toMatch(/taskkill[^\n]*\/IM\s+"?orca-terminal-daemon\.exe"?/)
// Scopes both kills to the uninstalling user: an elevated machine-wide uninstall must
// not reach another logged-on user's session.
expect(script).toMatch(/\/FI\s+"USERNAME eq /)
expect(script).toContain('$LOCALAPPDATA\\Orca\\daemon-host')
// Without this guard, uninstallOldVersion would kill the daemon on every update —
// defeating the relocation that keeps terminals alive across updates.
expect(hooks).toMatch(/\$\{ifNot\}\s+\$\{isUpdated\}/)
expect(script).toMatch(/\$\{ifNot\}\s+\$\{isUpdated\}/)
})
})
+25 -4
View File
@@ -5,6 +5,12 @@ import { createRequire } from 'node:module'
import { existsSync, readFileSync } from 'node:fs'
import { release } from 'node:os'
import { basename, dirname, resolve } from 'node:path'
import {
ensureWindowsProcessTreeCommandLinePatch,
inspectWindowsProcessTreeAddon,
stageWindowsProcessTreeNodeAddonApiHeaders,
windowsProcessTreeAddonPath
} from './windows-process-tree-gyp-rebuild.mjs'
const require = createRequire(import.meta.url)
const { assertNodePtyJobOwnership } = require('./node-pty-job-ownership.cjs')
@@ -253,11 +259,18 @@ function collectNativeModuleFailures() {
function loadNativeModule(moduleName) {
if (moduleName === '@vscode/windows-process-tree') {
// A bare require already loads the .node addon on win32, so it catches an
// ABI mismatch on its own. What it cannot catch is a snapshot that comes
// back empty -- the shape a blocked CreateToolhelp32Snapshot produces --
// so check the addon actually enumerates before calling the runtime healthy.
// A bare require loads the .node addon on win32, so it catches an ABI
// mismatch on its own. What it cannot catch is *which* addon loaded: the
// published tarball ships a prebuilt built from unpatched source that is
// node-addon-api, so it requires cleanly and then reads every process's
// command line out of its address space. Check the binary, not the load.
require(moduleName)
if (inspectWindowsProcessTreeAddon(windowsProcessTreeAddonPath()) === 'unpatched') {
throw new Error(
'the loaded addon still calls ReadProcessMemory, so it was not built from the patched ' +
'source. Rebuild it (pnpm run rebuild:electron) rather than using the published prebuild.'
)
}
return
}
if (moduleName === 'windows-native-registry') {
@@ -368,6 +381,14 @@ function getWindowsBuildNumber() {
function rebuildNodeRuntimeModules(moduleNames) {
for (const moduleName of moduleNames) {
const moduleDir = dirname(require.resolve(`${moduleName}/package.json`))
if (moduleName === '@vscode/windows-process-tree') {
// Why before node-gyp: this module is rebuilt precisely because the
// binary was the unpatched one, and pnpm materializes it unpatched often
// enough that compiling the source as-is would just rebuild the same
// reader and fail the verify pass.
ensureWindowsProcessTreeCommandLinePatch(moduleDir)
stageWindowsProcessTreeNodeAddonApiHeaders(moduleDir)
}
console.warn(`[native-runtime] Rebuilding ${moduleName} with node-gyp.`)
runPnpm(['exec', 'node-gyp', 'rebuild'], { cwd: moduleDir })
if (moduleName === 'node-pty' && process.platform === 'win32') {
@@ -12,6 +12,7 @@ import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { copyScriptWithLocalModules } from './script-module-dependencies.mjs'
const sourceScriptPath = fileURLToPath(new URL('./ensure-native-runtime.mjs', import.meta.url))
const sourceNodePtyJobOwnershipPath = fileURLToPath(
@@ -27,7 +28,6 @@ describe('ensure-native-runtime', () => {
const logPath = join(projectDir, 'native-runtime.log')
const markerPath = join(projectDir, 'rebuilt.marker')
const binDir = join(projectDir, 'bin')
copyFileSync(sourceScriptPath, scriptPath)
writeFakeNativeModules(projectDir)
writeNodePtyPatchFile(projectDir)
writeFakePnpm(binDir)
@@ -67,7 +67,6 @@ describe('ensure-native-runtime', () => {
const logPath = join(projectDir, 'native-runtime.log')
const markerPath = join(projectDir, 'rebuilt.marker')
const binDir = join(projectDir, 'bin')
copyFileSync(sourceScriptPath, scriptPath)
writeFakeNativeModules(projectDir, { windowsRegistryRequiresMarker: true })
writeNodePtyPatchFile(projectDir)
writeFakePnpm(binDir)
@@ -102,7 +101,6 @@ describe('ensure-native-runtime', () => {
const logPath = join(projectDir, 'native-runtime.log')
const markerPath = join(projectDir, 'rebuilt.marker')
const binDir = join(projectDir, 'bin')
copyFileSync(sourceScriptPath, scriptPath)
writeLoadableNativeModules(projectDir)
writeNodePtyPatchFile(projectDir)
writeFakePnpm(binDir)
@@ -137,7 +135,6 @@ describe('ensure-native-runtime', () => {
const logPath = join(projectDir, 'native-runtime.log')
const markerPath = join(projectDir, 'rebuilt.marker')
const binDir = join(projectDir, 'bin')
copyFileSync(sourceScriptPath, scriptPath)
writeLoadableNativeModules(projectDir)
writeNodePtyPatchFile(projectDir)
writePatchedNodePtyBuildArtifacts(projectDir)
@@ -171,7 +168,6 @@ describe('ensure-native-runtime', () => {
const logPath = join(projectDir, 'native-runtime.log')
const markerPath = join(projectDir, 'rebuilt.marker')
const binDir = join(projectDir, 'bin')
copyFileSync(sourceScriptPath, scriptPath)
writeLoadableNativeModules(projectDir, { nativeDir: '../build/Release/' })
writeNodePtyPatchFile(projectDir)
writePatchedNodePtyBuildArtifacts(projectDir)
@@ -198,7 +194,9 @@ describe('ensure-native-runtime', () => {
function mkTempProject() {
const projectDir = mkdtempSync(join(tmpdir(), 'orca-native-runtime-'))
mkdirSync(join(projectDir, 'config', 'scripts'), { recursive: true })
// Walked, not listed: the script imports windows-process-tree-gyp-rebuild.mjs, and a fixture
// missing it fails every case with a module-resolution error instead of the defect under test.
copyScriptWithLocalModules(sourceScriptPath, join(projectDir, 'config', 'scripts'))
copyFileSync(
sourceNodePtyJobOwnershipPath,
join(projectDir, 'config', 'scripts', 'node-pty-job-ownership.cjs')
@@ -0,0 +1,75 @@
import assert from 'node:assert/strict'
import { join } from 'node:path'
import { performance } from 'node:perf_hooks'
import { fileURLToPath } from 'node:url'
import { build } from 'esbuild'
const root = fileURLToPath(new URL('../..', import.meta.url))
const bundled = await build({
stdin: {
contents: `export { selectDeletionRoots } from './file-explorer-batch-deletion';
export { isPathEqualOrDescendant } from './file-explorer-paths';`,
resolveDir: join(root, 'src/renderer/src/components/right-sidebar'),
loader: 'ts'
},
alias: { '@': join(root, 'src/renderer/src') },
bundle: true,
platform: 'node',
format: 'esm',
write: false,
logLevel: 'silent'
})
const { selectDeletionRoots, isPathEqualOrDescendant } = await import(
`data:text/javascript;base64,${Buffer.from(bundled.outputFiles[0].text).toString('base64')}`
)
// Original production selector; both paths use the same path-comparison implementation.
function original(nodes) {
return nodes.filter(
(n) =>
!nodes.some(
(other) => other !== n && other.isDirectory && isPathEqualOrDescendant(n.path, other.path)
)
)
}
function measure(run, nodes) {
for (let index = 0; index < 3; index++) {
run(nodes)
}
const samples = []
for (let index = 0; index < 11; index++) {
const start = performance.now()
run(nodes)
samples.push(performance.now() - start)
}
return samples.sort((a, b) => a - b)[5]
}
const results = []
for (const [fileCount, directoryCount] of [
[100, 0],
[1000, 0],
[5000, 0],
[5000, 5],
[0, 100]
]) {
const nodes = Array.from({ length: fileCount + directoryCount }, (_, index) => ({
name: `item-${index}`,
path: `/repo/item-${index}`,
relativePath: `item-${index}`,
isDirectory: index >= fileCount,
depth: 0
}))
const expected = original(nodes)
const actual = selectDeletionRoots(nodes)
assert.equal(actual.length, expected.length)
actual.forEach((node, index) => assert.equal(node, expected[index]))
results.push({
fileCount,
directoryCount,
beforeMs: measure(original, nodes),
afterMs: measure(selectDeletionRoots, nodes)
})
}
console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2))
@@ -0,0 +1,53 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { stripTypeScriptTypes } from 'node:module'
import { performance } from 'node:perf_hooks'
const baseline = process.argv[2]
if (!baseline) {
throw new Error('Usage: node config/scripts/mobile-file-ranking-benchmark.mjs <baseline-ref>')
}
async function load(source) {
const js = stripTypeScriptTypes(source, { mode: 'transform' })
return await import(`data:text/javascript;base64,${Buffer.from(js).toString('base64')}`)
}
function measure(fn, paths, query) {
for (let warmup = 0; warmup < 10; warmup++) {
fn(paths, query, 16)
}
const samples = []
for (let i = 0; i < 9; i++) {
const start = performance.now()
fn(paths, query, 16)
samples.push(performance.now() - start)
}
return samples.sort((a, b) => a - b)[4]
}
const results = []
for (const [file, name] of [
['src/main/runtime/runtime-mobile-file-path-search.ts', 'rankRuntimeMobileFilePaths'],
['mobile/src/session/mobile-native-chat-autocomplete.ts', 'rankSuggestions']
]) {
const before = (
await load(execFileSync('git', ['show', `${baseline}:${file}`], { encoding: 'utf8' }))
)[name]
const after = (await load(readFileSync(file, 'utf8')))[name]
for (const count of [100, 100000]) {
const paths = Array.from(
{ length: count },
(_, i) => `src/components/workspace/group-${i % 100}/file-${i}.tsx`
)
for (const query of ['file-9', 'missing', 'workspace']) {
assert.deepEqual(after(paths, query, 16), before(paths, query, 16))
results.push({
function: name,
paths: count,
query,
beforeMs: measure(before, paths, query),
afterMs: measure(after, paths, query)
})
}
}
}
console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2))
@@ -0,0 +1,58 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { build } from 'esbuild'
const sourcePath = 'mobile/src/components/mobile-markdown-preview-html.ts'
const baselineRef = process.argv[2]
if (!baselineRef) {
throw new Error(
'Usage: node config/scripts/mobile-markdown-placeholder-benchmark.mjs <baseline-ref>'
)
}
async function load(source) {
const result = await build({
stdin: { contents: source, resolveDir: dirname(resolve(sourcePath)), loader: 'ts' },
bundle: true,
write: false,
platform: 'node',
format: 'esm'
})
return (
await import(
`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`
)
).normalizeMobileMarkdownPreviewHtml
}
const before = await load(
execFileSync('git', ['show', `${baselineRef}:${sourcePath}`], { encoding: 'utf8' })
)
const after = await load(readFileSync(sourcePath, 'utf8'))
function measure(fn, input, repeats) {
const samples = []
for (let run = 0; run < repeats; run++) {
const start = performance.now()
fn(input)
samples.push(performance.now() - start)
}
return samples.sort((a, b) => a - b)[Math.floor(samples.length / 2)]
}
const results = []
for (const [shape, input] of [
['ordinary Markdown', '# Hello\n\n<p>Use `Array<string>` and <b>bold</b>.</p>'],
...[2048, 8192, 16384].map((length) => [
`${length} underscore collision`,
`\uE000ORCA_MD_CODE_${'_'.repeat(length)}0\uE000 and \`Array<string>\``
])
]) {
assert.equal(after(input), before(input))
results.push({
shape,
bytes: Buffer.byteLength(input),
beforeMs: measure(before, input, 5),
afterMs: measure(after, input, 15)
})
}
console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2))
@@ -0,0 +1,155 @@
import {
cpSync,
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { isAbsolute, join, parse, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { runProcessSync } from '../../src/shared/child-process/run-process.ts'
import { resolveCliCommand } from '../../src/shared/node-cli-command-resolution.ts'
import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts'
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
/**
* Run the command that actually consumes the patch hashes.
*
* A hash comparison is not this check. `@vscode/windows-process-tree@0.8.0` shipped
* twice with a hand-computed `sha256(patchBytes)` in the lockfile, and two separate
* reviews "verified" it by recomputing the same number the same wrong way. pnpm
* hashes the **LF-normalized** content, so a CRLF patch makes the raw digest a value
* pnpm will never produce, and `--frozen-lockfile` dies with
* ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on every runner. An independent check that
* repeats the original assumption is not independent; only the installer is.
*
* `--lockfile-only --ignore-scripts` keeps it to the resolution pnpm rejects on,
* with no node_modules and no native builds.
*/
const PROJECT_DIR = resolve(import.meta.dirname, '../..')
const WINDOWS_PROCESS_TREE_PATCH = '@vscode__windows-process-tree@0.8.0.patch'
/**
* Which pnpm to run belongs to pnpm-cli-invocation.mjs, not to this file: naming
* the Windows shim here is what windows-cmd-shim-spawn-boundary.test.mjs rejects.
* Its `shell` is dropped on purpose -- runProcessSync refuses that flag and
* already drives a shim through the interpreter itself.
*/
function resolvePnpmInvocation() {
const { command, prefixArgs } = resolvePnpmCliInvocation()
if (isAbsolute(command)) {
return existsSync(command) ? { program: command, prefixArgs } : null
}
// Bare name only when npm_execpath is unset (bare `vitest`, not `pnpm test`).
// Drop the extension so the shared resolver tries every executable form of it.
const resolved = resolveCliCommand(parse(command).name)
return isAbsolute(resolved) ? { program: resolved, prefixArgs } : null
}
describe('patched dependencies', () => {
it('installs with --frozen-lockfile, which is what validates every patch hash', () => {
const pnpm = resolvePnpmInvocation()
expect(pnpm, 'pnpm must be installed; it is the only thing that can check this').not.toBeNull()
// A copy, because a --frozen-lockfile run still rewrites parts of the
// lockfile this repo does not track, and the real one must not move.
const scratch = mkdtempSync(join(tmpdir(), 'orca-frozen-install-'))
try {
for (const file of ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']) {
copyFileSync(join(PROJECT_DIR, file), join(scratch, file))
}
mkdirSync(join(scratch, 'config'), { recursive: true })
cpSync(join(PROJECT_DIR, 'config', 'patches'), join(scratch, 'config', 'patches'), {
recursive: true
})
const result = runProcessSync({
program: pnpm.program,
args: [
...pnpm.prefixArgs,
'install',
'--frozen-lockfile',
'--lockfile-only',
'--ignore-scripts'
],
cwd: scratch,
timeoutMs: 300_000
})
expect(result.code, `${result.stdout}\n${result.stderr}`).toBe(0)
} finally {
removeTreeSync(scratch)
}
// The 300s spawn budget is only reachable if the case is allowed to take it;
// config/vitest.config.ts caps every case at 30s by default.
}, 300_000)
/**
* `--lockfile-only` resolves; it never applies a patch. So the case above is
* bounded to hash consistency, and the actual question -- can pnpm still put
* the patched reader on disk? -- had nothing covering it.
*
* One package, patch applied for real, assert the marker landed. Scoped to the
* single dependency so it stays a ~2s check rather than a full install.
*/
it('materializes the patched command-line reader on a real install', () => {
const pnpm = resolvePnpmInvocation()
expect(pnpm, 'pnpm must be installed; it is the only thing that can check this').not.toBeNull()
const scratch = mkdtempSync(join(tmpdir(), 'orca-patch-apply-'))
try {
mkdirSync(join(scratch, 'config', 'patches'), { recursive: true })
copyFileSync(
join(PROJECT_DIR, 'config', 'patches', WINDOWS_PROCESS_TREE_PATCH),
join(scratch, 'config', 'patches', WINDOWS_PROCESS_TREE_PATCH)
)
writeFileSync(
join(scratch, 'package.json'),
`${JSON.stringify(
{
name: 'orca-patch-apply-probe',
version: '1.0.0',
dependencies: { '@vscode/windows-process-tree': '0.8.0' }
},
null,
2
)}\n`
)
writeFileSync(
join(scratch, 'pnpm-workspace.yaml'),
'packages: []\n' +
'patchedDependencies:\n' +
` '@vscode/windows-process-tree@0.8.0': config/patches/${WINDOWS_PROCESS_TREE_PATCH}\n`
)
const result = runProcessSync({
program: pnpm.program,
args: [...pnpm.prefixArgs, 'install', '--no-frozen-lockfile', '--ignore-scripts'],
cwd: scratch,
timeoutMs: 300_000
})
expect(result.code, `${result.stdout}\n${result.stderr}`).toBe(0)
const materialized = readFileSync(
join(
scratch,
'node_modules',
'@vscode',
'windows-process-tree',
'src',
'process_commandline.cc'
),
'utf8'
)
expect(materialized).toContain('kProcessCommandLineInformation')
// The whole point of the patch: the upstream reader is gone, not merely
// supplemented.
expect(materialized).not.toContain('ReadProcessMemory')
} finally {
removeTreeSync(scratch)
}
}, 300_000)
})
+9 -1
View File
@@ -213,13 +213,17 @@ const LINUX_PACKAGE_TESTS = [
const WINDOWS_PACKAGE_TESTS = [
...LINUX_PACKAGE_TESTS,
'config/scripts/rebuild-native-deps.test.mjs',
'config/scripts/rebuild-native-deps-windows-process-tree.test.mjs',
'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts',
'src/shared/child-process/windows-command-line.win32.test.ts',
'src/shared/child-process/windows-cmd-shim-resolution.test.ts',
'src/shared/child-process/windows-cmd-shim-resolution.win32.test.ts',
'src/main/agent-hooks/windows-hook-payload-delivery.test.ts',
'src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts',
'src/main/windows/windows-pty-job.win32.test.ts',
'src/main/windows/windows-host-job.win32.test.ts',
'src/main/windows/windows-process-tree-command-line-patch.test.ts',
'src/main/windows-live-tree-kill.win32.test.ts',
'src/main/wsl/wsl-runner.test.ts',
'src/main/wsl/wsl-guest-environment.test.ts',
@@ -228,14 +232,18 @@ const WINDOWS_PACKAGE_TESTS = [
'src/main/wsl/wsl-w1-w3-contract.test.ts',
'src/shared/source-scan/source-tree-scan.test.ts',
'src/main/cli/wsl-cli-powershell-boundary.test.ts',
'src/main/computer/desktop-script-runtime-host.win32.test.ts',
'src/main/cursor/hook-service.test.ts',
'src/main/orca-profiles/profile-index-store.test.ts',
'src/main/startup/windows-install-dir-acl-repair.win32.test.ts',
'src/main/runtime/repo-worktree-admin-fingerprint.test.ts',
'src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts',
'src/shared/secure-file-fsync-flags.test.ts',
'src/shared/secure-path-windows-acl.win32.test.ts',
'src/main/runtime/unreadable-secret-store-preservation.win32.test.ts',
'src/main/ipc/pty-codex-account-attribution.test.ts',
'src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts'
'src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts',
'src/relay/windows-port-scan.win32.test.ts'
]
const DESKTOP_IRRELEVANT_PREFIXES = [
+8 -8
View File
@@ -636,13 +636,8 @@ describe('PR E2E gate contract', () => {
.filter((spec) => nativeGateExpression.test(readFileSync(join(projectDir, spec), 'utf8')))
expect(nativeGatedSpecs.length).toBeGreaterThan(0)
// Why exempt: the digit repro needs a nested gnome-shell, which no hosted runner provides
// (headless mutter never answers RemoteDesktop.CreateSession); the macOS spec needs a real
// macOS input source, and no macOS runner exists on any PR or scheduled lane.
const unreachableSpecs = new Set([
'tests/e2e/terminal-hangul-terminating-digit-native.spec.ts',
'tests/e2e/terminal-macos-2set-korean-native.spec.ts'
])
// The macOS spec needs a native input source; PR and scheduled IME lanes use Linux.
const unreachableSpecs = new Set(['tests/e2e/terminal-macos-2set-korean-native.spec.ts'])
const unclaimed = nativeGatedSpecs.filter(
(spec) => !unreachableSpecs.has(spec) && !nativeImeRunner.includes(spec)
)
@@ -682,8 +677,13 @@ describe('PR E2E gate contract', () => {
// Why pin the titles: the runner requires one receipt per name, so a rename that nobody
// mirrored here would fail the lane loudly instead of quietly halving it.
const nativeDigitSpec = readFileSync(
join(projectDir, 'tests/e2e/terminal-hangul-terminating-digit-native.spec.ts'),
'utf8'
)
expect(nativeDigitSpec).toContain('appendImeEngagementReceipt(testInfo.title, trace)')
for (const title of EXPECTED_NATIVE_IME_TESTS) {
expect(nativeImeSpec, title).toContain(title)
expect(nativeImeSpec + nativeDigitSpec, title).toContain(title)
}
})
@@ -0,0 +1,60 @@
import assert from 'node:assert/strict'
import { performance } from 'node:perf_hooks'
import { build } from 'esbuild'
const bundled = await build({
entryPoints: ['src/shared/quick-open-filter.ts'],
bundle: true,
platform: 'node',
format: 'esm',
write: false,
logLevel: 'silent'
})
const { shouldExcludeQuickOpenRelPath: after } = await import(
`data:text/javascript;base64,${Buffer.from(bundled.outputFiles[0].text).toString('base64')}`
)
// Original production predicate, including its exact boundary check.
function before(relPath, prefixes) {
for (const prefix of prefixes) {
if (relPath === prefix) {
return true
}
if (relPath.length > prefix.length && relPath.startsWith(`${prefix}/`)) {
return true
}
}
return false
}
const files = Array.from(
{ length: 100000 },
(_, index) => `src/components/group-${index % 100}/file-${index}.tsx`
)
function run(fn, prefixes) {
let excluded = 0
for (const file of files) {
excluded += Number(fn(file, prefixes))
}
return excluded
}
function measure(fn, prefixes) {
run(fn, prefixes)
const samples = []
for (let index = 0; index < 5; index++) {
const start = performance.now()
run(fn, prefixes)
samples.push(performance.now() - start)
}
return samples.sort((a, b) => a - b)[2]
}
const results = []
for (const count of [0, 10, 100, 500]) {
const prefixes = Array.from({ length: count }, (_, index) => `nested-worktrees/worktree-${index}`)
assert.equal(run(after, prefixes), run(before, prefixes))
results.push({
files: files.length,
exclusions: count,
beforeMs: measure(before, prefixes),
afterMs: measure(after, prefixes)
})
}
console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2))
@@ -4,6 +4,8 @@ import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
gitLineEndingEnv,
initGitWorkTree,
mkTempProject,
runRebuildScript,
writeFakeElectronRebuild,
@@ -14,7 +16,8 @@ import {
writeFakeWindowsProcessTreeWithNodeAddonApi,
writeFakeWindowsRegistry,
writeNodePtyPatchFile,
writePatchedNodePtyBuildArtifacts
writePatchedNodePtyBuildArtifacts,
writeWindowsProcessTreePatchFile
} from './rebuild-native-deps-test-fixtures.mjs'
describe('rebuild-native-deps patched node-pty rebuild', () => {
@@ -85,6 +88,91 @@ describe('rebuild-native-deps patched node-pty rebuild', () => {
}
})
const commandLineSourcePath = (projectDir) =>
join(
projectDir,
'node_modules',
'@vscode',
'windows-process-tree',
'src',
'process_commandline.cc'
)
// Why inside a git work tree: `git apply` run under one prefixes patch paths
// with the cwd-relative prefix, silently skips what does not match, and still
// exits 0. The package dir is always under the project root in production, so
// a fixture in %TEMP% alone would pass while the real repair did nothing.
//
// Why both line-ending modes: the patch is stored LF while upstream ships this
// source CRLF, so whether the pre-image matches depends on `core.autocrlf` --
// and under `false`, Git's own built-in default, it did not. The repair blinds
// git to the repo, so that value comes from global config, i.e. from whichever
// option the developer's installer wrote. Pinning both makes the case cover the
// host that breaks rather than the host that happens to run it.
for (const autocrlf of ['false', 'true']) {
it(`repairs an un-applied command-line patch in a work tree (autocrlf=${autocrlf})`, () => {
const projectDir = mkTempProject()
try {
initGitWorkTree(projectDir)
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir)
writeFakeNodePtyConptyPayload(projectDir, 'x64')
writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir, {
commandLinePatchApplied: false
})
writeWindowsProcessTreePatchFile(projectDir)
const result = runRebuildScript(
projectDir,
{
npm_config_platform: 'win32',
npm_config_arch: 'x64',
...gitLineEndingEnv(autocrlf)
},
['--platform=win32', '--arch=x64', '--force']
)
expect(result.status, result.stderr).toBe(0)
expect(readFileSync(commandLineSourcePath(projectDir), 'utf8')).toContain(
'kProcessCommandLineInformation'
)
} finally {
removeTreeSync(projectDir)
}
})
}
// Why fail rather than build: an unpatched command-line reader compiles fine
// and then opens every process with PROCESS_VM_READ to walk its PEB, which is
// the primitive the patch exists to remove.
it('refuses a Windows rebuild when the command-line patch cannot be applied', () => {
const projectDir = mkTempProject()
try {
initGitWorkTree(projectDir)
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir)
writeFakeNodePtyConptyPayload(projectDir, 'x64')
writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir, { commandLinePatchApplied: false })
// No patch file, so the repair has nothing to apply.
const result = runRebuildScript(
projectDir,
{ npm_config_platform: 'win32', npm_config_arch: 'x64' },
['--platform=win32', '--arch=x64', '--force']
)
expect(result.status).not.toBe(0)
expect(result.stderr).toContain('process_commandline.cc')
expect(readFileSync(commandLineSourcePath(projectDir), 'utf8')).not.toContain(
'kProcessCommandLineInformation'
)
} finally {
removeTreeSync(projectDir)
}
})
it('restores the ConPTY runtime payload after a Windows Electron rebuild', () => {
const projectDir = mkTempProject()
@@ -256,4 +344,37 @@ describe('rebuild-native-deps patched node-pty rebuild', () => {
}
}
)
// The binary this step produces is the one copied into the packaged app. The
// relay build checks its own artifact and ensure-native-runtime checks what it
// loads; nothing checked this one, so a rebuild that quietly emitted the
// upstream reader shipped. Both non-clean states have to fail, which is the
// caller the tri-state was missing: after a rebuild that reported success, an
// absent binary is a broken build, not an absence to shrug at.
for (const [addon, expected] of [
['unpatched', 'still imports ReadProcessMemory'],
['none', 'is not there']
]) {
it(`fails a Windows rebuild that leaves ${addon} windows-process-tree bytes`, () => {
const projectDir = mkTempProject()
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { addon })
writeFakeNodePtyConptyPayload(projectDir, 'x64')
writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir)
const result = runRebuildScript(
projectDir,
{ npm_config_platform: 'win32', npm_config_arch: 'x64' },
['--platform=win32', '--arch=x64', '--force']
)
expect(result.status).not.toBe(0)
expect(result.stderr).toContain(expected)
} finally {
removeTreeSync(projectDir)
}
})
}
})
@@ -1,5 +1,12 @@
import { spawnSync } from 'node:child_process'
import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
import {
chmodSync,
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -15,6 +22,68 @@ const sourceNodePtyJobOwnershipPath = fileURLToPath(
const sourceWindowsProcessTreeGypRebuildPath = fileURLToPath(
new URL('./windows-process-tree-gyp-rebuild.mjs', import.meta.url)
)
const sourceWindowsProcessTreePatchPath = fileURLToPath(
new URL('../patches/@vscode__windows-process-tree@0.8.0.patch', import.meta.url)
)
/**
* The command-line reader as it is *before* the patch, taken from the patch's
* own pre-image so no upstream copy has to be vendored.
*
* Written back as **CRLF**, which is what `@vscode/windows-process-tree@0.8.0`
* actually ships: all 67 pre-image lines of this file carried a CR before the
* patch was normalized to LF. Rebuilding it with the patch's current newline
* instead would make fixture and patch agree by construction, on any encoding —
* which is exactly how a repair that cannot apply to the real package passed
* this suite.
*/
function unpatchedWindowsProcessTreeCommandLineSource() {
const lines = readFileSync(sourceWindowsProcessTreePatchPath, 'utf8').split('\n')
const start = lines.findIndex((line) =>
line.startsWith('diff --git a/src/process_commandline.cc ')
)
const rest = lines.slice(start + 1)
const end = rest.findIndex((line) => line.startsWith('diff --git '))
const preImage = (end === -1 ? rest : rest.slice(0, end))
.filter((line) => line.startsWith(' ') || line.startsWith('-'))
.filter((line) => !line.startsWith('---'))
.map((line) => line.slice(1).replace(/\r$/, ''))
.join('\r\n')
// Splitting drops the file's own trailing newline as an empty element, and
// `git apply` needs the bytes exact.
return `${preImage}\r\n`
}
/**
* Pin `core.autocrlf` for a spawned repair, whatever the host is set to.
*
* The repair blinds git to the surrounding repo with `GIT_DIR`, so the value it
* sees comes from global/system config — on a Git for Windows box that is
* whichever line-ending option the installer wrote, and `false` (Git's built-in
* default, "checkout as-is") is the one the repair used to fail under. A global
* config in a temp HOME outranks the system file, so this is deterministic
* rather than whatever the developer happens to have.
*/
export function gitLineEndingEnv(autocrlf) {
const home = mkdtempSync(join(tmpdir(), `orca-git-home-${autocrlf}-`))
writeFileSync(join(home, '.gitconfig'), `[core]\n\tautocrlf = ${autocrlf}\n`)
return { HOME: home, USERPROFILE: home }
}
/** Production always runs the repair from inside a work tree; `git apply` behaves differently there. */
export function initGitWorkTree(projectDir) {
for (const args of [['init'], ['config', 'user.email', 'a@b.c'], ['config', 'user.name', 't']]) {
spawnSync('git', args, { cwd: projectDir, encoding: 'utf8' })
}
}
export function writeWindowsProcessTreePatchFile(projectDir) {
mkdirSync(join(projectDir, 'config', 'patches'), { recursive: true })
copyFileSync(
sourceWindowsProcessTreePatchPath,
join(projectDir, 'config', 'patches', '@vscode__windows-process-tree@0.8.0.patch')
)
}
export function mkTempProject() {
const projectDir = mkdtempSync(join(tmpdir(), 'orca-rebuild-native-deps-'))
@@ -143,17 +212,46 @@ if (${JSON.stringify(createExecutable)}) {
)
}
export function writeFakeElectronRebuild(projectDir, { logPathEnv = null } = {}) {
/** Bytes that stand in for a compiled addon's import table. */
const FAKE_ADDON_BYTES = {
clean: 'MZ\0ntdll.dll\0NtQueryInformationProcess\0',
unpatched: 'MZ\0KERNEL32.dll\0ReadProcessMemory\0'
}
/**
* A rebuild that produces nothing leaves no addon to inspect, and the script now
* asserts the binary it just built is a patched one. Emit a stand-in so the
* fixture models a rebuild that actually succeeded. `addon` picks which kind,
* because "produced the upstream reader" and "produced nothing" are both real
* outcomes that assertion has to tell apart.
*/
export function writeFakeElectronRebuild(projectDir, { logPathEnv = null, addon = 'clean' } = {}) {
const rebuildDir = join(projectDir, 'node_modules', '@electron', 'rebuild')
mkdirSync(rebuildDir, { recursive: true })
writeFileSync(join(rebuildDir, 'package.json'), JSON.stringify({ type: 'module' }))
const emitAddon =
addon === 'none'
? ''
: `
const packageDir = join('node_modules', '@vscode', 'windows-process-tree')
if (existsSync(join(packageDir, 'package.json'))) {
mkdirSync(join(packageDir, 'build', 'Release'), { recursive: true })
writeFileSync(
join(packageDir, 'build', 'Release', 'windows_process_tree.node'),
${JSON.stringify(FAKE_ADDON_BYTES[addon])}
)
}`
const emitImports =
addon === 'none'
? ''
: "import { existsSync, mkdirSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\n"
writeFileSync(
join(rebuildDir, 'index.js'),
logPathEnv
? `
import { appendFileSync } from 'node:fs'
export async function rebuild(options) {
${emitImports}
export async function rebuild(options) {${emitAddon}
const logPath = process.env[${JSON.stringify(logPathEnv)}]
if (!logPath) {
return
@@ -171,7 +269,10 @@ export async function rebuild(options) {
)
}
`
: 'export async function rebuild() {}\n'
: `${emitImports}
export async function rebuild() {${emitAddon}
}
`
)
}
@@ -271,12 +372,22 @@ export function writeFakeWindowsProcessTree(projectDir) {
writeFileSync(join(processTreeDir, 'index.js'), 'module.exports = {}\n')
}
export function writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) {
export function writeFakeWindowsProcessTreeWithNodeAddonApi(
projectDir,
{ commandLinePatchApplied = true } = {}
) {
const processTreeDir = join(projectDir, 'node_modules', '@vscode', 'windows-process-tree')
const nodeAddonApiDir = join(processTreeDir, 'node_modules', 'node-addon-api')
mkdirSync(nodeAddonApiDir, { recursive: true })
writeFileSync(join(processTreeDir, 'package.json'), '{"dependencies":{"node-addon-api":"*"}}\n')
writeFileSync(join(processTreeDir, 'index.js'), 'module.exports = {}\n')
mkdirSync(join(processTreeDir, 'src'), { recursive: true })
writeFileSync(
join(processTreeDir, 'src', 'process_commandline.cc'),
commandLinePatchApplied
? '// kProcessCommandLineInformation = 60\n'
: unpatchedWindowsProcessTreeCommandLineSource()
)
writeFileSync(join(nodeAddonApiDir, 'package.json'), '{"name":"node-addon-api"}\n')
writeFileSync(join(nodeAddonApiDir, 'napi.h'), '// napi.h\n')
writeFileSync(join(nodeAddonApiDir, 'napi-inl.h'), '// napi-inl.h\n')
@@ -0,0 +1,103 @@
import { spawn } from 'node:child_process'
import { appendFileSync, copyFileSync, existsSync, mkdirSync } from 'node:fs'
import { createRequire } from 'node:module'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts'
import {
mkTempProject,
runRebuildScript,
writeFakeElectronRebuild,
writeFakeNodePtyConptyPayload,
writeFakeUsableElectronPackage,
writeFakeWindowsProcessTreeWithNodeAddonApi
} from './rebuild-native-deps-test-fixtures.mjs'
const require = createRequire(import.meta.url)
/** A real loadable addon, so the OS holds the same lock a running Orca holds. */
function repoAddonPath() {
try {
const entry = require.resolve('@vscode/windows-process-tree')
const built = join(entry, '..', '..', 'build', 'Release', 'windows_process_tree.node')
return existsSync(built) ? built : null
} catch {
return null
}
}
/**
* Stage a stale addon and keep it loaded, exactly as a running Orca does.
*
* The bytes are the repo's own patched build with the flagged import appended,
* because the guard keys on that symbol and the patched binary does not carry
* it. Trailing bytes are PE overlay, so the file still loads.
*/
async function stageLoadedStaleAddon(projectDir) {
const source = repoAddonPath()
const releaseDir = join(
projectDir,
'node_modules',
'@vscode',
'windows-process-tree',
'build',
'Release'
)
mkdirSync(releaseDir, { recursive: true })
const stale = join(releaseDir, 'windows_process_tree.node')
copyFileSync(source, stale)
appendFileSync(stale, 'ReadProcessMemory')
const holder = spawn(
process.execPath,
['-e', 'require(process.argv[1]); process.send("held"); setInterval(() => {}, 1000)', stale],
{ stdio: ['ignore', 'ignore', 'ignore', 'ipc'] }
)
await new Promise((resolve, reject) => {
holder.once('message', resolve)
holder.once('exit', () => reject(new Error('the addon holder exited before loading')))
})
return holder
}
// Why an end-to-end run: the defect was purely one of placement. The guard threw
// a real EPERM, and the classifier that turns that into "close running Orca"
// already existed -- the throw simply happened before the try that reaches it.
// Only the whole script exercises that.
describe.runIf(process.platform === 'win32')('rebuild-native-deps stale addon under lock', () => {
it.skipIf(!repoAddonPath())(
'reports a locked stale addon as a Windows file lock instead of an EPERM stack',
async () => {
const projectDir = mkTempProject()
let holder
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir)
writeFakeNodePtyConptyPayload(projectDir, process.arch)
writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir)
holder = await stageLoadedStaleAddon(projectDir)
const result = runRebuildScript(
projectDir,
{
npm_lifecycle_event: 'postinstall',
npm_config_platform: 'win32',
npm_config_arch: process.arch
},
['--platform=win32', `--arch=${process.arch}`, '--force']
)
expect(result.stderr).toContain(
'Close running Orca/Electron/dev processes for this worktree'
)
// Non-strict postinstall soft-exits on a lock; the next dev/start re-checks.
expect(result.status, result.stderr).toBe(0)
} finally {
holder?.kill()
removeTreeSync(projectDir)
}
}
)
})
+55 -9
View File
@@ -20,7 +20,12 @@
import { rebuild } from '@electron/rebuild'
import { execFileSync, spawnSync } from 'node:child_process'
import { stageWindowsProcessTreeNodeAddonApiHeaders } from './windows-process-tree-gyp-rebuild.mjs'
import {
ensureWindowsProcessTreeCommandLinePatch,
inspectWindowsProcessTreeAddon,
stageWindowsProcessTreeNodeAddonApiHeaders,
windowsProcessTreeAddonPath
} from './windows-process-tree-gyp-rebuild.mjs'
import {
copyFileSync,
existsSync,
@@ -141,15 +146,21 @@ if (!ignoreModules.includes('cpu-features')) {
}
}
if (
rebuildPlatform === 'win32' &&
modulesToRebuild.includes('@vscode/windows-process-tree') &&
existsSync(join(projectDir, 'node_modules', '@vscode', 'windows-process-tree', 'package.json'))
) {
stageWindowsProcessTreeNodeAddonApiHeaders()
}
try {
// Why inside the try: the patch guard deletes a stale addon binary, and that
// delete fails EPERM when the addon is loaded -- exactly the running-Orca case
// the catch below is written for. Outside, it aborted `pnpm install` with a
// raw stack instead of the "close running Orca/Electron processes" message.
if (
rebuildPlatform === 'win32' &&
modulesToRebuild.includes('@vscode/windows-process-tree') &&
existsSync(join(projectDir, 'node_modules', '@vscode', 'windows-process-tree', 'package.json'))
) {
stageWindowsProcessTreeNodeAddonApiHeaders()
if (ensureWindowsProcessTreeCommandLinePatch()) {
console.warn('[rebuild] Repaired the un-applied windows-process-tree command-line patch.')
}
}
await rebuild({
buildPath: projectDir,
electronVersion,
@@ -165,6 +176,7 @@ try {
force: true
})
restoreNodePtyWindowsConptyRuntime()
assertWindowsProcessTreeAddonIsPatched()
} catch (/** @type {any} */ err) {
console.error('[rebuild] Native module rebuild failed:', err?.message ?? err)
if (isWindowsNativeLockError(err)) {
@@ -184,6 +196,40 @@ try {
process.exit(1)
}
/**
* The binary this rebuild just produced is the one the packaged app ships.
*
* The relay build asserts its own artifact and `ensure-native-runtime.mjs`
* asserts what it loads, but nothing checked the addon that gets copied into the
* packaged `node_modules` -- so a rebuild that silently produced the upstream
* reader would reach users. Anything but `clean` fails: after a rebuild that
* reported success the binary must exist, so `missing` is a broken build, not an
* absence to shrug at. This is the caller that needs the state to be a state and
* not a boolean.
*/
function assertWindowsProcessTreeAddonIsPatched() {
if (
rebuildPlatform !== 'win32' ||
!modulesToRebuild.includes('@vscode/windows-process-tree') ||
!existsSync(join(projectDir, 'node_modules', '@vscode', 'windows-process-tree', 'package.json'))
) {
return
}
const addonPath = windowsProcessTreeAddonPath()
const state = inspectWindowsProcessTreeAddon(addonPath)
if (state === 'clean') {
return
}
throw new Error(
state === 'missing'
? `the rebuild reported success but ${addonPath} is not there, so the packaged app would ` +
'ship no windows-process-tree addon at all.'
: `${addonPath} still imports ReadProcessMemory, so it was not built from the patched ` +
'command-line reader. The packaged app would carry the primitive MDE scores as ' +
'credential dumping.'
)
}
function restoreNodePtyWindowsConptyRuntime() {
if (rebuildPlatform !== 'win32' || !onlyModules.includes('node-pty')) {
return
@@ -0,0 +1,47 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { stripTypeScriptTypes } from 'node:module'
import { performance } from 'node:perf_hooks'
import { redactString } from '../../src/main/observability/redactor.ts'
// Supply an unchanged redactor.ts snapshot to measure the actual previous production function.
const baselinePath = process.argv[2]
if (!baselinePath) {
throw new Error(
'Usage: node config/scripts/redactor-environment-lines-benchmark.mjs <baseline-redactor.ts>'
)
}
const baselineSource = stripTypeScriptTypes(readFileSync(baselinePath, 'utf8'))
const { redactString: before } = await import(
`data:text/javascript;base64,${Buffer.from(baselineSource).toString('base64')}`
)
function median(fn, input, repeats) {
const samples = []
for (let run = 0; run < repeats; run++) {
const started = performance.now()
fn(input)
samples.push(performance.now() - started)
}
return samples.sort((a, b) => a - b)[Math.floor(samples.length / 2)]
}
const rows = []
for (const [shape, input] of [
['8KiB blank lines', '\n'.repeat(8192)],
['16KiB blank lines', '\n'.repeat(16384)],
['32KiB blank lines', '\n'.repeat(32768)],
['32KiB blank lines then invalid key', `${'\n'.repeat(32768)}lowercase`],
['ordinary env', 'FOO=value\nBAR=other\n'],
['ordinary message', 'Cannot read directory /workspace/source: file not found']
]) {
assert.equal(redactString(input), before(input))
const beforeMs = median(before, input, 3)
const afterMs = median(redactString, input, 15)
rows.push({
shape,
bytes: Buffer.byteLength(input),
beforeMs,
afterMs,
speedup: beforeMs / afterMs
})
}
console.log(JSON.stringify({ node: process.version, platform: process.platform, rows }, null, 2))
@@ -0,0 +1,82 @@
import { execFileSync } from 'node:child_process'
import { resolve } from 'node:path'
import { RELAY_ARTIFACTS } from '../../src/shared/relay-artifacts.ts'
import { describe, expect, it } from 'vitest'
/**
* Guard the `.gitattributes` pin that keeps `config/relay-assets` on LF.
*
* `core.autocrlf=true` ships in the Git-for-Windows system config, so without a
* pin a Windows runner checks these out as CRLF. build-relay.mjs copies them
* verbatim into the bundle and hashes them byte-for-byte into `.version`, which
* names the immutable remote relay directory -- so a Windows-built client and a
* mac/Linux-built one disagree on the same release, and one SSH host ends up with
* two relay trees, each paying its own remote native-dep compile.
*
* Measured on v1.4.197: master-cloexec-patch.cjs shipped at 11229 bytes from the
* mac runner and 11547 (= 11229 + 318 lines) from the Windows one.
*/
const projectDir = resolve(import.meta.dirname, '../..')
function git(args) {
return execFileSync('git', args, { cwd: projectDir, encoding: 'utf8' })
}
/** `git check-attr -z` emits NUL-separated path/attr/value triples. */
function eolAttributes(paths) {
const fields = git(['check-attr', '-z', 'eol', '--', ...paths]).split('\0')
const found = new Map()
for (let index = 0; index + 2 < fields.length; index += 3) {
found.set(fields[index], fields[index + 2])
}
return found
}
/**
* Keyed off the manifest, not a directory: build-relay refuses to emit an
* artifact absent from RELAY_ARTIFACTS, so relocating an asset cannot slip
* past this the way a path glob would. esbuild bundles have no tracked
* source and contribute no hits, so they need no classifying.
*/
function trackedManifestSources() {
const paths = new Set()
for (const { filename } of RELAY_ARTIFACTS) {
const hits = git(['ls-files', '-z', '--', `*/${filename}`])
.split('\0')
.filter(Boolean)
for (const path of hits) {
paths.add(path)
}
}
return [...paths]
}
describe('config/relay-assets line-ending pin', () => {
it('pins every tracked relay artifact source to LF', () => {
const assets = trackedManifestSources()
expect(assets.length).toBeGreaterThan(0)
const attributes = eolAttributes(assets)
const unpinned = assets.filter((path) => attributes.get(path) !== 'lf')
expect(
unpinned,
'A relay asset left on the platform default gets CRLF on a Windows runner, ' +
'which changes the .version hash and splits one release across two remote ' +
'relay directories. Pin it in .gitattributes.'
).toEqual([])
})
// Why: the assertion above only sees files that exist today. These fix the
// pattern itself -- broad enough to cover a file added tomorrow, narrow enough
// not to claim neighbours.
it.each([
['config/relay-assets/example.cjs', 'lf'],
['config/relay-assets/nested/deeper/example.cjs', 'lf'],
['config/relay-assets/example.txt', 'lf'],
['config/relay-assets-extra/example.cjs', 'unspecified'],
['vendor/config/relay-assets/example.cjs', 'unspecified']
])('resolves %s to eol=%s', (path, expected) => {
expect(eolAttributes([path]).get(path)).toBe(expected)
})
})
@@ -0,0 +1,62 @@
#!/usr/bin/env node
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { stripTypeScriptTypes } from 'node:module'
import { performance } from 'node:perf_hooks'
// Pass the pre-change source saved with git show <base>:src/shared/relay-frame-buffer.ts.
const baselinePath = process.argv[2]
if (!baselinePath) {
throw new Error('Usage: node config/scripts/relay-frame-buffer-benchmark.mjs <baseline.ts>')
}
async function load(source) {
return (
await import(
`data:text/javascript;base64,${Buffer.from(stripTypeScriptTypes(source)).toString('base64')}`
)
).RelayFrameBuffer
}
const Before = await load(readFileSync(baselinePath, 'utf8'))
const After = await load(
readFileSync(new URL('../../src/shared/relay-frame-buffer.ts', import.meta.url), 'utf8')
)
function median(values) {
return values.sort((a, b) => a - b)[Math.floor(values.length / 2)]
}
for (const count of [1, 256, 16384, 65536]) {
const chunks = Array.from({ length: count }, (_, index) => Buffer.alloc(64, index % 256))
const expected = Buffer.concat(chunks)
for (const mode of ['take', 'discard']) {
const times = [[], []]
for (let round = 0; round < 9; round += 1) {
for (const arm of round % 2 === 0 ? [0, 1] : [1, 0]) {
const FrameBuffer = arm === 0 ? Before : After
const buffer = new FrameBuffer()
for (const chunk of chunks) {
buffer.append(chunk)
}
const start = performance.now()
const output = buffer[mode](expected.length)
times[arm].push(performance.now() - start)
if (mode === 'take') {
assert.deepEqual(output, expected)
}
assert.equal(buffer.length, 0)
buffer.append(Buffer.from('tail'))
assert.equal(buffer.drain().toString(), 'tail')
}
}
const beforeMs = median(times[0]),
afterMs = median(times[1])
console.log(
JSON.stringify({
mode,
chunks: count,
bytes: expected.length,
beforeMs,
afterMs,
speedup: beforeMs / afterMs
})
)
}
}
@@ -0,0 +1,260 @@
#!/usr/bin/env node
// Why: electron-builder re-runs `CopyElevateHelper.copy` on every NSIS pack, so the
// release rebuild overwrites the SignPath-signed `resources/elevate.exe` with the
// unsigned copy sitting in the electron-builder toolset cache. The release workflow
// swapped the cached copy first, but searched `<cache>/nsis` — a directory no current
// app-builder-lib layout creates (real ones are `<cache>/nsis-3.0.4.1/nsis-3.0.4.1-<hash>/`
// and `<cache>/nsis@<toolset>/nsis-bundle-<v>-<hash>/`), so the swap silently found
// nothing and v1.4.193/v1.4.194 shipped an unsigned UAC elevation helper.
import { copyFileSync, readdirSync, statSync } from 'node:fs'
import { createRequire } from 'node:module'
import { homedir, platform as osPlatform, tmpdir } from 'node:os'
import { join, parse, resolve } from 'node:path'
const require = createRequire(import.meta.url)
const ELEVATE_EXE = 'elevate.exe'
// `nsis` (the layout the old hardcoded path assumed), `nsis-3.0.4.1` (legacy bundle via
// `getBinFromUrl`), `nsis@1.2.1` (unified bundle). Not `customNsisBinary`: the
// `nsis-<version>` key `getBinFromCustomLoc` builds is only `getBin`'s in-process promise
// key, and the extract dir is named for the custom URL's parent segment, which need not
// start with `nsis` at all. Only the app-builder-lib probe covers that layout — which is
// why the probe, not this scan, is what decides whether the swap succeeded.
const NSIS_RELEASE_DIR = /^nsis(?:[-@].*)?$/i
// elevate.exe lives at the bundle root, one level under the release dir. The legacy
// bundle carries thousands of files under Contrib/, so an unbounded walk is both slow
// and a way to match something that is not a toolset copy.
const MAX_DEPTH = 3
function isFile(path) {
try {
return statSync(path).isFile()
} catch {
return false
}
}
/**
* Mirrors `getCacheDirectory` in app-builder-lib's `out/util/electronGet.js`, which is what
* decides where the NSIS bundle is unpacked. Kept as a local port rather than an import
* because the swap must still resolve a cache root when app-builder-lib cannot be loaded.
*/
export function resolveElectronBuilderCacheDir({
env = process.env,
platform = osPlatform(),
home = homedir(),
temp = tmpdir()
} = {}) {
const override = env.ELECTRON_BUILDER_CACHE?.trim()
if (override && parse(override).root) {
return override
}
if (platform === 'darwin') {
return join(home, 'Library', 'Caches', 'electron-builder')
}
if (platform === 'win32') {
const localAppData = env.LOCALAPPDATA?.trim()
// https://github.com/electron-userland/electron-builder/issues/1164
const isSystemUser =
localAppData?.toLowerCase().includes('\\windows\\system32\\') === true ||
env.USERNAME?.trim().toLowerCase() === 'system'
if (!localAppData || isSystemUser) {
return join(temp, 'electron-builder-cache')
}
return join(localAppData, 'electron-builder', 'Cache')
}
const xdgCache = env.XDG_CACHE_HOME
return xdgCache && parse(xdgCache).root
? join(xdgCache, 'electron-builder')
: join(home, '.cache', 'electron-builder')
}
function collectElevateFiles(dir, depth, found) {
let entries
try {
entries = readdirSync(dir, { withFileTypes: true })
} catch {
return found
}
for (const entry of entries) {
const path = join(dir, entry.name)
if (entry.isFile()) {
if (entry.name.toLowerCase() === ELEVATE_EXE) {
found.push(path)
}
} else if (entry.isDirectory() && depth > 1) {
collectElevateFiles(path, depth - 1, found)
}
}
return found
}
/**
* Every cached `elevate.exe` under an NSIS release directory of `cacheDir`, plus the
* `ELECTRON_BUILDER_NSIS_DIR` override copy when that is set.
*/
export function findCachedElevatePaths(cacheDir, { env = process.env } = {}) {
const found = []
const overrideDir = env.ELECTRON_BUILDER_NSIS_DIR?.trim()
if (overrideDir && isFile(join(overrideDir, ELEVATE_EXE))) {
found.push(join(overrideDir, ELEVATE_EXE))
}
let entries
try {
entries = readdirSync(cacheDir, { withFileTypes: true })
} catch {
return found
}
for (const entry of entries) {
if (entry.isDirectory() && NSIS_RELEASE_DIR.test(entry.name)) {
collectElevateFiles(join(cacheDir, entry.name), MAX_DEPTH, found)
}
}
return found
}
/**
* The exact path `CopyElevateHelper` will pack, asked of app-builder-lib itself. Returns the
* failure instead of logging it: an unavailable probe leaves the directory scan as the only
* signal, and the caller has to say that out loud rather than quietly passing.
*/
export async function resolveToolsetElevatePath(projectDir = process.cwd()) {
try {
const configPath = require.resolve(resolve(projectDir, 'config/electron-builder.config.cjs'))
const config = require(configPath)
const { getNsisElevatePath } = require('app-builder-lib/out/toolsets/windows.js')
const path = await getNsisElevatePath(config.toolsets?.nsis, config.nsis?.customNsisBinary)
return { path, error: null }
} catch (error) {
return { path: null, error: error.message }
}
}
/**
* Replaces every cached copy rather than picking one. Which bundle the rebuild packs
* depends on the toolset version resolved at pack time, and each cached copy is an
* unsigned `elevate.exe` that a later pack could reach for; the helper is a standalone
* UAC shim, not coupled to the NSIS version around it, so overwriting all of them is safe.
*
* `toolsetReplaced` is the signal that matters. A non-empty `replaced` only says that some
* cached copy was rewritten, which a stale release directory carried in by the
* `electron-builder-win-` prefix restore can satisfy on its own.
*/
export async function replaceCachedElevateHelpers({
signedPath,
cacheDir = resolveElectronBuilderCacheDir(),
projectDir = process.cwd(),
env = process.env,
probe = resolveToolsetElevatePath
} = {}) {
if (!isFile(signedPath)) {
throw new Error(`Signed elevate.exe not found: ${signedPath}`)
}
const targets = new Set(findCachedElevatePaths(cacheDir, { env }))
const { path: toolsetPath, error: toolsetError } = await probe(projectDir)
if (toolsetPath != null && isFile(toolsetPath)) {
targets.add(toolsetPath)
}
const replaced = []
for (const target of targets) {
copyFileSync(signedPath, target)
replaced.push(target)
}
return {
replaced,
cacheDir,
toolsetPath,
toolsetError,
toolsetReplaced: toolsetPath != null && replaced.includes(toolsetPath)
}
}
/**
* The annotations and exit code a swap result earns. Split out so every branch is testable
* without a subprocess — including the one that made this defect class possible, where the
* step passes because *a* cached copy was replaced while the copy the rebuild packs was not.
*/
export function summarizeSwap({ replaced, cacheDir, toolsetPath, toolsetError, toolsetReplaced }) {
if (toolsetPath != null && !toolsetReplaced) {
return {
annotations: [
{
level: 'error',
message:
`app-builder-lib resolves the elevate.exe the NSIS rebuild will pack to ${toolsetPath}, ` +
'but that path could not be replaced, so the installer will ship an unsigned UAC ' +
'elevation helper.'
}
],
exitCode: 1
}
}
if (replaced.length === 0) {
return {
annotations: [
{
level: 'error',
message:
`No cached elevate.exe found under ${cacheDir}; the NSIS rebuild will pack the unsigned ` +
'helper and ship an unsigned UAC elevation binary. The electron-builder toolset cache ' +
'layout has changed — update config/scripts/replace-cached-nsis-elevate.mjs.'
}
],
exitCode: 1
}
}
if (toolsetPath == null) {
// A green step must never quietly mean "the authoritative check did not run". The scan
// alone is satisfiable by a stale release directory that the `electron-builder-win-`
// prefix restore carried across a lockfile change, while the bundle the rebuild actually
// packs sits in a directory this scan does not match.
return {
annotations: [
{
level: 'warning',
message:
'Could not ask app-builder-lib which elevate.exe the NSIS rebuild will pack ' +
`(${toolsetError}); replaced ${replaced.length} copies found by scanning ${cacheDir} ` +
'alone, which a stale release directory can satisfy while the packed copy stays unsigned.'
}
],
exitCode: 0
}
}
return { annotations: [], exitCode: 0 }
}
// Why an exit code and not a warning: a swap that misses the copy the rebuild packs exits
// before that rebuild restores the unsigned helper, so a silent success here is
// indistinguishable from a release that shipped a signed one — which is how this went
// unnoticed for two releases. The workflow step is `continue-on-error`, so this annotates
// loudly without making a release unbuildable.
if (import.meta.filename === process.argv[1]) {
const signedPath = process.argv[2]
if (!signedPath) {
process.stderr.write('Usage: replace-cached-nsis-elevate.mjs <signed-elevate.exe>\n')
process.exit(2)
}
try {
const result = await replaceCachedElevateHelpers({ signedPath })
const { annotations, exitCode } = summarizeSwap(result)
for (const { level, message } of annotations) {
process.stdout.write(`::${level}::${message}\n`)
}
if (exitCode === 0) {
for (const path of result.replaced) {
const role = path === result.toolsetPath ? ' (the copy app-builder-lib will pack)' : ''
process.stdout.write(`Replaced ${path} with the SignPath-signed copy.${role}\n`)
}
}
process.exit(exitCode)
} catch (error) {
process.stdout.write(`::error::Could not replace the cached elevate.exe: ${error.message}\n`)
process.exit(1)
}
}
@@ -0,0 +1,364 @@
import { spawnSync } from 'node:child_process'
import {
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { parse } from 'yaml'
import {
findCachedElevatePaths,
replaceCachedElevateHelpers,
resolveElectronBuilderCacheDir,
summarizeSwap
} from './replace-cached-nsis-elevate.mjs'
// The probe is app-builder-lib asking itself where the packed elevate.exe lives; injected
// here so no test needs the network or a warm toolset cache.
const probeFound = (path) => async () => ({ path, error: null })
const probeUnavailable = async () => ({ path: null, error: 'app-builder-lib not loadable' })
const projectRoot = resolve(import.meta.dirname, '../..')
const scriptPath = join(projectRoot, 'config/scripts/replace-cached-nsis-elevate.mjs')
let scratch
beforeEach(() => {
scratch = mkdtempSync(join(tmpdir(), 'orca elevate swap '))
})
afterEach(() => {
rmSync(scratch, { recursive: true, force: true })
})
function makeCache(...relativeFiles) {
const cacheDir = join(scratch, 'Cache')
for (const relative of relativeFiles) {
const path = join(cacheDir, ...relative.split('/'))
mkdirSync(join(path, '..'), { recursive: true })
writeFileSync(path, 'unsigned-elevate')
}
mkdirSync(cacheDir, { recursive: true })
return cacheDir
}
describe('cached elevate.exe swap covers the real electron-builder layouts', () => {
// Why these exact shapes: `downloadBuilderToolset` unpacks to
// `<cache>/<releaseName>/<archive basename>-<url hash>/`, and `releaseName` is
// `nsis-3.0.4.1` on the legacy bundle (`getBinFromUrl`) and `nsis@<toolset>` on the
// unified bundle. The release workflow searched `<cache>/nsis`, which matches none of
// them. `customNsisBinary` is deliberately absent — see the probe suite below.
it.each([
['legacy bundle', 'nsis-3.0.4.1/nsis-3.0.4.1-1mx3n/elevate.exe'],
['unified bundle', 'nsis@1.2.1/nsis-bundle-3.12-k4d9x/elevate.exe'],
['bare nsis release dir', 'nsis/nsis-3.0.4.1/elevate.exe']
])('finds the cached helper in the %s layout', (_label, relative) => {
const cacheDir = makeCache(relative)
expect(findCachedElevatePaths(cacheDir, { env: {} })).toEqual([
join(cacheDir, ...relative.split('/'))
])
})
it('leaves other toolsets and the raw download dir alone', () => {
const cacheDir = makeCache(
'winCodeSign/winCodeSign-2.6.0-abc12/elevate.exe',
'downloads/nsis/elevate.exe'
)
expect(findCachedElevatePaths(cacheDir, { env: {} })).toEqual([])
})
// `nsis-resources-3.4.1` matches the release-dir pattern and is scanned. Documented
// rather than excluded: `getLegacyNsisResourcesBin` ships plugins, never an elevate.exe,
// so the over-match costs one cheap directory read and nothing else. Narrowing the
// pattern to exclude it would be a guess about a name app-builder-lib owns.
it('scans the resources bundle too, which ships no helper to find', () => {
expect(
findCachedElevatePaths(makeCache('nsis-resources-3.4.1/plugins/x86-unicode/nsProcess.dll'), {
env: {}
})
).toEqual([])
const planted = 'nsis-resources-3.4.1/nsis-resources-3.4.1-p8w1z/elevate.exe'
const cacheDir = makeCache(planted)
expect(findCachedElevatePaths(cacheDir, { env: {} })).toEqual([
join(cacheDir, ...planted.split('/'))
])
})
// The rebuild picks one bundle, and nothing outside app-builder-lib knows which.
// Replacing every cached copy is the deliberate answer to that ambiguity.
it('replaces every cached copy when several bundles are present', async () => {
const cacheDir = makeCache(
'nsis-3.0.4.1/nsis-3.0.4.1-1mx3n/elevate.exe',
'nsis@1.2.1/nsis-bundle-3.12-k4d9x/elevate.exe'
)
const signed = join(scratch, 'signed-elevate.exe')
writeFileSync(signed, 'signpath-signed-elevate')
const { replaced } = await replaceCachedElevateHelpers({
signedPath: signed,
cacheDir,
env: {},
probe: probeUnavailable
})
expect(replaced).toHaveLength(2)
for (const path of replaced) {
expect(readFileSync(path, 'utf8')).toBe('signpath-signed-elevate')
}
})
it('covers the ELECTRON_BUILDER_NSIS_DIR override copy', () => {
const overrideDir = join(scratch, 'nsis-override')
mkdirSync(overrideDir, { recursive: true })
writeFileSync(join(overrideDir, 'elevate.exe'), 'unsigned-elevate')
const cacheDir = makeCache()
expect(
findCachedElevatePaths(cacheDir, { env: { ELECTRON_BUILDER_NSIS_DIR: overrideDir } })
).toEqual([join(overrideDir, 'elevate.exe')])
})
it('resolves the cache root the same way app-builder-lib does', () => {
expect(
resolveElectronBuilderCacheDir({
env: { LOCALAPPDATA: 'C:\\Users\\runneradmin\\AppData\\Local' },
platform: 'win32'
})
).toBe(join('C:\\Users\\runneradmin\\AppData\\Local', 'electron-builder', 'Cache'))
expect(resolveElectronBuilderCacheDir({ env: {}, platform: 'darwin', home: '/Users/a' })).toBe(
join('/Users/a', 'Library', 'Caches', 'electron-builder')
)
expect(resolveElectronBuilderCacheDir({ env: { ELECTRON_BUILDER_CACHE: '/mnt/cache' } })).toBe(
'/mnt/cache'
)
})
// Proof against the layout actually on disk, not just the fixtures. Cross-checked
// against an independent unbounded walk so a search that scopes itself wrongly
// cannot pass by finding nothing — which is exactly how the inline path passed.
// Skipped only where no NSIS bundle has been downloaded into the cache yet.
it('finds every elevate.exe the real electron-builder cache holds', (ctx) => {
const cacheDir = resolveElectronBuilderCacheDir()
if (!existsSync(cacheDir)) {
// Reported as skipped, never as passed: this is the one test that checks the scan
// against a layout nobody wrote down, and a silent no-op here is the suite
// confirming itself. The Linux unit-test job has no electron-builder cache.
ctx.skip()
return
}
const walk = (dir) =>
readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name)
if (entry.isDirectory()) {
return walk(path)
}
return entry.name.toLowerCase() === 'elevate.exe' ? [path] : []
})
const onDisk = walk(cacheDir)
if (onDisk.length === 0) {
ctx.skip()
return
}
expect(findCachedElevatePaths(cacheDir, { env: {} }).sort()).toEqual(onDisk.sort())
})
})
describe('the probe, not the scan, decides whether the swap worked', () => {
// Why the probe is load-bearing: `getBinFromCustomLoc` passes `nsis-<version>` to `getBin`
// as its in-process promise key only — the extract dir is named for the custom URL's parent
// segment, so a customNsisBinary bundle can sit outside `nsis*` entirely.
it('covers a custom bundle the directory scan cannot match', async () => {
const relative = 'orca-nsis-mirror/nsis-custom-3.11-0zqp2/elevate.exe'
const cacheDir = makeCache(relative)
const packed = join(cacheDir, ...relative.split('/'))
const signed = join(scratch, 'signed-elevate.exe')
writeFileSync(signed, 'signpath-signed-elevate')
expect(findCachedElevatePaths(cacheDir, { env: {} })).toEqual([])
const result = await replaceCachedElevateHelpers({
signedPath: signed,
cacheDir,
env: {},
probe: probeFound(packed)
})
expect(result.toolsetReplaced).toBe(true)
expect(readFileSync(packed, 'utf8')).toBe('signpath-signed-elevate')
expect(summarizeSwap(result)).toEqual({ annotations: [], exitCode: 0 })
})
// The shape that reproduced the hole: release-cut.yml restores the toolset cache with
// `restore-keys: electron-builder-win-`, so a stale release directory survives a lockfile
// change. Replacing that stale copy satisfies `replaced.length > 0` on its own while the
// bundle the rebuild packs sits in a directory the scan never matches.
it('does not call a stale directory a success when the packed bundle is unmatched', async () => {
const stale = 'nsis-3.0.4.1/nsis-3.0.4.1-1mx3n/elevate.exe'
const packed = 'builder-nsis@4.0.0/nsis-bundle-4.0-k4d9x/elevate.exe'
const cacheDir = makeCache(stale, packed)
const signed = join(scratch, 'signed-elevate.exe')
writeFileSync(signed, 'signpath-signed-elevate')
const result = await replaceCachedElevateHelpers({
signedPath: signed,
cacheDir,
env: {},
probe: probeUnavailable
})
// The scan rewrote only the stale copy; the one that would be packed is untouched.
expect(result.replaced).toEqual([join(cacheDir, ...stale.split('/'))])
expect(readFileSync(join(cacheDir, ...packed.split('/')), 'utf8')).toBe('unsigned-elevate')
// So the run must not look clean.
const { annotations, exitCode } = summarizeSwap(result)
expect(exitCode).toBe(0)
expect(annotations).toHaveLength(1)
expect(annotations[0].level).toBe('warning')
expect(annotations[0].message).toContain('Could not ask app-builder-lib')
})
it('fails when the probe names a copy that could not be replaced', () => {
const summary = summarizeSwap({
replaced: ['C:/cache/nsis-3.0.4.1/nsis-3.0.4.1-1mx3n/elevate.exe'],
cacheDir: 'C:/cache',
toolsetPath: 'C:/cache/nsis@2.0.0/nsis-bundle-4.0-k4d9x/elevate.exe',
toolsetError: null,
toolsetReplaced: false
})
expect(summary.exitCode).toBe(1)
expect(summary.annotations[0].level).toBe('error')
expect(summary.annotations[0].message).toContain('will pack')
})
it('fails when nothing at all was replaced', () => {
const summary = summarizeSwap({
replaced: [],
cacheDir: 'C:/cache',
toolsetPath: null,
toolsetError: 'app-builder-lib not loadable',
toolsetReplaced: false
})
expect(summary.exitCode).toBe(1)
expect(summary.annotations[0].level).toBe('error')
expect(summary.annotations[0].message).toContain('No cached elevate.exe found')
})
})
describe('a cached elevate.exe miss is not silent', () => {
// ELECTRON_BUILDER_NSIS_DIR short-circuits app-builder-lib's own resolution before
// any download, so the probe fails offline instead of fetching the NSIS bundle.
function runScript(cacheDir, nsisDir, signedPath) {
return spawnSync(process.execPath, [scriptPath, signedPath], {
cwd: projectRoot,
encoding: 'utf8',
env: {
...process.env,
ELECTRON_BUILDER_CACHE: cacheDir,
ELECTRON_BUILDER_NSIS_DIR: nsisDir
}
})
}
it('exits non-zero with an ::error:: annotation when no cached copy is found', () => {
const cacheDir = makeCache()
const emptyNsisDir = join(scratch, 'empty-nsis')
mkdirSync(emptyNsisDir, { recursive: true })
const signed = join(scratch, 'signed-elevate.exe')
writeFileSync(signed, 'signpath-signed-elevate')
const result = runScript(cacheDir, emptyNsisDir, signed)
expect(result.status).toBe(1)
expect(result.stdout).toContain('::error::No cached elevate.exe found')
})
it('warns on the scan-only path so green never means the probe was skipped', () => {
const cacheDir = makeCache('nsis-3.0.4.1/nsis-3.0.4.1-1mx3n/elevate.exe')
const emptyNsisDir = join(scratch, 'empty-nsis')
mkdirSync(emptyNsisDir, { recursive: true })
const signed = join(scratch, 'signed-elevate.exe')
writeFileSync(signed, 'signpath-signed-elevate')
const result = runScript(cacheDir, emptyNsisDir, signed)
expect(result.status).toBe(0)
expect(result.stdout).not.toContain('::error::')
expect(result.stdout).toContain('::warning::Could not ask app-builder-lib')
expect(
readFileSync(join(cacheDir, 'nsis-3.0.4.1', 'nsis-3.0.4.1-1mx3n', 'elevate.exe'), 'utf8')
).toBe('signpath-signed-elevate')
})
// The healthy release-job path: app-builder-lib answers, so the copy it will pack is the
// one that gets replaced and there is nothing to warn about.
it('exits clean when the probe resolves the copy the rebuild will pack', () => {
const cacheDir = makeCache()
const nsisDir = join(scratch, 'nsis-bundle')
mkdirSync(nsisDir, { recursive: true })
writeFileSync(join(nsisDir, 'elevate.exe'), 'unsigned-elevate')
const signed = join(scratch, 'signed-elevate.exe')
writeFileSync(signed, 'signpath-signed-elevate')
const result = runScript(cacheDir, nsisDir, signed)
expect(result.status).toBe(0)
expect(result.stdout).not.toContain('::error::')
expect(result.stdout).not.toContain('::warning::')
expect(result.stdout).toContain('the copy app-builder-lib will pack')
expect(readFileSync(join(nsisDir, 'elevate.exe'), 'utf8')).toBe('signpath-signed-elevate')
})
})
describe('release-cut.yml swaps the cached elevate.exe through the resolver', () => {
function swapStep() {
const workflow = parse(
readFileSync(join(projectRoot, '.github/workflows/release-cut.yml'), 'utf8')
)
const step = workflow.jobs.build.steps.find(
(candidate) => candidate.name === 'Replace cached elevate.exe with the signed copy'
)
expect(step).toBeDefined()
return step
}
it('delegates the cache lookup to the script instead of an inline path', () => {
const step = swapStep()
expect(step.run).toContain('node config/scripts/replace-cached-nsis-elevate.mjs $signed')
// The hardcoded miss that shipped v1.4.193/v1.4.194 unsigned.
expect(step.run).not.toContain('electron-builder\\Cache\\nsis')
expect(step.run).not.toContain('-ErrorAction SilentlyContinue')
})
it('fails the step when the swap reports a miss', () => {
const step = swapStep()
// Matched as an executed statement: downgrading this to a Write-Host restores
// the silent fail-open that let the unsigned helper ship.
expect(step.run).toMatch(/if \(\$LASTEXITCODE -ne 0\) \{/)
expect(step.run).toMatch(/^\s*throw \$message\s*$/m)
expect(step.run).toContain('GITHUB_STEP_SUMMARY')
})
// Why kept: windows-signing-rehearsal.yml shares the electron-builder-win-<hash>
// cache key, so dropping this guard would let a test certificate reach a release cache.
it('still refuses to stage anything but a SignPath-signed helper', () => {
const step = swapStep()
expect(step.run).toContain("$signature.Status -ne 'Valid'")
expect(step.run).toContain("$subject -notlike '*CN=SignPath Foundation*'")
})
// The inner-signing chain stays fail-open: a loud red step, not an unbuildable release.
it('keeps the step unable to fail the release job', () => {
expect(swapStep()['continue-on-error']).toBe(true)
})
})
@@ -0,0 +1,55 @@
import assert from 'node:assert/strict'
import { performance } from 'node:perf_hooks'
import { extractIconHref } from '../../src/main/repo-icon-source-href.ts'
// Original production expressions, preserved for the before/after measurement.
const html =
/<link\b(?=[^>]*\brel=["'](?:icon|shortcut icon)["'])(?=[^>]*\bhref=["']([^"'?]+))[^>]*>/i
const object =
/(?=[^}]*\brel\s*:\s*["'](?:icon|shortcut icon)["'])(?=[^}]*\bhref\s*:\s*["']([^"'?]+))[^}]*/i
const original = (source) => source.match(html)?.[1] ?? source.match(object)?.[1] ?? null
function measurePair(source) {
original(source)
extractIconHref(source)
const beforeSamples = []
const afterSamples = []
for (let run = 0; run < 5; run++) {
const measurements = [
[original, beforeSamples],
[extractIconHref, afterSamples]
]
if (run % 2 === 1) {
measurements.reverse()
}
for (const [fn, samples] of measurements) {
const started = performance.now()
fn(source)
samples.push(performance.now() - started)
}
}
return {
beforeMs: beforeSamples.sort((a, b) => a - b)[2],
afterMs: afterSamples.sort((a, b) => a - b)[2]
}
}
const results = []
for (const size of [8192, 16384, 32768]) {
for (const shape of ['no icon', 'rel without href', 'unterminated link starts']) {
const source =
shape === 'unterminated link starts'
? '<link '.repeat(Math.floor(size / 6))
: 'a'.repeat(size) + (shape === 'rel without href' ? ' rel:"icon"' : '')
assert.equal(extractIconHref(source), original(source))
const { beforeMs, afterMs } = measurePair(source)
results.push({
shape,
bytes: Buffer.byteLength(source),
beforeMs,
afterMs,
speedup: beforeMs / afterMs
})
}
}
console.log(JSON.stringify({ node: process.version, platform: process.platform, results }, null, 2))
@@ -199,7 +199,8 @@ async function runInsideSession(evidenceDir) {
'test:e2e:headful',
'--workers=1',
'--',
'tests/e2e/terminal-ibus-hangul-native.spec.ts'
'tests/e2e/terminal-ibus-hangul-native.spec.ts',
'tests/e2e/terminal-hangul-terminating-digit-native.spec.ts'
],
{
cwd: projectDir,
@@ -0,0 +1,79 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { stripTypeScriptTypes } from 'node:module'
import { performance } from 'node:perf_hooks'
import { blankStringContents as after } from '../../src/shared/source-scan/source-tree-scan.ts'
const ref = process.argv[2]
if (!ref) {
throw new Error('Usage: node config/scripts/source-string-blanking-benchmark.mjs <baseline-ref>')
}
const source = execFileSync('git', ['show', `${ref}:src/shared/source-scan/source-tree-scan.ts`], {
encoding: 'utf8'
})
const { blankStringContents: before } = await import(
`data:text/javascript;base64,${Buffer.from(stripTypeScriptTypes(source)).toString('base64')}`
)
const tokens = [
'a',
'/',
'*',
' ',
'\n',
'\r',
'\t',
'\u00a0',
'\u2028',
'"',
"'",
'`',
'${',
'}',
'{',
'\\',
'(',
')',
'[',
']',
'=',
'+',
'-',
';'
]
let seed = 173
for (let sample = 0; sample < 3000; sample++) {
let input = ''
for (let token = 0; token < 40; token++) {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
input += tokens[seed % tokens.length]
}
assert.equal(after(input), before(input), JSON.stringify(input))
assert.equal(after(input, true), before(input, true), JSON.stringify(input))
}
function measure(fn, input) {
const samples = []
for (let run = 0; run < 3; run++) {
const start = performance.now()
fn(input)
samples.push(performance.now() - start)
}
return samples.sort((a, b) => a - b)[1]
}
const results = []
for (const lines of [100, 1000, 5000, 10000]) {
const input = 'const x = value / 2;\n'.repeat(lines)
assert.equal(after(input), before(input))
results.push({
lines,
bytes: Buffer.byteLength(input),
beforeMs: measure(before, input),
afterMs: measure(after, input)
})
}
console.log(
JSON.stringify(
{ node: process.version, platform: process.platform, differentialCases: 3000, results },
null,
2
)
)
@@ -13,7 +13,8 @@ export const IME_ENGAGEMENT_RECEIPT_ENV = 'ORCA_E2E_IME_ENGAGEMENT_RECEIPT'
/** The tests that must each leave a receipt. Pinned so deleting one cannot quietly shrink the lane. */
export const EXPECTED_NATIVE_IME_TESTS = [
'forwards the issue exact-byte sequence without loss or duplication',
'forwards the issue sentence stress sequence without leaked ASCII'
'forwards the issue sentence stress sequence without leaked ASCII',
'a digit typed right after a Hangul syllable reaches the pty'
]
function parseReceipts(text) {
@@ -4,7 +4,7 @@ import {
verifyImeEngagementReceipts
} from './terminal-ime-engagement-receipt.mjs'
const [firstTest, secondTest] = EXPECTED_NATIVE_IME_TESTS
const [firstTest, secondTest, thirdTest] = EXPECTED_NATIVE_IME_TESTS
function receipt(test, overrides = {}) {
return JSON.stringify({
@@ -18,9 +18,11 @@ function receipt(test, overrides = {}) {
describe('verifyImeEngagementReceipts', () => {
it('accepts a run where every expected test observed real composition', () => {
expect(verifyImeEngagementReceipts(`${receipt(firstTest)}\n${receipt(secondTest)}\n`)).toEqual(
[]
)
expect(
verifyImeEngagementReceipts(
`${receipt(firstTest)}\n${receipt(secondTest)}\n${receipt(thirdTest)}\n`
)
).toEqual([])
})
// The failure this whole mechanism exists for: Playwright reports a skipped test as a pass, so
@@ -35,13 +37,20 @@ describe('verifyImeEngagementReceipts', () => {
it('rejects a partial run where only one test reached the engine', () => {
expect(verifyImeEngagementReceipts(`${receipt(firstTest)}\n`)).toEqual([
`no engagement receipt for "${secondTest}" — it was skipped, filtered out, or renamed`
`no engagement receipt for "${secondTest}" — it was skipped, filtered out, or renamed`,
`no engagement receipt for "${thirdTest}" — it was skipped, filtered out, or renamed`
])
})
it('requires the digit receipt even when both original native tests passed', () => {
expect(verifyImeEngagementReceipts(`${receipt(firstTest)}\n${receipt(secondTest)}\n`)).toEqual([
`no engagement receipt for "${thirdTest}" — it was skipped, filtered out, or renamed`
])
})
it('rejects a run that typed keys but never opened a composition', () => {
const problems = verifyImeEngagementReceipts(
`${receipt(firstTest, { compositionStart: 0 })}\n${receipt(secondTest)}\n`
`${receipt(firstTest, { compositionStart: 0 })}\n${receipt(secondTest)}\n${receipt(thirdTest)}\n`
)
expect(problems).toEqual([
`"${firstTest}" recorded no compositionstart — the IME never engaged`
@@ -50,7 +59,7 @@ describe('verifyImeEngagementReceipts', () => {
it('rejects a composition that produced no Hangul, which a latin passthrough would satisfy', () => {
const problems = verifyImeEngagementReceipts(
`${receipt(firstTest, { hangulComposition: 0 })}\n${receipt(secondTest)}\n`
`${receipt(firstTest, { hangulComposition: 0 })}\n${receipt(secondTest)}\n${receipt(thirdTest)}\n`
)
expect(problems).toEqual([
`"${firstTest}" recorded no Hangul composition data — the engine produced no syllables`
@@ -59,7 +68,7 @@ describe('verifyImeEngagementReceipts', () => {
it('rejects a renamed test rather than counting it toward coverage', () => {
const problems = verifyImeEngagementReceipts(
`${receipt(firstTest)}\n${receipt(secondTest)}\n${receipt('some new scenario')}\n`
`${receipt(firstTest)}\n${receipt(secondTest)}\n${receipt(thirdTest)}\n${receipt('some new scenario')}\n`
)
expect(problems).toEqual([
'unexpected engagement receipt for "some new scenario" — update EXPECTED_NATIVE_IME_TESTS'
@@ -68,7 +77,7 @@ describe('verifyImeEngagementReceipts', () => {
it('reports a truncated receipt rather than parsing around it', () => {
const problems = verifyImeEngagementReceipts(
`${receipt(firstTest)}\n{"test":"trunc\n${receipt(secondTest)}\n`
`${receipt(firstTest)}\n{"test":"trunc\n${receipt(secondTest)}\n${receipt(thirdTest)}\n`
)
expect(problems).toEqual(['malformed receipt line: {"test":"trunc'])
})
@@ -53,6 +53,19 @@ describe('electron-builder dev-channel identity', () => {
expect(config.win.verifyUpdateCodeSignature).toBe(false)
})
// Why on every channel: the hook is the only handle electron-builder gives on
// the NSIS uninstaller, and it signs nothing — it relays the file to and from
// the CI SignPath request. Carrying it must not drag a publisherName onto a
// dev build, which is the failure the split above exists to prevent.
it('carries the uninstaller sign hook without changing publisherName semantics', () => {
for (const env of [{}, WIN_ADHOC_ENV]) {
const config = loadConfigWithEnv(env)
expect(typeof config.win.signtoolOptions.sign).toBe('function')
}
expect(loadConfigWithEnv({}).win.signtoolOptions.publisherName).toBe('SignPath Foundation')
expect(loadConfigWithEnv(WIN_ADHOC_ENV).win.signtoolOptions.publisherName).toBeUndefined()
})
it.each([
['hourly', { ORCA_WIN_HOURLY: '1' }, 'orca-hourly'],
['daily', { ORCA_WIN_DAILY: '1' }, 'orca-daily'],
@@ -9,7 +9,8 @@
* hop escapes the store and configure fails with "node_addon_api.gyp not
* found" (run 32999886072).
*/
import { copyFileSync, mkdirSync, realpathSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join, resolve } from 'node:path'
@@ -22,6 +23,16 @@ export const WINDOWS_PROCESS_TREE_PACKAGE_DIR = join(
'windows-process-tree'
)
export const WINDOWS_PROCESS_TREE_PATCH_PATH = join(
ROOT,
'config',
'patches',
'@vscode__windows-process-tree@0.8.0.patch'
)
/** Only the patched reader defines this; the upstream one walks the PEB. */
const COMMAND_LINE_PATCH_MARKER = 'kProcessCommandLineInformation'
export const WINDOWS_PROCESS_TREE_NODE_ADDON_API_HEADERS = [
'napi.h',
'napi-inl.h',
@@ -39,6 +50,119 @@ export function nodeGypRebuildInvocation(arch, packageDir = WINDOWS_PROCESS_TREE
}
}
/** The binary the addon actually loads. */
export function windowsProcessTreeAddonPath(packageDir = WINDOWS_PROCESS_TREE_PACKAGE_DIR) {
return join(packageDir, 'build', 'Release', 'windows_process_tree.node')
}
/** The import whose absence tells the patched binary from the published prebuilt. */
const FLAGGED_IMPORT = 'ReadProcessMemory'
/**
* Does this compiled addon still carry the flagged primitive?
*
* The patched reader never calls `ReadProcessMemory`, so the symbol is absent
* from its import table; the upstream build imports it. That makes this a
* property of the binary rather than of the source next to it, which matters
* because the published tarball ships a *loadable* prebuilt built from
* unpatched source: it is node-addon-api, so it satisfies a bare `require()`
* under both Node and Electron, and a skipped rebuild would use it.
*
* Tri-state, not a predicate: a binary that is not there has not been cleared,
* and a boolean makes "absent" indistinguishable from "verified clean" at every
* call site. Takes the binary path so the relay's staged addon -- which sits
* beside the bundle, with no package around it -- gets the same check.
*
* @param {string} addonPath
* @returns {'clean' | 'unpatched' | 'missing'}
*/
export function inspectWindowsProcessTreeAddon(addonPath) {
if (!existsSync(addonPath)) {
return 'missing'
}
return readFileSync(addonPath).includes(FLAGGED_IMPORT) ? 'unpatched' : 'clean'
}
/**
* Refuse to compile or load the upstream command-line reader.
*
* Unpatched, it opens every process with `PROCESS_VM_READ` and walks the PEB to
* recover the command line -- the primitive MDE scores as credential dumping,
* and the reason this package is patched at all. pnpm has been seen
* materializing this CRLF package with its patch missing, so repair the source
* from the patch file, and drop any binary that predates the repair.
*/
export function ensureWindowsProcessTreeCommandLinePatch(
packageDir = WINDOWS_PROCESS_TREE_PACKAGE_DIR
) {
const source = join(packageDir, 'src', 'process_commandline.cc')
if (!existsSync(source)) {
throw new Error(
`${source} is missing, so the command-line patch cannot be verified. Run pnpm install.`
)
}
let repaired = false
if (!readFileSync(source, 'utf8').includes(COMMAND_LINE_PATCH_MARKER)) {
try {
execFileSync(
'git',
[
// Why force the line-ending mode: the patch is stored LF (a contract
// test forbids CR bytes in it), but upstream ships this source CRLF,
// so its pre-image lines and the file's differ by a CR. Under
// `core.autocrlf=false` -- Git's own built-in default, and what
// "checkout as-is" selects in the Git for Windows installer -- git
// compares them literally, the hunk does not match, and the repair
// throws. `input` normalizes line endings for that comparison and
// nothing else, so a hunk whose real content drifted is still
// rejected. Measured: without it, apply exits 1 at autocrlf=false and
// 0 at true/input; with it, 0 for CRLF and LF sources under all three.
'-c',
'core.autocrlf=input',
'apply',
'--include=src/process_commandline.cc',
WINDOWS_PROCESS_TREE_PATCH_PATH
],
{
cwd: realpathSync(packageDir),
stdio: 'pipe',
// Why blind git to the repo: run inside a work tree, `git apply`
// prefixes patch paths with the cwd-relative prefix, silently skips
// everything that does not match -- and still exits 0. The package
// dir is always under the project root, so without this the repair
// reports success and changes nothing.
env: { ...process.env, GIT_DIR: join(packageDir, '.orca-no-such-git-dir') }
}
)
} catch (error) {
throw new Error(
'src/process_commandline.cc still reads the PEB, and repairing it from ' +
`${WINDOWS_PROCESS_TREE_PATCH_PATH} failed: ${error?.message ?? error}. Run pnpm install.`
)
}
if (!readFileSync(source, 'utf8').includes(COMMAND_LINE_PATCH_MARKER)) {
throw new Error(
'src/process_commandline.cc still reads the PEB after repair, so the patch did not ' +
'apply. Run pnpm install.'
)
}
repaired = true
}
// A binary from before the repair -- or the tarball's own prebuilt -- would
// otherwise survive a skipped rebuild and load the flagged reader anyway.
// Deleting it can fail EPERM against a loaded (memory-mapped) addon, which
// `force: true` does not cover -- it only swallows ENOENT. That throw is the
// caller's to classify as a Windows file lock, so it must not be swallowed.
if (inspectWindowsProcessTreeAddon(windowsProcessTreeAddonPath(packageDir)) === 'unpatched') {
rmSync(windowsProcessTreeAddonPath(packageDir), { force: true })
repaired = true
}
return repaired
}
// Patched binding.gyp includes deps/node-addon-api; the tarball does not ship those headers.
export function stageWindowsProcessTreeNodeAddonApiHeaders(
packageDir = WINDOWS_PROCESS_TREE_PACKAGE_DIR
@@ -10,8 +10,9 @@ import {
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
inspectWindowsProcessTreeAddon,
nodeGypRebuildInvocation,
stageWindowsProcessTreeNodeAddonApiHeaders,
WINDOWS_PROCESS_TREE_NODE_ADDON_API_HEADERS,
@@ -59,3 +60,40 @@ describe('windows-process-tree node-gyp rebuild', () => {
}
})
})
describe('inspecting a compiled windows-process-tree addon', () => {
let dir
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'orca-windows-process-tree-addon-'))
})
afterEach(() => {
rmSync(dir, { recursive: true, force: true })
})
it('reports a binary that still imports ReadProcessMemory as unpatched', () => {
const addonPath = join(dir, 'windows_process_tree.node')
writeFileSync(addonPath, Buffer.from('MZ\0\0KERNEL32.dll\0ReadProcessMemory\0', 'binary'))
expect(inspectWindowsProcessTreeAddon(addonPath)).toBe('unpatched')
})
it('reports a binary without the import as clean', () => {
const addonPath = join(dir, 'windows_process_tree.node')
writeFileSync(addonPath, Buffer.from('MZ\0\0ntdll.dll\0NtQueryInformationProcess\0', 'binary'))
expect(inspectWindowsProcessTreeAddon(addonPath)).toBe('clean')
})
// The whole point of the tri-state: absence is not evidence of safety, and a
// boolean made "there is no binary" indistinguishable from "checked, clean".
it('reports an absent binary as missing rather than clean', () => {
expect(inspectWindowsProcessTreeAddon(join(dir, 'windows_process_tree.node'))).toBe('missing')
})
it('inspects whatever path it is handed, including a relay-staged addon', () => {
// The relay loads `./windows-process-tree.node` beside its bundle, which is
// nowhere near a node_modules package directory.
const staged = join(dir, 'windows-process-tree.node')
writeFileSync(staged, Buffer.from('MZ\0\0ReadProcessMemory\0', 'binary'))
expect(inspectWindowsProcessTreeAddon(staged)).toBe('unpatched')
})
})
@@ -1,4 +1,5 @@
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
@@ -212,6 +213,7 @@ describe('Windows signing workflow contract', () => {
'Notify Slack that inner-binary signing is waiting for approval',
'Download signed inner binaries from SignPath',
'Restore signed inner binaries into unpacked app',
'Restore signed uninstaller for the installer rebuild',
'Replace cached elevate.exe with the signed copy',
'Rebuild NSIS installer from signed unpacked app'
]
@@ -222,3 +224,235 @@ describe('Windows signing workflow contract', () => {
}
})
})
// Why these exist: the NSIS uninstaller is generated inside electron-builder's
// uninstaller pass and deleted immediately after being embedded, so the only way
// CI can sign it is the export/import relay through win.signtoolOptions.sign.
// Every link is asserted here the way Orca.exe and conpty_console_list.node are.
describe('Windows NSIS uninstaller signing', () => {
const releaseSteps = () => readWorkflow('.github/workflows/release-cut.yml').jobs.build.steps
const stepNamed = (steps, name) => steps.find((step) => step.name === name)
const EXPORT_ENV = 'ORCA_WIN_UNINSTALLER_EXPORT_PATH'
const SIGNED_ENV = 'ORCA_WIN_UNINSTALLER_SIGNED_PATH'
it('exports the uninstaller from the first Windows build', () => {
const build = stepNamed(releaseSteps(), 'Build Windows release artifacts')
expect(build.env[EXPORT_ENV]).toContain('uninstaller-signing')
expect(build.env[EXPORT_ENV]).toContain('orca-uninstaller.exe')
})
// Why this is a test and not a comment: `files` in the electron-builder config
// is all-negation, so app-builder packs whatever is left in the checkout root.
// These steps retry, and a retried attempt would pack an unsigned .exe into
// app.asar — the very defect this chain removes. Every relay path must live
// outside the checkout.
it('keeps every relay path out of the packed checkout', () => {
const relayEnvValues = [
...releaseSteps(),
...readWorkflow('.github/workflows/windows-signing-rehearsal.yml').jobs.rehearse.steps
].flatMap((step) => [step.env?.[EXPORT_ENV], step.env?.[SIGNED_ENV]].filter(Boolean))
expect(relayEnvValues.length).toBe(4)
for (const value of relayEnvValues) {
expect(value).toContain('runner.temp')
expect(value).not.toContain('github.workspace')
}
const relayScripts = [
...releaseSteps(),
...readWorkflow('.github/workflows/windows-signing-rehearsal.yml').jobs.rehearse.steps
]
.map((step) => step.run ?? '')
.filter((run) => run.includes('uninstaller-signing'))
expect(relayScripts.length).toBeGreaterThan(0)
for (const run of relayScripts) {
// Why count occurrences rather than assert `toContain` once: a step
// carrying two relay paths could root the first in RUNNER_TEMP and leave
// the second bare-relative — which resolves against the checkout, and is
// exactly the shape of the defect this test exists to catch.
const mentions = run.match(/uninstaller-signing/g) ?? []
const rooted = run.match(/Join-Path \$env:RUNNER_TEMP 'uninstaller-signing/g) ?? []
expect(rooted.length, run).toBe(mentions.length)
expect(run).not.toContain('$env:GITHUB_WORKSPACE')
}
})
it('stages the uninstaller into the same request as the inner binaries', () => {
const stage = stepNamed(releaseSteps(), 'Stage unsigned inner PE files for signing')
expect(stage.run).toContain('uninstaller-signing\\unsigned\\orca-uninstaller.exe')
expect(stage.run).toContain('uninstaller\\orca-uninstaller.exe')
// No third SignPath request: exactly two submissions, as budgeted for the
// 1h + 4h approval waits inside the 360-minute job cap.
const submissions = releaseSteps().filter(
(step) => step.uses === 'signpath/github-action-submit-signing-request@v2'
)
expect(submissions).toHaveLength(2)
})
// A staged-but-unreturned uninstaller must not fail the inner chain, or a
// SignPath artifact-configuration gap would cost the inner-binary signatures.
it('keeps the uninstaller out of the inner-binary copy-back list', () => {
const stage = stepNamed(releaseSteps(), 'Stage unsigned inner PE files for signing')
const restoreInner = stepNamed(
releaseSteps(),
'Restore signed inner binaries into unpacked app'
)
expect(stage.run).not.toMatch(/\$list\.Add\(['"]uninstaller/)
expect(restoreInner.run).not.toContain('orca-uninstaller.exe')
})
// This step's outcome gates the upload of every inner binary, so a filesystem
// error while staging the uninstaller must not escape — otherwise one
// uninstaller-specific failure costs every inner-binary signature, which is
// strictly worse than the behaviour before this chain existed.
it('cannot let an uninstaller staging failure cost the inner-binary signatures', () => {
const stage = stepNamed(releaseSteps(), 'Stage unsigned inner PE files for signing')
const uninstallerBlock = stage.run.slice(stage.run.indexOf('$exportedUninstaller'))
expect(stage.run).toMatch(/try \{[\s\S]*\$exportedUninstaller[\s\S]*\} catch \{/)
expect(uninstallerBlock).toContain('::warning::Could not stage the NSIS uninstaller')
expect(uninstallerBlock).not.toContain('throw')
// Explicit, so the catch does not silently depend on GitHub's
// $ErrorActionPreference='Stop' default for `shell: pwsh`.
expect(uninstallerBlock).toContain('New-Item -ItemType Directory -Force -Path (Split-Path')
expect(uninstallerBlock).toMatch(/New-Item[^\r\n]*-ErrorAction Stop/)
expect(uninstallerBlock).toMatch(/Copy-Item[^\r\n]*-ErrorAction Stop/)
// The upload it gates still keys off this step, so the catch is load-bearing.
expect(stepNamed(releaseSteps(), 'Upload unsigned inner binaries for SignPath').if).toContain(
"steps.stage-inner.outcome == 'success'"
)
})
it('re-injects the signed uninstaller into the rebuilt installer', () => {
const steps = releaseSteps()
const restore = stepNamed(steps, 'Restore signed uninstaller for the installer rebuild')
const rebuild = stepNamed(steps, 'Rebuild NSIS installer from signed unpacked app')
const names = steps.map((step) => step.name)
expect(restore.if).toContain('github.run_attempt == 1')
expect(restore.if).toContain("steps.restore-signed-inner.outcome == 'success'")
expect(restore.run).toContain('orca-uninstaller.exe')
expect(names.indexOf(restore.name)).toBeLessThan(names.indexOf(rebuild.name))
expect(rebuild.env[SIGNED_ENV]).toContain('uninstaller-signing')
// The rebuild must not depend on the uninstaller leg: a missing signed
// uninstaller ships today's installer, it does not skip the rebuild.
expect(rebuild.if).not.toContain('restore-signed-uninstaller')
})
// NSIS hides the uninstaller in a compressed data section the bundled 7za
// cannot read, so the gate proves it from the sign hook's digest receipt
// instead of extracting it — and only when the relay actually ran.
it('reports the embedded uninstaller in the inner-binary evidence gate', () => {
const gate = stepNamed(releaseSteps(), 'Verify Windows inner binary signatures')
expect(gate.env.UNINSTALLER_SIGNING_COMPLETED).toBe(
"${{ steps.restore-signed-uninstaller.outcome == 'success' }}"
)
expect(gate.run).toContain('.embedded-sha256')
expect(gate.run).toContain("$env:UNINSTALLER_SIGNING_COMPLETED -eq 'true'")
expect(gate.run).toContain('not signed by SignPath Foundation: Uninstall Orca.exe')
// The uninstaller must not join the 7z payload loop, which cannot see it.
expect(gate.run).not.toContain("$targets += 'Uninstall Orca.exe'")
})
it('rehearses the uninstaller leg end to end', () => {
const steps = readWorkflow('.github/workflows/windows-signing-rehearsal.yml').jobs.rehearse
.steps
const names = steps.map((step) => step.name)
const pack = stepNamed(steps, 'Package Windows app and export the NSIS uninstaller')
const rebuild = stepNamed(steps, 'Build NSIS installer from signed unpacked app')
const verify = stepNamed(steps, 'Verify signatures end to end')
// --dir never produces an uninstaller, so the rehearsal has to build the
// installer the way release-cut's first Windows pass does.
expect(pack.run).toContain('--win --publish never')
expect(pack.run).not.toContain('--dir')
expect(pack.env[EXPORT_ENV]).toContain('orca-uninstaller.exe')
expect(names).toContain('Restore signed uninstaller for the installer rebuild')
expect(rebuild.env[SIGNED_ENV]).toContain('orca-uninstaller.exe')
expect(verify.run).toContain('.embedded-sha256')
// The receipt only proves the import leg ran. The rehearsal is where the
// shipped uninstaller itself gets checked — the release job cannot install
// onto the runner it publishes from.
expect(verify.run).toContain('shipped: Uninstall Orca.exe')
expect(verify.run).toContain('-tnsis')
expect(verify.run).toContain("-ArgumentList '/S'")
})
// This workflow is the merge gate, so it must not be able to fail on its own
// artefact: 7-Zip's NSIS handler is unreliable enough that its output has to
// be corroborated before a signature verdict is drawn from it.
it('never lets an unreliable extract fail the rehearsal', () => {
const steps = readWorkflow('.github/workflows/windows-signing-rehearsal.yml').jobs.rehearse
.steps
const verify = stepNamed(steps, 'Verify signatures end to end')
// The 7-Zip route is only trusted when it reproduces the relayed bytes;
// otherwise it falls through to the install route rather than failing.
expect(verify.run).toContain(
'Write-Host "7-Zip\'s NSIS output did not match the relayed digest; falling back to a silent install."'
)
expect(verify.run).toMatch(/\$installedUninstaller = \$null\r?\n\s*\}/)
// The comparison that is not tautological: a file NSIS wrote out, against
// the digest the sign hook recorded.
expect(verify.run).toContain('$shippedDigest -ne $expectedDigest')
expect(verify.run).toContain('the uninstaller the installer ships is not the relayed one')
// An installer that prompts must not hang to the 360-minute job cap, and
// the app it launches must not outlive the step holding install-dir handles.
expect(verify.run).toContain('-PassThru')
expect(verify.run).toContain('$installerProcess.WaitForExit(300000)')
expect(verify.run).toContain('the silent install did not exit within 5 minutes')
expect(verify.run).toMatch(/for \(\$attempt = 0; \$attempt -lt 20; \$attempt\+\+\)/)
expect(verify.run).toContain("Get-Process -Name 'orca-terminal-daemon'")
})
// resources\elevate.exe is downgraded to advisory because app-builder-lib's
// CopyElevateHelper clobbers it on every nsis pack — a pre-existing defect
// that predates the uninstaller relay and is being tracked separately. The
// escape hatch it needed is the kind that quietly grows until the gate
// asserts nothing, so pin it to exactly that one file.
it('confines the advisory escape hatch to elevate.exe', () => {
const steps = readWorkflow('.github/workflows/windows-signing-rehearsal.yml').jobs.rehearse
.steps
const verify = stepNamed(steps, 'Verify signatures end to end')
const advisoryCalls = verify.run
.split('\n')
.filter((line) => line.includes('-Advisory') && line.includes('Test-Signature'))
expect(advisoryCalls).toHaveLength(1)
expect(advisoryCalls[0]).toContain('installed: $relative')
expect(verify.run).toContain("if ($relative -eq 'resources\\elevate.exe')")
// Both uninstaller verdicts stay fatal — the whole point of the gate.
for (const call of ['relayed: orca-uninstaller.exe', 'shipped: Uninstall Orca.exe']) {
const line = verify.run
.split('\n')
.find((it) => it.includes(`Test-Signature`) && it.includes(call))
expect(line, call).toBeDefined()
expect(line, call).not.toContain('-Advisory')
}
// An advisory must still reach the evidence artifact, or downgrading it
// becomes indistinguishable from deleting the check.
expect(verify.run).toContain('ADVISORY (known pre-existing')
expect(verify.run).toContain('$script:advisories.Add($problem)')
})
it('wires the electron-builder sign hook that the relay depends on', () => {
const require = createRequire(import.meta.url)
const configPath = resolve(projectDir, 'config/electron-builder.config.cjs')
delete require.cache[require.resolve(configPath)]
const config = require(configPath)
expect(typeof config.win.signtoolOptions.sign).toBe('function')
delete require.cache[require.resolve(configPath)]
})
})
@@ -0,0 +1,111 @@
// Why this exists: the NSIS uninstaller is the one Orca binary SignPath never
// saw. app-builder-lib builds it in a separate makensis pass, hands it to the
// packager's sign hook, embeds it in the installer, then deletes it
// (NsisTarget.computeScriptAndSignUninstaller → packager.signIf(uninstallerPath),
// then `unlink(defines.UNINSTALLER_OUT_FILE)`). That hook is the only moment the
// file exists on disk, so it is the only place a post-hoc signer can reach it.
//
// Orca does not sign during electron-builder — SignPath signs afterwards, behind
// a human approval — so instead of signing, this hook relays: build 1 exports the
// unsigned uninstaller so CI can put it in the existing inner-binaries SignPath
// request, and the rebuild-from-signed-tree pass swaps the signed bytes back in
// before makensis embeds them.
//
// Trap for whoever adds a real certificate to the Windows build: a custom sign
// hook *replaces* signtool rather than running alongside it — windowsSignToolManager
// does `const executor = customSign || (config => this.doSign(config))`. Inert
// today (no CSC_LINK/WIN_CSC_LINK anywhere in the Windows workflows), but setting
// one would silently sign nothing until this hook learns to delegate.
//
// Trap for whoever adds a second NSIS target or arch: app-builder-lib names the
// intermediate uninstaller per target *and* arch, while the relay is a single
// pair of env vars. Two targets would race — last write wins on export, every
// installer would embed the same uninstaller, and the receipt could not tell.
// Release is x64-only `--win` with `win.target` unset (so `["nsis"]`) today.
const { createHash } = require('node:crypto')
const { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } = require('node:fs')
const { basename, dirname } = require('node:path')
// app-builder-lib names the intermediate uninstaller `<installer basename>__uninstaller.exe`.
const UNINSTALLER_BASENAME_SUFFIX = '__uninstaller.exe'
// Why a receipt: NSIS embeds the uninstaller in its own compressed data section,
// not in the app 7z payload the evidence gate extracts, so the shipped installer
// cannot be inspected for it with the bundled 7za. The receipt records the digest
// of the exact bytes handed to makensis, which the gate compares against the
// SignPath-returned file — proving what was embedded without extracting it.
const EMBEDDED_RECEIPT_SUFFIX = '.embedded-sha256'
const isNsisUninstallerArtifact = (filePath) =>
typeof filePath === 'string' && basename(filePath).endsWith(UNINSTALLER_BASENAME_SUFFIX)
/**
* Pure relay. Returns a short verdict string for logging and tests.
* Never throws: a relay failure must ship today's installer, not break the build.
*/
function relayNsisUninstaller({
filePath,
exportPath,
signedPath,
fs = { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync }
}) {
if (!isNsisUninstallerArtifact(filePath)) {
return 'not-uninstaller'
}
try {
// Import wins over export: the rebuild pass must embed the signed bytes even
// though it also regenerates an unsigned uninstaller of its own.
if (signedPath) {
if (!fs.existsSync(signedPath)) {
return 'signed-missing'
}
fs.copyFileSync(signedPath, filePath)
const digest = createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')
fs.writeFileSync(`${signedPath}${EMBEDDED_RECEIPT_SUFFIX}`, digest)
return 'imported'
}
if (exportPath) {
fs.mkdirSync(dirname(exportPath), { recursive: true })
fs.copyFileSync(filePath, exportPath)
return 'exported'
}
return 'idle'
} catch (error) {
return `failed: ${error.message}`
}
}
const VERDICT_MESSAGES = {
imported: (paths) => `embedded the SignPath-signed uninstaller from ${paths.signedPath}`,
exported: (paths) => `exported the unsigned uninstaller to ${paths.exportPath}`,
'signed-missing': (paths) =>
`no signed uninstaller at ${paths.signedPath}; embedding the unsigned one (fail-open)`
}
/**
* electron-builder `win.signtoolOptions.sign` hook. Called for every Windows
* executable, twice per file (once per signing hash), so it must be cheap for
* non-uninstaller paths and idempotent for the uninstaller.
*/
function signWindowsUninstallerViaSignPath(configuration) {
const paths = {
filePath: configuration?.path,
exportPath: process.env.ORCA_WIN_UNINSTALLER_EXPORT_PATH || undefined,
signedPath: process.env.ORCA_WIN_UNINSTALLER_SIGNED_PATH || undefined
}
const verdict = relayNsisUninstaller(paths)
const message = VERDICT_MESSAGES[verdict]
if (message) {
console.log(`[win-uninstaller-signing] ${message(paths)}`)
} else if (verdict.startsWith('failed')) {
console.warn(`[win-uninstaller-signing] ${verdict}; embedding the unsigned uninstaller.`)
}
}
module.exports = {
EMBEDDED_RECEIPT_SUFFIX,
UNINSTALLER_BASENAME_SUFFIX,
isNsisUninstallerArtifact,
relayNsisUninstaller,
signWindowsUninstallerViaSignPath
}
@@ -0,0 +1,235 @@
import { createHash } from 'node:crypto'
import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { createRequire } from 'node:module'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const {
EMBEDDED_RECEIPT_SUFFIX,
isNsisUninstallerArtifact,
relayNsisUninstaller,
signWindowsUninstallerViaSignPath
} = require('./windows-uninstaller-signing.cjs')
const makeDir = () => mkdtempSync(join(tmpdir(), 'orca-uninstaller-signing-'))
describe('isNsisUninstallerArtifact', () => {
// The name app-builder-lib's NsisTarget.computeScriptAndSignUninstaller gives
// the intermediate uninstaller; the hook keys off nothing else.
it('matches only electron-builder intermediate uninstallers', () => {
expect(isNsisUninstallerArtifact('C:\\dist\\orca-windows-setup.__uninstaller.exe')).toBe(true)
expect(isNsisUninstallerArtifact('/dist/orca-windows-setup.__uninstaller.exe')).toBe(true)
expect(isNsisUninstallerArtifact('C:\\dist\\win-unpacked\\Orca.exe')).toBe(false)
expect(isNsisUninstallerArtifact('C:\\dist\\orca-windows-setup.exe')).toBe(false)
expect(isNsisUninstallerArtifact(undefined)).toBe(false)
})
})
describe('relayNsisUninstaller', () => {
const writeUninstaller = (dir, contents) => {
const filePath = join(dir, 'orca-windows-setup.__uninstaller.exe')
writeFileSync(filePath, contents)
return filePath
}
it('ignores every file that is not the uninstaller', () => {
const dir = makeDir()
const filePath = join(dir, 'Orca.exe')
writeFileSync(filePath, 'app')
expect(relayNsisUninstaller({ filePath, exportPath: join(dir, 'out', 'x.exe') })).toBe(
'not-uninstaller'
)
})
it('exports the unsigned uninstaller, creating the destination directory', () => {
const dir = makeDir()
const filePath = writeUninstaller(dir, 'unsigned-uninstaller')
const exportPath = join(dir, 'uninstaller-signing', 'unsigned', 'orca-uninstaller.exe')
expect(relayNsisUninstaller({ filePath, exportPath })).toBe('exported')
expect(readFileSync(exportPath, 'utf8')).toBe('unsigned-uninstaller')
})
it('overwrites the freshly built uninstaller with the signed bytes', () => {
const dir = makeDir()
const filePath = writeUninstaller(dir, 'rebuild-unsigned')
const signedPath = join(dir, 'signed', 'orca-uninstaller.exe')
mkdirSync(join(dir, 'signed'))
writeFileSync(signedPath, 'signpath-signed')
expect(relayNsisUninstaller({ filePath, signedPath })).toBe('imported')
expect(readFileSync(filePath, 'utf8')).toBe('signpath-signed')
})
// The receipt is the evidence gate's only handle on the embedded uninstaller:
// NSIS hides it in a compressed section the bundled 7za cannot read.
it('records the digest of the bytes it handed makensis', () => {
const dir = makeDir()
const filePath = writeUninstaller(dir, 'rebuild-unsigned')
const signedPath = join(dir, 'signed', 'orca-uninstaller.exe')
mkdirSync(join(dir, 'signed'))
writeFileSync(signedPath, 'signpath-signed')
relayNsisUninstaller({ filePath, signedPath })
const expected = createHash('sha256').update('signpath-signed').digest('hex')
expect(readFileSync(`${signedPath}${EMBEDDED_RECEIPT_SUFFIX}`, 'utf8')).toBe(expected)
})
it('leaves no receipt when the signed uninstaller never came back', () => {
const dir = makeDir()
const filePath = writeUninstaller(dir, 'unsigned-uninstaller')
const signedPath = join(dir, 'absent', 'orca-uninstaller.exe')
relayNsisUninstaller({ filePath, signedPath })
expect(existsSync(`${signedPath}${EMBEDDED_RECEIPT_SUFFIX}`)).toBe(false)
})
// Import wins so the rebuild pass embeds the signed bytes even though it also
// regenerates an unsigned uninstaller of its own.
it('prefers importing over exporting when both are configured', () => {
const dir = makeDir()
const filePath = writeUninstaller(dir, 'rebuild-unsigned')
const signedPath = join(dir, 'signed', 'orca-uninstaller.exe')
mkdirSync(join(dir, 'signed'))
writeFileSync(signedPath, 'signpath-signed')
expect(
relayNsisUninstaller({ filePath, signedPath, exportPath: join(dir, 'out', 'x.exe') })
).toBe('imported')
expect(readFileSync(filePath, 'utf8')).toBe('signpath-signed')
})
// Fail-open: a missing or unwritable relay must leave the build with today's
// unsigned uninstaller, never throw.
it('leaves the unsigned uninstaller in place when no signed copy came back', () => {
const dir = makeDir()
const filePath = writeUninstaller(dir, 'unsigned-uninstaller')
expect(
relayNsisUninstaller({ filePath, signedPath: join(dir, 'absent', 'orca-uninstaller.exe') })
).toBe('signed-missing')
expect(readFileSync(filePath, 'utf8')).toBe('unsigned-uninstaller')
})
it('swallows filesystem errors instead of failing the build', () => {
const dir = makeDir()
const filePath = writeUninstaller(dir, 'unsigned-uninstaller')
const fs = {
existsSync: () => true,
mkdirSync: () => {},
copyFileSync: () => {
throw new Error('EACCES')
}
}
expect(relayNsisUninstaller({ filePath, exportPath: join(dir, 'x.exe'), fs })).toBe(
'failed: EACCES'
)
})
it('does nothing when neither relay path is configured (local builds)', () => {
const dir = makeDir()
const filePath = writeUninstaller(dir, 'unsigned-uninstaller')
expect(relayNsisUninstaller({ filePath })).toBe('idle')
expect(readFileSync(filePath, 'utf8')).toBe('unsigned-uninstaller')
})
})
// Why a suite of its own: this is the function electron-builder actually calls,
// and it runs inside `Build Windows release artifacts`, which has no
// continue-on-error. If it throws, the release job dies before a single
// SignPath request is made. Nothing else in the chain guards that.
describe('signWindowsUninstallerViaSignPath', () => {
const RELAY_VARS = ['ORCA_WIN_UNINSTALLER_EXPORT_PATH', 'ORCA_WIN_UNINSTALLER_SIGNED_PATH']
const withEnv = (env, run) => {
const saved = Object.fromEntries(RELAY_VARS.map((key) => [key, process.env[key]]))
const apply = (values) => {
for (const key of RELAY_VARS) {
if (values[key] === undefined) {
delete process.env[key]
} else {
process.env[key] = values[key]
}
}
}
apply({ ...Object.fromEntries(RELAY_VARS.map((key) => [key, undefined])), ...env })
try {
return run()
} finally {
apply(saved)
}
}
const writeBuiltUninstaller = (dir) => {
const filePath = join(dir, 'orca-windows-setup.__uninstaller.exe')
writeFileSync(filePath, 'built-by-makensis')
return filePath
}
it.each([
['a missing configuration', undefined],
['a configuration with no path', {}],
['a non-uninstaller path', { path: 'C:\\dist\\win-unpacked\\Orca.exe' }]
])('never throws on %s', (_label, configuration) => {
withEnv({ ORCA_WIN_UNINSTALLER_EXPORT_PATH: join(makeDir(), 'out', 'x.exe') }, () => {
expect(() => signWindowsUninstallerViaSignPath(configuration)).not.toThrow()
})
})
// electron-builder calls the hook once per signing hash (sha1 then sha256),
// so both legs have to survive running twice over the same file.
it('is idempotent across the sha1 and sha256 invocations on both legs', () => {
const dir = makeDir()
const filePath = writeBuiltUninstaller(dir)
const exportPath = join(dir, 'relay', 'unsigned', 'orca-uninstaller.exe')
withEnv({ ORCA_WIN_UNINSTALLER_EXPORT_PATH: exportPath }, () => {
signWindowsUninstallerViaSignPath({ path: filePath })
signWindowsUninstallerViaSignPath({ path: filePath })
})
expect(readFileSync(exportPath, 'utf8')).toBe('built-by-makensis')
const signedPath = join(dir, 'relay', 'signed', 'orca-uninstaller.exe')
mkdirSync(join(dir, 'relay', 'signed'), { recursive: true })
writeFileSync(signedPath, 'signpath-signed')
withEnv({ ORCA_WIN_UNINSTALLER_SIGNED_PATH: signedPath }, () => {
signWindowsUninstallerViaSignPath({ path: filePath })
signWindowsUninstallerViaSignPath({ path: filePath })
})
expect(readFileSync(filePath, 'utf8')).toBe('signpath-signed')
expect(readFileSync(`${signedPath}${EMBEDDED_RECEIPT_SUFFIX}`, 'utf8')).toBe(
createHash('sha256').update('signpath-signed').digest('hex')
)
})
// An unwritable destination is the realistic filesystem failure, and it must
// cost the uninstaller signature rather than the release job.
it('never throws when the export destination cannot be created', () => {
const dir = makeDir()
const filePath = writeBuiltUninstaller(dir)
const blocker = join(dir, 'blocker')
writeFileSync(blocker, 'not a directory')
withEnv({ ORCA_WIN_UNINSTALLER_EXPORT_PATH: join(blocker, 'sub', 'x.exe') }, () => {
expect(() => signWindowsUninstallerViaSignPath({ path: filePath })).not.toThrow()
})
expect(readFileSync(filePath, 'utf8')).toBe('built-by-makensis')
})
it('does nothing when neither relay variable is set (local Windows builds)', () => {
const dir = makeDir()
const filePath = writeBuiltUninstaller(dir)
withEnv({}, () => {
expect(() => signWindowsUninstallerViaSignPath({ path: filePath })).not.toThrow()
})
expect(readFileSync(filePath, 'utf8')).toBe('built-by-makensis')
})
})