import { createRequire } from 'module' import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs' import { tmpdir } from 'os' import { dirname, join, resolve } from 'path' import { afterAll, describe, expect, it } from 'vitest' const nodeRequire = createRequire(import.meta.url) const asar = nodeRequire('@electron/asar') as { createPackage: (source: string, destination: string) => Promise } const { hasWindowsSherpaRuntime } = nodeRequire('../../scripts/prepare-win-runtime.cjs') as { hasWindowsSherpaRuntime: (runtimeRoot: string) => boolean } const { validateAsarRuntimeDependencies, validateFfmpegRuntime, validateKoffiRuntime, validateReaderSkillRuntime, validateSherpaRuntime, validateSilkWasmRuntime, validateSystemOcrRuntime, findMacosHelperPaths, signMacosHelpers, signMacosAppBundle } = nodeRequire('../../scripts/after-pack.cjs') as { validateAsarRuntimeDependencies: (runtimeResources: string) => void validateFfmpegRuntime: (runtimeResources: string, platform?: NodeJS.Platform) => void validateKoffiRuntime: (runtimeResources: string, platform: NodeJS.Platform, arch: string) => void validateReaderSkillRuntime: (runtimeResources: string) => string validateSherpaRuntime: (runtimeResources: string, platform: NodeJS.Platform, arch: string) => void validateSilkWasmRuntime: (runtimeResources: string) => void validateSystemOcrRuntime: ( runtimeResources: string, platform: NodeJS.Platform, arch: string ) => void findMacosHelperPaths: (runtimeResources: string) => string[] signMacosHelpers: (runtimeResources: string, run?: CodesignRunner) => string[] signMacosAppBundle: (appBundlePath: string, run?: CodesignRunner) => string } type CodesignRunner = (args: string[]) => void function createCodesignStub(options: { verifyFails?: (args: string[]) => boolean } = {}): { calls: string[][] run: CodesignRunner } { const calls: string[][] = [] const run: CodesignRunner = (args) => { calls.push(args) if (options.verifyFails && args[0] === '--verify' && options.verifyFails(args)) { throw new Error('code object is not signed at all') } } return { calls, run } } const root = mkdtempSync(join(tmpdir(), 'wxe-runtime-package-')) const { pruneIntelMacKeyTool, pruneForeignArchConnectors, pruneForeignArchNativeRuntimes, validateRuntimeBinaryArchitecture, findSendRuntime, enforceSendRuntimeBoundary, default: afterPack } = nodeRequire('../../scripts/after-pack.cjs') as { pruneIntelMacKeyTool: ( runtimeResources: string, platform: NodeJS.Platform, arch: string ) => string | null pruneForeignArchConnectors: ( runtimeResources: string, platform: NodeJS.Platform, arch: string ) => string[] pruneForeignArchNativeRuntimes: ( runtimeResources: string, platform: NodeJS.Platform, arch: string ) => string[] validateRuntimeBinaryArchitecture: ( filePath: string, platform: NodeJS.Platform, arch: string, label: string ) => void findSendRuntime: (runtimeResources: string) => string | null enforceSendRuntimeBoundary: ( runtimeResources: string, platform: NodeJS.Platform, bundlesSendRuntime?: boolean ) => string | null default: (context: AfterPackContext) => Promise } type AfterPackContext = { appOutDir: string electronPlatformName: NodeJS.Platform arch?: number packager: { appInfo: { productFilename: string } } } const { readBinaryArchitectures } = nodeRequire('../../scripts/binary-arch.cjs') as { readBinaryArchitectures: (filePath: string) => string[] } const CPU_TYPE_X86_64 = 0x01000007 const CPU_TYPE_ARM64 = 0x0100000c function writeThinMachO(filePath: string, cpuType: number): void { const buffer = Buffer.alloc(32) buffer.writeUInt32LE(0xfeedfacf, 0) buffer.writeUInt32LE(cpuType, 4) writeFileSync(filePath, buffer) } describe('production runtime packaging', () => { afterAll(() => rmSync(root, { recursive: true, force: true })) it('requires the complete unpacked silk-wasm runtime', () => { const packagePath = join(root, 'resources', 'app.asar.unpacked', 'node_modules', 'silk-wasm') mkdirSync(join(packagePath, 'lib'), { recursive: true }) writeFileSync(join(packagePath, 'package.json'), '{}') writeFileSync(join(packagePath, 'lib', 'index.cjs'), 'module.exports = {}') expect(() => validateSilkWasmRuntime(join(root, 'resources'))).toThrow(/silk\.wasm/) writeFileSync(join(packagePath, 'lib', 'silk.wasm'), Buffer.from([0, 97, 115, 109])) expect(() => validateSilkWasmRuntime(join(root, 'resources'))).not.toThrow() }) it('requires the bundled Reader Skill declared by extraResources', () => { const resources = join(root, 'reader-skill-resources') const skillPath = join(resources, 'skill', 'tracememo-reader', 'SKILL.md') const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8') expect(config).toContain('docs/skill/tracememo-reader') expect(config).toContain('to: skill/tracememo-reader') expect(() => validateReaderSkillRuntime(resources)).toThrow( /Missing bundled TraceMemo Reader Skill/ ) mkdirSync(dirname(skillPath), { recursive: true }) writeFileSync(skillPath, '# TraceMemo Reader\n') expect(validateReaderSkillRuntime(resources)).toBe(skillPath) }) it('keeps silk-wasm in electron-builder asarUnpack', () => { const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8') expect(config).toContain('node_modules/silk-wasm/**') }) it('uses a Windows x64-only resource set', () => { const config = readFileSync(resolve(__dirname, '../../electron-builder.win.yml'), 'utf8') expect(config).toContain('key/win32/x64/**') expect(config).toContain('wcdb/win32/x64/**') expect(config).toContain('electronLanguages:') expect(config).not.toContain('win32-arm64') expect(config).not.toContain('macos/') }) it('installs cross-platform optional dependencies before Windows packaging', () => { const packageJson = JSON.parse( readFileSync(resolve(__dirname, '../../package.json'), 'utf8') ) as { scripts: Record } expect(packageJson.scripts['prepare:win-runtime']).toBe( 'node scripts/prepare-win-runtime.cjs && npm run prepare:ffmpeg:win' ) expect(packageJson.scripts['build:win']).toContain('npm run prepare:win-runtime') expect(packageJson.scripts['release:win']).toContain('npm run prepare:win-runtime') }) it('detects an incomplete Windows sherpa runtime before invoking pnpm', () => { const runtimeRoot = join(root, 'win-sherpa-runtime') mkdirSync(runtimeRoot, { recursive: true }) expect(hasWindowsSherpaRuntime(runtimeRoot)).toBe(false) writeFileSync(join(runtimeRoot, 'package.json'), '{}') expect(hasWindowsSherpaRuntime(runtimeRoot)).toBe(false) writeFileSync(join(runtimeRoot, 'sherpa-onnx.node'), 'fixture') expect(hasWindowsSherpaRuntime(runtimeRoot)).toBe(true) }) it('rejects an app archive with missing runtime dependencies', async () => { const resources = join(root, 'asar-resources') const source = join(root, 'asar-source') mkdirSync(source, { recursive: true }) writeFileSync(join(source, 'package.json'), '{}') mkdirSync(resources, { recursive: true }) await asar.createPackage(source, join(resources, 'app.asar')) expect(() => validateAsarRuntimeDependencies(resources)).toThrow( /Missing packaged runtime dependencies:.*@electron-toolkit\/utils/ ) }) it('accepts runtime dependencies when asar uses Windows path separators', async () => { const resources = join(root, 'asar-complete-resources') const source = join(root, 'asar-complete-source') const packages = [ '@electron-toolkit/preload', '@electron-toolkit/utils', 'archiver', 'electron-updater', 'ffmpeg-static', 'fs-extra', 'jsonrepair', 'koffi' ] for (const packageName of packages) { const packagePath = join(source, 'node_modules', ...packageName.split('/')) mkdirSync(packagePath, { recursive: true }) writeFileSync(join(packagePath, 'package.json'), '{}') } mkdirSync(resources, { recursive: true }) await asar.createPackage(source, join(resources, 'app.asar')) expect(() => validateAsarRuntimeDependencies(resources)).not.toThrow() }) it('requires and unpacks the bundled ffmpeg-static executable', () => { const resources = join(root, 'ffmpeg-resources') const ffmpegPath = join( resources, 'app.asar.unpacked', 'node_modules', 'ffmpeg-static', 'ffmpeg' ) expect(() => validateFfmpegRuntime(resources, 'darwin')).toThrow(/ffmpeg-static/) mkdirSync(dirname(ffmpegPath), { recursive: true }) writeFileSync(ffmpegPath, 'fixture') expect(() => validateFfmpegRuntime(resources, 'darwin')).not.toThrow() const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8') expect(config).toContain('node_modules/ffmpeg-static/**') }) it('requires the matching Windows and macOS sherpa native runtime', () => { const resources = join(root, 'sherpa-resources') const unpacked = join(resources, 'app.asar.unpacked', 'node_modules') const base = join(unpacked, 'sherpa-onnx-node') mkdirSync(base, { recursive: true }) writeFileSync(join(base, 'package.json'), '{}') writeFileSync(join(base, 'sherpa-onnx.js'), 'module.exports = {}') expect(() => validateSherpaRuntime(resources, 'win32', 'x64')).toThrow(/win-x64/) const windows = join(unpacked, 'sherpa-onnx-win-x64') mkdirSync(windows, { recursive: true }) writeFileSync(join(windows, 'package.json'), '{}') writeFileSync(join(windows, 'sherpa-onnx.node'), 'fixture') expect(() => validateSherpaRuntime(resources, 'win32', 'x64')).not.toThrow() expect(() => validateSherpaRuntime(resources, 'darwin', 'arm64')).toThrow(/darwin-arm64/) const mac = join(unpacked, 'sherpa-onnx-darwin-arm64') mkdirSync(mac, { recursive: true }) writeFileSync(join(mac, 'package.json'), '{}') writeFileSync(join(mac, 'sherpa-onnx.node'), 'fixture') expect(() => validateSherpaRuntime(resources, 'darwin', 'arm64')).not.toThrow() const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8') expect(config).toContain('node_modules/sherpa-onnx-node/**') expect(config).toContain('node_modules/sherpa-onnx-*/**') }) it('requires the matching System OCR native runtime', () => { const resources = join(root, 'system-ocr-resources') const modules = join(resources, 'app.asar.unpacked', 'node_modules', '@napi-rs') const base = join(modules, 'system-ocr') expect(() => validateSystemOcrRuntime(resources, 'darwin', 'arm64')).toThrow( /Missing unpacked System OCR runtime:.*system-ocr/ ) mkdirSync(base, { recursive: true }) writeFileSync(join(base, 'package.json'), '{}') writeFileSync(join(base, 'index.js'), 'module.exports = {}') const mac = join(modules, 'system-ocr-darwin-arm64') mkdirSync(mac, { recursive: true }) writeFileSync(join(mac, 'package.json'), '{}') writeFileSync(join(mac, 'system-ocr.darwin-arm64.node'), 'fixture') expect(() => validateSystemOcrRuntime(resources, 'darwin', 'arm64')).not.toThrow() // Windows 的原生包名带 -msvc 后缀,查找规则必须跟着改。 expect(() => validateSystemOcrRuntime(resources, 'win32', 'x64')).toThrow(/win32-x64-msvc/) const windows = join(modules, 'system-ocr-win32-x64-msvc') mkdirSync(windows, { recursive: true }) writeFileSync(join(windows, 'package.json'), '{}') writeFileSync(join(windows, 'system-ocr.win32-x64-msvc.node'), 'fixture') expect(() => validateSystemOcrRuntime(resources, 'win32', 'x64')).not.toThrow() // Linux 不是 supported target,不应做硬校验。 expect(() => validateSystemOcrRuntime(resources, 'linux', 'x64')).not.toThrow() }) it('requires the koffi native module for the packaged platform', () => { const resources = join(root, 'koffi-resources') const modules = join(resources, 'app.asar.unpacked', 'node_modules', '@koromix') expect(() => validateKoffiRuntime(resources, 'darwin', 'x64')).toThrow( /Missing macOS Koffi native module:.*koffi-darwin-x64/ ) expect(() => validateKoffiRuntime(resources, 'darwin', 'arm64')).toThrow( /koffi-darwin-arm64[/\\]darwin_arm64[/\\]koffi\.node/ ) expect(() => validateKoffiRuntime(resources, 'win32', 'x64')).toThrow( /Missing Windows Koffi native module:.*koffi-win32-x64/ ) // koffi 运行期按 `${platform}-${arch}` 拼目录名,darwin 用 darwin_, // win32 用 win32_x64(见 node_modules/koffi/src/koffi/index.cjs)。 for (const segments of [ ['koffi-darwin-x64', 'darwin_x64'], ['koffi-darwin-arm64', 'darwin_arm64'], ['koffi-win32-x64', 'win32_x64'] ]) { const nativeDirectory = join(modules, ...segments) mkdirSync(nativeDirectory, { recursive: true }) writeFileSync(join(nativeDirectory, 'koffi.node'), 'fixture') } expect(() => validateKoffiRuntime(resources, 'darwin', 'x64')).not.toThrow() expect(() => validateKoffiRuntime(resources, 'darwin', 'arm64')).not.toThrow() expect(() => validateKoffiRuntime(resources, 'win32', 'x64')).not.toThrow() // 没有对应原生包的组合应静默跳过,而不是误报。 expect(() => validateKoffiRuntime(resources, 'linux', 'x64')).not.toThrow() expect(() => validateKoffiRuntime(resources, 'win32', 'arm64')).not.toThrow() }) it('declares the cross-arch native runtimes pnpm 7 would otherwise skip', () => { const packageJson = JSON.parse( readFileSync(resolve(__dirname, '../../package.json'), 'utf8') ) as { dependencies: Record } // pnpm 7.33.7 不支持 supportedArchitectures,非宿主平台的可选依赖会被静默跳过, // 而这些原生包必须在 dependencies 里显式声明,否则打包阶段才在 afterPack 报缺。 for (const name of [ 'sherpa-onnx-darwin-x64', 'sherpa-onnx-win-x64', '@napi-rs/system-ocr-darwin-x64', '@napi-rs/system-ocr-win32-x64-msvc', '@koromix/koffi-darwin-x64', '@koromix/koffi-win32-x64' ]) { expect(packageJson.dependencies).toHaveProperty(name) } }) it('finds only the macOS helpers that exist in packaged resources', () => { const resources = join(root, 'helper-detect-resources', 'resources') mkdirSync(resources, { recursive: true }) expect(findMacosHelperPaths(join(root, 'helper-detect-resources'))).toEqual([]) const helperPath = join(resources, 'xkey_helper') writeFileSync(helperPath, 'fixture') const versionedPath = join(resources, 'xkey_helper_4_1_13') writeFileSync(versionedPath, 'fixture') expect(findMacosHelperPaths(join(root, 'helper-detect-resources'))).toEqual([ helperPath, versionedPath ]) }) it('ad-hoc signs packaged helpers whose signature is missing or modified', () => { const resources = join(root, 'helper-sign-resources', 'resources') mkdirSync(resources, { recursive: true }) const helperPath = join(resources, 'xkey_helper') writeFileSync(helperPath, 'fixture') const stub = createCodesignStub({ verifyFails: (args) => !args.includes('--arch') }) expect(signMacosHelpers(join(root, 'helper-sign-resources'), stub.run)).toEqual([helperPath]) expect(stub.calls).toContainEqual(['--force', '--sign', '-', helperPath]) expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'arm64', helperPath]) expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'x86_64', helperPath]) if (process.platform !== 'win32') { expect(statSync(helperPath).mode & 0o777).toBe(0o755) } }) it('keeps helpers that already verify strictly without re-signing them', () => { const resources = join(root, 'helper-valid-resources', 'resources') mkdirSync(resources, { recursive: true }) const helperPath = join(resources, 'xkey_helper') writeFileSync(helperPath, 'fixture') const stub = createCodesignStub() expect(signMacosHelpers(join(root, 'helper-valid-resources'), stub.run)).toEqual([helperPath]) expect(stub.calls.filter((args) => args[0] === '--force')).toEqual([]) expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'arm64', helperPath]) expect(stub.calls).toContainEqual(['--verify', '--strict', '--arch', 'x86_64', helperPath]) }) it('fails packaging when a helper signature cannot be repaired', () => { const resources = join(root, 'helper-broken-resources', 'resources') mkdirSync(resources, { recursive: true }) writeFileSync(join(resources, 'xkey_helper'), 'fixture') const stub = createCodesignStub({ verifyFails: () => true }) expect(() => signMacosHelpers(join(root, 'helper-broken-resources'), stub.run)).toThrow( /xkey_helper \(arm64\)/ ) }) it('ad-hoc signs an invalid app bundle and verifies it strictly', () => { const appBundle = join(root, 'TraceMemo.app') const calls: string[][] = [] let verifyCount = 0 const run: CodesignRunner = (args) => { calls.push(args) if (args[0] === '--verify') { verifyCount += 1 if (verifyCount === 1) throw new Error('bundle is not signed') } } expect(signMacosAppBundle(appBundle, run)).toBe(appBundle) expect(calls).toEqual([ ['--verify', '--strict', appBundle], ['--force', '--sign', '-', appBundle], ['--verify', '--strict', appBundle] ]) }) it('keeps an app bundle that already verifies strictly', () => { const appBundle = join(root, 'Valid.app') const stub = createCodesignStub() expect(signMacosAppBundle(appBundle, stub.run)).toBe(appBundle) expect(stub.calls).toEqual([['--verify', '--strict', appBundle]]) }) it('fails packaging when the app bundle cannot be made strictly valid', () => { const appBundle = join(root, 'Broken.app') const stub = createCodesignStub({ verifyFails: () => true }) expect(() => signMacosAppBundle(appBundle, stub.run)).toThrow( /app bundle signature verification failed/ ) }) }) describe('per-architecture macOS packaging', () => { const archRoot = mkdtempSync(join(tmpdir(), 'wxe-runtime-arch-')) afterAll(() => rmSync(archRoot, { recursive: true, force: true })) it('reads architectures out of Mach-O and PE binaries', () => { const x64 = join(archRoot, 'fixture-x64') const arm64 = join(archRoot, 'fixture-arm64') writeThinMachO(x64, CPU_TYPE_X86_64) writeThinMachO(arm64, CPU_TYPE_ARM64) expect(readBinaryArchitectures(x64)).toEqual(['x64']) expect(readBinaryArchitectures(arm64)).toEqual(['arm64']) expect(readBinaryArchitectures(resolve(__dirname, 'runtime-packaging.test.ts'))).toEqual([]) }) it('keeps the Intel Mac key helper only in x64 macOS bundles', () => { const resources = join(archRoot, 'key-tool', 'Contents', 'Resources') const keyToolDirectory = join(resources, 'resources', 'macos-key-tool') const helper = join(keyToolDirectory, 'intel_mac_key_helper') mkdirSync(keyToolDirectory, { recursive: true }) writeFileSync(helper, 'fixture') expect(pruneIntelMacKeyTool(resources, 'darwin', 'x64')).toBe(helper) expect(existsSync(helper)).toBe(true) expect(pruneIntelMacKeyTool(resources, 'darwin', 'arm64')).toBeNull() expect(existsSync(keyToolDirectory)).toBe(false) }) it('fails an x64 bundle that lost the Intel Mac key helper', () => { const resources = join(archRoot, 'key-tool-missing', 'Contents', 'Resources') mkdirSync(resources, { recursive: true }) expect(() => pruneIntelMacKeyTool(resources, 'darwin', 'x64')).toThrow(/macos-key-tool/) expect(pruneIntelMacKeyTool(resources, 'win32', 'x64')).toBeNull() }) it('drops bundled connectors built for another architecture', () => { const resources = join(archRoot, 'connectors', 'Contents', 'Resources') const foreign = join(resources, 'resources', 'connectors', 'wechat', 'darwin-arm64') const matching = join(resources, 'resources', 'connectors', 'wechat', 'darwin-x64') mkdirSync(foreign, { recursive: true }) mkdirSync(matching, { recursive: true }) expect(pruneForeignArchConnectors(resources, 'darwin', 'x64')).toEqual(['wechat/darwin-arm64']) expect(existsSync(foreign)).toBe(false) expect(existsSync(matching)).toBe(true) expect(pruneForeignArchConnectors(resources, 'darwin', 'universal')).toEqual([]) expect(existsSync(matching)).toBe(true) }) it('drops native runtime packages built for other platforms', () => { const resources = join(archRoot, 'native-runtimes', 'Contents', 'Resources') const modules = join(resources, 'app.asar.unpacked', 'node_modules') for (const name of [ 'sherpa-onnx-darwin-x64', 'sherpa-onnx-darwin-arm64', 'sherpa-onnx-linux-x64', 'sherpa-onnx-node' ]) { mkdirSync(join(modules, name), { recursive: true }) } for (const name of ['koffi-darwin-x64', 'koffi-win32-x64']) { mkdirSync(join(modules, '@koromix', name), { recursive: true }) } expect(pruneForeignArchNativeRuntimes(resources, 'darwin', 'x64').sort()).toEqual([ '@koromix/koffi-win32-x64', 'sherpa-onnx-darwin-arm64', 'sherpa-onnx-linux-x64' ]) expect(existsSync(join(modules, 'sherpa-onnx-darwin-x64'))).toBe(true) expect(existsSync(join(modules, 'sherpa-onnx-node'))).toBe(true) expect(existsSync(join(modules, '@koromix', 'koffi-darwin-x64'))).toBe(true) expect(existsSync(join(modules, '@koromix', 'koffi-win32-x64'))).toBe(false) expect(pruneForeignArchNativeRuntimes(resources, 'darwin', 'universal')).toEqual([]) expect(existsSync(join(modules, 'sherpa-onnx-darwin-x64'))).toBe(true) }) it('rejects a bundled binary built for another architecture', () => { const binary = join(archRoot, 'bundled-arm64') writeThinMachO(binary, CPU_TYPE_ARM64) expect(() => validateRuntimeBinaryArchitecture(binary, 'darwin', 'x64', 'Bundled ffmpeg') ).toThrow(/arm64 but this bundle targets x64/) expect(() => validateRuntimeBinaryArchitecture(binary, 'darwin', 'arm64', 'Bundled ffmpeg') ).not.toThrow() }) }) describe('send runtime packaging boundary', () => { const boundaryRoot = mkdtempSync(join(tmpdir(), 'wxe-send-runtime-')) afterAll(() => rmSync(boundaryRoot, { recursive: true, force: true })) function resourcesWithSendRuntime(name: string, unpacked = false): string { const resources = join(boundaryRoot, name, 'Contents', 'Resources') const runtime = unpacked ? join(resources, 'app.asar.unpacked', 'resources', 'runtime', 'darwin-arm64') : join(resources, 'resources', 'runtime', 'darwin-arm64') mkdirSync(runtime, { recursive: true }) writeFileSync(join(runtime, 'tm-wechat-host'), 'fixture') return resources } it('accepts a macOS bundle without the send runtime', () => { const resources = join(boundaryRoot, 'clean', 'Contents', 'Resources') mkdirSync(resources, { recursive: true }) expect(findSendRuntime(resources)).toBeNull() expect(enforceSendRuntimeBoundary(resources, 'darwin', false)).toBeNull() }) it('fails a bundle that still carries the send runtime', () => { expect(() => enforceSendRuntimeBoundary(resourcesWithSendRuntime('dirty'), 'darwin', false) ).toThrow(/must not include the WeChat send runtime/) }) it('detects the send runtime when it lands under app.asar.unpacked', () => { const resources = resourcesWithSendRuntime('unpacked', true) expect(findSendRuntime(resources)).toBe( join(resources, 'app.asar.unpacked', 'resources', 'runtime', 'darwin-arm64') ) expect(() => enforceSendRuntimeBoundary(resources, 'darwin', false)).toThrow( /must not include the WeChat send runtime/ ) }) it('requires the send runtime when the build is configured to bundle it', () => { const missing = join(boundaryRoot, 'runtime-missing', 'Contents', 'Resources') mkdirSync(missing, { recursive: true }) expect(() => enforceSendRuntimeBoundary(missing, 'darwin', true)).toThrow( /requires the WeChat send runtime/ ) const present = resourcesWithSendRuntime('runtime-ok') expect(enforceSendRuntimeBoundary(present, 'darwin', true)).toBe( join(present, 'resources', 'runtime', 'darwin-arm64') ) }) it('ignores non-macOS bundles', () => { const resources = resourcesWithSendRuntime('win32') expect(enforceSendRuntimeBoundary(resources, 'win32', false)).toBeNull() expect(enforceSendRuntimeBoundary(resources, 'win32', true)).toBeNull() }) it('keeps the send runtime build off the release publishing path', () => { const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf8')) as { scripts: Record } expect(Object.keys(pkg.scripts).filter((name) => name.includes('send-runtime'))).toEqual([ 'build:mac:arm64:send-runtime' ]) expect(pkg.scripts['build:mac:arm64:send-runtime']).toContain('TM_SEND_RUNTIME_BUILD=1') expect(pkg.scripts['build:mac:arm64:send-runtime']).toContain( 'electron-builder.send-runtime.yml' ) expect(pkg.scripts['build:mac:arm64:send-runtime']).not.toContain('--publish') for (const [name, command] of Object.entries(pkg.scripts)) { if (!name.startsWith('release')) continue expect(command).not.toContain('electron-builder.send-runtime.yml') expect(command).not.toContain('TM_SEND_RUNTIME_BUILD') } }) it('excludes the send runtime in the default builder config', () => { const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8') expect(config).toContain("'!runtime/darwin-arm64/**'") }) // 只测函数是测不出「边界校验有没有被接上去」的。这个 bundle 故意不含 app.asar 与 // Reader Skill:一旦 afterPack 把边界校验排到其它校验之后,抛出的就会是 // "Missing packaged application archive" 而不是边界错误,用例即失败。 it('applies the boundary check inside afterPack ahead of every other validation', async () => { const appOutDir = join(boundaryRoot, 'wired') const runtime = join( appOutDir, 'TraceMemo.app', 'Contents', 'Resources', 'resources', 'runtime', 'darwin-arm64' ) mkdirSync(runtime, { recursive: true }) writeFileSync(join(runtime, 'tm-wechat-host'), 'fixture') await expect( afterPack({ appOutDir, electronPlatformName: 'darwin', arch: 3, packager: { appInfo: { productFilename: 'TraceMemo' } } }) ).rejects.toThrow(/must not include the WeChat send runtime/) }) }) describe('release publishing policy', () => { it('uploads GitHub releases as drafts until the notes are reviewed', () => { for (const configFile of ['electron-builder.yml', 'electron-builder.win.yml']) { const config = readFileSync(resolve(__dirname, `../../${configFile}`), 'utf8') expect(config).toContain('releaseType: draft') expect(config).not.toContain('releaseType: release') } }) })