Files
goodbuddy/src/main/agent/runtime-discovery.test.ts
T
mesalogo f81f342cde feat: open local runtime clients from the composer
Add composer shortcuts for local Continue and OpenCode terminals and the official DeepSeek Harness Web client, using the current project and model bridge. Manage browser service reuse, stop actions and terminal cleanup. Upgrade DeepSeek Harness to 0.1.7-rc.2 and Cordis to 4.0.4.

Include the coordinated IPC handler extraction and related tests and documentation. Remote native-client coordinator wiring, managed Node consolidation, Ask MCP mappings and full desktop/package acceptance remain unfinished; this commit records the implemented local workflow and supporting SSH groundwork.

Validation: typecheck and lint passed; 267 focused tests passed and 5 skipped. No new real-provider calls were made for this commit. Prior real-model evidence and remaining acceptance work are documented in the feature progress files.

Release note: 本地项目选择 Continue 或 OpenCode 后,可从输入框下方打开原生终端;选择 DS 后,可打开使用当前项目和模型的官方 Web 客户端。客户端使用独立会话,DS 网页关闭后服务继续运行,可在 GoodBuddy 中停止;远程入口尚未完成。
2026-09-27 08:38:00 +08:00

275 lines
7.8 KiB
TypeScript

import { EventEmitter } from 'node:events'
import { realpath } from 'node:fs/promises'
import { basename, dirname, resolve } 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'
})
if (process.versions.electron) {
expect(detection.version).toBeUndefined()
} else {
expect(detection.version).toMatch(/^\d+\.\d+\.\d+/u)
}
})
it('trusts a configured execution path without reading or launching it', async () => {
process.env.PATH = ''
process.env.Path = ''
const candidate = resolve(
dirname(fileURLToPath(import.meta.url)),
'runtime-that-does-not-exist'
)
const detection = await detectRuntimeBinary({
binaryPath: candidate,
validation: 'path-only',
binaryNames: ['binary-that-does-not-exist'],
label: 'Managed Runtime'
})
expect(detection).toEqual({
available: true,
path: candidate,
source: 'configured',
detail: 'Managed Runtime 已就绪'
})
})
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.7-rc.2',
binaryNames: [],
label: 'GoodBuddy DeepSeek Harness Host'
})
expect(detection).toMatchObject({
available: true,
path: await realpath(bundledScript),
version: '0.1.7-rc.2',
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)
})
})