Files
orca/config/scripts/rebuild-native-deps-node-pty.test.mjs
Neil 5fed670a50 fix(rebuild): refuse to compile node-pty source that lacks the MSYS breakaway denial (#21968)
The addon gate already rejects a conpty.node without the L"msys-2.0.dll"
marker, in the Electron probe and after the rebuild. But the rebuild compiles
whatever node_modules/node-pty holds, and pnpm only materializes that from the
patch at install time. On a Windows dev checkout whose node_modules predated
the denial, --force compiled for minutes, rewrote conpty.node byte-identical
and unpatched, and the gate then advised "rebuild from source" -- the step
that had just run.

Read src/win/conpty.cc before compiling. If it lacks the literal, stop before
the rebuild and say to run pnpm install, which re-applies the current patch.
An absent source file is not judged; the addon gate still reads the binary.
2026-09-21 03:00:44 -07:00

607 lines
23 KiB
JavaScript

import { existsSync, readFileSync } from 'node:fs'
import { removeTreeSync } from '../../src/shared/windows-transient-lock-removal.ts'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
gitLineEndingEnv,
initGitWorkTree,
mkTempProject,
runRebuildScript,
writeFakeElectronRebuild,
writeFakeLoadableNodePty,
writeFakeNodePtyConptyPayload,
writeFakeNodePtyConptySource,
writeFakeUsableElectronPackage,
writeFakeWindowsProcessTree,
writeFakeWindowsProcessTreeWithNodeAddonApi,
writeFakeWindowsRegistry,
writeNodePtyPatchFile,
writePatchedNodePtyBuildArtifacts,
writeWindowsProcessTreePatchFile
} from './rebuild-native-deps-test-fixtures.mjs'
describe('rebuild-native-deps patched node-pty rebuild', () => {
it.skipIf(process.platform !== 'win32')(
'repairs a missing ConPTY runtime before probing without recompiling node-pty',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { nativeDir: '../build/Release/' })
writeFakeWindowsRegistry(projectDir)
writeFakeWindowsProcessTree(projectDir)
writeFakeNodePtyConptyPayload(projectDir, process.arch)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: process.arch
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Restored node-pty ConPTY runtime files')
expect(result.stdout).toContain(
'Native modules already load in Electron; skipping rebuild.'
)
expect(existsSync(rebuildLogPath)).toBe(false)
} finally {
removeTreeSync(projectDir)
}
}
)
it('stages windows-process-tree node-addon-api headers before a Windows rebuild', () => {
const projectDir = mkTempProject()
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir)
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, result.stderr).toBe(0)
expect(
readFileSync(
join(
projectDir,
'node_modules',
'@vscode',
'windows-process-tree',
'deps',
'node-addon-api',
'napi.h'
),
'utf8'
)
).toBe('// napi.h\n')
} finally {
removeTreeSync(projectDir)
}
})
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('refuses a Windows rebuild when the process creation-time patch is missing', () => {
const projectDir = mkTempProject()
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir)
writeFakeNodePtyConptyPayload(projectDir, 'x64')
writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir, { creationTimePatchApplied: false })
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 creation-time patch')
} finally {
removeTreeSync(projectDir)
}
})
it('restores the ConPTY runtime payload after a Windows Electron rebuild', () => {
const projectDir = mkTempProject()
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir)
writeFakeNodePtyConptyPayload(projectDir, 'x64')
const result = runRebuildScript(
projectDir,
{ npm_config_platform: 'win32', npm_config_arch: 'x64' },
['--platform=win32', '--arch=x64', '--force']
)
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Restored node-pty ConPTY runtime files for win10-x64')
const runtimeDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release', 'conpty')
expect(readFileSync(join(runtimeDir, 'conpty.dll'), 'utf8')).toBe('conpty.dll x64')
expect(readFileSync(join(runtimeDir, 'OpenConsole.exe'), 'utf8')).toBe('OpenConsole.exe x64')
} finally {
removeTreeSync(projectDir)
}
})
it.skipIf(process.platform !== 'win32')(
'does not rebuild a healthy node-pty when another Windows addon fails its probe',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir)
writeFakeWindowsProcessTree(projectDir)
writeFakeNodePtyConptyPayload(projectDir, process.arch)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: process.arch
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Rebuilding failed native modules: @orca/windows-registry')
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['@orca/windows-registry'])
} finally {
removeTreeSync(projectDir)
}
}
)
it.skipIf(process.platform !== 'win32')(
'rebuilds a loadable ConPTY native that lacks Orca job ownership',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { ownsPtyJob: false })
writeFakeWindowsRegistry(projectDir)
writeFakeWindowsProcessTree(projectDir)
writeFakeNodePtyConptyPayload(projectDir, process.arch)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: process.arch
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Rebuilding failed native modules: node-pty')
expect(result.stdout).toContain('missing listJobProcessIds')
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
} finally {
removeTreeSync(projectDir)
}
}
)
// The shape measured on a Windows dev checkout: the addon loads under
// Electron, exports all three job functions, and predates the denial. A bare
// require proves nothing about it; the probe has to read the binary.
it.skipIf(process.platform !== 'win32')(
'rebuilds a loadable ConPTY native that owns its job but predates the MSYS breakaway denial',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { cygwinBreakawayDenied: false })
writeFakeWindowsRegistry(projectDir)
writeFakeWindowsProcessTree(projectDir)
writeFakeNodePtyConptyPayload(projectDir, process.arch)
writeFakeNodePtyConptySource(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: process.arch
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Rebuilding failed native modules: node-pty')
expect(result.stdout).toContain('predates the Cygwin/MSYS job-breakaway denial')
expect(result.stdout).not.toContain('skipping rebuild')
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
} finally {
removeTreeSync(projectDir)
}
}
)
it.skipIf(process.platform === 'win32')(
'rebuilds when Electron can load node-pty but patched build artifacts are missing',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir)
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir)
writeNodePtyPatchFile(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain(
'Patched node-pty build artifacts are missing; rebuilding from source.'
)
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
expect(rebuildCall.ignoreModules).toEqual(['cpu-features'])
expect(rebuildCall.force).toBe(true)
} finally {
removeTreeSync(projectDir)
}
}
)
it.skipIf(process.platform === 'win32')(
'keeps the Electron load-probe fast path once patched node-pty artifacts exist',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir)
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { nativeDir: '../build/Release/' })
writeNodePtyPatchFile(projectDir)
writePatchedNodePtyBuildArtifacts(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain(
'Native modules already load in Electron; skipping rebuild.'
)
expect(existsSync(rebuildLogPath)).toBe(false)
} finally {
removeTreeSync(projectDir)
}
}
)
it.skipIf(process.platform === 'win32')(
'rebuilds when patched artifacts exist but Electron falls back to node-pty prebuilds',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir)
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { nativeDir: '../prebuilds/darwin-arm64/' })
writeNodePtyPatchFile(projectDir)
writePatchedNodePtyBuildArtifacts(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Rebuilding failed native modules: node-pty')
expect(result.stdout).toContain("expected build/Release so Orca's node-pty patch is active")
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
expect(rebuildCall.force).toBe(true)
} finally {
removeTreeSync(projectDir)
}
}
)
// 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)
}
})
}
// The Electron probe carries this check too, but it is skipped whenever the
// Electron package binary is unusable. Every job export predates the MSYS
// breakaway denial, so without reading the binary this step would hand the
// packaged app one that leaks every Git Bash child out of its pane's job.
it('fails a Windows rebuild that leaves an addon predating the MSYS breakaway denial', () => {
const projectDir = mkTempProject()
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir)
writeFakeNodePtyConptyPayload(projectDir, 'x64', { cygwinBreakawayDenied: false })
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('predates the Cygwin/MSYS job-breakaway denial')
} finally {
removeTreeSync(projectDir)
}
})
// Measured on a Windows dev checkout whose node_modules predated the denial:
// `--force` compiled for minutes, rewrote conpty.node byte-identical and
// unpatched, and the addon gate then said "rebuild from source" -- the step
// that had just run. The source is readable before the compile; read it.
it('refuses to compile node-pty source that lacks the denial, before the rebuild runs', () => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeNodePtyConptyPayload(projectDir, 'x64')
writeFakeNodePtyConptySource(projectDir, { cygwinBreakawayDenied: false })
writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir)
const result = runRebuildScript(
projectDir,
{
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: 'x64'
},
['--platform=win32', '--arch=x64', '--force']
)
expect(result.status).not.toBe(0)
expect(result.stderr).toContain(join('src', 'win', 'conpty.cc'))
expect(result.stderr).toContain('`pnpm install`')
expect(result.stderr).not.toContain('Rebuild node-pty from source')
expect(existsSync(rebuildLogPath)).toBe(false)
} finally {
removeTreeSync(projectDir)
}
})
it('compiles node-pty when its source carries the denial', () => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeNodePtyConptyPayload(projectDir, 'x64')
writeFakeNodePtyConptySource(projectDir)
writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir)
const result = runRebuildScript(
projectDir,
{
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: 'x64'
},
['--platform=win32', '--arch=x64', '--force']
)
expect(result.status, result.stderr).toBe(0)
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toContain('node-pty')
} finally {
removeTreeSync(projectDir)
}
})
it('accepts a Windows rebuild whose addon carries the denial', () => {
const projectDir = mkTempProject()
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir)
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, result.stderr).toBe(0)
expect(result.stderr).not.toContain('job-breakaway denial')
} finally {
removeTreeSync(projectDir)
}
})
// A cross-host rebuild does not necessarily leave a win32 addon on this disk,
// and neither does a tree with no node-pty in it. That must warn, not fail an
// install that was working.
it('warns rather than fails when no addon is expected on this disk', () => {
const projectDir = mkTempProject()
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir)
writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir)
const result = runRebuildScript(
projectDir,
{ npm_config_platform: 'win32', npm_config_arch: 'x64' },
['--platform=win32', '--arch=x64', '--force']
)
expect(result.status, result.stderr).toBe(0)
expect(result.stderr + result.stdout).toContain('could not check the MSYS job-breakaway')
} finally {
removeTreeSync(projectDir)
}
})
// The other half: on the host that will run this install, a missing addon is
// not an absence to shrug at. loadNativeModule falls through to the published
// prebuild, which is the binary that leaks every MSYS pane child.
// Runs only on Windows -- nothing else can make a win32 rebuild same-host.
it.skipIf(process.platform !== 'win32')(
'fails a same-host Windows rebuild that left no addon, naming the prebuild that would load',
() => {
const projectDir = mkTempProject()
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir)
writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir)
writeFakeLoadableNodePty(projectDir, { nativeDir: `prebuilds/win32-${process.arch}` })
const result = runRebuildScript(
projectDir,
{ npm_config_platform: 'win32', npm_config_arch: process.arch },
['--platform=win32', `--arch=${process.arch}`, '--force']
)
expect(result.status).not.toBe(0)
expect(result.stderr).toContain(join('build', 'Release', 'conpty.node'))
expect(result.stderr).toContain(join('prebuilds', `win32-${process.arch}`, 'conpty.node'))
} finally {
removeTreeSync(projectDir)
}
}
)
})