import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseArgs, assertSafeTempDir, expectedSha256FromManifest, findInstaller, findUninstaller, verifyInstallLayout, listContents, } from '../../scripts/nsis-install-smoke.mjs'; import { sha256File } from '../../scripts/offline-install-check.mjs'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); function tempRoot() { return fs.mkdtempSync(path.join(os.tmpdir(), 'aps-nsis-test-')); } test('nsis-smoke: parseArgs accepts positional installer and flags', () => { const args = parseArgs([ 'C:/tmp/aps-agent-desktop-0.1.0.exe', '--run-uninstaller', '--launch', '--launch-seconds', '6', '--timeout', '120000', '--keep-temp-dir', '--expected-sha256', 'b'.repeat(64), ]); assert.equal(args.installer, path.resolve('C:/tmp/aps-agent-desktop-0.1.0.exe')); assert.equal(args.runUninstaller, true); assert.equal(args.launch, true); assert.equal(args.launchSeconds, 6); assert.equal(args.timeoutMs, 120000); assert.equal(args.keepTempDir, true); assert.equal(args.expectedSha256, 'b'.repeat(64)); }); test('nsis-smoke: --no-launch is the explicit default', () => { assert.equal(parseArgs(['--launch', '--no-launch']).launch, false); assert.equal(parseArgs([]).launch, false); }); test('nsis-smoke: parseArgs rejects bad hash and unknown options', () => { assert.throws(() => parseArgs(['--expected-sha256', 'zz'])); assert.throws(() => parseArgs(['--nope'])); }); test('nsis-smoke: assertSafeTempDir accepts only own prefixed dirs under the temp root', () => { const root = tempRoot(); try { const own = fs.mkdtempSync(path.join(root, 'aps-nsis-smoke-')); const guarded = assertSafeTempDir(own, root); assert.equal(guarded.resolved, path.resolve(own)); assert.ok(guarded.relative.startsWith('aps-nsis-smoke-')); assert.throws(() => assertSafeTempDir(root, root), /temp root itself/); assert.throws(() => assertSafeTempDir(path.join(os.tmpdir(), 'elsewhere'), root), /outside/); const wrongPrefix = fs.mkdtempSync(path.join(root, 'other-prefix-')); assert.throws(() => assertSafeTempDir(wrongPrefix, root), /prefix/); assert.throws(() => assertSafeTempDir(path.join(root, '..', 'escape'), root), /outside/); fs.rmSync(wrongPrefix, { recursive: true, force: true }); fs.rmSync(own, { recursive: true, force: true }); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('nsis-smoke: verifyInstallLayout validates main-exe electron-builder layout', () => { const root = tempRoot(); try { const installDir = path.join(root, 'aps-nsis-smoke-layout'); fs.mkdirSync(path.join(installDir, 'resources', 'sidecar'), { recursive: true }); fs.writeFileSync(path.join(installDir, '工业智核 APS.exe'), 'x'); fs.writeFileSync(path.join(installDir, 'resources', 'app.asar'), 'x'); fs.writeFileSync(path.join(installDir, 'resources', 'sidecar', 'aps-sidecar.exe'), 'x'); fs.writeFileSync(path.join(installDir, 'Uninstall 工业智核 APS.exe'), 'x'); const layout = verifyInstallLayout(installDir, '工业智核 APS'); assert.equal(layout.valid, true); assert.equal(layout.mainExe, true); assert.equal(layout.appAsar, true); assert.equal(layout.sidecar, true); assert.equal(path.basename(layout.uninstaller), 'Uninstall 工业智核 APS.exe'); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('nsis-smoke: verifyInstallLayout reports broken layouts', () => { const root = tempRoot(); try { const dir = path.join(root, 'aps-nsis-smoke-broken'); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, '工业智核 APS.exe'), 'x'); const layout = verifyInstallLayout(dir, '工业智核 APS'); assert.equal(layout.valid, false); assert.ok(layout.reasons.length > 0); assert.equal(verifyInstallLayout(path.join(root, 'nope'), '工业智核 APS').valid, false); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('nsis-smoke: expectedSha256FromManifest reads the nsis-installer entry', () => { const hash = expectedSha256FromManifest({ entries: [{ id: 'nsis-installer', sha256: 'CD'.repeat(32) }] }); assert.equal(hash, 'cd'.repeat(32)); assert.equal(expectedSha256FromManifest(null), null); assert.equal(expectedSha256FromManifest({ entries: [] }), null); assert.equal(expectedSha256FromManifest({ entries: [{ id: 'other', sha256: 'a'.repeat(64) }] }), null); assert.equal(expectedSha256FromManifest({ entries: [{ id: 'nsis-installer', sha256: 'not-hex' }] }), null); }); test('nsis-smoke: findInstaller resolves explicit paths only', () => { const root = tempRoot(); try { const missing = findInstaller({ root, explicit: path.join(root, 'nope.exe'), minBytes: 1 }); assert.equal(missing.exists, false); assert.deepEqual(missing.candidates, [path.join(root, 'nope.exe')]); const file = path.join(root, 'fake.exe'); fs.writeFileSync(file, Buffer.alloc(1024, 1)); const found = findInstaller({ root, explicit: file, minBytes: 1 }); assert.equal(found.exists, true); assert.equal(found.source, 'explicit'); assert.equal(found.bytes, 1024); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('nsis-smoke: findInstaller uses the manifest entry when present', () => { const root = tempRoot(); try { const releaseDir = path.join(root, 'apps', 'desktop', 'release'); fs.mkdirSync(releaseDir, { recursive: true }); const file = path.join(releaseDir, 'aps-agent-desktop-0.1.0.exe'); fs.writeFileSync(file, Buffer.alloc(2048, 2)); const manifest = { baseDir: root, entries: [{ id: 'nsis-installer', file: 'apps/desktop/release/aps-agent-desktop-0.1.0.exe' }] }; const found = findInstaller({ root, manifest, minBytes: 1 }); assert.equal(found.exists, true); assert.equal(found.source, 'manifest'); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('nsis-smoke: findUninstaller matches the electron-builder naming', () => { const root = tempRoot(); try { const dir = path.join(root, 'aps-nsis-smoke-u'); fs.mkdirSync(dir, { recursive: true }); assert.equal(findUninstaller(dir, '工业智核 APS'), null); fs.writeFileSync(path.join(dir, 'Uninstall 工业智核 APS.exe'), 'x'); assert.equal(path.basename(findUninstaller(dir, '工业智核 APS')), 'Uninstall 工业智核 APS.exe'); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('nsis-smoke: listContents walks nested files and sorts by size', () => { const root = tempRoot(); try { const dir = path.join(root, 'aps-nsis-smoke-contents'); fs.mkdirSync(path.join(dir, 'resources', 'sidecar'), { recursive: true }); fs.writeFileSync(path.join(dir, 'small.bin'), Buffer.alloc(10, 1)); fs.writeFileSync(path.join(dir, 'resources', 'big.bin'), Buffer.alloc(120, 2)); fs.writeFileSync(path.join(dir, 'resources', 'sidecar', 'mid.bin'), Buffer.alloc(60, 3)); const result = listContents(dir, 2); assert.equal(result.fileCount, 3); assert.equal(result.totalBytes, 190); assert.equal(result.top.length, 2); assert.equal(result.top[0].bytes, 120); assert.equal(result.top[1].bytes, 60); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('nsis-smoke: sha256File is deterministic and matches hex format', async () => { const root = tempRoot(); try { const file = path.join(root, 'sample.bin'); fs.writeFileSync(file, Buffer.alloc(4096, 7)); const first = await sha256File(file); const second = await sha256File(file); assert.equal(first, second); assert.match(first, /^[0-9a-f]{64}$/); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('nsis-smoke: auto-discovery finds the real installer on this machine', (t) => { const found = findInstaller({ root: repoRoot }); if (!found.exists) { t.skip('NSIS installer not built on this machine'); return; } assert.ok(found.bytes >= 50 * 1024 * 1024); assert.ok(found.file.endsWith('.exe')); });