Files
hermes-agent/apps/desktop/scripts/bundle-env.mjs
ethernet 0ecfcc2881 fix(ci): predict the desktop smoke's Hermes home from the baked bundle env
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.
2026-09-15 20:32:31 -04:00

46 lines
2.3 KiB
JavaScript

// Defaults must run before bundled modules resolve paths or onboarding flags.
// An esbuild define would replace reads, not populate the child process env.
/** Validate a bundle environment object: plain object of identifiers to
* strings (defaults) or null (clears). Shared by the banner writer and the
* smoke driver's stamp reader.
* @param {unknown} values @returns {Record<string, string|null>} */
export function validateBundleEnvironment(values) {
if (!values || Array.isArray(values) || typeof values !== 'object') {
throw new Error('Bundle environment must be a JSON object')
}
for (const [key, value] of Object.entries(values)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || (value !== null && (typeof value !== 'string' || value.includes('\0')))) {
throw new Error('Bundle environment requires valid names and string values without NUL, or null to clear')
}
}
return /** @type {Record<string, string|null>} */ (values)
}
/** Apply bundle environment defaults/clears to a base environment and return a
* new object without mutating it. This is the pure twin of the side-effecting
* loop the bundled banner emits at launch — keep the two in lockstep (pinned
* by bundle-env.test.mjs). Clears (null) set the key to an empty string
* unconditionally so a stale registry or ambient value cannot win; defaults
* fill only an absent key, so an explicit runtime value always wins.
* @param {Record<string, string|undefined>} env
* @param {Record<string, string|null>} values */
export function applyBundleEnvironment(env, values) {
const result = { ...env }
for (const [key, value] of Object.entries(values)) {
if (value === null) result[key] = ''
else if (result[key] == null) result[key] = value
}
return result
}
/** @param {string} raw @returns {string} */
export function environmentDefaultsBanner(raw) {
const values = validateBundleEnvironment(JSON.parse(raw))
// Keep clears present-but-empty so path resolution cannot restore registry
// defaults. The emitted loop is the side-effecting twin of
// applyBundleEnvironment above; it cannot call it (the banner runs before
// the bundle's imports), so the two must stay in lockstep — see the test.
return `\nfor (const [key, value] of ${JSON.stringify(Object.entries(values))}) { if (value === null) process.env[key] = ''; else process.env[key] ??= value; }\n`
}