Files
hermes-agent/apps/desktop/scripts/verify-side-by-side-macos.mjs
ethernet 9214174e07 fix(ci): lint legs after the main merge
- check_no_tmp_literals: resolve scratch via tempfile/os.tmpdir; the termux
  container mount point is one marked variable per script
- ruff TID251: desktop E2E fixtures may reach PM internals like tests do;
  the pm.runtime_stage ban message no longer names a module that never existed
- auth_codex: build the capped httpx stream subclass on first use so importing
  hermes_cli.auth_codex no longer forces httpx (the lazy proxy in auth_constants
  was defeated by a module-scope base class; broke lanes without httpx)
- desktop-smoke: launchApp is a parameter; the bundle-env test substitutes a
  refusing launcher instead of letting Playwright spawn a dying binary
  (3 unhandled rejections failed the tests-js lane)
2026-09-19 23:25:18 -04:00

315 lines
14 KiB
JavaScript

#!/usr/bin/env node
// Native fixture proof, not signed Hermes artifact E2E.
// Run: node apps/desktop/scripts/verify-side-by-side-macos.mjs <icon-input-root>
// Inputs: cases.json [{label, env}], plus <label>/icon{,-dark}.icns from
// scripts/generate_icons.py. Use stable, canary, commit-a, commit-b, in order.
// Existing Electron/build dependencies are read-only. All writes stay in /tmp.
import assert from 'node:assert/strict'
import { spawn, spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import os from 'node:os'
import path from 'node:path'
import { setTimeout as delay } from 'node:timers/promises'
import { fileURLToPath, pathToFileURL } from 'node:url'
assert.equal(process.platform, 'darwin', 'Run this proof on native macOS')
const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..')
const desktop = path.join(repo, 'apps/desktop')
const require = createRequire(path.join(desktop, 'package.json'))
const builder = path.dirname(require.resolve('app-builder-lib'))
const { AppInfo } = await import(pathToFileURL(path.join(builder, 'appInfo.js')))
const { MacPackager } = await import(pathToFileURL(path.join(builder, 'macPackager.js')))
const { createMacApp } = await import(pathToFileURL(path.join(builder, 'electron/mac/electronMac.js')))
const { build } = require('esbuild')
const electron = path.dirname(require.resolve('electron'))
const electronApp = path.join(electron, 'dist/Electron.app')
assert.ok(process.argv[2], 'Pass the icon input root with cases.json and generated ICNS files')
const inputs = path.resolve(process.argv[2])
const cases = JSON.parse(fs.readFileSync(path.join(inputs, 'cases.json'), 'utf8'))
assert.deepEqual(
cases.map(row => row.label),
['stable', 'canary', 'commit-a', 'commit-b']
)
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-sxs-macos-'))
const home = path.join(root, 'home')
fs.mkdirSync(home)
const env = {
...process.env,
HOME: home,
CFFIXED_USER_HOME: home,
TMPDIR: root,
HERMES_HOME: path.join(home, '.hermes'),
HERMES_RUNTIME_DIR: path.join(root, 'runtime')
}
for (const key of Object.keys(env)) {
if (
/^(HERMES_(BUILD_COMMIT|PAYLOAD_TAG|PAYLOAD_VERSION|DESKTOP_)|ELECTRON_RUN_AS_NODE|NODE_OPTIONS|BUILD_NUMBER)/.test(
key
)
)
delete env[key]
}
const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex')
const rows = []
const children = new Set()
console.log(`Native macOS fixture proof: ${root}`)
function checked(command, args, options = {}) {
const result = spawnSync(command, args, { encoding: 'utf8', env, timeout: 120_000, ...options })
fs.appendFileSync(
path.join(root, 'commands.log'),
JSON.stringify({ command, args, status: result.status, stdout: result.stdout, stderr: result.stderr }) + '\n'
)
if (result.error) throw result.error
assert.equal(result.status, 0, `${command} ${args.join(' ')}\n${result.stdout}\n${result.stderr}`)
return result.stdout.trim()
}
function plist(file) {
checked('/usr/bin/plutil', ['-lint', file])
return JSON.parse(checked('/usr/bin/plutil', ['-convert', 'json', '-o', '-', file]))
}
async function waitFor(predicate, detail) {
const deadline = Date.now() + 20_000
while (Date.now() < deadline) {
if (predicate()) return
await delay(50)
}
throw new Error(`Timed out: ${detail}`)
}
async function launch(row, label, hold) {
const receipt = path.join(root, `${label}.json`)
const log = fs.openSync(path.join(root, `${label}.log`), 'w')
const child = spawn(row.binary, ['--no-first-run', '--disable-gpu', '--disable-crash-reporter'], {
cwd: root,
env: { ...env, PROBE_RECEIPT: receipt, PROBE_HOLD: hold ? '1' : '0' },
stdio: ['ignore', log, log]
})
fs.closeSync(log)
children.add(child)
child.once('exit', () => children.delete(child))
await waitFor(() => fs.existsSync(receipt) || child.exitCode !== null || child.signalCode !== null, label)
assert.ok(fs.existsSync(receipt), `${label} exited before receipt; see ${label}.log`)
const result = JSON.parse(fs.readFileSync(receipt, 'utf8'))
if (!hold || !result.lock) await waitFor(() => child.exitCode !== null || child.signalCode !== null, `${label} exit`)
return { child, result, receipt }
}
async function stop(child) {
if (child.exitCode === null && child.signalCode === null) {
child.kill('SIGTERM')
await waitFor(() => child.exitCode !== null || child.signalCode !== null, 'fixture exit')
}
}
try {
for (const { label, env: flavor } of cases) {
const facts = JSON.parse(
checked(
process.execPath,
[
'--input-type=module',
'-e',
`
import identity from ${JSON.stringify(pathToFileURL(path.join(desktop, 'product-identity.cjs')).href)};
import config from ${JSON.stringify(pathToFileURL(path.join(desktop, 'electron-builder.config.cjs')).href)};
console.log(JSON.stringify({identity,config}));
`
],
{ env: { ...env, HERMES_DESKTOP_VARIANT: 'bundled', ...flavor } }
)
)
const { identity, config } = facts
const metadata = {
...JSON.parse(fs.readFileSync(path.join(desktop, 'package.json'), 'utf8')),
...config.extraMetadata
}
const appInfo = new AppInfo({ config, metadata }, null, config.mac)
const out = path.join(root, label)
fs.mkdirSync(out)
checked('/bin/cp', ['-R', electronApp, path.join(out, 'Electron.app')])
// Exercise the actual builder transformations, not a handwritten plist.
const adapter = {
config,
appInfo,
platformOptions: config.mac,
platformSpecificBuildOptions: config.mac,
framework: { distMacOsAppName: 'Electron.app' },
fileAssociations: [],
getPlatformConfig: () => ({ config: config.mac }),
getIconPath: async () => path.join(inputs, label, 'icon.icns'),
applyCommonInfo: MacPackager.prototype.applyCommonInfo
}
await createMacApp(adapter, out, null, 'mac')
const bundle = path.join(out, `${appInfo.productFilename}.app`)
const contents = path.join(bundle, 'Contents')
const info = plist(path.join(contents, 'Info.plist'))
assert.equal(info.CFBundleIdentifier, identity.appId)
for (const key of ['CFBundleName', 'CFBundleDisplayName', 'CFBundleExecutable'])
assert.equal(info[key], identity.displayName)
assert.ok(fs.existsSync(path.join(contents, 'MacOS', info.CFBundleExecutable)))
const helpers = fs
.readdirSync(path.join(contents, 'Frameworks'))
.filter(name => name.endsWith('.app'))
.map(name => {
const dir = path.join(contents, 'Frameworks', name, 'Contents')
const helper = plist(path.join(dir, 'Info.plist'))
assert.ok(helper.CFBundleIdentifier.startsWith(`${identity.appId}.helper`))
assert.ok(fs.existsSync(path.join(dir, 'MacOS', helper.CFBundleExecutable)))
return { name, id: helper.CFBundleIdentifier, executable: helper.CFBundleExecutable }
})
assert.ok(helpers.length > 0)
assert.equal(
hash(path.join(contents, 'Resources', info.CFBundleIconFile)),
hash(path.join(inputs, label, 'icon.icns'))
)
const icons = []
for (const filename of ['icon.icns', 'icon-dark.icns']) {
const icon = path.join(inputs, label, filename)
const decoded = path.join(out, `${filename}.iconset`)
checked('/usr/bin/iconutil', ['-c', 'iconset', '-o', decoded, icon])
const frames = fs
.readdirSync(decoded)
.filter(name => name.endsWith('.png'))
.map(frame => {
const dimensions = checked('/usr/bin/sips', [
'-g',
'pixelWidth',
'-g',
'pixelHeight',
path.join(decoded, frame)
])
const size = /^icon_(\d+)x\d+(@2x)?\.png$/.exec(frame)
assert.ok(size, `Unexpected native ICNS frame: ${frame}`)
const expected = Number(size[1]) * (size[2] ? 2 : 1)
const width = Number(/pixelWidth:\s*(\d+)/.exec(dimensions)?.[1])
const height = Number(/pixelHeight:\s*(\d+)/.exec(dimensions)?.[1])
assert.deepEqual([width, height], [expected, expected])
return { frame, width, height }
})
assert.ok(frames.length >= 7, `${label} ${filename}: incomplete native ICNS decode`)
assert.equal(Math.max(...frames.map(frame => frame.width)), 1024)
icons.push({ filename, sha256: hash(icon), frames })
}
const resources = path.join(contents, 'Resources')
fs.rmSync(path.join(resources, 'default_app.asar'), { force: true })
const appDir = path.join(resources, 'app')
fs.mkdirSync(appDir)
fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ ...metadata, main: 'main.mjs' }))
await build({
stdin: {
contents: `
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { app } from 'electron';
import { applyDesktopIdentity, PRODUCT_IDENTITY } from ${JSON.stringify(path.join(desktop, 'electron/product-identity.ts'))};
const initial = { name: app.getName(), userData: app.getPath('userData'), appData: app.getPath('appData') };
// HOME and CFFIXED_USER_HOME redirect native path discovery, not identity.
assert.ok(initial.userData.startsWith(process.env.HOME + '/'), JSON.stringify(initial));
const selected = applyDesktopIdentity(app);
const final = { name: app.getName(), userData: app.getPath('userData'), appData: app.getPath('appData') };
assert.ok(final.userData.startsWith(process.env.HOME + '/'));
fs.mkdirSync(final.userData, {recursive:true});
app.setPath('sessionData', final.userData);
const lock = app.requestSingleInstanceLock();
const result = { initial, final, selected, identity: PRODUCT_IDENTITY, lock, pid: process.pid, versions: process.versions, packaged: app.isPackaged, secondInstances: 0 };
const save = () => {
fs.writeFileSync(process.env.PROBE_RECEIPT + '.tmp', JSON.stringify(result, null, 2));
fs.renameSync(process.env.PROBE_RECEIPT + '.tmp', process.env.PROBE_RECEIPT);
};
app.on('second-instance', () => { result.secondInstances++; save(); });
if (!lock) { save(); app.exit(0); }
else app.whenReady().then(() => { app.dock?.hide(); save(); if (process.env.PROBE_HOLD !== '1') app.exit(0); });
setTimeout(() => app.exit(3), 90000).unref();
`,
resolveDir: desktop,
loader: 'ts'
},
outfile: path.join(appDir, 'main.mjs'),
bundle: true,
platform: 'node',
format: 'esm',
external: ['electron'],
define: { __HERMES_PRODUCT_IDENTITY__: JSON.stringify(identity) }
})
const row = {
label,
identity,
bundle,
binary: path.join(contents, 'MacOS', info.CFBundleExecutable),
info,
helpers,
icons,
updaterCache: appInfo.updaterCacheDirName,
packageName: metadata.name,
packageProductName: metadata.productName
}
rows.push(row)
console.log(`Bundle identity and native ICNS decode PASS: ${label}`)
}
for (const field of ['bundle', 'updaterCache']) assert.equal(new Set(rows.map(row => row[field])).size, 4)
assert.equal(new Set(rows.map(row => row.info.CFBundleIdentifier)).size, 4)
assert.equal(
new Set(rows.flatMap(row => row.helpers.map(helper => helper.id))).size,
rows.reduce((n, row) => n + row.helpers.length, 0)
)
for (const index of [0, 1]) assert.equal(new Set(rows.map(row => row.icons[index].sha256)).size, 4)
const running = []
for (const row of rows) {
const instance = await launch(row, `${row.label}-primary`, true)
assert.equal(instance.result.lock, true)
assert.equal(instance.result.packaged, true)
assert.equal(instance.result.initial.name, row.packageProductName)
assert.equal(path.basename(instance.result.initial.userData), row.packageProductName)
assert.equal(
path.basename(instance.result.final.userData),
row.label === 'stable' ? row.packageProductName : row.identity.appNamePascal
)
running.push(instance)
row.runtime = instance.result
}
assert.equal(new Set(running.map(row => row.result.final.userData)).size, 4)
const duplicate = await launch(rows[1], 'canary-duplicate', false)
assert.equal(duplicate.result.lock, false)
await waitFor(
() => JSON.parse(fs.readFileSync(running[1].receipt, 'utf8')).secondInstances === 1,
'canary second-instance event'
)
for (const instance of running) assert.equal(instance.child.exitCode, null)
await stop(running[1].child)
const restarted = await launch(rows[1], 'canary-restarted', false)
assert.equal(restarted.result.lock, true)
assert.equal(restarted.result.final.userData, running[1].result.final.userData)
for (const index of [0, 2, 3]) assert.equal(running[index].child.exitCode, null)
fs.writeFileSync(
path.join(root, 'receipt.json'),
JSON.stringify(
{
scope:
'Native builder transformation and Electron fixture only; no release build, signing, registration, update or Hermes backend E2E.',
platform: os.release(),
arch: process.arch,
node: process.version,
electron: JSON.parse(fs.readFileSync(path.join(electron, 'package.json'), 'utf8')).version,
builder: JSON.parse(fs.readFileSync(path.join(builder, '../package.json'), 'utf8')).version,
testSha256: hash(fileURLToPath(import.meta.url)),
sourceHashes: Object.fromEntries(
['product-identity.cjs', 'electron-builder.config.cjs', 'electron/product-identity.ts'].map(file => [
file,
hash(path.join(desktop, file))
])
),
rows,
duplicate: duplicate.result,
restarted: restarted.result
},
null,
2
)
)
console.log(
`PASS: four concurrent identities; duplicate rejected; canary restart isolated. Receipt: ${root}/receipt.json`
)
} finally {
for (const child of children) await stop(child)
}