mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
* fix(electron-backend): make test suite and lint host-agnostic across Windows/Linux checkouts Windows checkouts (core.autocrlf=true) had 13 pre-existing jest failures and 5 Windows-only lint errors in electron-backend while Linux CI was green: - embedded-mpv-native-source.spec: normalize CRLF after readFileSync so multi-line source assertions match on autocrlf checkouts - worker-runtime-paths.spec: build expected candidate paths with path.join instead of hardcoded POSIX strings - external-player-launch-context: join darwin-only paths (.app bundle executables, Homebrew Caskroom) with path.posix.join so simulated darwin platforms resolve correctly on win32 hosts (no-op on macOS) - app.spec: build packaged-navigation fixtures with path.resolve + pathToFileURL; file:///tmp/... is not a valid win32 file URL - lint target: quote the eslint glob. The unquoted ** was expanded by the POSIX shell on Linux (shallow match), so CI linted only a subset of files while Windows passed the literal pattern to ESLint and linted the full tree - the hosts checked different file sets - fix the 5 errors full-tree linting surfaces: prefer-const in epg-worker.service and database.worker-connection, no-unsafe-finally in external-player-session-registry and embedded-mpv-native.service (rewritten as catch-swallow with identical semantics, pinned by new regression tests), intentional no-control-regex in the recording filename sanitizer; drop stale unused disable directives - document the quoted-glob convention in CLAUDE.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: mirror the quoted-lint-glob convention into AGENTS.md AGENTS.md already mirrors the neighbouring max-lines/baseline paragraph from CLAUDE.md, and it requires coding conventions to stay in sync between the two files. The quoted-glob rule landed only in CLAUDE.md, so agents bootstrapping from AGENTS.md could reintroduce a host-dependent lint target. Also corrects the wording in both copies: the shallow expansion happens on macOS as well as Linux — /bin/sh has no globstar on either — and the target still exits 0 with a broken glob, which is why this went unnoticed. Adds the file-count check that catches it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
214 lines
7.1 KiB
TypeScript
214 lines
7.1 KiB
TypeScript
import * as path from 'path';
|
|
import {
|
|
getNativeModuleSearchPaths,
|
|
loadNativeModuleFromSearchPaths,
|
|
registerNativeModuleSearchPaths,
|
|
resolveWorkerRuntimeBootstrap,
|
|
} from './worker-runtime-paths';
|
|
|
|
// The implementation joins candidate paths with the host path module, so the
|
|
// expected values are built with path.join too — hardcoded POSIX strings
|
|
// would fail on win32 checkouts.
|
|
function packagedWorkerPath(root: string, workerFilename: string): string {
|
|
return path.join(
|
|
root,
|
|
'dist',
|
|
'apps',
|
|
'electron-backend',
|
|
'workers',
|
|
workerFilename
|
|
);
|
|
}
|
|
|
|
function unpackedNodeModulesPaths(root: string): string[] {
|
|
return [
|
|
path.join(root, 'app.asar.unpacked', 'node_modules'),
|
|
path.join(
|
|
root,
|
|
'app.asar.unpacked',
|
|
'electron-backend',
|
|
'node_modules'
|
|
),
|
|
path.join(
|
|
root,
|
|
'app.asar.unpacked',
|
|
'dist',
|
|
'apps',
|
|
'electron-backend',
|
|
'node_modules'
|
|
),
|
|
];
|
|
}
|
|
|
|
describe('worker-runtime-paths', () => {
|
|
it('resolves the development worker path when the worker file exists', () => {
|
|
const developmentWorkerDir =
|
|
'/workspace/dist/apps/electron-backend/workers';
|
|
const workerPath = path.join(
|
|
developmentWorkerDir,
|
|
'database.worker.js'
|
|
);
|
|
const bootstrap = resolveWorkerRuntimeBootstrap({
|
|
isPackaged: false,
|
|
workerFilename: 'database.worker.js',
|
|
developmentWorkerDir,
|
|
fileExists: (filePath) => filePath === workerPath,
|
|
});
|
|
|
|
expect(bootstrap).toEqual({
|
|
workerPath,
|
|
workerPathCandidates: [workerPath],
|
|
});
|
|
});
|
|
|
|
it('prefers process.resourcesPath for packaged worker resolution', () => {
|
|
const resourcesPath = '/Applications/IPTVnator.app/Contents/Resources';
|
|
const resourcesWorkerPath = packagedWorkerPath(
|
|
resourcesPath,
|
|
'epg-parser.worker.js'
|
|
);
|
|
const bootstrap = resolveWorkerRuntimeBootstrap({
|
|
isPackaged: true,
|
|
workerFilename: 'epg-parser.worker.js',
|
|
developmentWorkerDir: '/unused',
|
|
resourcesPath,
|
|
appPath:
|
|
'/Applications/IPTVnator.app/Contents/Resources/app.asar',
|
|
fileExists: (filePath) => filePath === resourcesWorkerPath,
|
|
});
|
|
|
|
expect(bootstrap.workerPath).toBe(resourcesWorkerPath);
|
|
expect(bootstrap.nativeModuleSearchPaths).toEqual(
|
|
unpackedNodeModulesPaths(resourcesPath)
|
|
);
|
|
});
|
|
|
|
it('falls back to appPath dirname when the resourcesPath candidate is missing', () => {
|
|
const appWorkerPath = packagedWorkerPath(
|
|
'/opt/IPTVnator/resources',
|
|
'database.worker.js'
|
|
);
|
|
const bootstrap = resolveWorkerRuntimeBootstrap({
|
|
isPackaged: true,
|
|
workerFilename: 'database.worker.js',
|
|
developmentWorkerDir: '/unused',
|
|
resourcesPath: '/tmp/runtime-resources',
|
|
appPath: '/opt/IPTVnator/resources/app.asar',
|
|
fileExists: (filePath) => filePath === appWorkerPath,
|
|
});
|
|
|
|
expect(bootstrap.workerPath).toBe(appWorkerPath);
|
|
expect(bootstrap.workerPathCandidates).toEqual([
|
|
packagedWorkerPath('/tmp/runtime-resources', 'database.worker.js'),
|
|
appWorkerPath,
|
|
]);
|
|
});
|
|
|
|
it('throws actionable errors when the worker file cannot be resolved', () => {
|
|
expect(() =>
|
|
resolveWorkerRuntimeBootstrap({
|
|
isPackaged: true,
|
|
workerFilename: 'database.worker.js',
|
|
developmentWorkerDir: '/unused',
|
|
resourcesPath: '/resources',
|
|
appPath: '/opt/IPTVnator/resources/app.asar',
|
|
fileExists: () => false,
|
|
})
|
|
).toThrow(
|
|
[
|
|
'Unable to resolve worker "database.worker.js".',
|
|
'Tried:',
|
|
`- ${packagedWorkerPath('/resources', 'database.worker.js')}`,
|
|
`- ${packagedWorkerPath(
|
|
'/opt/IPTVnator/resources',
|
|
'database.worker.js'
|
|
)}`,
|
|
].join('\n')
|
|
);
|
|
});
|
|
|
|
it('de-duplicates native module search paths when resource roots match', () => {
|
|
expect(
|
|
getNativeModuleSearchPaths({
|
|
resourcesPath: '/resources',
|
|
appPath: '/resources/app.asar',
|
|
})
|
|
).toEqual(unpackedNodeModulesPaths('/resources'));
|
|
});
|
|
|
|
it('loads native modules from the first working candidate path', () => {
|
|
const requireFactory = jest.fn((entryPath: string) => {
|
|
if (entryPath === path.join('/second', 'index.js')) {
|
|
return ((moduleName: string) => ({
|
|
moduleName,
|
|
entryPath,
|
|
})) as NodeRequire;
|
|
}
|
|
|
|
throw new Error('bad path');
|
|
});
|
|
|
|
const moduleValue = loadNativeModuleFromSearchPaths({
|
|
moduleName: 'better-sqlite3',
|
|
loggerLabel: '[Worker]',
|
|
searchPaths: ['/first', '/second'],
|
|
fileExists: (searchPath) => searchPath !== '/first-missing',
|
|
requireFactory,
|
|
});
|
|
|
|
expect(moduleValue).toEqual({
|
|
moduleName: 'better-sqlite3',
|
|
entryPath: path.join('/second', 'index.js'),
|
|
});
|
|
});
|
|
|
|
it('registers native module search paths in NODE_PATH and module globalPaths', () => {
|
|
const env: NodeJS.ProcessEnv = {};
|
|
const moduleApi = {
|
|
globalPaths: ['/global'],
|
|
};
|
|
|
|
const registeredPaths = registerNativeModuleSearchPaths(
|
|
['/first', '/second', '/global'],
|
|
{
|
|
env,
|
|
moduleApi,
|
|
}
|
|
);
|
|
|
|
expect(registeredPaths).toEqual(['/first', '/second', '/global']);
|
|
expect(env.NODE_PATH).toBe(
|
|
['/first', '/second', '/global'].join(path.delimiter)
|
|
);
|
|
expect(moduleApi.globalPaths).toEqual(['/first', '/second', '/global']);
|
|
});
|
|
|
|
it('throws aggregated errors when no native module candidate works', () => {
|
|
expect(() =>
|
|
loadNativeModuleFromSearchPaths({
|
|
moduleName: 'better-sqlite3',
|
|
loggerLabel: '[Worker]',
|
|
searchPaths: ['/missing', '/broken'],
|
|
fileExists: (searchPath) => searchPath === '/broken',
|
|
requireFactory: () => {
|
|
throw new Error('Cannot find module');
|
|
},
|
|
fallbackRequire: () => {
|
|
throw new Error('fallback failed');
|
|
},
|
|
})
|
|
).toThrow(
|
|
[
|
|
'[Worker] Unable to load native module "better-sqlite3".',
|
|
'Tried:',
|
|
'- /missing',
|
|
'- /broken',
|
|
'Failures:',
|
|
'- /missing (missing)',
|
|
'- /broken (Cannot find module)',
|
|
'- require(better-sqlite3) (fallback failed)',
|
|
].join('\n')
|
|
);
|
|
});
|
|
});
|