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
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Validate source (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
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
The desktop runtime still pinned the rc.6 Harness family. It now uses the coherent rc.8 package set, adapts the revised image-attachment limits and error contract, and derives release checks from the lockfile. The managed SSH Agent package remains unchanged. Release note: 内置 DeepSeek Harness 已升级至 0.1.0-rc.8,并适配新版图片附件限制;托管 SSH Agent 包不受影响。
248 lines
7.1 KiB
TypeScript
248 lines
7.1 KiB
TypeScript
import { EventEmitter } from 'node:events'
|
|
import { realpath } from 'node:fs/promises'
|
|
import { basename, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
detectAgentRuntimes,
|
|
detectRuntimeBinary,
|
|
validateRuntimeVersion,
|
|
type RuntimeVersionProcess
|
|
} from './runtime-discovery'
|
|
|
|
const originalPath = process.env.PATH
|
|
const originalPathCase = process.env.Path
|
|
|
|
afterEach(() => {
|
|
if (originalPath === undefined) {
|
|
delete process.env.PATH
|
|
} else {
|
|
process.env.PATH = originalPath
|
|
}
|
|
if (originalPathCase === undefined) {
|
|
delete process.env.Path
|
|
} else {
|
|
process.env.Path = originalPathCase
|
|
}
|
|
})
|
|
|
|
describe('runtime discovery', () => {
|
|
it('launches a detached POSIX version probe and awaits bounded tree cleanup', async () => {
|
|
const child =
|
|
new EventEmitter() as RuntimeVersionProcess & EventEmitter
|
|
child.exitCode = null
|
|
child.pid = 2718
|
|
child.kill = vi.fn()
|
|
child.stdout = new EventEmitter()
|
|
child.stderr = new EventEmitter()
|
|
const spawnProcess = vi.fn(() => child)
|
|
let releaseCleanup!: () => void
|
|
const cleanupBlocked = new Promise<void>((resolve) => {
|
|
releaseCleanup = resolve
|
|
})
|
|
const terminateProcessTree = vi.fn(async () => {
|
|
await cleanupBlocked
|
|
})
|
|
|
|
const validation = validateRuntimeVersion('runtime', {
|
|
platform: 'linux',
|
|
spawnProcess,
|
|
terminateProcessTree,
|
|
outputLimit: 4,
|
|
terminationWaitMs: 25
|
|
})
|
|
;(child.stdout as EventEmitter).emit('data', '12345')
|
|
let completed = false
|
|
void validation.then(() => {
|
|
completed = true
|
|
})
|
|
await Promise.resolve()
|
|
|
|
expect(completed).toBe(false)
|
|
expect(spawnProcess).toHaveBeenCalledWith(
|
|
'runtime',
|
|
['--version'],
|
|
expect.objectContaining({ detached: true })
|
|
)
|
|
expect(terminateProcessTree).toHaveBeenCalledWith(child, {
|
|
platform: 'linux',
|
|
processGroup: true,
|
|
signal: 'SIGKILL',
|
|
waitMs: 25
|
|
})
|
|
|
|
releaseCleanup()
|
|
await expect(validation).resolves.toEqual({ valid: false })
|
|
})
|
|
|
|
it('canonicalizes and validates a configured ordinary file first', async () => {
|
|
process.env.PATH = ''
|
|
process.env.Path = ''
|
|
|
|
const detection = await detectRuntimeBinary({
|
|
binaryPath: process.execPath,
|
|
binaryNames: ['binary-that-does-not-exist'],
|
|
label: 'Test CLI'
|
|
})
|
|
|
|
expect(detection).toMatchObject({
|
|
available: true,
|
|
path: await realpath(process.execPath),
|
|
source: 'configured'
|
|
})
|
|
expect(detection.version).toMatch(/^\d+\.\d+\.\d+/u)
|
|
})
|
|
|
|
it('rejects relative configured paths without resolving them from cwd', async () => {
|
|
process.env.PATH = ''
|
|
process.env.Path = ''
|
|
|
|
await expect(
|
|
detectRuntimeBinary({
|
|
binaryPath: 'relative/runtime',
|
|
binaryNames: ['goodbuddy-runtime-that-does-not-exist'],
|
|
label: 'Test CLI'
|
|
})
|
|
).resolves.toEqual({
|
|
available: false,
|
|
detail: expect.stringContaining('必须为绝对路径')
|
|
})
|
|
})
|
|
|
|
it('finds executable names from absolute PATH directories', async () => {
|
|
process.env.PATH = dirname(process.execPath)
|
|
process.env.Path = dirname(process.execPath)
|
|
|
|
const detection = await detectRuntimeBinary({
|
|
binaryPath: '',
|
|
binaryNames: [basename(process.execPath)],
|
|
label: 'Test CLI'
|
|
})
|
|
|
|
expect(detection).toMatchObject({
|
|
available: true,
|
|
path: await realpath(process.execPath),
|
|
source: 'automatic'
|
|
})
|
|
})
|
|
|
|
it('prefers a configured binary over the bundled runtime', async () => {
|
|
const detection = await detectRuntimeBinary({
|
|
binaryPath: process.execPath,
|
|
bundledPath: process.execPath,
|
|
binaryNames: ['goodbuddy-runtime-that-does-not-exist'],
|
|
label: 'Test CLI'
|
|
})
|
|
|
|
expect(detection).toMatchObject({
|
|
available: true,
|
|
path: await realpath(process.execPath),
|
|
source: 'configured'
|
|
})
|
|
expect(detection.detail).not.toContain('内置')
|
|
})
|
|
|
|
it('prefers a bundled runtime over PATH discovery', async () => {
|
|
process.env.PATH = dirname(process.execPath)
|
|
process.env.Path = dirname(process.execPath)
|
|
|
|
const detection = await detectRuntimeBinary({
|
|
binaryPath: '',
|
|
bundledPath: process.execPath,
|
|
binaryNames: [basename(process.execPath)],
|
|
label: 'Test CLI'
|
|
})
|
|
|
|
expect(detection).toMatchObject({
|
|
available: true,
|
|
path: await realpath(process.execPath),
|
|
source: 'bundled'
|
|
})
|
|
expect(detection.detail).toContain('内置')
|
|
})
|
|
|
|
it('allows a bundled script to defer execution validation to its host adapter', async () => {
|
|
process.env.PATH = ''
|
|
process.env.Path = ''
|
|
const bundledScript = fileURLToPath(import.meta.url)
|
|
|
|
const detection = await detectRuntimeBinary({
|
|
binaryPath: '',
|
|
bundledPath: bundledScript,
|
|
bundledValidation: 'canonical-file',
|
|
bundledVersion: '1.5.47',
|
|
binaryNames: ['goodbuddy-runtime-that-does-not-exist'],
|
|
label: 'Script Runtime'
|
|
})
|
|
|
|
expect(detection).toMatchObject({
|
|
available: true,
|
|
path: await realpath(bundledScript),
|
|
version: '1.5.47',
|
|
source: 'bundled'
|
|
})
|
|
expect(detection.detail).toBe(
|
|
'内置 Script Runtime 1.5.47 已就绪'
|
|
)
|
|
})
|
|
|
|
it('accepts a controlled bundled harness when no custom host is configured', async () => {
|
|
const bundledScript = fileURLToPath(import.meta.url)
|
|
const detection = await detectRuntimeBinary({
|
|
binaryPath: '',
|
|
bundledPath: bundledScript,
|
|
bundledValidation: 'canonical-file',
|
|
bundledVersion: '0.1.0-rc.8',
|
|
binaryNames: [],
|
|
label: 'GoodBuddy DeepSeek Harness Host'
|
|
})
|
|
|
|
expect(detection).toMatchObject({
|
|
available: true,
|
|
path: await realpath(bundledScript),
|
|
version: '0.1.0-rc.8',
|
|
source: 'bundled'
|
|
})
|
|
expect(detection.detail).toContain('内置')
|
|
})
|
|
|
|
it('does not discover arbitrary DeepSeek Harness hosts from PATH', async () => {
|
|
process.env.PATH = dirname(process.execPath)
|
|
process.env.Path = dirname(process.execPath)
|
|
|
|
await expect(
|
|
detectRuntimeBinary({
|
|
binaryPath: '',
|
|
allowAutomaticDiscovery: false,
|
|
binaryNames: [basename(process.execPath)],
|
|
label: 'GoodBuddy DeepSeek Harness Host'
|
|
})
|
|
).resolves.toEqual({
|
|
available: false,
|
|
detail: expect.stringContaining('未自动检测到')
|
|
})
|
|
})
|
|
|
|
it('returns both runtime detections without exposing PATH contents', async () => {
|
|
const privatePathValue = `${dirname(process.execPath)}-private-path-value`
|
|
process.env.PATH = privatePathValue
|
|
process.env.Path = privatePathValue
|
|
|
|
const result = await detectAgentRuntimes({
|
|
opencodeBinaryPath: process.execPath,
|
|
continueBinaryPath: process.execPath
|
|
})
|
|
|
|
expect(result.opencode).toMatchObject({
|
|
available: true,
|
|
path: await realpath(process.execPath)
|
|
})
|
|
expect(result.continue).toMatchObject({
|
|
available: true,
|
|
path: await realpath(process.execPath)
|
|
})
|
|
expect(result.deepseekHarness.available).toBe(false)
|
|
expect(JSON.stringify(result)).not.toContain(privatePathValue)
|
|
})
|
|
})
|