Files
goodbuddy/tests/build-release.test.ts
T
mesalogo f9137f0463
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
fix: restore cross-platform release packaging
The unpublished 0.11.3 candidate could not validate the DeepSeek Harness node-pty prebuilds on every native target, and catalog signing could misread large compound Agent payloads. Release packaging now verifies the rc.8 native layout and writes deterministic stored .gbagent archives with bounded central-directory, mode, CRC, SHA-256, and signature checks.

The 0.11.4 recovery also pins reviewed dependency lifecycle scripts, packages the omitted ONNX Runtime license, and carries the approved unpublished notes forward without changing the immutable failed tags or adding a LoongArch package.

Release note: 修复跨平台桌面包和大文件 Agent 包的发行校验失败,并将未发布的 0.11.3 内容顺延至 0.11.4。
2026-08-26 01:15:34 +08:00

1251 lines
32 KiB
TypeScript

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 { 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<void>
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<string, string>
optionalDependencies?: Record<string, string>
},
packageLock?: {
packages?: Record<
string,
{
version?: string
resolved?: string
integrity?: string
optionalDependencies?: Record<string, string>
}
>
}
) => {
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<string, string>
optionalDependencies?: Record<string, string>
}
packageLock: {
packages?: Record<
string,
{
version?: string
resolved?: string
integrity?: string
optionalDependencies?: Record<string, string>
}
>
}
npmInvocation: () => {
command: string
prefixArgs: string[]
}
runCapture: (
command: string,
arguments_: string[]
) => Promise<string>
extractArchive: (
archivePath: string,
destination: string
) => Promise<void>
}
) => Promise<() => void>
verifyHarnessPackage: (
resources: string,
options: ReleaseOptions,
dependencies?: {
listPackage: (asarPath: string) => string[]
statFile: (
asarPath: string,
filePath: string
) => {
files?: Record<string, unknown>
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<void>
const windowsOptions: ReleaseOptions = {
platform: 'windows',
arch: 'x64',
formats: ['nsis', 'portable'],
skipBuild: false,
dryRun: false,
unsigned: false,
help: false
}
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')
mkdirSync(
join(directory, 'resources', 'runtimes', 'opencode'),
{ recursive: true }
)
mkdirSync(
join(directory, 'resources', 'runtimes', 'continue'),
{ 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/runtimes/opencode/opencode.exe', 'MZ'],
['resources/runtimes/continue/package.json', '{}'],
[
'resources/runtimes/npm/bin/npm-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', 'zip']],
['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<string, string>
}
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/**/*',
'package.json',
'!node_modules/npm{,/**/*}'
])
expect(resourceSources).toContain('resources/skills')
expect(packageJson.build.extraResources).toContainEqual({
from: 'node_modules',
to: 'runtimes',
filter: ['npm{,/**/*}']
})
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')
try {
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.0-rc.8'
}
})
)
writeFileSync(
join(projectRoot, 'package-lock.json'),
JSON.stringify({
packages: {
[`node_modules/${packageName}`]: {
version: '1.18.9',
integrity
}
}
})
)
writeFileSync(
join(
projectRoot,
'.runtime-resources',
'x64',
'opencode.exe'
),
executable
)
writeFileSync(
join(
projectRoot,
'.runtime-resources',
'x64',
'.ready.json'
),
JSON.stringify({
packageName,
version: '1.18.9',
integrity,
executableSha256: createHash('sha256')
.update(executable)
.digest('hex')
})
)
await expect(
prepareBundledRuntimes({
electronPlatformName: 'win32',
arch: 1,
packager: { projectDir: projectRoot }
})
).resolves.toBeUndefined()
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.0-rc.8'
})
} 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('!<arch>\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 })
}
})
})