Commit desktop bundles can bake environment defaults/clears (--bundle-unset HERMES_DESKTOP_USER_DATA_DIR, HERMES_HOME=null) that stomp the smoke driver's --home/--user-data pin, so every native smoke threw "Desktop did not honor the isolated home and userData directories". Instead of pinning, the driver replays the bundle env over its launch env through the same resolver the app runs, seeds the predicted home (bailing rather than wiping when it is not empty), and verifies the app landed there via a new hermesHome report on the version bridge. The --user-data equality check now applies only when the artifact bakes no env. - Extract the pure path resolver into electron/data-paths.mjs (data-paths.ts is now a typed re-export) so Node's type-stripped driver can import it. - Add applyBundleEnvironment/validateBundleEnvironment as the pure twin of the bundle banner, pinned by a lockstep test. - Record bundleEnv in the install stamp and add readBundledBundleEnv. - Report hermesHome from hermes:version and assert it equals the predicted home.
92 lines
5.5 KiB
JavaScript
92 lines
5.5 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 = { KEEP: 'default', MISSING: 'default', CLEARED: null, OVERRIDDEN: 'default', SUFFIX: 'baked' }
|
|
const keys = Object.keys(defaults)
|
|
const base = { KEEP: 'x', CLEARED: 'runtime', OVERRIDDEN: 'runtime', 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 viaBanner = JSON.parse(execFileSync(process.execPath, [outfile], { env: { ...process.env, ...base }, encoding: 'utf8' }))
|
|
const viaFunction = Object.fromEntries(keys.map(key => [key, applyBundleEnvironment(base, defaults)[key]]))
|
|
expect(viaFunction).toEqual(viaBanner)
|
|
expect(viaFunction).toEqual({
|
|
KEEP: 'x', MISSING: 'default', CLEARED: '', OVERRIDDEN: 'runtime', SUFFIX: 'explicit',
|
|
})
|
|
} 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', LITERAL: 'a=b "q"\n$(no)', EMPTY: '' }
|
|
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"}', '{"NAME":1}', '{"NAME":"\\u0000"}']) {
|
|
expect(() => environmentDefaultsBanner(bad)).toThrow()
|
|
}
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|