fix: align Linux package builds (AppImage/deb/Flatpak) with target-specific extraResources (#1623)
* fix: align Linux package builds (AppImage/deb/Flatpak) with target-specific extraResources
The .deb, AppImage, and Flatpak builds had inconsistent package inclusion
due to ${os}-${arch} template variables in global extraResources not
expanding consistently for all Linux targets.
Changes:
- Move Python runtime from global extraResources to target-specific config
- Add platform-specific extraResources for mac, win, appImage, deb, flatpak
- Create verify-linux-packages.cjs script to inspect package contents
- Add verify:linux and test:verify-linux npm scripts
- Update release.yml and beta-release.yml CI/CD workflows with verification
This ensures electron-builder processes resources correctly for each package
type, avoiding template variable resolution issues and enabling early detection
of missing critical files (secretstorage, pydantic_core, claude_agent_sdk).
* style: apply Biome formatting to verify-linux-packages.cjs
* fix: address PR review feedback
- Fix Windows Python path: use python directory, not python.exe directly
- Move Linux extraResources to linux block (not appImage/deb targets)
- Remove redundant __dirname shadowing in verification script
- Tighten file pattern matching to reduce false positives
- Export CRITICAL_PACKAGES for use in tests
- Only run main() when script is executed directly
The appImage and deb blocks are target options in electron-builder,
not platform options. They don't support extraResources. Use
build.linux.extraResources instead for all Linux targets.
The Windows tarball extracts with a 'python' directory containing
python.exe, so copy the entire directory, not the exe file directly.
* fix: use ${os} template variable for platform paths
Use ${os} instead of hardcoded platform names (darwin, win32) to match
the directory structure created by download-python.cjs.
The download script uses electron-builder platform names (mac, win, linux)
via toElectronBuilderPlatform(), so the extraResources paths must use
${os} which resolves to these same names:
- macOS: ${os} -> mac (not darwin)
- Windows: ${os} -> win (not win32)
- Linux: ${os} -> linux
* fix: address PR review feedback for verification script
- Export verification functions (findPackages, verifyFileList, verifyAppImage, verifyDeb, verifyFlatpak)
- Extract common verification logic to reduce duplication (DRY)
- Fix pattern matching to avoid false positives:
- Python binary check explicitly excludes python-site-packages
- Backend check requires resources/ prefix
- Package check requires python-site-packages/ prefix
- Update tests to use actual exported functions instead of re-implementing logic
- Fix test data to match real package file formats (AppImage uses './' prefix)
- All 12 unit tests now pass
This addresses all issues raised by CodeRabbit AI and Auto Claude PR reviews.
* refactor: extract Flatpak size threshold to named constant
Add FLATPAK_MIN_SIZE_MB constant (50 MB) alongside CRITICAL_PACKAGES.
This makes the threshold self-documenting and centralized for easier maintenance.
Also improves error message to include expected size for clarity.
* fix: add maxBuffer to spawnSync and remove Flatpak early return
- Add 50MB maxBuffer to bsdtar spawnSync call for large AppImages
- Add 50MB maxBuffer to dpkg-deb spawnSync call for large deb packages
- Remove early return in verifyFlatpak when flatpak CLI is missing
- File existence/size checks now run even without flatpak CLI
Fixes CodeRabbit review feedback.
* test: fix test to actually call findPackages and address low severity issues
- Fix test to properly call findPackages() with mocked fs.existsSync/readdirSync
- Add test for missing dist directory handling
- Add test for duplicate package warnings
- Change commandExists to use POSIX-compliant 'command -v' instead of 'which'
- Add warnings for duplicate packages in findPackages function
- Remove unused imports and variables from tests
Fixes AI review findings:
- NEW-001 [MEDIUM]: Test now calls findPackages function
- NEW-003 [LOW]: Duplicate packages now trigger warnings
- NEW-005 [LOW]: commandExists now uses POSIX-compliant 'command -v'
* security: fix shell injection vulnerability in commandExists
- Change from shell interpolation (sh -c 'command -v \$cmd') to direct which call
- Use spawnSync with argument array to avoid shell interpretation
- This prevents potential command injection if function is called with untrusted input
Fixes CodeRabbit security review finding.
* fix: normalize paths to handle trailing slashes from archive tools
- Add normalizePath helper to remove trailing slashes
- Update Python binary and backend directory checks to use normalized paths
- Add test case for paths with trailing slashes (bsdtar/dpkg-deb output)
Fixes CodeRabbit review finding about archive tools emitting directories
with trailing slashes like './resources/python/' or 'resources/python/'.
Now handles:
- './resources/python'
- './resources/python/'
- 'resources/python'
- 'resources/python/'
* fix: fail CI when critical verification tools are missing
- Add critical flag when bsdtar or dpkg-deb is missing
- Update main function to fail when critical verifications are skipped
- Provide helpful installation instructions when tools are missing
Fixes Sentry review finding about CI false positives. The script now
exits with error code 1 when AppImage or deb verification is skipped
due to missing required tools (bsdtar, dpkg-deb).
Note: flatpak CLI remains non-critical since basic file/size checks
still work without it.
Regarding CodeRabbit's suggestion to use platform helpers:
- This is a standalone .cjs script that runs in Node.js (not Electron)
- Platform helpers (findExecutable) are TypeScript modules for the app
- The 'which' command is universally available on Linux CI systems
* fix: add spawnSync error checks and improve Flatpak tests
MEDIUM severity fixes:
- Add result.error checks in verifyAppImage for spawnSync failures
- Add result.error checks in verifyDeb for spawnSync failures
- These catch OS-level spawn failures (permission denied, memory issues)
LOW severity fix:
- Refactor Flatpak tests to call actual verifyFlatpak function
- Mock fs.existsSync and fs.statSync similar to findPackages tests
- Add test case for non-existent Flatpak files
Increases test coverage confidence by testing actual function implementation
instead of manually reimplementing logic in tests.
Fixes AI review findings NEW-CODE-001, NEW-CODE-002, NEW-CODE-003.
* test: add spawnSync mock coverage for verifyAppImage and verifyDeb
- Add test coverage for verifyAppImage and verifyDeb functions
- Mock spawnSync at module level by clearing require cache
- Test cases cover:
- Successful extraction
- result.error set (OS-level spawn failures)
- result.status !== 0 (tool returns error)
- Missing required tools (bsdtar, dpkg-deb)
Fixes AI review finding NEW-001 [MEDIUM] about missing test coverage
for the newly-added error handling code.
Increases test count from 16 to 24 tests.
---------
Co-authored-by: StillKnotKnown <stillknotknown@users.noreply.github.com>
Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
@@ -447,6 +447,9 @@ jobs:
|
||||
SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}
|
||||
SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE }}
|
||||
|
||||
- name: Verify Linux packages
|
||||
run: cd apps/frontend && npm run verify:linux
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -383,6 +383,9 @@ jobs:
|
||||
SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}
|
||||
SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE }}
|
||||
|
||||
- name: Verify Linux packages
|
||||
run: cd apps/frontend && npm run verify:linux
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
"package:win": "node scripts/package-with-python.cjs --win",
|
||||
"package:linux": "node scripts/package-with-python.cjs --linux",
|
||||
"package:flatpak": "node scripts/package-with-python.cjs --linux flatpak",
|
||||
"verify:linux": "node scripts/verify-linux-packages.cjs dist",
|
||||
"test:verify-linux": "node --test scripts/verify-linux-packages.test.mjs",
|
||||
"start:packaged:mac": "open dist/mac-arm64/Auto-Claude.app || open dist/mac/Auto-Claude.app",
|
||||
"start:packaged:win": "start \"\" \"dist\\win-unpacked\\Auto-Claude.exe\"",
|
||||
"start:packaged:linux": "./dist/linux-unpacked/auto-claude",
|
||||
@@ -165,14 +167,6 @@
|
||||
"from": "resources/icon.ico",
|
||||
"to": "icon.ico"
|
||||
},
|
||||
{
|
||||
"from": "python-runtime/${os}-${arch}/python",
|
||||
"to": "python"
|
||||
},
|
||||
{
|
||||
"from": "python-runtime/${os}-${arch}/site-packages",
|
||||
"to": "python-site-packages"
|
||||
},
|
||||
{
|
||||
"from": "../backend",
|
||||
"to": "backend",
|
||||
@@ -202,6 +196,16 @@
|
||||
"target": [
|
||||
"dmg",
|
||||
"zip"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "python-runtime/${os}-${arch}/python",
|
||||
"to": "python"
|
||||
},
|
||||
{
|
||||
"from": "python-runtime/${os}-${arch}/site-packages",
|
||||
"to": "python-site-packages"
|
||||
}
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
@@ -209,6 +213,16 @@
|
||||
"target": [
|
||||
"nsis",
|
||||
"zip"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "python-runtime/${os}-${arch}/python",
|
||||
"to": "python"
|
||||
},
|
||||
{
|
||||
"from": "python-runtime/${os}-${arch}/site-packages",
|
||||
"to": "python-site-packages"
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
@@ -218,7 +232,17 @@
|
||||
"deb",
|
||||
"flatpak"
|
||||
],
|
||||
"category": "Development"
|
||||
"category": "Development",
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "python-runtime/${os}-${arch}/python",
|
||||
"to": "python"
|
||||
},
|
||||
{
|
||||
"from": "python-runtime/${os}-${arch}/site-packages",
|
||||
"to": "python-site-packages"
|
||||
}
|
||||
]
|
||||
},
|
||||
"flatpak": {
|
||||
"runtime": "org.freedesktop.Platform",
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify Linux package contents to ensure alignment between AppImage, deb, and Flatpak.
|
||||
*
|
||||
* This script extracts and inspects each Linux package format to verify that critical
|
||||
* files (Python binary, backend code, Python packages) are present and correctly bundled.
|
||||
*
|
||||
* Usage: node scripts/verify-linux-packages.cjs [dist-dir]
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
// Critical Python packages that must be present
|
||||
const CRITICAL_PACKAGES = [
|
||||
'secretstorage', // Linux OAuth token storage
|
||||
'pydantic_core',
|
||||
'claude_agent_sdk',
|
||||
'dotenv',
|
||||
];
|
||||
|
||||
// Minimum expected Flatpak file size (50 MB)
|
||||
// Flatpak files are large OCI archives; anything smaller is suspicious
|
||||
// Based on observed minimum sizes of valid builds
|
||||
const FLATPAK_MIN_SIZE_MB = 50;
|
||||
|
||||
// Colors for terminal output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
cyan: '\x1b[36m',
|
||||
};
|
||||
|
||||
function log(message, color = colors.reset) {
|
||||
console.log(`${color}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
function logSuccess(message) {
|
||||
log(`✓ ${message}`, colors.green);
|
||||
}
|
||||
|
||||
function logError(message) {
|
||||
log(`✗ ${message}`, colors.red);
|
||||
}
|
||||
|
||||
function logWarning(message) {
|
||||
log(`⚠ ${message}`, colors.yellow);
|
||||
}
|
||||
|
||||
function logInfo(message) {
|
||||
log(`ℹ ${message}`, colors.cyan);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command exists
|
||||
* Uses 'which' directly without shell interpolation to prevent command injection
|
||||
*/
|
||||
function commandExists(cmd) {
|
||||
const result = spawnSync('which', [cmd], { stdio: 'ignore' });
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all Linux packages in the dist directory
|
||||
*/
|
||||
function findPackages(distDir) {
|
||||
const packages = {
|
||||
appImage: null,
|
||||
deb: null,
|
||||
flatpak: null,
|
||||
};
|
||||
|
||||
if (!fs.existsSync(distDir)) {
|
||||
logError(`Distribution directory not found: ${distDir}`);
|
||||
return packages;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(distDir);
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(distDir, file);
|
||||
|
||||
if (file.endsWith('.AppImage')) {
|
||||
if (!packages.appImage) {
|
||||
packages.appImage = fullPath;
|
||||
} else {
|
||||
logWarning(`Multiple AppImage files found, using first: ${path.basename(packages.appImage)}`);
|
||||
}
|
||||
} else if (file.endsWith('.deb')) {
|
||||
if (!packages.deb) {
|
||||
packages.deb = fullPath;
|
||||
} else {
|
||||
logWarning(`Multiple deb files found, using first: ${path.basename(packages.deb)}`);
|
||||
}
|
||||
} else if (file.endsWith('.flatpak')) {
|
||||
if (!packages.flatpak) {
|
||||
packages.flatpak = fullPath;
|
||||
} else {
|
||||
logWarning(`Multiple Flatpak files found, using first: ${path.basename(packages.flatpak)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common file list verification logic
|
||||
* @param {string[]} files - List of files from package
|
||||
* @param {string} packageType - Type of package (for error messages)
|
||||
* @returns {Object} Verification result with verified flag and issues array
|
||||
*
|
||||
* File formats:
|
||||
* - AppImage (bsdtar): './resources/python', './resources/backend/file.py'
|
||||
* - deb (dpkg-deb -c): 'resources/python', 'resources/backend/file.py' (in last column)
|
||||
*/
|
||||
function verifyFileList(files, packageType) {
|
||||
const issues = [];
|
||||
|
||||
// Normalize paths by removing trailing slashes (archive tools commonly add these)
|
||||
const normalizePath = (p) => p.replace(/\/+$/, '');
|
||||
|
||||
// Check for Python binary directory
|
||||
// AppImage: './resources/python' or './resources/python/' (with trailing slash)
|
||||
// deb: 'resources/python' or 'resources/python/' (with trailing slash)
|
||||
// Must NOT match 'resources/python-site-packages'
|
||||
const pythonBinFound = files.some((f) => {
|
||||
const normalized = normalizePath(f);
|
||||
return (
|
||||
(normalized === './resources/python' ||
|
||||
normalized === 'resources/python' ||
|
||||
normalized.endsWith('/resources/python')) &&
|
||||
!f.includes('python-site-packages')
|
||||
);
|
||||
});
|
||||
if (!pythonBinFound) {
|
||||
issues.push(`Python binary directory not found in ${packageType}`);
|
||||
}
|
||||
|
||||
// Check for backend directory (must be under resources/)
|
||||
const backendFound = files.some((f) => {
|
||||
const normalized = normalizePath(f);
|
||||
return (
|
||||
f.includes('./resources/backend/') ||
|
||||
f.includes('resources/backend/') ||
|
||||
normalized === './resources/backend' ||
|
||||
normalized === 'resources/backend'
|
||||
);
|
||||
});
|
||||
if (!backendFound) {
|
||||
issues.push(`Backend directory not found in ${packageType}`);
|
||||
}
|
||||
|
||||
// Check for critical Python packages (must be under python-site-packages/)
|
||||
for (const pkg of CRITICAL_PACKAGES) {
|
||||
// Match: './resources/python-site-packages/secretstorage/__init__.py'
|
||||
// Match: 'resources/python-site-packages/secretstorage/__init__.py'
|
||||
// Don't match: '/some/other/path/secretstorage/'
|
||||
const found = files.some(
|
||||
(f) => f.includes(`python-site-packages/${pkg}/`) || f.includes(`python-site-packages/${pkg}.`),
|
||||
);
|
||||
if (!found) {
|
||||
issues.push(`Python package not found: ${pkg}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
verified: issues.length === 0,
|
||||
issues,
|
||||
fileCount: files.filter((f) => f.trim()).length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify AppImage contents using bsdtar (libarchive)
|
||||
*/
|
||||
function verifyAppImage(appImagePath) {
|
||||
logInfo(`Verifying AppImage: ${path.basename(appImagePath)}`);
|
||||
|
||||
// Check if bsdtar is available
|
||||
if (!commandExists('bsdtar')) {
|
||||
logWarning('bsdtar not found. Install with: sudo apt-get install libarchive-tools');
|
||||
logWarning('Skipping AppImage verification');
|
||||
return { verified: false, reason: 'bsdtar not available', critical: true };
|
||||
}
|
||||
|
||||
// Extract file list from AppImage using bsdtar
|
||||
const result = spawnSync('bsdtar', ['-t', '-f', appImagePath], {
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf-8',
|
||||
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large file listings
|
||||
});
|
||||
|
||||
// Check for spawn errors (e.g., permission denied, memory issues)
|
||||
if (result.error) {
|
||||
logError(`Failed to execute bsdtar: ${result.error.message}`);
|
||||
return { verified: false, reason: `Command execution failed: ${result.error.message}` };
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
logError(`Failed to read AppImage: ${result.stderr}`);
|
||||
return { verified: false, reason: 'Failed to extract file list' };
|
||||
}
|
||||
|
||||
const files = result.stdout.split('\n');
|
||||
return verifyFileList(files, 'AppImage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify deb package contents
|
||||
*/
|
||||
function verifyDeb(debPath) {
|
||||
logInfo(`Verifying deb package: ${path.basename(debPath)}`);
|
||||
|
||||
// Check if dpkg is available
|
||||
if (!commandExists('dpkg-deb')) {
|
||||
logWarning('dpkg-deb not found. Skipping deb verification');
|
||||
return { verified: false, reason: 'dpkg-deb not available', critical: true };
|
||||
}
|
||||
|
||||
// List contents of deb package
|
||||
const result = spawnSync('dpkg-deb', ['-c', debPath], {
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf-8',
|
||||
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large file listings
|
||||
});
|
||||
|
||||
// Check for spawn errors (e.g., permission denied, memory issues)
|
||||
if (result.error) {
|
||||
logError(`Failed to execute dpkg-deb: ${result.error.message}`);
|
||||
return { verified: false, reason: `Command execution failed: ${result.error.message}` };
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
logError(`Failed to read deb package: ${result.stderr}`);
|
||||
return { verified: false, reason: 'Failed to extract file list' };
|
||||
}
|
||||
|
||||
const files = result.stdout.split('\n');
|
||||
return verifyFileList(files, 'deb package');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Flatpak package contents
|
||||
* Note: Flatpak is more complex to inspect, so we do basic validation
|
||||
*/
|
||||
function verifyFlatpak(flatpakPath) {
|
||||
logInfo(`Verifying Flatpak package: ${path.basename(flatpakPath)}`);
|
||||
|
||||
const issues = [];
|
||||
|
||||
// Check if flatpak command is available for detailed validation
|
||||
const hasFlatpakCli = commandExists('flatpak');
|
||||
if (!hasFlatpakCli) {
|
||||
logWarning('flatpak command not found. Skipping detailed Flatpak verification');
|
||||
// Continue with basic file existence/size checks
|
||||
}
|
||||
|
||||
// Check if file exists and is not empty
|
||||
if (!fs.existsSync(flatpakPath)) {
|
||||
return { verified: false, issues: ['Flatpak file does not exist'] };
|
||||
}
|
||||
|
||||
const stats = fs.statSync(flatpakPath);
|
||||
if (stats.size === 0) {
|
||||
return { verified: false, issues: ['Flatpak file is empty'] };
|
||||
}
|
||||
|
||||
// Flatpak files are large OCI archives, so we just verify file size and basic structure
|
||||
// Detailed content inspection would require mounting or extracting the flatpak
|
||||
if (stats.size < FLATPAK_MIN_SIZE_MB * 1024 * 1024) {
|
||||
// Less than minimum size is suspicious
|
||||
issues.push(
|
||||
`Flatpak file seems too small (${(stats.size / 1024 / 1024).toFixed(2)} MB, expected at least ${FLATPAK_MIN_SIZE_MB} MB)`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
verified: issues.length === 0,
|
||||
issues,
|
||||
size: stats.size,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main verification function
|
||||
*/
|
||||
function main() {
|
||||
const distDir = process.argv[2] || path.join(__dirname, '..', 'dist');
|
||||
|
||||
log('\n=== Linux Package Verification ===\n', colors.blue);
|
||||
logInfo(`Distribution directory: ${distDir}\n`);
|
||||
|
||||
const packages = findPackages(distDir);
|
||||
|
||||
// Report found packages
|
||||
if (packages.appImage) {
|
||||
logSuccess(`Found AppImage: ${path.basename(packages.appImage)}`);
|
||||
} else {
|
||||
logWarning('No AppImage found');
|
||||
}
|
||||
|
||||
if (packages.deb) {
|
||||
logSuccess(`Found deb: ${path.basename(packages.deb)}`);
|
||||
} else {
|
||||
logWarning('No deb package found');
|
||||
}
|
||||
|
||||
if (packages.flatpak) {
|
||||
logSuccess(`Found Flatpak: ${path.basename(packages.flatpak)}`);
|
||||
} else {
|
||||
logWarning('No Flatpak package found');
|
||||
}
|
||||
|
||||
if (!packages.appImage && !packages.deb && !packages.flatpak) {
|
||||
logError('\nNo Linux packages found to verify!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log('');
|
||||
|
||||
// Verify each package
|
||||
const results = {};
|
||||
|
||||
if (packages.appImage) {
|
||||
results.appImage = verifyAppImage(packages.appImage);
|
||||
}
|
||||
|
||||
if (packages.deb) {
|
||||
results.deb = verifyDeb(packages.deb);
|
||||
}
|
||||
|
||||
if (packages.flatpak) {
|
||||
results.flatpak = verifyFlatpak(packages.flatpak);
|
||||
}
|
||||
|
||||
// Print results
|
||||
log('\n=== Verification Results ===\n', colors.blue);
|
||||
|
||||
let hasFailures = false;
|
||||
let hasCriticalSkips = false;
|
||||
|
||||
for (const [type, result] of Object.entries(results)) {
|
||||
if (result.reason) {
|
||||
if (result.critical) {
|
||||
logError(`${type}: CRITICAL - SKIPPED (${result.reason})`);
|
||||
hasCriticalSkips = true;
|
||||
} else {
|
||||
logWarning(`${type}: SKIPPED (${result.reason})`);
|
||||
}
|
||||
} else if (result.verified) {
|
||||
logSuccess(`${type}: VERIFIED`);
|
||||
if (result.fileCount) {
|
||||
logInfo(` Files: ${result.fileCount}`);
|
||||
}
|
||||
if (result.size) {
|
||||
logInfo(` Size: ${(result.size / 1024 / 1024).toFixed(2)} MB`);
|
||||
}
|
||||
} else {
|
||||
logError(`${type}: FAILED`);
|
||||
hasFailures = true;
|
||||
for (const issue of result.issues || []) {
|
||||
logError(` - ${issue}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log('');
|
||||
|
||||
if (hasFailures || hasCriticalSkips) {
|
||||
logError('\n=== VERIFICATION FAILED ===\n');
|
||||
if (hasFailures) {
|
||||
log('Some packages are missing critical files. This will cause runtime errors.\n', colors.red);
|
||||
}
|
||||
if (hasCriticalSkips) {
|
||||
log('Some packages could not be verified due to missing required tools.\n', colors.red);
|
||||
log('Install required tools:\n', colors.red);
|
||||
log(' - bsdtar: sudo apt-get install libarchive-tools\n', colors.red);
|
||||
log(' - dpkg-deb: sudo apt-get install dpkg\n', colors.red);
|
||||
}
|
||||
process.exit(1);
|
||||
} else {
|
||||
logSuccess('\n=== ALL PACKAGES VERIFIED ===\n');
|
||||
log('All Linux packages contain the required files.\n', colors.green);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Only run main if this file is executed directly (not imported)
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
// Export for testing
|
||||
module.exports = {
|
||||
CRITICAL_PACKAGES,
|
||||
findPackages,
|
||||
verifyFileList,
|
||||
verifyAppImage,
|
||||
verifyDeb,
|
||||
verifyFlatpak,
|
||||
};
|
||||
@@ -0,0 +1,533 @@
|
||||
/**
|
||||
* Tests for verify-linux-packages.cjs
|
||||
*
|
||||
* These tests cover the core logic by calling the actual exported functions.
|
||||
*/
|
||||
|
||||
import { describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import fs from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
// Get child_process and save original spawnSync
|
||||
const childProcess = require('child_process');
|
||||
const originalSpawnSync = childProcess.spawnSync;
|
||||
|
||||
// Helper to reload the verification module with a mocked spawnSync
|
||||
function loadWithMockedSpawnSync(mockFn) {
|
||||
// Set the mock before requiring
|
||||
childProcess.spawnSync = mockFn;
|
||||
// Clear the module cache
|
||||
delete require.cache[require.resolve('./verify-linux-packages.cjs')];
|
||||
// Re-require the module
|
||||
return require('./verify-linux-packages.cjs');
|
||||
}
|
||||
|
||||
function restoreSpawnSync() {
|
||||
childProcess.spawnSync = originalSpawnSync;
|
||||
delete require.cache[require.resolve('./verify-linux-packages.cjs')];
|
||||
}
|
||||
|
||||
// Load the module normally for tests that don't need spawnSync mocking
|
||||
const {
|
||||
CRITICAL_PACKAGES,
|
||||
findPackages,
|
||||
verifyFileList,
|
||||
verifyFlatpak,
|
||||
} = require('./verify-linux-packages.cjs');
|
||||
|
||||
describe('verify-linux-packages', () => {
|
||||
describe('package finding logic', () => {
|
||||
it('should identify all three Linux package types', () => {
|
||||
// Test that findPackages correctly identifies .AppImage, .deb, and .flatpak files
|
||||
const mockFiles = [
|
||||
'Auto-Claude-2.7.5-linux-x86_64.AppImage',
|
||||
'auto-claude_2.7.5_amd64.deb',
|
||||
'com.autoclaude.ui_2.7.5_linux_x86_64.flatpak',
|
||||
'latest-mac.yml',
|
||||
'latest.yml',
|
||||
];
|
||||
|
||||
const distDir = '/test/dist';
|
||||
|
||||
// Mock fs.existsSync to return true (directory exists)
|
||||
const existsSync = mock.method(fs, 'existsSync', mock.fn(() => true));
|
||||
// Mock fs.readdirSync to return our test files
|
||||
const readdirSync = mock.method(fs, 'readdirSync', mock.fn(() => mockFiles));
|
||||
|
||||
try {
|
||||
const result = findPackages(distDir);
|
||||
|
||||
// Verify the expected results
|
||||
assert.equal(result.appImage, '/test/dist/Auto-Claude-2.7.5-linux-x86_64.AppImage');
|
||||
assert.equal(result.deb, '/test/dist/auto-claude_2.7.5_amd64.deb');
|
||||
assert.equal(result.flatpak, '/test/dist/com.autoclaude.ui_2.7.5_linux_x86_64.flatpak');
|
||||
} finally {
|
||||
existsSync.mock.restore();
|
||||
readdirSync.mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle missing packages gracefully', () => {
|
||||
// Test behavior when packages are missing
|
||||
const mockFiles = ['latest-mac.yml', 'latest.yml'];
|
||||
const distDir = '/test/dist';
|
||||
|
||||
const existsSync = mock.method(fs, 'existsSync', mock.fn(() => true));
|
||||
const readdirSync = mock.method(fs, 'readdirSync', mock.fn(() => mockFiles));
|
||||
|
||||
try {
|
||||
const result = findPackages(distDir);
|
||||
|
||||
assert.equal(result.appImage, null);
|
||||
assert.equal(result.deb, null);
|
||||
assert.equal(result.flatpak, null);
|
||||
} finally {
|
||||
existsSync.mock.restore();
|
||||
readdirSync.mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle missing dist directory', () => {
|
||||
// Test behavior when dist directory doesn't exist
|
||||
const distDir = '/test/dist';
|
||||
|
||||
const existsSync = mock.method(fs, 'existsSync', mock.fn(() => false));
|
||||
|
||||
try {
|
||||
const result = findPackages(distDir);
|
||||
|
||||
// Should return empty packages object without error
|
||||
assert.equal(result.appImage, null);
|
||||
assert.equal(result.deb, null);
|
||||
assert.equal(result.flatpak, null);
|
||||
} finally {
|
||||
existsSync.mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should warn about duplicate packages', () => {
|
||||
// Test behavior when multiple packages of same type exist
|
||||
const mockFiles = [
|
||||
'Auto-Claude-2.7.5-linux-x86_64.AppImage',
|
||||
'Auto-Claude-2.7.5-linux-x86_64.AppImage', // Duplicate
|
||||
'auto-claude_2.7.5_amd64.deb',
|
||||
'auto-claude_2.7.5_amd64.deb', // Duplicate
|
||||
'com.autoclaude.ui_2.7.5_linux_x86_64.flatpak',
|
||||
];
|
||||
const distDir = '/test/dist';
|
||||
|
||||
const existsSync = mock.method(fs, 'existsSync', mock.fn(() => true));
|
||||
const readdirSync = mock.method(fs, 'readdirSync', mock.fn(() => mockFiles));
|
||||
|
||||
try {
|
||||
const result = findPackages(distDir);
|
||||
|
||||
// Should still find packages, using first occurrence
|
||||
assert.equal(result.appImage, '/test/dist/Auto-Claude-2.7.5-linux-x86_64.AppImage');
|
||||
assert.equal(result.deb, '/test/dist/auto-claude_2.7.5_amd64.deb');
|
||||
assert.equal(result.flatpak, '/test/dist/com.autoclaude.ui_2.7.5_linux_x86_64.flatpak');
|
||||
} finally {
|
||||
existsSync.mock.restore();
|
||||
readdirSync.mock.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('critical packages list', () => {
|
||||
it('should contain all required Linux packages', () => {
|
||||
assert.ok(CRITICAL_PACKAGES.includes('secretstorage'), 'secretstorage must be present for Linux OAuth');
|
||||
assert.ok(CRITICAL_PACKAGES.includes('pydantic_core'), 'pydantic_core must be present');
|
||||
assert.ok(CRITICAL_PACKAGES.includes('claude_agent_sdk'), 'claude_agent_sdk must be present');
|
||||
assert.ok(CRITICAL_PACKAGES.includes('dotenv'), 'dotenv must be present');
|
||||
});
|
||||
});
|
||||
|
||||
describe('file content verification logic', () => {
|
||||
it('should detect Python binary in file list', () => {
|
||||
// AppImage format uses './' prefix
|
||||
const mockFiles = [
|
||||
'usr/bin/auto-claude',
|
||||
'./resources/python',
|
||||
'./resources/backend/core/client.py',
|
||||
'./resources/python-site-packages/secretstorage/__init__.py',
|
||||
'./resources/python-site-packages/pydantic_core/__init__.py',
|
||||
'./resources/python-site-packages/claude_agent_sdk/__init__.py',
|
||||
'./resources/python-site-packages/dotenv/__init__.py',
|
||||
];
|
||||
|
||||
const result = verifyFileList(mockFiles, 'test-package');
|
||||
assert.ok(result.verified, 'Should detect Python binary directory');
|
||||
assert.equal(result.issues.length, 0);
|
||||
});
|
||||
|
||||
it('should detect Python binary with trailing slashes', () => {
|
||||
// Archive tools like bsdtar/dpkg-deb commonly output directories with trailing slashes
|
||||
const mockFiles = [
|
||||
'usr/bin/auto-claude',
|
||||
'./resources/python/', // Trailing slash
|
||||
'resources/backend/', // Trailing slash
|
||||
'./resources/python-site-packages/secretstorage/__init__.py',
|
||||
'./resources/python-site-packages/pydantic_core/__init__.py',
|
||||
'./resources/python-site-packages/claude_agent_sdk/__init__.py',
|
||||
'./resources/python-site-packages/dotenv/__init__.py',
|
||||
];
|
||||
|
||||
const result = verifyFileList(mockFiles, 'test-package');
|
||||
assert.ok(result.verified, 'Should detect Python binary directory with trailing slash');
|
||||
assert.equal(result.issues.length, 0);
|
||||
});
|
||||
|
||||
it('should detect backend directory in file list', () => {
|
||||
const mockFiles = [
|
||||
'usr/bin/auto-claude',
|
||||
'./resources/python',
|
||||
'./resources/backend/core/client.py',
|
||||
'./resources/python-site-packages/secretstorage/__init__.py',
|
||||
'./resources/python-site-packages/pydantic_core/__init__.py',
|
||||
'./resources/python-site-packages/claude_agent_sdk/__init__.py',
|
||||
'./resources/python-site-packages/dotenv/__init__.py',
|
||||
];
|
||||
|
||||
const result = verifyFileList(mockFiles, 'test-package');
|
||||
assert.ok(result.verified, 'Should detect backend directory');
|
||||
assert.equal(result.issues.length, 0);
|
||||
});
|
||||
|
||||
it('should detect critical Python packages', () => {
|
||||
const mockFiles = [
|
||||
'usr/bin/auto-claude',
|
||||
'./resources/python',
|
||||
'./resources/backend/core/client.py',
|
||||
'./resources/python-site-packages/secretstorage/__init__.py',
|
||||
'./resources/python-site-packages/pydantic_core/__init__.py',
|
||||
'./resources/python-site-packages/claude_agent_sdk/__init__.py',
|
||||
'./resources/python-site-packages/dotenv/__init__.py',
|
||||
];
|
||||
|
||||
const result = verifyFileList(mockFiles, 'test-package');
|
||||
assert.ok(result.verified, 'Should detect all critical packages');
|
||||
assert.equal(result.issues.length, 0);
|
||||
});
|
||||
|
||||
it('should report missing packages', () => {
|
||||
const mockFiles = [
|
||||
'usr/bin/auto-claude',
|
||||
'./resources/python',
|
||||
'./resources/backend/core/client.py',
|
||||
'./resources/python-site-packages/dotenv/__init__.py',
|
||||
];
|
||||
|
||||
const result = verifyFileList(mockFiles, 'test-package');
|
||||
|
||||
assert.ok(!result.verified, 'Should fail verification');
|
||||
assert.ok(result.issues.includes('Python package not found: secretstorage'));
|
||||
assert.ok(result.issues.includes('Python package not found: pydantic_core'));
|
||||
assert.ok(result.issues.includes('Python package not found: claude_agent_sdk'));
|
||||
assert.ok(!result.issues.some((i) => i.includes('dotenv')));
|
||||
});
|
||||
|
||||
it('should not match python-site-packages when looking for python binary', () => {
|
||||
const mockFiles = [
|
||||
'usr/bin/auto-claude',
|
||||
'./resources/python-site-packages/secretstorage/__init__.py',
|
||||
'./resources/python-site-packages/pydantic_core/__init__.py',
|
||||
'./resources/python-site-packages/claude_agent_sdk/__init__.py',
|
||||
'./resources/python-site-packages/dotenv/__init__.py',
|
||||
// Note: NO './resources/python' entry
|
||||
];
|
||||
|
||||
const result = verifyFileList(mockFiles, 'test-package');
|
||||
|
||||
assert.ok(!result.verified, 'Should fail verification');
|
||||
assert.ok(result.issues.some((i) => i.includes('Python binary directory not found')));
|
||||
});
|
||||
|
||||
it('should not match unrelated paths when looking for packages', () => {
|
||||
const mockFiles = [
|
||||
'usr/bin/auto-claude',
|
||||
'./resources/python',
|
||||
'./resources/backend/core/client.py',
|
||||
// These paths end with package names but are NOT under python-site-packages
|
||||
'./some/other/path/secretstorage/file.txt',
|
||||
'./unrelated/dotenv/config',
|
||||
'./another/pydantic_core/standalone/__init__.py',
|
||||
];
|
||||
|
||||
const result = verifyFileList(mockFiles, 'test-package');
|
||||
|
||||
assert.ok(!result.verified, 'Should fail verification');
|
||||
assert.ok(result.issues.some((i) => i.includes('Python package not found: secretstorage')));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Flatpak file validation', () => {
|
||||
it('should reject empty Flatpak files', () => {
|
||||
const flatpakPath = '/test/app.flatpak';
|
||||
const mockStat = { size: 0 };
|
||||
|
||||
// Mock fs.existsSync to return true
|
||||
const existsSync = mock.method(fs, 'existsSync', mock.fn(() => true));
|
||||
// Mock fs.statSync to return empty file stats
|
||||
const statSync = mock.method(fs, 'statSync', mock.fn(() => mockStat));
|
||||
|
||||
try {
|
||||
const result = verifyFlatpak(flatpakPath);
|
||||
|
||||
assert.ok(!result.verified, 'Should reject empty Flatpak files');
|
||||
assert.ok(result.issues.includes('Flatpak file is empty'));
|
||||
} finally {
|
||||
existsSync.mock.restore();
|
||||
statSync.mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should warn about suspiciously small Flatpak files', () => {
|
||||
const flatpakPath = '/test/app.flatpak';
|
||||
const mockStat = { size: 10 * 1024 * 1024 }; // 10 MB
|
||||
|
||||
const existsSync = mock.method(fs, 'existsSync', mock.fn(() => true));
|
||||
const statSync = mock.method(fs, 'statSync', mock.fn(() => mockStat));
|
||||
|
||||
try {
|
||||
const result = verifyFlatpak(flatpakPath);
|
||||
|
||||
assert.ok(!result.verified, 'Should fail verification for too-small files');
|
||||
assert.ok(result.issues.some((i) => i.includes('too small')));
|
||||
} finally {
|
||||
existsSync.mock.restore();
|
||||
statSync.mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept reasonable Flatpak file sizes', () => {
|
||||
const flatpakPath = '/test/app.flatpak';
|
||||
const mockStat = { size: 133 * 1024 * 1024 }; // 133 MB (typical size)
|
||||
|
||||
const existsSync = mock.method(fs, 'existsSync', mock.fn(() => true));
|
||||
const statSync = mock.method(fs, 'statSync', mock.fn(() => mockStat));
|
||||
|
||||
try {
|
||||
const result = verifyFlatpak(flatpakPath);
|
||||
|
||||
assert.ok(result.verified, 'Should accept reasonable Flatpak file sizes');
|
||||
assert.equal(result.issues.length, 0);
|
||||
} finally {
|
||||
existsSync.mock.restore();
|
||||
statSync.mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle non-existent Flatpak files', () => {
|
||||
const flatpakPath = '/test/nonexistent.flatpak';
|
||||
|
||||
const existsSync = mock.method(fs, 'existsSync', mock.fn(() => false));
|
||||
|
||||
try {
|
||||
const result = verifyFlatpak(flatpakPath);
|
||||
|
||||
assert.ok(!result.verified, 'Should reject non-existent Flatpak files');
|
||||
assert.ok(result.issues.includes('Flatpak file does not exist'));
|
||||
} finally {
|
||||
existsSync.mock.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppImage verification', () => {
|
||||
const appImagePath = '/test/Auto-Claude-2.7.5-linux-x86_64.AppImage';
|
||||
|
||||
it('should successfully verify valid AppImage', () => {
|
||||
const mockFiles = [
|
||||
'./resources/python',
|
||||
'./resources/backend/core/client.py',
|
||||
'./resources/python-site-packages/secretstorage/__init__.py',
|
||||
'./resources/python-site-packages/pydantic_core/__init__.py',
|
||||
'./resources/python-site-packages/claude_agent_sdk/__init__.py',
|
||||
'./resources/python-site-packages/dotenv/__init__.py',
|
||||
];
|
||||
|
||||
const mockFn = (cmd, args) => {
|
||||
if (cmd === 'which' && args[0] === 'bsdtar') {
|
||||
return { status: 0, stdout: '/usr/bin/bsdtar', stderr: '' };
|
||||
}
|
||||
if (cmd === 'bsdtar') {
|
||||
return { status: 0, stdout: mockFiles.join('\n'), stderr: '', error: undefined };
|
||||
}
|
||||
return { status: 1, stderr: 'Unknown command' };
|
||||
};
|
||||
|
||||
const { verifyAppImage } = loadWithMockedSpawnSync(mockFn);
|
||||
const result = verifyAppImage(appImagePath);
|
||||
|
||||
restoreSpawnSync();
|
||||
|
||||
assert.ok(result.verified, 'Should verify valid AppImage');
|
||||
assert.equal(result.issues.length, 0);
|
||||
assert.equal(result.fileCount, mockFiles.length);
|
||||
});
|
||||
|
||||
it('should handle spawn errors (OS-level failures)', () => {
|
||||
const spawnError = new Error('EACCES: permission denied');
|
||||
|
||||
const mockFn = (cmd) => {
|
||||
if (cmd === 'which') {
|
||||
return { status: 0, stdout: '/usr/bin/bsdtar', stderr: '', error: undefined };
|
||||
}
|
||||
if (cmd === 'bsdtar') {
|
||||
return { status: null, stdout: '', stderr: '', error: spawnError };
|
||||
}
|
||||
return { status: 1, stderr: 'Unknown command' };
|
||||
};
|
||||
|
||||
const { verifyAppImage } = loadWithMockedSpawnSync(mockFn);
|
||||
const result = verifyAppImage(appImagePath);
|
||||
|
||||
restoreSpawnSync();
|
||||
|
||||
assert.ok(!result.verified, 'Should fail on spawn error');
|
||||
assert.ok(result.reason.includes('Command execution failed'));
|
||||
assert.ok(result.reason.includes('permission denied'));
|
||||
});
|
||||
|
||||
it('should handle non-zero exit status from bsdtar', () => {
|
||||
const mockFn = (cmd) => {
|
||||
if (cmd === 'which') {
|
||||
return { status: 0, stdout: '/usr/bin/bsdtar', stderr: '', error: undefined };
|
||||
}
|
||||
if (cmd === 'bsdtar') {
|
||||
return { status: 1, stdout: '', stderr: 'bsdtar: Error: Not an AppImage file', error: undefined };
|
||||
}
|
||||
return { status: 1, stderr: 'Unknown command' };
|
||||
};
|
||||
|
||||
const { verifyAppImage } = loadWithMockedSpawnSync(mockFn);
|
||||
const result = verifyAppImage(appImagePath);
|
||||
|
||||
restoreSpawnSync();
|
||||
|
||||
assert.ok(!result.verified, 'Should fail on non-zero status');
|
||||
assert.equal(result.reason, 'Failed to extract file list');
|
||||
});
|
||||
|
||||
it('should handle missing bsdtar tool', () => {
|
||||
const mockFn = (cmd) => {
|
||||
if (cmd === 'which') {
|
||||
return { status: 1, stdout: '', stderr: '', error: undefined };
|
||||
}
|
||||
return { status: 1, stderr: 'Unknown command' };
|
||||
};
|
||||
|
||||
const { verifyAppImage } = loadWithMockedSpawnSync(mockFn);
|
||||
const result = verifyAppImage(appImagePath);
|
||||
|
||||
restoreSpawnSync();
|
||||
|
||||
assert.ok(!result.verified, 'Should fail when bsdtar is missing');
|
||||
assert.equal(result.reason, 'bsdtar not available');
|
||||
assert.ok(result.critical, 'Should be marked as critical');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deb package verification', () => {
|
||||
const debPath = '/test/auto-claude_2.7.5_amd64.deb';
|
||||
|
||||
it('should successfully verify valid deb package', () => {
|
||||
const mockFiles = [
|
||||
'drwxr-xr-x root/root 0 2025-01-01 00:00 ./resources/python',
|
||||
'-rw-r--r-- root/root 1234 2025-01-01 00:00 ./resources/backend/core/client.py',
|
||||
'-rw-r--r-- root/root 567 2025-01-01 00:00 ./resources/python-site-packages/secretstorage/__init__.py',
|
||||
'-rw-r--r-- root/root 789 2025-01-01 00:00 ./resources/python-site-packages/pydantic_core/__init__.py',
|
||||
'-rw-r--r-- root/root 456 2025-01-01 00:00 ./resources/python-site-packages/claude_agent_sdk/__init__.py',
|
||||
'-rw-r--r-- root/root 321 2025-01-01 00:00 ./resources/python-site-packages/dotenv/__init__.py',
|
||||
];
|
||||
|
||||
const mockFn = (cmd, args) => {
|
||||
if (cmd === 'which' && args[0] === 'dpkg-deb') {
|
||||
return { status: 0, stdout: '/usr/bin/dpkg-deb', stderr: '', error: undefined };
|
||||
}
|
||||
if (cmd === 'dpkg-deb') {
|
||||
return { status: 0, stdout: mockFiles.join('\n'), stderr: '', error: undefined };
|
||||
}
|
||||
return { status: 1, stderr: 'Unknown command' };
|
||||
};
|
||||
|
||||
const { verifyDeb } = loadWithMockedSpawnSync(mockFn);
|
||||
const result = verifyDeb(debPath);
|
||||
|
||||
restoreSpawnSync();
|
||||
|
||||
assert.ok(result.verified, 'Should verify valid deb package');
|
||||
assert.equal(result.issues.length, 0);
|
||||
assert.equal(result.fileCount, mockFiles.length);
|
||||
});
|
||||
|
||||
it('should handle spawn errors (OS-level failures)', () => {
|
||||
const spawnError = new Error('ENOMEM: Cannot allocate memory');
|
||||
|
||||
const mockFn = (cmd) => {
|
||||
if (cmd === 'which') {
|
||||
return { status: 0, stdout: '/usr/bin/dpkg-deb', stderr: '', error: undefined };
|
||||
}
|
||||
if (cmd === 'dpkg-deb') {
|
||||
return { status: null, stdout: '', stderr: '', error: spawnError };
|
||||
}
|
||||
return { status: 1, stderr: 'Unknown command' };
|
||||
};
|
||||
|
||||
const { verifyDeb } = loadWithMockedSpawnSync(mockFn);
|
||||
const result = verifyDeb(debPath);
|
||||
|
||||
restoreSpawnSync();
|
||||
|
||||
assert.ok(!result.verified, 'Should fail on spawn error');
|
||||
assert.ok(result.reason.includes('Command execution failed'));
|
||||
assert.ok(result.reason.includes('Cannot allocate memory'));
|
||||
});
|
||||
|
||||
it('should handle non-zero exit status from dpkg-deb', () => {
|
||||
const mockFn = (cmd) => {
|
||||
if (cmd === 'which') {
|
||||
return { status: 0, stdout: '/usr/bin/dpkg-deb', stderr: '', error: undefined };
|
||||
}
|
||||
if (cmd === 'dpkg-deb') {
|
||||
return { status: 2, stdout: '', stderr: 'dpkg-deb: error: cannot read archive', error: undefined };
|
||||
}
|
||||
return { status: 1, stderr: 'Unknown command' };
|
||||
};
|
||||
|
||||
const { verifyDeb } = loadWithMockedSpawnSync(mockFn);
|
||||
const result = verifyDeb(debPath);
|
||||
|
||||
restoreSpawnSync();
|
||||
|
||||
assert.ok(!result.verified, 'Should fail on non-zero status');
|
||||
assert.equal(result.reason, 'Failed to extract file list');
|
||||
});
|
||||
|
||||
it('should handle missing dpkg-deb tool', () => {
|
||||
const mockFn = (cmd) => {
|
||||
if (cmd === 'which') {
|
||||
return { status: 1, stdout: '', stderr: '', error: undefined };
|
||||
}
|
||||
return { status: 1, stderr: 'Unknown command' };
|
||||
};
|
||||
|
||||
const { verifyDeb } = loadWithMockedSpawnSync(mockFn);
|
||||
const result = verifyDeb(debPath);
|
||||
|
||||
restoreSpawnSync();
|
||||
|
||||
assert.ok(!result.verified, 'Should fail when dpkg-deb is missing');
|
||||
assert.equal(result.reason, 'dpkg-deb not available');
|
||||
assert.ok(result.critical, 'Should be marked as critical');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user