Files
hermes-agent/apps/desktop/scripts/bundle-env.test.mjs

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 })
}
})