import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { createHash } from 'node:crypto' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Writable } from 'node:stream' import { afterAll, beforeAll, describe, expect, it } from 'vitest' interface ReleaseOptions { platform: 'windows' | 'macos' | 'linux' arch: 'x64' | 'arm64' formats: string[] skipBuild: boolean dryRun: boolean unsigned: boolean help: boolean } interface ReleaseBuilderModule { assertReplaceableOutput: ( directory: string, options: ReleaseOptions ) => void buildElectronBuilderArguments: ( options: ReleaseOptions, outputDirectory: string ) => string[] electronBuilderEnvironment: ( options: ReleaseOptions, environment?: NodeJS.ProcessEnv ) => NodeJS.ProcessEnv createPortableZip: ( unpackedDirectory: string, zipPath: string, dependencies?: { openOutput: (filePath: string) => Writable } ) => Promise detectBinaryArchitecture: ( buffer: Buffer ) => 'x64' | 'arm64' | undefined parseArguments: ( arguments_: string[], environment?: { platform: string; arch: string } ) => ReleaseOptions parsePackedPackageMetadata: ( output: string, expected: { name: string version: string integrity: string } ) => { name: string version: string integrity: string filename: string } lockedTargetRuntimePackage: ( packageName: string, packageJson?: { dependencies?: Record optionalDependencies?: Record }, packageLock?: { packages?: Record< string, { version?: string resolved?: string integrity?: string optionalDependencies?: Record } > } ) => { name: string version: string integrity: string } replaceOutput: ( stagingDirectory: string, destination: string, options: ReleaseOptions ) => void targetRuntimePackageNames: ( options: ReleaseOptions ) => string[] targetHarnessPaths: ( options: ReleaseOptions ) => { nodePtyBinary: string nodePtyDirectory: string } stageTargetRuntimeDependencies: ( options: ReleaseOptions, dependencies?: { root: string packageJson: { dependencies?: Record optionalDependencies?: Record } packageLock: { packages?: Record< string, { version?: string resolved?: string integrity?: string optionalDependencies?: Record } > } npmInvocation: () => { command: string prefixArgs: string[] } runCapture: ( command: string, arguments_: string[] ) => Promise extractArchive: ( archivePath: string, destination: string ) => Promise } ) => Promise<() => void> verifyHarnessPackage: ( resources: string, options: ReleaseOptions, dependencies?: { listPackage: (asarPath: string) => string[] statFile: ( asarPath: string, filePath: string ) => { files?: Record link?: string size?: number } extractFile: ( asarPath: string, filePath: string ) => Buffer } ) => void verifyArtifacts: ( directory: string, options: ReleaseOptions ) => void verifyPortableZip: (filePath: string) => void verifyArchiveIntegrity: ( filePath: string, expectedIntegrity: string ) => void writeManifest: ( directory: string, options: ReleaseOptions ) => Promise<{ files: Array<{ name: string size: number sha256: string }> }> } const require = createRequire(import.meta.url) const packageVersion = ( require('../package.json') as { version: string } ).version const releaseBuilder = require( '../build/build-release.cjs' ) as ReleaseBuilderModule const releaseConfig = require( '../build/electron-builder.release.cjs' ) as { extraResources: Array<{ from: string to?: string filter?: string[] }> } const prepareBundledRuntimes = require( '../build/runtime-hooks.cjs' ) as (context: { electronPlatformName: string arch: number packager: { projectDir: string } }) => Promise const windowsOptions: ReleaseOptions = { platform: 'windows', arch: 'x64', formats: ['nsis', 'portable'], skipBuild: false, dryRun: false, unsigned: false, help: false } const electronProcess = process as NodeJS.Process & { noAsar?: boolean } const originalNoAsar = electronProcess.noAsar beforeAll(() => { electronProcess.noAsar = true }) afterAll(() => { electronProcess.noAsar = originalNoAsar }) function pe(machine: number): Buffer { const buffer = Buffer.alloc(256) buffer.write('MZ', 0, 'ascii') buffer.writeUInt32LE(128, 0x3c) buffer.write('PE\0\0', 128, 'ascii') buffer.writeUInt16LE(machine, 132) return buffer } function elf(machine: number, bigEndian = false): Buffer { const buffer = Buffer.alloc(64) buffer.set([0x7f, 0x45, 0x4c, 0x46]) buffer[5] = bigEndian ? 2 : 1 if (bigEndian) { buffer.writeUInt16BE(machine, 18) } else { buffer.writeUInt16LE(machine, 18) } return buffer } function machO(cpuType: number): Buffer { const buffer = Buffer.alloc(8) buffer.writeUInt32LE(0xfeedfacf, 0) buffer.writeUInt32LE(cpuType, 4) return buffer } function portableDirectory(parent: string): string { const directory = join(parent, 'portable') const plugin = join(directory, 'resources', 'runtimes', 'opencode-config', 'node_modules', '@opencode-ai', 'plugin') mkdirSync(join(plugin, 'dist'), { recursive: true }) writeFileSync(join(plugin, 'package.json'), '{"name":"@opencode-ai/plugin","version":"1.18.29"}') writeFileSync(join(plugin, 'dist', 'index.js'), 'export const tool = true') mkdirSync( join(directory, 'resources', 'runtimes', 'opencode'), { recursive: true } ) mkdirSync(join(directory, 'resources', 'licenses'), { recursive: true }) mkdirSync( join(directory, 'resources', 'runtimes', 'continue'), { recursive: true } ) mkdirSync( join(directory, 'resources', 'tool-environment'), { recursive: true } ) mkdirSync( join( directory, 'resources', 'runtimes', 'npm', 'node_modules', 'graceful-fs' ), { recursive: true } ) mkdirSync( join( directory, 'resources', 'runtimes', 'npm', 'bin' ), { recursive: true } ) for (const [path, content] of [ ['GoodBuddy.exe', 'MZ'], ['resources/app.asar', 'asar'], ['resources/release-notes.json', '{}'], ['resources/icon.ico', 'icon'], ['resources/tray-icon.png', 'tray'], [ 'resources/tool-environment/managed-python-artifacts.json', '{"artifacts":[]}' ], ['resources/runtimes/opencode/opencode.exe', 'MZ'], ['resources/runtimes/opencode/rg.exe', 'MZ'], ['resources/licenses/vscode-ripgrep-MIT.txt', 'MIT'], ['resources/runtimes/continue/package.json', '{}'], [ 'resources/runtimes/npm/bin/npm-cli.js', 'require("../lib/cli.js")' ], [ 'resources/runtimes/npm/bin/npx-cli.js', 'require("../lib/cli.js")' ], [ 'resources/runtimes/npm/package.json', '{"version":"11.19.0"}' ], [ 'resources/runtimes/npm/node_modules/graceful-fs/package.json', '{"name":"graceful-fs"}' ] ] satisfies Array<[string, string]>) { writeFileSync(join(directory, ...path.split('/')), content) } return directory } function endOfCentralDirectory( entryCount: number, centralSize: number, centralOffset = 0 ): Buffer { const buffer = Buffer.alloc(22) buffer.writeUInt32LE(0x06054b50, 0) buffer.writeUInt16LE(entryCount, 8) buffer.writeUInt16LE(entryCount, 10) buffer.writeUInt32LE(centralSize, 12) buffer.writeUInt32LE(centralOffset, 16) return buffer } describe('release build arguments', () => { it.each([ ['win32', 'x64', 'windows', ['nsis', 'portable']], ['darwin', 'arm64', 'macos', ['dmg']], ['linux', 'x64', 'linux', ['AppImage', 'deb', 'rpm']] ])( 'uses %s host defaults', (host, arch, platform, formats) => { expect( releaseBuilder.parseArguments([], { platform: host, arch }) ).toMatchObject({ platform, arch, formats }) } ) it('normalizes aliases, formats, and duplicate formats', () => { expect( releaseBuilder.parseArguments( [ '--platform', 'mac', '--arch', 'arm64', '--format', 'DMG,zip,dmg', '--skip-build', '--dry-run', '--unsigned' ], { platform: 'win32', arch: 'x64' } ) ).toEqual({ platform: 'macos', arch: 'arm64', formats: ['dmg', 'zip'], skipBuild: true, dryRun: true, unsigned: true, help: false }) }) it.each([ [['--platform', 'freebsd'], '不支持当前系统'], [['--arch', 'ia32'], '不支持的架构'], [['--format', 'zip'], '不支持打包格式'], [['--platform', 'linux', '--unsigned'], '仅支持 macOS'], [['--unknown'], '未知参数'] ])('rejects invalid arguments', (arguments_, message) => { expect(() => releaseBuilder.parseArguments(arguments_, { platform: 'linux', arch: 'x64' }) ).toThrow(message) }) it('builds target-specific electron-builder arguments', () => { const arguments_ = releaseBuilder.buildElectronBuilderArguments( { platform: 'windows', arch: 'arm64', formats: ['nsis', 'portable'], skipBuild: false, dryRun: false, unsigned: false, help: false }, 'C:\\release-stage' ) expect(arguments_).toEqual( expect.arrayContaining([ '--config', expect.stringMatching( /build[\\/]electron-builder\.release\.cjs$/u ), '--win', 'nsis', 'dir', '--arm64', '--config.directories.output=C:\\release-stage', '--publish', 'never', expect.stringContaining('nsis.artifactName=') ]) ) expect(arguments_).not.toContain('portable') expect( arguments_.some((argument) => argument.includes('portable.artifactName=') ) ).toBe(false) }) it('explicitly disables notarization for unsigned macOS packages', () => { const arguments_ = releaseBuilder.buildElectronBuilderArguments( { ...windowsOptions, platform: 'macos', formats: ['dmg', 'zip'], unsigned: true }, '/release-stage' ) expect(arguments_).toContain('--config.mac.notarize=false') }) it('removes signing credentials from unsigned macOS builds', () => { const environment = releaseBuilder.electronBuilderEnvironment( { ...windowsOptions, platform: 'macos', formats: ['dmg', 'zip'], unsigned: true }, { CSC_LINK: 'certificate', CSC_KEY_PASSWORD: 'password', APPLE_API_KEY: 'api-key', APPLE_API_KEY_ID: 'key-id', APPLE_API_ISSUER: 'issuer', KEEP_ME: 'present' } ) expect(environment).toMatchObject({ CSC_IDENTITY_AUTO_DISCOVERY: 'false', KEEP_ME: 'present' }) expect(environment).not.toHaveProperty('CSC_LINK') expect(environment).not.toHaveProperty('CSC_KEY_PASSWORD') expect(environment).not.toHaveProperty('APPLE_API_KEY') expect(environment).not.toHaveProperty('APPLE_API_KEY_ID') expect(environment).not.toHaveProperty('APPLE_API_ISSUER') }) it('pins and unpacks only the required Harness native packages', () => { const packageJson = require('../package.json') as { build: { asarUnpack: string[] npmRebuild: boolean extraResources: Array<{ from: string to: string }> } optionalDependencies: Record } expect(packageJson.build.npmRebuild).toBe(false) expect(packageJson.build.asarUnpack).toEqual( expect.arrayContaining([ 'out/main/package.json', 'out/main/deepseek-harness-*', 'out/main/chunks/**/*', 'node_modules/node-pty/lib/**/*', 'node_modules/node-pty/package.json', 'node_modules/node-pty/prebuilds/**/*', 'node_modules/node-pty/build/Release/**/*', 'node_modules/koffi/**/*', 'node_modules/@koromix/koffi-*/**/*', 'node_modules/@napi-rs/canvas{,/**/*}', 'node_modules/@napi-rs/canvas-*/**/*' ]) ) expect(packageJson.build.asarUnpack).not.toContain( 'node_modules/npm/**/*' ) expect(packageJson.build.extraResources).toContainEqual({ from: 'resources/licenses/onnxruntime-MIT.txt', to: 'licenses/onnxruntime-web-MIT.txt' }) expect(packageJson.optionalDependencies).toEqual({ '@koromix/koffi-darwin-arm64': '3.1.4', '@koromix/koffi-darwin-x64': '3.1.4', '@koromix/koffi-linux-arm64': '3.1.4', '@koromix/koffi-linux-x64': '3.1.4', '@koromix/koffi-win32-arm64': '3.1.4', '@koromix/koffi-win32-x64': '3.1.4' }) }) it('identifies the native packages required by each release target', () => { expect( releaseBuilder.targetRuntimePackageNames({ ...windowsOptions, arch: 'arm64' }) ).toEqual([ '@koromix/koffi-win32-arm64', '@napi-rs/canvas-win32-arm64-msvc' ]) expect( releaseBuilder.targetRuntimePackageNames({ ...windowsOptions, platform: 'linux', formats: ['AppImage', 'deb', 'rpm'] }) ).toEqual([ '@koromix/koffi-linux-x64', '@napi-rs/canvas-linux-x64-gnu' ]) expect( releaseBuilder.targetRuntimePackageNames({ ...windowsOptions, platform: 'macos', arch: 'arm64', formats: ['dmg', 'zip'] }) ).toEqual([ '@koromix/koffi-darwin-arm64', '@napi-rs/canvas-darwin-arm64' ]) }) it('stages a missing target Canvas package from locked metadata', async () => { const runtimeRoot = mkdtempSync( join(tmpdir(), 'goodbuddy-release-canvas-stage-') ) const canvasPackage = '@napi-rs/canvas-win32-arm64-msvc' const koffiPackage = '@koromix/koffi-win32-arm64' const archiveContents = Buffer.from('locked Canvas archive') const integrity = `sha512-${createHash('sha512') .update(archiveContents) .digest('base64')}` const packageJson = { dependencies: { '@napi-rs/canvas': '1.0.3' }, optionalDependencies: { [koffiPackage]: '3.1.4' } } const packageLock = { packages: { 'node_modules/@napi-rs/canvas': { version: '1.0.3', optionalDependencies: { [canvasPackage]: '1.0.3' } }, [`node_modules/${canvasPackage}`]: { version: '1.0.3', resolved: 'https://registry.npmjs.org/locked-canvas.tgz', integrity }, [`node_modules/${koffiPackage}`]: { version: '3.1.4', resolved: 'https://registry.npmjs.org/locked-koffi.tgz', integrity: 'sha512-locked' } } } const koffiDirectory = join( runtimeRoot, 'node_modules', ...koffiPackage.split('/') ) const canvasDirectory = join( runtimeRoot, 'node_modules', ...canvasPackage.split('/') ) const invocations: string[][] = [] let cleanup: () => void = () => undefined try { mkdirSync(koffiDirectory, { recursive: true }) writeFileSync( join(koffiDirectory, 'package.json'), JSON.stringify({ name: koffiPackage, version: '3.1.4' }) ) cleanup = await releaseBuilder.stageTargetRuntimeDependencies( { ...windowsOptions, arch: 'arm64' }, { root: runtimeRoot, packageJson, packageLock, npmInvocation: () => ({ command: 'npm-test', prefixArgs: [] }), runCapture: async (_command, arguments_) => { invocations.push(arguments_) const destination = arguments_[ arguments_.indexOf('--pack-destination') + 1 ] if (!destination) { throw new Error('pack destination is missing') } writeFileSync( join(destination, 'locked-canvas.tgz'), archiveContents ) return JSON.stringify([ { name: canvasPackage, version: '1.0.3', integrity, filename: 'locked-canvas.tgz' } ]) }, extractArchive: async (_archive, destination) => { writeFileSync( join(destination, 'package.json'), JSON.stringify({ name: canvasPackage, version: '1.0.3' }) ) } } ) expect(invocations).toHaveLength(1) expect(invocations[0]).toEqual( expect.arrayContaining([ 'pack', `${canvasPackage}@1.0.3`, '--ignore-scripts', '--json' ]) ) expect(invocations[0]).not.toContain( `${canvasPackage}@latest` ) expect( JSON.parse( readFileSync( join(canvasDirectory, 'package.json'), 'utf8' ) ) ).toMatchObject({ name: canvasPackage, version: '1.0.3' }) cleanup() cleanup = () => undefined expect(existsSync(canvasDirectory)).toBe(false) expect(existsSync(koffiDirectory)).toBe(true) } finally { cleanup() rmSync(runtimeRoot, { recursive: true, force: true }) } }) it.each([ [ 'the target package version differs', { version: '1.0.4', resolved: 'https://registry.npmjs.org/canvas.tgz', integrity: 'sha512-locked' } ], [ 'target package integrity is missing', { version: '1.0.3', resolved: 'https://registry.npmjs.org/canvas.tgz' } ] ])('fails closed when %s in the lockfile', (_case, targetLock) => { const canvasPackage = '@napi-rs/canvas-win32-arm64-msvc' expect(() => releaseBuilder.lockedTargetRuntimePackage( canvasPackage, { dependencies: { '@napi-rs/canvas': '1.0.3' } }, { packages: { 'node_modules/@napi-rs/canvas': { version: '1.0.3', optionalDependencies: { [canvasPackage]: '1.0.3' } }, [`node_modules/${canvasPackage}`]: targetLock } } ) ).toThrow('目标 Runtime 依赖未完整锁定') }) it('validates packed target dependency metadata and archive integrity', () => { const directory = mkdtempSync( join(tmpdir(), 'goodbuddy-release-dependency-test-') ) const archive = join(directory, 'target.tgz') const contents = Buffer.from('locked target dependency') writeFileSync(archive, contents) const integrity = `sha512-${createHash('sha512') .update(contents) .digest('base64')}` const expected = { name: '@koromix/koffi-win32-arm64', version: '3.1.4', integrity } try { expect( releaseBuilder.parsePackedPackageMetadata( JSON.stringify([ { ...expected, filename: 'koffi-win32-arm64.tgz' } ]), expected ) ).toMatchObject({ ...expected, filename: 'koffi-win32-arm64.tgz' }) releaseBuilder.verifyArchiveIntegrity(archive, integrity) expect(() => releaseBuilder.verifyArchiveIntegrity( archive, `sha512-${Buffer.alloc(64).toString('base64')}` ) ).toThrow('完整性校验失败') expect(() => releaseBuilder.parsePackedPackageMetadata( JSON.stringify([ { ...expected, version: '3.1.5', filename: 'koffi-win32-arm64.tgz' } ]), expected ) ).toThrow('元数据不匹配') } finally { rmSync(directory, { recursive: true, force: true }) } }) it('keeps installable Agent resources out of ordinary desktop packages', () => { const packageJson = require('../package.json') as { build: { files: string[] extraResources: Array<{ from: string to?: string filter?: string[] }> } } const resourceSources = packageJson.build.extraResources.map( (entry) => entry.from ) expect(packageJson.build.files).toEqual([ 'out/main/**/*', 'out/preload/**/*', 'out/renderer/**/*', 'package.json', '!node_modules/npm{,/**/*}' ]) expect(resourceSources).toContain('resources/skills') expect(packageJson.build.extraResources).toContainEqual({ from: 'resources/tool-environment/managed-python-artifacts.json', to: 'tool-environment/managed-python-artifacts.json' }) expect(packageJson.build.extraResources).toContainEqual({ from: 'node_modules', to: 'runtimes', filter: ['npm{,/**/*}'] }) expect( existsSync(join('node_modules', 'npm', 'bin', 'npm-cli.js')) ).toBe(true) expect( existsSync(join('node_modules', 'npm', 'bin', 'npx-cli.js')) ).toBe(true) expect(resourceSources).toContain( 'resources/agent-release-keys.json' ) expect(resourceSources).toContain('agent-runtime-lock.json') expect(resourceSources).toContain('remote-runtime-lock.json') expect( resourceSources.some((source) => source.startsWith('.agent-resources/') ) ).toBe(false) expect( resourceSources.some((source) => source.startsWith('.remote-runtime-resources/') ) ).toBe(false) expect( resourceSources.some((source) => source.startsWith('tests/')) ).toBe(false) expect( existsSync(join('resources', 'skills', 'web-3d-game')) ).toBe(false) expect( existsSync(join('resources', 'web-3d-game-mcp.mjs')) ).toBe(false) expect( existsSync( join( 'tests', 'fixtures', 'web-3d-game-skill', 'SKILL.md' ) ) ).toBe(true) }) it('keeps remote Agent and Runtime payloads out of release packages', () => { const resourceEntries = releaseConfig.extraResources expect( resourceEntries.filter((entry) => entry.from.startsWith('.agent-resources/') ) ).toHaveLength(0) expect( resourceEntries.filter((entry) => entry.from.startsWith('.remote-runtime-resources/') ) ).toHaveLength(0) expect( resourceEntries.filter( (entry) => entry.from === 'agent-runtime-lock.json' ) ).toHaveLength(1) expect( resourceEntries.filter( (entry) => entry.from === 'remote-runtime-lock.json' ) ).toHaveLength(1) expect( resourceEntries.filter( (entry) => entry.from === 'resources/agent-release-keys.json' ) ).toHaveLength(1) }) it('prepares desktop Runtimes without Agent artifacts', async () => { const projectRoot = mkdtempSync( join(tmpdir(), 'goodbuddy-runtime-hook-') ) const packageName = 'opencode-windows-x64-baseline' const integrity = 'sha512-test-integrity' const executable = Buffer.from('desktop runtime') const ripgrepPackageName = '@vscode/ripgrep-win32-x64' const ripgrepIntegrity = 'sha512-ripgrep-test-integrity' const ripgrepExecutable = Buffer.from('ripgrep runtime') try { const configPlugin = join( projectRoot, '.runtime-resources', 'opencode-config', 'node_modules', '@opencode-ai', 'plugin' ) mkdirSync(configPlugin, { recursive: true }) writeFileSync(join(configPlugin, 'package.json'), JSON.stringify({ version: '1.18.29' })) const webPackages = { '@deepseek-ai/dsh': { version: '0.1.7-rc.2', dependencies: { sharp: '0.35.4', koffi: '3.1.4' } }, sharp: { version: '0.35.4', optionalDependencies: { '@img/sharp-win32-x64': '0.35.4' } }, 'sharp/node_modules/@img/sharp-win32-x64': { version: '0.35.4' }, koffi: { version: '3.1.4', optionalDependencies: { '@koromix/koffi-win32-x64': '3.1.4' } }, '@koromix/koffi-win32-x64': { version: '3.1.4' } } for (const [name, manifest] of Object.entries(webPackages)) { const directory = join(projectRoot, 'node_modules', name) mkdirSync(directory, { recursive: true }) writeFileSync(join(directory, 'package.json'), JSON.stringify({ name, ...manifest })) } mkdirSync(join(projectRoot, 'out', 'main'), { recursive: true }) mkdirSync( join(projectRoot, '.runtime-resources', 'x64'), { recursive: true } ) writeFileSync( join(projectRoot, 'package.json'), JSON.stringify({ dependencies: { '@deepseek-ai/dsh-llm': '0.1.7-rc.2', '@vscode/ripgrep': '1.18.0' } }) ) writeFileSync( join(projectRoot, 'package-lock.json'), JSON.stringify({ packages: { [`node_modules/${packageName}`]: { version: '1.18.29', integrity }, [`node_modules/${ripgrepPackageName}`]: { version: '1.18.0', integrity: ripgrepIntegrity } } }) ) writeFileSync( join( projectRoot, '.runtime-resources', 'x64', 'opencode.exe' ), executable ) writeFileSync( join( projectRoot, '.runtime-resources', 'x64', '.ready.json' ), JSON.stringify({ packageName, version: '1.18.29', integrity, executableSha256: createHash('sha256') .update(executable) .digest('hex') }) ) writeFileSync( join(projectRoot, '.runtime-resources', 'x64', 'rg.exe'), ripgrepExecutable ) writeFileSync( join(projectRoot, '.runtime-resources', 'x64', '.ripgrep-ready.json'), JSON.stringify({ packageName: ripgrepPackageName, version: '1.18.0', integrity: ripgrepIntegrity, executableSha256: createHash('sha256') .update(ripgrepExecutable) .digest('hex') }) ) await expect( prepareBundledRuntimes({ electronPlatformName: 'win32', arch: 1, packager: { projectDir: projectRoot } }) ).resolves.toBeUndefined() expect(existsSync(join(projectRoot, '.runtime-resources', 'dsh-x64', 'node_modules', '@deepseek-ai', 'dsh', 'package.json'))).toBe(true) expect( existsSync(join(projectRoot, '.agent-resources')) ).toBe(false) expect( JSON.parse( readFileSync( join(projectRoot, 'out', 'main', 'package.json'), 'utf8' ) ) ).toMatchObject({ name: '@deepseek-ai/dsh-llm', version: '0.1.7-rc.2' }) } finally { rmSync(projectRoot, { recursive: true, force: true }) } }) }) describe('release binary architecture detection', () => { it.each([ [pe(0x8664), 'x64'], [pe(0xaa64), 'arm64'], [elf(62), 'x64'], [elf(183, true), 'arm64'], [machO(0x01000007), 'x64'], [machO(0x0100000c), 'arm64'], [Buffer.from('not an executable'), undefined] ])('detects binary headers', (buffer, expected) => { expect( releaseBuilder.detectBinaryArchitecture(buffer) ).toBe(expected) }) }) describe('release Harness package verification', () => { it.each([ ['windows', 'x64', 'prebuilds/win32-x64/conpty.node'], ['windows', 'arm64', 'prebuilds/win32-arm64/conpty.node'], ['macos', 'x64', 'prebuilds/darwin-x64/pty.node'], ['macos', 'arm64', 'prebuilds/darwin-arm64/pty.node'], ['linux', 'x64', 'prebuilds/linux-x64/pty.node'], ['linux', 'arm64', 'prebuilds/linux-arm64/pty.node'] ] as const)( 'verifies the rc.8 node-pty prebuild for %s %s', (platform, arch, expected) => { expect( releaseBuilder.targetHarnessPaths({ ...windowsOptions, platform, arch }).nodePtyBinary ).toBe(expected) } ) it('fails closed when the packaged Harness Host is missing', () => { const directory = mkdtempSync( join(tmpdir(), 'goodbuddy-harness-closure-') ) const resources = join(directory, 'resources') try { mkdirSync(resources, { recursive: true }) writeFileSync(join(resources, 'app.asar'), 'asar') expect(() => releaseBuilder.verifyHarnessPackage( resources, windowsOptions, { listPackage: () => [], statFile: () => ({ size: 1 }), extractFile: () => Buffer.from('{}') } ) ).toThrow('DeepSeek Harness Host缺失') } finally { rmSync(directory, { recursive: true, force: true }) } }) }) describe('release output safety', () => { it('preserves an existing portable ZIP when exclusive creation fails', async () => { const directory = mkdtempSync( join(tmpdir(), 'goodbuddy-portable-existing-') ) try { const unpacked = portableDirectory(directory) const zipPath = join(directory, 'portable.zip') writeFileSync(zipPath, 'keep-existing-output') await expect( releaseBuilder.createPortableZip(unpacked, zipPath) ).rejects.toMatchObject({ code: 'EEXIST' }) expect(readFileSync(zipPath, 'utf8')).toBe( 'keep-existing-output' ) } finally { rmSync(directory, { recursive: true, force: true }) } }) it('handles portable ZIP output stream failures without hanging', async () => { const directory = mkdtempSync( join(tmpdir(), 'goodbuddy-portable-write-error-') ) try { const unpacked = portableDirectory(directory) const failure = new Error('simulated ZIP write failure') const output = new Writable({ write(_chunk, _encoding, callback) { callback(failure) } }) await expect( releaseBuilder.createPortableZip( unpacked, join(directory, 'unused.zip'), { openOutput: () => output } ) ).rejects.toThrow('simulated ZIP write failure') } finally { rmSync(directory, { recursive: true, force: true }) } }) it.each([ [ 'an excessive entry count', endOfCentralDirectory(50_001, 46) ], [ 'an oversized central directory', endOfCentralDirectory(7, 64 * 1024 * 1024 + 1) ] ])('rejects %s before reading ZIP central data', (_case, bytes) => { const directory = mkdtempSync( join(tmpdir(), 'goodbuddy-portable-invalid-') ) try { const zipPath = join(directory, 'portable.zip') writeFileSync(zipPath, bytes) expect(() => releaseBuilder.verifyPortableZip(zipPath) ).toThrow('中央目录无效') } finally { rmSync(directory, { recursive: true, force: true }) } }) it('writes a deterministic artifact manifest with streaming hashes', async () => { const directory = mkdtempSync( join(tmpdir(), 'goodbuddy-release-manifest-') ) try { writeFileSync(join(directory, 'artifact.exe'), 'release') const manifest = await releaseBuilder.writeManifest( directory, windowsOptions ) expect(manifest.files).toEqual([ { name: 'artifact.exe', size: 7, sha256: 'a4d451ec23463726f72c43d64c710968f6b602cd653b4de8adee1b556240a829' } ]) expect( JSON.parse( readFileSync( join(directory, 'release-manifest.json'), 'utf8' ) ) ).toMatchObject({ productName: 'GoodBuddy', platform: 'windows', arch: 'x64' }) } finally { rmSync(directory, { recursive: true, force: true }) } }) it('refuses to replace an unrecognized non-empty directory', () => { const directory = mkdtempSync( join(tmpdir(), 'goodbuddy-release-unsafe-') ) try { writeFileSync(join(directory, 'user-file.txt'), 'keep') expect(() => releaseBuilder.assertReplaceableOutput( directory, windowsOptions ) ).toThrow('拒绝覆盖未识别的发布目录') } finally { rmSync(directory, { recursive: true, force: true }) } }) it('atomically replaces a recognized release directory', () => { const parent = mkdtempSync( join(tmpdir(), 'goodbuddy-release-replace-') ) const destination = join(parent, 'windows-x64') const staging = join(parent, 'staging') try { mkdirSync(destination) mkdirSync(staging) writeFileSync( join(destination, 'release-manifest.json'), JSON.stringify({ formatVersion: 1, productName: 'GoodBuddy', platform: 'windows', arch: 'x64' }) ) writeFileSync(join(destination, 'old.exe'), 'old') writeFileSync(join(staging, 'new.exe'), 'new') releaseBuilder.replaceOutput( staging, destination, windowsOptions ) expect(existsSync(join(destination, 'new.exe'))).toBe(true) expect(existsSync(join(destination, 'old.exe'))).toBe(false) expect(existsSync(staging)).toBe(false) } finally { rmSync(parent, { recursive: true, force: true }) } }) it('requires one artifact for every requested format', async () => { const directory = mkdtempSync( join(tmpdir(), 'goodbuddy-release-artifacts-') ) try { writeFileSync( join( directory, `GoodBuddy-${packageVersion}-windows-x64-setup.exe` ), 'MZ' ) const unpacked = portableDirectory(directory) const portableZip = join( directory, `GoodBuddy-${packageVersion}-windows-x64-portable.zip` ) await releaseBuilder.createPortableZip( unpacked, portableZip ) expect(() => releaseBuilder.verifyArtifacts( directory, windowsOptions ) ).not.toThrow() rmSync(portableZip) expect(() => releaseBuilder.verifyArtifacts( directory, windowsOptions ) ).toThrow('portable 产物数量错误') } finally { rmSync(directory, { recursive: true, force: true }) } }) it('validates AppImage, DEB, and RPM package signatures', () => { const directory = mkdtempSync( join(tmpdir(), 'goodbuddy-linux-artifacts-') ) const options: ReleaseOptions = { platform: 'linux', arch: 'x64', formats: ['AppImage', 'deb', 'rpm'], skipBuild: false, dryRun: false, unsigned: false, help: false } try { writeFileSync( join(directory, 'GoodBuddy-linux-x86_64.AppImage'), elf(62) ) writeFileSync( join(directory, 'GoodBuddy-linux-amd64.deb'), Buffer.from('!\n', 'ascii') ) const rpm = Buffer.alloc(96) rpm.set([0xed, 0xab, 0xee, 0xdb]) writeFileSync( join(directory, 'GoodBuddy-linux-x86_64.rpm'), rpm ) expect(() => releaseBuilder.verifyArtifacts(directory, options) ).not.toThrow() writeFileSync( join(directory, 'GoodBuddy-linux-x86_64.rpm'), 'not-rpm' ) expect(() => releaseBuilder.verifyArtifacts(directory, options) ).toThrow('RPM lead magic') } finally { rmSync(directory, { recursive: true, force: true }) } }) })