diff --git a/config/scripts/package-electron-install-owner.test.mjs b/config/scripts/package-electron-install-owner.test.mjs new file mode 100644 index 00000000000..3e1e3cf0d09 --- /dev/null +++ b/config/scripts/package-electron-install-owner.test.mjs @@ -0,0 +1,60 @@ +import { readFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { describe, expect, it } from 'vitest' +import { parse } from 'yaml' + +const projectDir = resolve(import.meta.dirname, '../..') +const readProject = (file) => readFileSync(join(projectDir, file), 'utf8') +const packageJson = JSON.parse(readProject('package.json')) +const pnpmWorkspace = parse(readProject('pnpm-workspace.yaml')) + +const OWNED_ELECTRON_REBUILD = 'node config/scripts/rebuild-native-deps.mjs' +// Why exact tokens and not /electron/i or a substring: the owner's own path has no "electron" +// in it, so a keyword check waves a duplicated rebuild through -- the case this contract is +// named for (#20787). Substring matching has the opposite fault: `install-app-deps` would also +// reject a `check-install-app-deps-version.mjs` that installs nothing. `rebuild:electron` is +// package.json's alias for the owned script, so running it is the same takeover. +const ELECTRON_INSTALL_COMMANDS = [ + OWNED_ELECTRON_REBUILD, + 'config/scripts/rebuild-native-deps.mjs', + 'rebuild:electron', + 'electron-rebuild', + 'electron-builder', + 'install-app-deps' +] +const tokenize = (step) => step.split(/[\s]+/).flatMap((word) => [word, ...word.split(/[@]/)]) +const takesOverElectronInstall = (step) => { + if (step.includes(OWNED_ELECTRON_REBUILD)) { + return true + } + const tokens = new Set(tokenize(step)) + return ELECTRON_INSTALL_COMMANDS.some((command) => tokens.has(command)) +} + +describe('Electron binary install ownership', () => { + it('keeps root postinstall as the single Electron binary install owner', () => { + // The invariant is that the root postinstall owns the Electron binary install, not that + // nothing may run after it -- pinning the whole string broke every open PR (#20726). + const steps = packageJson.scripts.postinstall.split('&&').map((step) => step.trim()) + expect(steps[0]).toBe(OWNED_ELECTRON_REBUILD) + for (const step of steps.slice(1)) { + expect(takesOverElectronInstall(step)).toBe(false) + } + expect(pnpmWorkspace.allowBuilds).not.toHaveProperty('electron') + }) + + // Why a separate case: the assertion above only reads the real postinstall, so it cannot show + // a bad chain would be caught. #20787 shipped a keyword check that missed a duplicated + // rebuild; these fixtures pin the rejections themselves. + it('rejects a chained step that would take over the Electron install', () => { + expect(takesOverElectronInstall(OWNED_ELECTRON_REBUILD)).toBe(true) + expect(takesOverElectronInstall('npx electron-rebuild')).toBe(true) + expect(takesOverElectronInstall('npx electron-builder install-app-deps')).toBe(true) + expect(takesOverElectronInstall('node config/scripts/sync-anti-slop-plugin.mjs')).toBe(false) + expect(takesOverElectronInstall('node config/scripts/check-electron-version.mjs')).toBe(false) + expect(takesOverElectronInstall('pnpm run rebuild:electron')).toBe(true) + expect(takesOverElectronInstall('node config/scripts/check-install-app-deps-version.mjs')).toBe( + false + ) + }) +}) diff --git a/config/scripts/package-electron-runtime-contract.test.mjs b/config/scripts/package-electron-runtime-contract.test.mjs index b5874a47c03..7ede0fc6208 100644 --- a/config/scripts/package-electron-runtime-contract.test.mjs +++ b/config/scripts/package-electron-runtime-contract.test.mjs @@ -24,18 +24,6 @@ describe('Electron runtime package contract', () => { linux: createPackagedRuntimeNodeModuleResources('linux') } - it('keeps root postinstall as the single Electron binary install owner', () => { - // Why not an exact match: the invariant is that the root postinstall owns the Electron - // binary install, not that nothing else may run after it. Pinning the whole string made - // any unrelated chained step (a lint-plugin sync, say) a CI failure for every open PR. - const postinstall = packageJson.scripts.postinstall - const steps = postinstall.split('&&').map((step) => step.trim()) - expect(steps[0]).toBe('node config/scripts/rebuild-native-deps.mjs') - // No later step may take over the Electron install the first step owns. - expect(steps.slice(1).join(' ')).not.toMatch(/electron/i) - expect(pnpmWorkspace.allowBuilds).not.toHaveProperty('electron') - }) - it('keeps the native Windows registry addon optional and platform-gated', () => { const rebuildScript = readProject('config/scripts/rebuild-native-deps.mjs') const ensureScript = readProject('config/scripts/ensure-native-runtime.mjs')