import assert from 'node:assert/strict'; import fs from 'node:fs'; import { createRequire } from 'node:module'; import os from 'node:os'; import { dirname, join } from 'node:path'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; import { readWindowsRuntimePin } from '../embedded-mpv/windows-runtime-pin.mjs'; const currentDir = dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const packageMetadata = JSON.parse( fs.readFileSync(join(currentDir, '..', '..', 'package.json'), 'utf8') ); const buildAndMakeWorkflow = fs.readFileSync( join(currentDir, '..', '..', '.github', 'workflows', 'build-and-make.yaml'), 'utf8' ); const windowsRuntimeRefreshWorkflow = fs.readFileSync( join( currentDir, '..', '..', '.github', 'workflows', 'refresh-windows-embedded-mpv-runtime.yaml' ), 'utf8' ); const windowsRuntimePin = readWindowsRuntimePin(); const electronBuilderConfig = JSON.parse( fs.readFileSync( join(currentDir, '..', '..', 'electron-builder.json'), 'utf8' ) ); const electronProjectConfig = JSON.parse( fs.readFileSync( join( currentDir, '..', '..', 'apps', 'electron-backend', 'project.json' ), 'utf8' ) ); const makerOptions = JSON.parse( fs.readFileSync( join( currentDir, '..', '..', 'apps', 'electron-backend', 'src', 'app', 'options', 'maker.options.json' ), 'utf8' ) ); const generatedMetadataConfigPath = 'apps/electron-backend/src/app/options/electron-builder.metadata.generated.json'; const packageLayoutVerifier = fs.readFileSync( join(currentDir, 'verify-electron-package-layout.mjs'), 'utf8' ); const flatpakLauncherValidationSource = fs.readFileSync( join(currentDir, 'flatpak-launcher-validation.cjs'), 'utf8' ); const electronAfterPackSource = fs.readFileSync( join(currentDir, 'electron-after-pack.cjs'), 'utf8' ); const frameCopyFilesModulePath = join( currentDir, 'embedded-mpv-frame-copy-files.cjs' ); const embeddedMpvPackagingSource = fs.readFileSync( join(currentDir, 'embedded-mpv-packaging.cjs'), 'utf8' ); const embeddedMpvBuildSource = fs.readFileSync( join( currentDir, '..', '..', 'apps', 'electron-backend', 'build-embedded-mpv.js' ), 'utf8' ); const embeddedMpvStageRuntimeSource = fs.readFileSync( join(currentDir, '..', 'embedded-mpv', 'stage-runtime.mjs'), 'utf8' ); const embeddedMpvWindowsArchiveStageSource = fs.readFileSync( join(currentDir, '..', 'embedded-mpv', 'stage-windows-runtime-archive.mjs'), 'utf8' ); const embeddedMpvWin32Source = fs.readFileSync( join( currentDir, '..', '..', 'apps', 'electron-backend', 'native', 'src', 'embedded_mpv_win32.cc' ), 'utf8' ); const { validatePackagedEmbeddedMpv } = require('./embedded-mpv-packaging.cjs'); function writeWindowsHelperFixture(helperPath, importedDllName) { const peOffset = 0x80; const optionalHeaderOffset = peOffset + 24; const optionalHeaderSize = 0xf0; const sectionTableOffset = optionalHeaderOffset + optionalHeaderSize; const importRva = 0x1000; const importRawOffset = 0x200; const importNameOffset = 0x30; const image = Buffer.alloc(0x400); image.write('MZ', 0, 'ascii'); image.writeUInt32LE(peOffset, 0x3c); image.write('PE\0\0', peOffset, 'ascii'); image.writeUInt16LE(0x8664, peOffset + 4); image.writeUInt16LE(1, peOffset + 6); image.writeUInt16LE(optionalHeaderSize, peOffset + 20); image.writeUInt16LE(0x20b, optionalHeaderOffset); image.writeUInt32LE(0x200, optionalHeaderOffset + 60); image.writeUInt32LE(16, optionalHeaderOffset + 108); image.writeUInt32LE(importRva, optionalHeaderOffset + 120); image.writeUInt32LE(40, optionalHeaderOffset + 124); image.write('.idata\0\0', sectionTableOffset, 'ascii'); image.writeUInt32LE(0x200, sectionTableOffset + 8); image.writeUInt32LE(importRva, sectionTableOffset + 12); image.writeUInt32LE(0x200, sectionTableOffset + 16); image.writeUInt32LE(importRawOffset, sectionTableOffset + 20); image.writeUInt32LE(importRva + importNameOffset, importRawOffset + 12); image.write(`${importedDllName}\0`, importRawOffset + importNameOffset); fs.writeFileSync(helperPath, image); } test('Linux package identity does not expose the internal Electron backend project name', () => { assert.equal(electronBuilderConfig.productName, 'IPTVnator'); assert.equal(electronBuilderConfig.extraMetadata?.name, 'iptvnator'); assert.equal(electronBuilderConfig.extraMetadata?.productName, 'IPTVnator'); assert.equal(electronBuilderConfig.linux?.executableName, 'iptvnator'); assert.equal(electronBuilderConfig.extraMetadata?.desktopName, 'iptvnator'); assert.ok( electronBuilderConfig.linux?.executableArgs?.includes( '--ozone-platform=x11' ) ); }); test('AppImage desktop metadata supports AppManager without changing other Linux packages', async () => { // Exercise the installed builder: target-specific desktop fields must merge // with Linux defaults, and the builder must keep generating the version. const builderRequire = createRequire(require.resolve('electron-builder')); const { default: AppImageTarget } = builderRequire( 'app-builder-lib/out/targets/appimage/AppImageTarget' ); const { LinuxTargetHelper } = builderRequire( 'app-builder-lib/out/targets/LinuxTargetHelper' ); for (const version of [ packageMetadata.version, '0.24.1', '0.25.0-beta.1', ]) { const config = structuredClone(electronBuilderConfig); const packager = { config, platformSpecificBuildOptions: config.linux, executableName: config.linux.executableName, fileAssociations: config.fileAssociations, info: { metadata: { ...packageMetadata, ...config.extraMetadata } }, appInfo: { productName: config.productName, description: packageMetadata.description, buildVersion: version, }, }; const helper = new LinuxTargetHelper(packager); const target = new AppImageTarget( 'AppImage', packager, helper, os.tmpdir() ); const desktop = await target.desktopEntry.value; const fields = Object.fromEntries( desktop .split('\n') .filter((line) => line.includes('=')) .map((line) => { const separator = line.indexOf('='); return [ line.slice(0, separator), line.slice(separator + 1), ]; }) ); assert.equal(fields['X-AppImage-Name'], 'IPTVnator'); assert.equal(fields['X-AppImage-Version'], version); assert.equal( fields['X-AppImage-Homepage'], 'https://github.com/4gray/iptvnator' ); assert.equal( fields['X-AppImage-UpdateURL'], 'https://github.com/4gray/iptvnator' ); assert.equal(fields.StartupWMClass, 'iptvnator'); assert.equal(fields.Exec, 'AppRun --ozone-platform=x11 %U'); assert.equal( fields.MimeType, 'audio/x-mpegurl;application/vnd.apple.mpegurl;' ); // A runner-wide architecture override would mislabel the ARM builds. assert.equal(fields['X-AppImage-Arch'], undefined); const otherDesktop = await helper.computeDesktopEntry( config.linux, 'iptvnator %U' ); assert.doesNotMatch(otherDesktop, /^X-AppImage-/m); assert.match(otherDesktop, /^StartupWMClass=iptvnator$/m); assert.equal(helper.getDesktopFileName(), 'iptvnator'); } }); test('playlist file associations are registered with the operating system', () => { // Without these the OS never offers IPTVnator as a handler, so every // runtime path for an OS-supplied playlist is unreachable by double-click. assert.deepEqual(electronBuilderConfig.fileAssociations, [ { ext: 'm3u', name: 'M3U Playlist', description: 'IPTV playlist', mimeType: 'audio/x-mpegurl', role: 'Viewer', }, { ext: 'm3u8', name: 'M3U8 Playlist', description: 'IPTV playlist', mimeType: 'application/vnd.apple.mpegurl', role: 'Viewer', }, ]); // Electron Builder derives the Linux desktop entry's MimeType from these // `mimeType` fields, and assigns it *after* spreading `linux.desktop.entry` // — so declaring MimeType there instead would be silently overwritten. assert.equal( electronBuilderConfig.linux?.desktop?.entry?.MimeType, undefined ); // Each association needs its own extension: Electron Builder derives the // per-extension NSIS registry entries and the Linux `` in // /usr/share/mime from `ext`, one mimeType per association. const extensions = electronBuilderConfig.fileAssociations.map( (association) => association.ext ); assert.equal(new Set(extensions).size, extensions.length); }); test('GitHub Releases auto-update metadata is generated and uploaded', () => { // \r?\n keeps this host-agnostic: Windows checkouts with autocrlf see // CRLF in the workflow file. const releaseFiles = buildAndMakeWorkflow.match( /files: \|\r?\n([\s\S]*?)\r?\n\s+env:/ )?.[1]; assert.ok(releaseFiles, 'release upload files block must exist'); assert.ok( packageMetadata.dependencies?.['electron-updater'], 'electron-updater must be a runtime dependency because the packaged app imports it' ); assert.deepEqual(electronBuilderConfig.publish, [ { provider: 'github', owner: '4gray', repo: 'iptvnator', }, ]); assert.deepEqual(electronBuilderConfig.mac?.target, [ { target: 'dmg', arch: ['x64', 'arm64'], }, { target: 'zip', arch: ['x64', 'arm64'], }, ]); assert.match(buildAndMakeWorkflow, /dist\/executables\/\*\*\/latest\.yml/); assert.match( buildAndMakeWorkflow, /dist\/executables\/\*\*\/latest-mac\.yml/ ); assert.match( buildAndMakeWorkflow, /dist\/executables\/\*\*\/latest-linux\*\.yml/ ); assert.match(buildAndMakeWorkflow, /dist\/executables\/\*\*\/\*\.blockmap/); assert.match(buildAndMakeWorkflow, /Merge macOS updater metadata/); assert.match(buildAndMakeWorkflow, /artifacts\/latest-mac\.yml/); assert.doesNotMatch( releaseFiles, /artifacts\/macos-(?:x64|arm64)-artifacts\/latest-mac\.yml/ ); assert.match( buildAndMakeWorkflow, /artifacts\/linux-portable-artifacts\/latest-linux\*\.yml/ ); assert.match( buildAndMakeWorkflow, /artifacts\/windows-artifacts\/latest\.yml/ ); const makeCommands = [ ...buildAndMakeWorkflow.matchAll(/run: (pnpm run make:app[^\n]*)/g), ].map((match) => match[1].trim()); assert.ok(makeCommands.length >= 2, 'workflow must package Electron apps'); assert.deepEqual( [...new Set(makeCommands)], ['pnpm run make:app -- --publishPolicy=never'] ); }); test('generated Electron package metadata mirrors the root package identity', async () => { const { buildElectronBuilderMetadata, buildElectronPackageMetadata } = await import('./generate-electron-builder-metadata.mjs'); const generatedElectronPackage = buildElectronPackageMetadata( packageMetadata, electronBuilderConfig, { name: 'electron-backend', version: '0.0.1', dependencies: { 'better-sqlite3': '12.5.0', }, } ); assert.deepEqual( buildElectronBuilderMetadata(packageMetadata, electronBuilderConfig) .extraMetadata, { name: packageMetadata.name, productName: electronBuilderConfig.productName, version: packageMetadata.version, description: packageMetadata.description, author: packageMetadata.author, homepage: packageMetadata.homepage, license: packageMetadata.license, main: electronBuilderConfig.extraMetadata.main, } ); assert.deepEqual(generatedElectronPackage, { name: packageMetadata.name, productName: electronBuilderConfig.productName, version: packageMetadata.version, description: packageMetadata.description, author: packageMetadata.author, homepage: packageMetadata.homepage, license: packageMetadata.license, main: electronBuilderConfig.extraMetadata.main, dependencies: { 'better-sqlite3': '12.5.0', }, }); }); test('nx-electron packaging prepares metadata before make/package', () => { assert.equal(makerOptions.extends, generatedMetadataConfigPath); assert.deepEqual( electronProjectConfig.targets['generate-builder-metadata'].dependsOn, ['electron-backend:build'] ); assert.deepEqual( electronProjectConfig.targets['generate-builder-metadata'].outputs, [ `{workspaceRoot}/${generatedMetadataConfigPath}`, '{workspaceRoot}/dist/apps/electron-backend/package.json', ] ); assert.ok( electronProjectConfig.targets.make.dependsOn.includes( 'electron-backend:generate-builder-metadata' ) ); assert.ok( electronProjectConfig.targets.package.dependsOn.includes( 'electron-backend:generate-builder-metadata' ) ); }); test('package layout verifier uses canonical helpers and direct dependencies', () => { assert.ok(packageMetadata.devDependencies?.['@electron/asar']); assert.match(packageLayoutVerifier, /require\(['"]@electron\/asar['"]\)/); assert.doesNotMatch(packageLayoutVerifier, /electronBuilderRequire/); assert.match( packageLayoutVerifier, /buildElectronBuilderMetadata\(\s*packageMetadata,\s*electronBuilderConfig\s*\)\.extraMetadata/s ); assert.doesNotMatch( packageLayoutVerifier, /const packagedPackageMetadata = \{\s*name:/s ); assert.doesNotMatch( packageLayoutVerifier, /builderEffectiveConfigPath && fileExists\(builderEffectiveConfigPath\)/ ); assert.match(packageLayoutVerifier, /IPTVNATOR_LINUX_FRAME_COPY_PROFILE/); assert.match(packageLayoutVerifier, /profile:\s*linuxFrameCopyProfile/); assert.match(packageLayoutVerifier, /targetNames:\s*linuxTargetNames/); assert.match( packageLayoutVerifier, /validateLinuxProfileTargets\(\s*linuxFrameCopyProfile,\s*linuxTargetNames\s*\)/s ); assert.match(packageLayoutVerifier, /dirArch !== 'x64'/); assert.doesNotMatch(packageLayoutVerifier, /getEmbeddedMpvAddonArch/); assert.match(electronAfterPackSource, /targetArch !== 'x64'/); assert.match( packageLayoutVerifier, /const\s*{\s*resolveLinuxLauncherLayout\s*}\s*=\s*require\(['"]\.\/linux-launcher-layout\.cjs['"]\)/ ); assert.match( packageLayoutVerifier, /const\s*{\s*validateFlatpakLauncher\s*,?\s*}\s*=\s*require\(['"]\.\/flatpak-launcher-validation\.cjs['"]\)/ ); assert.match( packageLayoutVerifier, /function verifyLinuxLauncher\(\s*resourceDir,\s*targetNames,\s*errors\s*\)/ ); assert.match( packageLayoutVerifier, /resolveLinuxLauncherLayout\(\s*targetNames,\s*linuxExecutableName\s*\)/ ); assert.match( packageLayoutVerifier, /verifyLinuxLauncher\(\s*resourceDir,\s*linuxTargetNames,\s*errors\s*\)/ ); const launcherVerifier = packageLayoutVerifier.match( /function verifyLinuxLauncher\([\s\S]*?\n}\n\nfunction verifyFlatpakPermissions/ )?.[0]; assert.ok(launcherVerifier); assert.ok( launcherVerifier.indexOf('resolveLinuxLauncherLayout(') < launcherVerifier.indexOf('const launcherBinaryPath') ); assert.match( launcherVerifier, /if \(!launcherLayout\.wrapperRequired\) \{\s*errors\.push\(\s*\.\.\.validateFlatpakLauncher\(\s*appDir,\s*linuxExecutableName\s*\)\s*\);\s*return;\s*\}\s*const launcherBinaryPath[\s\S]*?fs\.readFileSync\(launcherPath,\s*['"]utf8['"]\)/ ); }); test('Flatpak launcher validation locks descriptor-based ELF inspection', () => { assert.match( flatpakLauncherValidationSource, /const expectedElfMagic = Buffer\.from\(\[\s*0x7f,\s*0x45,\s*0x4c,\s*0x46,?\s*\]\)/ ); assert.match( flatpakLauncherValidationSource, /descriptor = fs\.openSync\(\s*launcherPath,\s*fs\.constants\.O_RDONLY\s*\|\s*fs\.constants\.O_NOFOLLOW\s*\)/ ); assert.match( flatpakLauncherValidationSource, /launcherStat = fs\.fstatSync\(descriptor\)/ ); assert.match( flatpakLauncherValidationSource, /const elfMagic = Buffer\.alloc\(expectedElfMagic\.length\)/ ); assert.match( flatpakLauncherValidationSource, /bytesRead = fs\.readSync\(\s*descriptor,\s*elfMagic,\s*0,\s*elfMagic\.length,\s*0\s*\)/ ); assert.match( flatpakLauncherValidationSource, /bytesRead !== expectedElfMagic\.length/ ); assert.match( flatpakLauncherValidationSource, /finally\s*{\s*try\s*{\s*fs\.closeSync\(descriptor\)/ ); const openOffset = flatpakLauncherValidationSource.indexOf( 'descriptor = fs.openSync(' ); const statOffset = flatpakLauncherValidationSource.indexOf( 'launcherStat = fs.fstatSync(descriptor)' ); const readOffset = flatpakLauncherValidationSource.indexOf( 'bytesRead = fs.readSync(' ); const finallyOffset = flatpakLauncherValidationSource.indexOf( '} finally {', readOffset ); const closeOffset = flatpakLauncherValidationSource.indexOf( 'fs.closeSync(descriptor)', finallyOffset ); assert.ok( openOffset < statOffset && statOffset < readOffset && readOffset < finallyOffset && finallyOffset < closeOffset ); }); test('nx-electron executors load against the installed Nx exports', () => { const { executors } = require('nx-electron/executors.json'); for (const [name, { implementation }] of Object.entries(executors)) { const executor = require( `nx-electron/${implementation.replace(/^\.\//, '')}` ); assert.equal(typeof executor.default, 'function', `${name} must load`); } }); test('nx-electron packaging does not copy duplicate root package metadata', () => { const nxElectronExecutorPath = require.resolve('nx-electron/src/executors/package/executor.js'); const nxElectronExecutor = fs.readFileSync(nxElectronExecutorPath, 'utf8'); assert.doesNotMatch(nxElectronExecutor, /['"]\.\/package\.json['"]/); assert.match( nxElectronExecutor, /filter:\s*\[['"]index\.js['"],\s*['"]package\.json['"]\]/ ); }); test('embedded MPV runtime binaries are unpacked on every supported desktop platform', () => { const asarUnpack = electronBuilderConfig.asarUnpack ?? []; for (const requiredPattern of [ '**/*.node', '**/*.dylib', '**/*.dll', '**/*.so', '**/*.so.*', '**/embedded-mpv-runtime.json', ]) { assert.ok( asarUnpack.includes(requiredPattern), `electron-builder asarUnpack must include ${requiredPattern}` ); } }); test('embedded MPV native payload is owned exclusively by afterPack outside app.asar', () => { assert.ok( electronBuilderConfig.files.includes( '!electron-backend/native{,/**/*}' ), 'electron-builder files must exclude the entire pre-afterPack native payload from app.asar' ); assert.match( packageLayoutVerifier, /collectEmbeddedMpvNativeArchiveEntries/ ); }); test('embedded MPV package validation accepts Windows runtime files and Linux process isolation', () => { const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-')); try { for (const [platform, runtimeFile] of [ ['windows', 'mpv-2.dll'], ['windows', 'libmpv-2.dll'], ['windows', 'mpv.dll'], ['windows', 'libmpv.dll'], ]) { // One fixture dir per runtime-file scenario: the frame-copy // artifacts written below must not leak into the next // iteration's missing-artifact assertions. const resourceDir = join( tempDir, `${platform}-${runtimeFile.replace(/[\\/]/g, '_')}` ); const nativeDir = join( resourceDir, 'app.asar.unpacked', 'electron-backend', 'native' ); fs.mkdirSync(join(nativeDir, 'lib'), { recursive: true }); fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), ''); fs.writeFileSync( join(nativeDir, 'embedded-mpv-runtime.json'), JSON.stringify({ origin: 'vendored-lgpl' }) ); fs.writeFileSync(join(nativeDir, runtimeFile), ''); // Windows packages that ship the addon must also ship the // frame-copy engine artifacts built by the same binding.gyp run. const missingWindowsFrameCopyErrors = validatePackagedEmbeddedMpv( resourceDir, { platform, required: true } ); assert.ok( missingWindowsFrameCopyErrors.some((error) => error.includes('iptvnator_mpv_helper.exe') ) ); assert.ok( missingWindowsFrameCopyErrors.some((error) => error.includes('embedded_mpv_frame_reader.node') ) ); writeWindowsHelperFixture( join(nativeDir, 'iptvnator_mpv_helper.exe'), runtimeFile ); fs.writeFileSync( join(nativeDir, 'embedded_mpv_frame_reader.node'), '' ); assert.deepEqual( validatePackagedEmbeddedMpv(resourceDir, { platform, required: true, }), [] ); } const darwinResourceDir = join(tempDir, 'darwin'); const darwinNativeDir = join( darwinResourceDir, 'app.asar.unpacked', 'electron-backend', 'native' ); fs.mkdirSync(join(darwinNativeDir, 'lib'), { recursive: true }); fs.writeFileSync(join(darwinNativeDir, 'embedded_mpv.node'), ''); fs.writeFileSync( join(darwinNativeDir, 'embedded-mpv-runtime.json'), JSON.stringify({ origin: 'vendored-lgpl' }) ); fs.writeFileSync(join(darwinNativeDir, 'lib', 'libmpv.2.dylib'), ''); // macOS packages that ship the addon must also ship the frame-copy // engine artifacts built by the same binding.gyp run. const missingFrameCopyErrors = validatePackagedEmbeddedMpv( darwinResourceDir, { platform: 'darwin', required: true } ); assert.ok( missingFrameCopyErrors.some((error) => error.includes('iptvnator_mpv_helper') ) ); assert.ok( missingFrameCopyErrors.some((error) => error.includes('embedded_mpv_frame_reader.node') ) ); fs.writeFileSync(join(darwinNativeDir, 'iptvnator_mpv_helper'), ''); fs.writeFileSync( join(darwinNativeDir, 'embedded_mpv_frame_reader.node'), '' ); // Host-agnostic assertion: on non-macOS hosts the validator also // reports that link validation needs a macOS host, so only the // frame-copy artifact requirement is asserted here. const remainingErrors = validatePackagedEmbeddedMpv(darwinResourceDir, { platform: 'darwin', required: true, }); assert.ok( !remainingErrors.some((error) => error.includes('frame-copy artifact') ), `unexpected frame-copy errors: ${remainingErrors.join('; ')}` ); const linuxResourceDir = join(tempDir, 'linux'); const linuxNativeDir = join( linuxResourceDir, 'app.asar.unpacked', 'electron-backend', 'native' ); fs.mkdirSync(linuxNativeDir, { recursive: true }); fs.writeFileSync(join(linuxNativeDir, 'embedded_mpv.node'), ''); fs.writeFileSync( join(linuxNativeDir, 'embedded-mpv-runtime.json'), JSON.stringify({ schemaVersion: 1, origin: 'external-mpv-process', platform: 'linux', arch: 'x64', runtimeMode: 'native-view-only', frameCopyAvailable: false, artifacts: { addon: 'embedded_mpv.node', }, nativeViewFallback: 'process-isolated mpv --wid', }) ); assert.deepEqual( validatePackagedEmbeddedMpv(linuxResourceDir, { platform: 'linux', required: false, }), [] ); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } }); test('Windows frame-copy packages reject an mpv DLL that exists only under native/lib', () => { const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-')); try { for (const runtimeFile of [ 'mpv-2.dll', 'libmpv-2.dll', 'mpv.dll', 'libmpv.dll', ]) { const resourceDir = join(tempDir, runtimeFile); const nativeDir = join( resourceDir, 'app.asar.unpacked', 'electron-backend', 'native' ); fs.mkdirSync(join(nativeDir, 'lib'), { recursive: true }); fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), ''); fs.writeFileSync( join(nativeDir, 'embedded-mpv-runtime.json'), JSON.stringify({ origin: 'vendored-lgpl' }) ); writeWindowsHelperFixture( join(nativeDir, 'iptvnator_mpv_helper.exe'), runtimeFile ); fs.writeFileSync( join(nativeDir, 'embedded_mpv_frame_reader.node'), '' ); fs.writeFileSync(join(nativeDir, 'lib', runtimeFile), ''); const errors = validatePackagedEmbeddedMpv(resourceDir, { platform: 'windows', required: true, }); assert.ok( errors.some( (error) => error.includes( 'beside the Windows frame-copy helper' ) && error.includes(nativeDir) ), `${runtimeFile} under native/lib must not satisfy the helper DLL requirement: ${errors.join('; ')}` ); } } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } }); test('Windows frame-copy packages require the DLL imported by the helper', () => { const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-')); try { const nativeDir = join( tempDir, 'app.asar.unpacked', 'electron-backend', 'native' ); fs.mkdirSync(nativeDir, { recursive: true }); fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), ''); fs.writeFileSync( join(nativeDir, 'embedded-mpv-runtime.json'), JSON.stringify({ origin: 'vendored-lgpl' }) ); writeWindowsHelperFixture( join(nativeDir, 'iptvnator_mpv_helper.exe'), 'mpv-2.dll' ); fs.writeFileSync(join(nativeDir, 'embedded_mpv_frame_reader.node'), ''); fs.writeFileSync(join(nativeDir, 'libmpv.dll'), ''); const errors = validatePackagedEmbeddedMpv(tempDir, { platform: 'windows', required: true, }); assert.ok( errors.some( (error) => error.includes('imports mpv-2.dll') && error.includes(join(nativeDir, 'mpv-2.dll')) ), `a different accepted DLL must not satisfy the helper import: ${errors.join('; ')}` ); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } }); test('Windows frame-copy package validation fails closed for a malformed helper', () => { const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-')); try { const nativeDir = join( tempDir, 'app.asar.unpacked', 'electron-backend', 'native' ); fs.mkdirSync(nativeDir, { recursive: true }); fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), ''); fs.writeFileSync( join(nativeDir, 'embedded-mpv-runtime.json'), JSON.stringify({ origin: 'vendored-lgpl' }) ); fs.writeFileSync(join(nativeDir, 'iptvnator_mpv_helper.exe'), 'MZ'); fs.writeFileSync(join(nativeDir, 'embedded_mpv_frame_reader.node'), ''); fs.writeFileSync(join(nativeDir, 'mpv-2.dll'), ''); const errors = validatePackagedEmbeddedMpv(tempDir, { platform: 'windows', required: true, }); assert.ok( errors.some((error) => error.includes( 'Unable to inspect Windows frame-copy helper imports' ) ), `malformed helper must fail package validation: ${errors.join('; ')}` ); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } }); test('Windows embedded MPV staging preserves import-library DLL basenames', () => { assert.match( embeddedMpvStageRuntimeSource, /win32:\s*\[\s*'mpv-2\.dll',\s*'libmpv-2\.dll',\s*'mpv\.dll',\s*'libmpv\.dll'\s*\]/ ); assert.match( embeddedMpvWindowsArchiveStageSource, /path\.join\(normalizedPrefix,\s*'bin',\s*path\.basename\(runtimeDll\)\)/ ); assert.doesNotMatch( embeddedMpvWindowsArchiveStageSource, /normalizedWindowsDllName/ ); assert.match(embeddedMpvBuildSource, /'libmpv-2\.dll'/); assert.match(embeddedMpvPackagingSource, /libmpv-2\.dll/); }); test('Windows embedded MPV archive staging keeps CI downloads bounded and quiet', () => { assert.match( embeddedMpvWindowsArchiveStageSource, /parsedUrl\.protocol === 'https:'/ ); assert.doesNotMatch( embeddedMpvWindowsArchiveStageSource, /parsedUrl\.protocol === 'http:'/ ); assert.match( embeddedMpvWindowsArchiveStageSource, /fs\.createReadStream\(filePath\)/ ); assert.doesNotMatch( embeddedMpvWindowsArchiveStageSource, /hash\.update\(fs\.readFileSync\(filePath\)\)/ ); assert.match( embeddedMpvWindowsArchiveStageSource, /runResult\(\s*'tar',\s*\['-xf', archivePath, '-C', extractRoot\],\s*\{\s*stdio: 'pipe',\s*\}\s*\)/s ); }); test('embedded MPV packaging helpers use a cross-platform module name', () => { assert.match( electronAfterPackSource, /require\(['"]\.\/embedded-mpv-packaging\.cjs['"]\)/ ); assert.match( packageLayoutVerifier, /require\(['"]\.\/embedded-mpv-packaging\.cjs['"]\)/ ); }); test('frame-copy packaging file operations enforce modes and remove stale artifacts', () => { assert.ok( fs.existsSync(frameCopyFilesModulePath), 'shared frame-copy packaging file helper must exist' ); const { preparePackagedFrameCopyArtifacts, removeStaleFrameCopyArtifacts, } = require(frameCopyFilesModulePath); const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'impv-fc-files-')); try { const helperPath = join(tempDir, 'iptvnator_mpv_helper'); const windowsHelperPath = join(tempDir, 'iptvnator_mpv_helper.exe'); const readerPath = join(tempDir, 'embedded_mpv_frame_reader.node'); fs.writeFileSync(helperPath, '#!/bin/sh\n'); fs.chmodSync(helperPath, 0o644); fs.writeFileSync(windowsHelperPath, 'exe'); fs.writeFileSync(readerPath, 'reader'); if (process.platform !== 'win32') { preparePackagedFrameCopyArtifacts(tempDir, 'darwin'); assert.notEqual( fs.statSync(helperPath).mode & 0o111, 0, 'macOS helper must be executable after packaging' ); } preparePackagedFrameCopyArtifacts(tempDir, 'linux'); assert.equal( fs.existsSync(helperPath), false, 'Linux packages must omit the unsupported frame-copy helper' ); assert.equal( fs.existsSync(windowsHelperPath), false, 'Linux packages must omit stale Windows frame-copy helpers too' ); fs.writeFileSync(helperPath, '#!/bin/sh\n'); removeStaleFrameCopyArtifacts(tempDir); assert.equal(fs.existsSync(helperPath), false); assert.equal(fs.existsSync(windowsHelperPath), false); assert.equal(fs.existsSync(readerPath), false); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } }); test('Windows CI packages embedded MPV from a staged x64 runtime', () => { const requireEmbeddedMpvLines = buildAndMakeWorkflow .split(/\r?\n/) .filter((line) => line.includes('IPTVNATOR_REQUIRE_EMBEDDED_MPV:')); assert.equal( packageMetadata.scripts?.['embedded-mpv:stage-runtime:windows-archive'], 'node tools/embedded-mpv/stage-windows-runtime-archive.mjs' ); assert.match( buildAndMakeWorkflow, /name:\s+Stage Windows embedded MPV runtime archive/ ); assert.match(buildAndMakeWorkflow, /runner:\s+windows-2022/); assert.match( buildAndMakeWorkflow, /name:\s+Resolve pinned Windows Embedded MPV runtime/ ); assert.match( buildAndMakeWorkflow, /node tools\/embedded-mpv\/windows-runtime-pin\.mjs --github-output/ ); assert.match( buildAndMakeWorkflow, /WINDOWS_RUNTIME_URL:\s+\$\{\{ steps\.windows-embedded-mpv-runtime-pin\.outputs\.url \}\}/ ); assert.match( buildAndMakeWorkflow, /WINDOWS_RUNTIME_SHA256:\s+\$\{\{ steps\.windows-embedded-mpv-runtime-pin\.outputs\.sha256 \}\}/ ); assert.doesNotMatch( buildAndMakeWorkflow, /(?:vars|secrets)\.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME/ ); assert.doesNotMatch(buildAndMakeWorkflow, /zhongfly\/mpv-winbuild/); assert.equal(windowsRuntimePin.repository, 'zhongfly/mpv-winbuild'); assert.match(windowsRuntimePin.asset.name, /^mpv-dev-lgpl-x86_64-/); assert.match(windowsRuntimePin.asset.sha256, /^[a-f0-9]{64}$/); assert.match( windowsRuntimePin.upstream.licenseClaim, /checksum and archive layout, not the complete transitive license closure/ ); assert.match( windowsRuntimeRefreshWorkflow, /node tools\/embedded-mpv\/update-windows-runtime-pin\.mjs/ ); assert.match( windowsRuntimeRefreshWorkflow, /node --test\s+tools\/embedded-mpv\/windows-runtime-pin\.test\.mjs/ ); assert.doesNotMatch( windowsRuntimeRefreshWorkflow, /electron-package-identity\.test\.mjs/ ); assert.match( windowsRuntimeRefreshWorkflow, /automation\/windows-embedded-mpv-runtime-pin/ ); assert.match(windowsRuntimeRefreshWorkflow, /secrets\.PAT/); assert.match( windowsRuntimeRefreshWorkflow, /actions\/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1/ ); assert.match( windowsRuntimeRefreshWorkflow, /actions\/setup-node@820762786026740c76f36085b0efc47a31fe5020/ ); assert.doesNotMatch( windowsRuntimeRefreshWorkflow, /actions\/(?:checkout|setup-node)@v\d+/ ); assert.match( buildAndMakeWorkflow, /name:\s+Override Windows arch in electron-builder\.json/ ); assert.match( buildAndMakeWorkflow, /IPTVNATOR_REQUIRE_EMBEDDED_MPV:\s+\$\{\{\s*\(matrix\.os == 'linux' \|\| matrix\.os == 'windows'/ ); assert.match(embeddedMpvStageRuntimeSource, /\.dll\.a/); assert.match(embeddedMpvBuildSource, /\.dll\.a/); assert.match( embeddedMpvWindowsArchiveStageSource, /checksum-and-layout-only/ ); assert.match( embeddedMpvWindowsArchiveStageSource, /not-independently-verified/ ); assert.ok(requireEmbeddedMpvLines.length > 0); for (const line of requireEmbeddedMpvLines) { assert.doesNotMatch(line, /cache-hit/); } }); test('Windows embedded MPV native build uses wide Win32 cursor resources', () => { assert.match( embeddedMpvWin32Source, /LoadCursorW\(nullptr,\s*MAKEINTRESOURCEW\(32512\)\)/ ); assert.doesNotMatch( embeddedMpvWin32Source, /LoadCursorW\(nullptr,\s*IDC_ARROW\)/ ); }); test('embedded MPV package validation rejects missing required Windows runtime', () => { const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-')); try { const nativeDir = join( tempDir, 'app.asar.unpacked', 'electron-backend', 'native' ); fs.mkdirSync(join(nativeDir, 'lib'), { recursive: true }); fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), ''); fs.writeFileSync( join(nativeDir, 'embedded-mpv-runtime.json'), JSON.stringify({ origin: 'vendored-lgpl' }) ); const errors = validatePackagedEmbeddedMpv(tempDir, { platform: 'windows', required: true, }); assert.match(errors.join('\n'), /Missing bundled embedded MPV runtime/); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } }); test('embedded MPV package validation rejects bundled Linux libmpv', () => { const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-')); try { const nativeDir = join( tempDir, 'app.asar.unpacked', 'electron-backend', 'native' ); fs.mkdirSync(join(nativeDir, 'lib'), { recursive: true }); fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), ''); fs.writeFileSync( join(nativeDir, 'embedded-mpv-runtime.json'), JSON.stringify({ origin: 'external-mpv-process' }) ); fs.writeFileSync(join(nativeDir, 'lib', 'libmpv.so'), ''); const errors = validatePackagedEmbeddedMpv(tempDir, { platform: 'linux', required: true, }); assert.match(errors.join('\n'), /must not bundle libmpv/); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } }); test('embedded MPV package validation rejects frame-copy helpers in Linux packages', () => { const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-')); try { const nativeDir = join( tempDir, 'app.asar.unpacked', 'electron-backend', 'native' ); fs.mkdirSync(nativeDir, { recursive: true }); fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), ''); fs.writeFileSync( join(nativeDir, 'embedded-mpv-runtime.json'), JSON.stringify({ origin: 'external-mpv-process' }) ); fs.writeFileSync(join(nativeDir, 'iptvnator_mpv_helper'), ''); fs.writeFileSync(join(nativeDir, 'iptvnator_mpv_helper.exe'), ''); const errors = validatePackagedEmbeddedMpv(tempDir, { platform: 'linux', required: true, }); const message = errors.join('\n'); assert.match(message, /must not ship frame-copy helpers/); assert.match(message, /iptvnator_mpv_helper\n/); assert.match(message, /iptvnator_mpv_helper\.exe/); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } });