95 lines
5.7 KiB
JavaScript
95 lines
5.7 KiB
JavaScript
import { build } from 'esbuild'
|
|
import { execFileSync } from 'node:child_process'
|
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { expect, test } from 'vitest'
|
|
|
|
import { applyBundleEnvironment, environmentDefaultsBanner } from './bundle-env.mjs'
|
|
|
|
test('explicit clears beat inherited homes and prevent Windows registry fallback before spawning', async () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'hermes-bundle-clear-'))
|
|
const paths = fileURLToPath(new URL('../electron/data-paths.ts', import.meta.url))
|
|
const defaults = { HERMES_HOME: null, HERMES_DATA_DIR_SUFFIX: 'magic-test' }
|
|
try {
|
|
const entry = join(root, 'entry.mjs')
|
|
writeFileSync(entry, `
|
|
import {resolveDesktopHermesHome} from ${JSON.stringify(paths)};
|
|
import {execFileSync} from 'node:child_process';
|
|
let registryReads = 0;
|
|
const home = resolveDesktopHermesHome({
|
|
home: 'C:/Users/test', platform: 'win32', env: process.env,
|
|
readWindowsHome: () => { registryReads++; return 'C:/old-hermes'; }
|
|
});
|
|
console.log(JSON.stringify({cleared: process.env.HERMES_HOME, home, registryReads,
|
|
child: execFileSync(process.execPath, ['-p', 'process.env.HERMES_HOME'], {
|
|
env: {...process.env, HERMES_HOME: home}, encoding: 'utf8'
|
|
}).trim()}));
|
|
`)
|
|
const outfile = join(root, 'bundle.mjs')
|
|
await build({ entryPoints: [entry], bundle: true, platform: 'node', format: 'esm', outfile,
|
|
banner: { js: environmentDefaultsBanner(JSON.stringify(defaults)) } })
|
|
const env = { ...process.env, HERMES_HOME: 'C:/old-hermes', LOCALAPPDATA: 'C:/Users/test/AppData/Local' }
|
|
delete env.HERMES_DATA_DIR_SUFFIX
|
|
delete env.HERMES_DESKTOP_USER_DATA_DIR
|
|
const actual = JSON.parse(execFileSync(process.execPath, [outfile], { env, encoding: 'utf8' }))
|
|
expect(actual).toEqual({ cleared: '', home: 'C:\\Users\\test\\AppData\\Local\\hermesmagic-test',
|
|
child: 'C:\\Users\\test\\AppData\\Local\\hermesmagic-test', registryReads: 0 })
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('applyBundleEnvironment replays the banner semantics for defaults, runtime overrides and clears', async () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'hermes-bundle-pure-'))
|
|
const defaults = { HERMES_HOME: null, HERMES_DATA_DIR_SUFFIX: 'baked', HERMES_GUEST_ONBOARDING: '1' }
|
|
const keys = Object.keys(defaults)
|
|
const base = { HERMES_HOME: 'runtime', HERMES_DATA_DIR_SUFFIX: 'explicit' }
|
|
try {
|
|
// The bundled banner must produce the same effective environment the pure
|
|
// function computes, so the smoke driver can predict the app's home from
|
|
// the same bundle env data without running a child process.
|
|
writeFileSync(join(root, 'reader.mjs'), `export const values = Object.fromEntries(${JSON.stringify(keys)}.map(key => [key, process.env[key]]));`, 'utf8')
|
|
const entry = join(root, 'entry.mjs')
|
|
writeFileSync(entry, `import {values} from './reader.mjs'; console.log(JSON.stringify(values));`, 'utf8')
|
|
const outfile = join(root, 'bundle.mjs')
|
|
await build({ entryPoints: [entry], bundle: true, platform: 'node', format: 'esm', outfile, banner: { js: environmentDefaultsBanner(JSON.stringify(defaults)) } })
|
|
const env = { ...process.env, ...base }
|
|
delete env.HERMES_GUEST_ONBOARDING
|
|
const viaBanner = JSON.parse(execFileSync(process.execPath, [outfile], { env, encoding: 'utf8' }))
|
|
const viaFunction = Object.fromEntries(keys.map(key => [key, applyBundleEnvironment(base, defaults)[key]]))
|
|
expect(viaFunction).toEqual(viaBanner)
|
|
expect(viaFunction).toEqual({
|
|
HERMES_HOME: '', HERMES_DATA_DIR_SUFFIX: 'explicit', HERMES_GUEST_ONBOARDING: '1',
|
|
})
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('baked defaults precede imported module initialization and reach children without overriding explicit env', async () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'hermes-bundle-env-'))
|
|
const defaults = { HERMES_GUEST_ONBOARDING: '1', HERMES_DATA_DIR_SUFFIX: 'magic-test', HERMES_SHARED_AUTH_DIR: 'a=b "q"\n$(no)', HERMES_SKIP_INTRO: '' }
|
|
const env = { ...process.env }
|
|
for (const key of Object.keys(defaults)) {
|
|
delete env[key]
|
|
}
|
|
try {
|
|
writeFileSync(join(root, 'reader.mjs'), `export const values = Object.fromEntries(${JSON.stringify(Object.keys(defaults))}.map(key => [key, process.env[key]]));`, 'utf8')
|
|
const entry = join(root, 'entry.mjs')
|
|
writeFileSync(entry, `import {values} from './reader.mjs'; import {execFileSync} from 'node:child_process'; console.log(JSON.stringify({values, child: execFileSync(process.execPath, ['-p', 'process.env.HERMES_DATA_DIR_SUFFIX'], {encoding:'utf8'}).trim()}));`, 'utf8')
|
|
const outfile = join(root, 'bundle.mjs')
|
|
await build({ entryPoints: [entry], bundle: true, platform: 'node', format: 'esm', outfile, banner: { js: environmentDefaultsBanner(JSON.stringify(defaults)) } })
|
|
const run = extra => JSON.parse(execFileSync(process.execPath, [outfile], { env: { ...env, ...extra }, encoding: 'utf8' }))
|
|
expect(run({})).toEqual({ values: defaults, child: defaults.HERMES_DATA_DIR_SUFFIX })
|
|
expect(run({ HERMES_DATA_DIR_SUFFIX: '-explicit', HERMES_GUEST_ONBOARDING: '' })).toEqual({ values: { ...defaults, HERMES_DATA_DIR_SUFFIX: '-explicit', HERMES_GUEST_ONBOARDING: '' }, child: '-explicit' })
|
|
for (const bad of ['[]', 'null', '{"BAD-NAME":"x"}', '{"HERMES_HOME":1}', '{"HERMES_HOME":"\\u0000"}',
|
|
'{"NODE_OPTIONS":"--require=evil"}', '{"PATH":null}', '{"HERMES_PYTHON":"/untrusted/python"}']) {
|
|
expect(() => environmentDefaultsBanner(bad)).toThrow()
|
|
}
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|