Files
goodbuddy/tests/build-release.test.ts
T
mesalogo 21ac8330ea
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (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
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
fix: preserve runtime configuration references
Projects, channels, conversations, and Agent Runtimes now retain dynamic default references while explicit model selections remain pinned. Compatible fallback resolution stays aligned across settings validation, runtime creation, labels, context metrics, DeepSeek Harness browser routing, and remote project saves.

Prepare the bilingual Desktop 0.12.11 release metadata and keep Agent 0.11.22 as the published remote baseline.

Release note: 修复默认模型与 Runtime 配置变化后项目、通道和对话选择失去跟随关系的问题;显式选择仍保持固定,新请求使用当前兼容配置。
2026-09-10 06:27:48 +08:00

1319 lines
35 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 { 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<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
}
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<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: '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' }))
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.2-rc.1',
'@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, '.agent-resources'))
).toBe(false)
expect(
JSON.parse(
readFileSync(
join(projectRoot, 'out', 'main', 'package.json'),
'utf8'
)
)
).toMatchObject({
name: '@deepseek-ai/dsh-llm',
version: '0.1.2-rc.1'
})
} 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 })
}
})
})