b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
119 lines
5.5 KiB
TypeScript
119 lines
5.5 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import test from 'node:test'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ELECTRON_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
|
|
// TODO FIXME these tests all grep source code for specific things. This is an antipattern.
|
|
// Tests should NEVER read src, only assert behavior.
|
|
|
|
function readElectronFile(name) {
|
|
return fs.readFileSync(path.join(ELECTRON_DIR, name), 'utf8').replace(/\r\n/g, '\n')
|
|
}
|
|
|
|
function requireHiddenChildOptions(source, needle) {
|
|
const match = needle instanceof RegExp ? needle.exec(source) : null
|
|
const index = needle instanceof RegExp ? (match?.index ?? -1) : source.indexOf(needle)
|
|
assert.notEqual(index, -1, `missing call site: ${needle}`)
|
|
const snippet = source.slice(index, index + 700)
|
|
assert.match(
|
|
snippet,
|
|
/hiddenWindowsChildOptions\(/,
|
|
`expected ${needle} to wrap child-process options with hiddenWindowsChildOptions`
|
|
)
|
|
}
|
|
|
|
test('desktop background child processes opt into hidden Windows consoles', () => {
|
|
const source = readElectronFile('main.ts')
|
|
|
|
assert.match(source, /function hiddenWindowsChildOptions\(options: any = \{\}\)/)
|
|
|
|
requireHiddenChildOptions(source, "execFileSync(\n 'reg'")
|
|
requireHiddenChildOptions(source, /execFileSync\(\s*pyExe/)
|
|
requireHiddenChildOptions(source, /spawn\(\s*resolveGitBinary\(\)/)
|
|
requireHiddenChildOptions(source, "execFileSync('taskkill'")
|
|
requireHiddenChildOptions(source, /spawn\(\s*command,\s*args/)
|
|
requireHiddenChildOptions(source, "spawn('curl'")
|
|
requireHiddenChildOptions(source, /spawn\(\s*backend\.command,\s*backend\.args/)
|
|
requireHiddenChildOptions(source, /hermesProcess = spawn\(\s*backend\.command,\s*backend\.args/)
|
|
requireHiddenChildOptions(source, /spawn\(\s*py,\s*\['-m', 'hermes_cli\.main', 'uninstall', '--gui-summary'\]/)
|
|
|
|
assert.match(source, /function unwrapWindowsVenvHermesCommand\(command, backendArgs\)/)
|
|
assert.match(source, /function getVenvSitePackagesEntries\(venvRoot\)/)
|
|
assert.match(source, /path\.join\(venvRoot, 'Lib', 'site-packages'\)/)
|
|
assert.match(source, /args: \['-m', 'hermes_cli\.main', \.\.\.backendArgs\]/)
|
|
})
|
|
|
|
test('desktop backend launches console python so child consoles are inherited, not pythonw', () => {
|
|
const source = readElectronFile('main.ts')
|
|
|
|
// The flash fix is structural: the backend runs as a console-subsystem
|
|
// python.exe under hiddenWindowsChildOptions() (-> CREATE_NO_WINDOW), so it
|
|
// owns ONE windowless console that every descendant spawn inherits. Launching
|
|
// it as GUI-subsystem pythonw.exe is what made each child allocate (and flash)
|
|
// its own console, so the backend command must never be pythonw.
|
|
assert.doesNotMatch(source, /pythonw\.exe'\)/, 'backend must not be launched via pythonw.exe')
|
|
assert.doesNotMatch(
|
|
source,
|
|
/function getNoConsoleVenvPython\b/,
|
|
'pythonw-conversion helper should be gone; console python is launched directly'
|
|
)
|
|
assert.doesNotMatch(
|
|
source,
|
|
/function applyWindowsNoConsoleSpawnHints\b/,
|
|
'pythonw spawn-hint rewriter should be gone'
|
|
)
|
|
|
|
// Console python restores stdout, so the port is announced on the normal
|
|
// HERMES_DASHBOARD_READY stdout line — no ready-file side channel is set.
|
|
assert.doesNotMatch(source, /readyFile: true/, 'no backend should opt into the pythonw ready-file path')
|
|
|
|
// Both desktop backend launches must still go through hiddenWindowsChildOptions
|
|
// so the single backend console is created windowless.
|
|
requireHiddenChildOptions(source, /spawn\(\s*backend\.command,\s*backend\.args/)
|
|
requireHiddenChildOptions(source, /hermesProcess = spawn\(\s*backend\.command,\s*backend\.args/)
|
|
})
|
|
|
|
test('desktop backend teardown tree-kills Windows backend descendants', () => {
|
|
const source = readElectronFile('main.ts')
|
|
|
|
const helperIndex = source.indexOf('function stopBackendChild(child)')
|
|
assert.notEqual(helperIndex, -1, 'missing backend teardown helper')
|
|
const helperSnippet = source.slice(helperIndex, helperIndex + 500)
|
|
assert.match(helperSnippet, /IS_WINDOWS && Number\.isInteger\(child\.pid\)/)
|
|
assert.match(helperSnippet, /forceKillProcessTree\(child\.pid\)/)
|
|
assert.match(helperSnippet, /child\.kill\('SIGTERM'\)/)
|
|
|
|
const resetIndex = source.indexOf('function resetHermesConnection()')
|
|
assert.notEqual(resetIndex, -1, 'missing resetHermesConnection')
|
|
const resetSnippet = source.slice(resetIndex, resetIndex + 300)
|
|
assert.match(resetSnippet, /stopBackendChild\(hermesProcess\)/)
|
|
assert.doesNotMatch(resetSnippet, /hermesProcess\.kill\('SIGTERM'\)/)
|
|
|
|
const quitIndex = source.indexOf("app.on('before-quit'")
|
|
assert.notEqual(quitIndex, -1, 'missing before-quit handler')
|
|
const quitSnippet = source.slice(quitIndex, quitIndex + 900)
|
|
assert.match(quitSnippet, /stopBackendChild\(hermesProcess\)/)
|
|
assert.doesNotMatch(quitSnippet, /hermesProcess\.kill\('SIGTERM'\)/)
|
|
})
|
|
|
|
test('intentional or interactive desktop child processes stay documented', () => {
|
|
const source = readElectronFile('main.ts')
|
|
|
|
assert.match(source, /windowsHide: false/)
|
|
assert.match(source, /handOffWindowsBootstrapRecovery/)
|
|
assert.match(source, /'--repair', '--branch'/)
|
|
assert.match(source, /'--update', '--branch'/)
|
|
assert.match(source, /nodePty\.spawn\(command, args/)
|
|
assert.match(source, /spawn\('cmd\.exe', \['\/c', 'start'/)
|
|
})
|
|
|
|
test('bootstrap PowerShell runner hides Windows console children', () => {
|
|
const source = readElectronFile('bootstrap-runner.ts')
|
|
|
|
assert.match(source, /function hiddenWindowsChildOptions\(options = \{\}\)/)
|
|
requireHiddenChildOptions(source, /spawn\(\s*ps,\s*fullArgs/)
|
|
})
|