agent-ecosystem/test/main/build/electronBuilderAfterPack.test.ts
infiniti 9c438e7c84
fix: harden Windows frontend path handling
Harden Windows path handling and packaged app smoke checks.
2026-05-16 17:34:50 +03:00

295 lines
8.3 KiB
TypeScript

// @vitest-environment node
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
const afterPackModule = require('../../../scripts/electron-builder/afterPack.cjs');
const {
detectBinaryMetadata,
parseElf,
parseMachO,
parsePortableExecutable,
pruneNodePtyArtifacts,
validateNativeBinaries,
} = afterPackModule._internal;
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'after-pack-test-'));
}
function writeFile(filePath: string, content: Buffer): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content);
}
function createMachOBuffer(arch: 'arm64' | 'x64'): Buffer {
const cpuType = arch === 'arm64' ? 0x0100000c : 0x01000007;
const buffer = Buffer.alloc(8);
buffer.writeUInt32LE(0xfeedfacf, 0);
buffer.writeUInt32LE(cpuType, 4);
return buffer;
}
function createElfBuffer(arch: 'arm64' | 'x64'): Buffer {
const machine = arch === 'arm64' ? 0x00b7 : 0x003e;
const buffer = Buffer.alloc(64);
buffer[0] = 0x7f;
buffer[1] = 0x45;
buffer[2] = 0x4c;
buffer[3] = 0x46;
buffer[5] = 1;
buffer.writeUInt16LE(machine, 18);
return buffer;
}
function createPortableExecutableBuffer(arch: 'arm64' | 'ia32' | 'x64'): Buffer {
const machine = arch === 'arm64' ? 0xaa64 : arch === 'ia32' ? 0x014c : 0x8664;
const buffer = Buffer.alloc(256);
buffer[0] = 0x4d;
buffer[1] = 0x5a;
buffer.writeUInt32LE(0x80, 0x3c);
buffer[0x80] = 0x50;
buffer[0x81] = 0x45;
buffer[0x82] = 0x00;
buffer[0x83] = 0x00;
buffer.writeUInt16LE(machine, 0x84);
return buffer;
}
describe('electron-builder afterPack', () => {
const tempDirs: string[] = [];
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
it('parses native binary headers for all supported bundle formats', async () => {
const tempDir = createTempDir();
tempDirs.push(tempDir);
const machoPath = path.join(tempDir, 'arm64.node');
const elfPath = path.join(tempDir, 'linux.node');
const pePath = path.join(tempDir, 'win.node');
writeFile(machoPath, createMachOBuffer('arm64'));
writeFile(elfPath, createElfBuffer('x64'));
writeFile(pePath, createPortableExecutableBuffer('arm64'));
await expect(detectBinaryMetadata(machoPath)).resolves.toEqual({
format: 'mach-o',
archs: new Set(['arm64']),
});
await expect(detectBinaryMetadata(elfPath)).resolves.toEqual({
format: 'elf',
archs: new Set(['x64']),
});
await expect(detectBinaryMetadata(pePath)).resolves.toEqual({
format: 'pe',
archs: new Set(['arm64']),
});
expect(parseMachO(createMachOBuffer('x64'))).toEqual({
format: 'mach-o',
archs: new Set(['x64']),
});
expect(parseElf(createElfBuffer('arm64'))).toEqual({
format: 'elf',
archs: new Set(['arm64']),
});
expect(parsePortableExecutable(createPortableExecutableBuffer('x64'))).toEqual({
format: 'pe',
archs: new Set(['x64']),
});
});
it('prunes node-pty prebuilds that do not match the target platform and arch', async () => {
const tempDir = createTempDir();
tempDirs.push(tempDir);
const prebuildsDir = path.join(tempDir, 'node_modules', 'node-pty', 'prebuilds');
const binDir = path.join(tempDir, 'node_modules', 'node-pty', 'bin');
writeFile(path.join(prebuildsDir, 'darwin-arm64', 'pty.node'), createMachOBuffer('arm64'));
writeFile(path.join(prebuildsDir, 'darwin-x64', 'pty.node'), createMachOBuffer('x64'));
writeFile(path.join(prebuildsDir, 'win32-x64', 'pty.node'), createPortableExecutableBuffer('x64'));
writeFile(path.join(binDir, 'darwin-arm64-143', 'node-pty.node'), createMachOBuffer('arm64'));
writeFile(path.join(binDir, 'darwin-x64-143', 'node-pty.node'), createMachOBuffer('x64'));
const removed = await pruneNodePtyArtifacts(tempDir, 'darwin', 'arm64');
expect(removed).toEqual(
expect.arrayContaining([
path.join(prebuildsDir, 'darwin-x64'),
path.join(prebuildsDir, 'win32-x64'),
path.join(binDir, 'darwin-x64-143'),
])
);
expect(fs.existsSync(path.join(prebuildsDir, 'darwin-arm64'))).toBe(true);
expect(fs.existsSync(path.join(binDir, 'darwin-arm64-143'))).toBe(true);
expect(fs.existsSync(path.join(prebuildsDir, 'darwin-x64'))).toBe(false);
});
it('fails validation when a foreign-arch native binary remains in the bundle', async () => {
const tempDir = createTempDir();
tempDirs.push(tempDir);
writeFile(
path.join(tempDir, 'Contents', 'Resources', 'app.asar.unpacked', 'bad.node'),
createMachOBuffer('x64')
);
await expect(validateNativeBinaries(tempDir, 'darwin', 'arm64')).resolves.toEqual([
{
path: path.join('Contents', 'Resources', 'app.asar.unpacked', 'bad.node'),
format: 'mach-o',
archs: ['x64'],
},
]);
});
it('accepts a clean arm64 mac bundle after pruning', async () => {
const tempDir = createTempDir();
tempDirs.push(tempDir);
writeFile(
path.join(tempDir, 'Contents', 'MacOS', 'Agent Teams UI'),
createMachOBuffer('arm64')
);
writeFile(
path.join(
tempDir,
'Contents',
'Resources',
'app.asar.unpacked',
'node_modules',
'node-pty',
'build',
'Release',
'pty.node'
),
createMachOBuffer('arm64')
);
writeFile(
path.join(
tempDir,
'Contents',
'Resources',
'app.asar.unpacked',
'node_modules',
'node-pty',
'prebuilds',
'darwin-arm64',
'pty.node'
),
createMachOBuffer('arm64')
);
writeFile(
path.join(
tempDir,
'Contents',
'Resources',
'app.asar.unpacked',
'node_modules',
'node-pty',
'prebuilds',
'darwin-x64',
'pty.node'
),
createMachOBuffer('x64')
);
await afterPackModule({
appOutDir: tempDir,
electronPlatformName: 'darwin',
arch: 3,
});
expect(
fs.existsSync(
path.join(
tempDir,
'Contents',
'Resources',
'app.asar.unpacked',
'node_modules',
'node-pty',
'prebuilds',
'darwin-x64'
)
)
).toBe(false);
});
it('accepts a clean x64 Windows bundle with optional standalone helper binaries', async () => {
const tempDir = createTempDir();
tempDirs.push(tempDir);
writeFile(path.join(tempDir, 'Agent Teams UI.exe'), createPortableExecutableBuffer('x64'));
writeFile(
path.join(
tempDir,
'resources',
'app.asar.unpacked',
'node_modules',
'node-pty',
'build',
'Release',
'pty.node'
),
createPortableExecutableBuffer('x64')
);
const pageantPath = path.join(
tempDir,
'resources',
'app.asar.unpacked',
'node_modules',
'ssh2',
'util',
'pagent.exe'
);
const armConptyDir = path.join(
tempDir,
'resources',
'app.asar.unpacked',
'node_modules',
'node-pty',
'third_party',
'conpty',
'1.23.251008001',
'win10-arm64'
);
writeFile(pageantPath, createPortableExecutableBuffer('ia32'));
writeFile(path.join(armConptyDir, 'conpty.dll'), createPortableExecutableBuffer('arm64'));
writeFile(path.join(armConptyDir, 'OpenConsole.exe'), createPortableExecutableBuffer('arm64'));
await afterPackModule({
appOutDir: tempDir,
electronPlatformName: 'win32',
arch: 1,
});
expect(fs.existsSync(pageantPath)).toBe(true);
expect(fs.existsSync(armConptyDir)).toBe(false);
});
it('still reports unrelated ia32 Windows binaries in an x64 bundle', async () => {
const tempDir = createTempDir();
tempDirs.push(tempDir);
const badBinaryPath = path.join(tempDir, 'resources', 'app.asar.unpacked', 'bad-helper.exe');
writeFile(badBinaryPath, createPortableExecutableBuffer('ia32'));
await expect(validateNativeBinaries(tempDir, 'win32', 'x64')).resolves.toEqual([
{
path: path.join('resources', 'app.asar.unpacked', 'bad-helper.exe'),
format: 'pe',
archs: ['ia32'],
},
]);
});
});