chore: prepare GoodBuddy 0.8.2
Cross-platform packages / Validate source (push) Waiting to run
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / Publish GitHub Release (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions

This commit is contained in:
lofyer
2026-08-06 22:47:13 +08:00
parent 8d00e6371d
commit b8fc7bc86e
114 changed files with 22916 additions and 1560 deletions
+668 -4
View File
@@ -1,9 +1,13 @@
import { EventEmitter } from 'node:events'
import { resolve } from 'node:path'
import { createServer } from 'node:http'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { PassThrough } from 'node:stream'
import type { createOpencodeClient } from '@opencode-ai/sdk/v2'
import type spawn from 'cross-spawn'
import { describe, expect, it, vi } from 'vitest'
import type { KnowledgeMcpGateway } from './knowledge-mcp-gateway'
import {
OpenCodeRuntime,
type OpenCodeRuntimeDependencies
@@ -189,7 +193,16 @@ function runClient(events: Record<string, unknown>[]) {
reply: permissionReply
},
mcp: {
add: vi.fn().mockResolvedValue({ data: true, error: undefined }),
add: vi
.fn()
.mockImplementation(
async (input: { name: string }) => ({
data: {
[input.name]: { status: 'connected' }
},
error: undefined
})
),
disconnect: vi
.fn()
.mockResolvedValue({ data: true, error: undefined })
@@ -404,12 +417,20 @@ describe('OpenCodeRuntime embedded launcher', () => {
spawnOptions?.env?.OPENCODE_CONFIG_CONTENT ?? '{}'
) as Record<string, unknown>
expect(config).toMatchObject({
model: 'anthropic/private-model',
model: 'goodbuddy-anthropic/private-model',
provider: {
anthropic: {
'goodbuddy-anthropic': {
npm: '@ai-sdk/anthropic',
options: {
apiKey: 'private-key',
baseURL: 'https://model.example/v1'
},
models: {
'private-model': {
provider: {
npm: '@ai-sdk/anthropic'
}
}
}
}
}
@@ -417,6 +438,305 @@ describe('OpenCodeRuntime embedded launcher', () => {
await runtime.dispose()
})
it('isolates an explicit profile from unrelated inherited credentials', async () => {
const child = fakeChild()
const { deps, spawnMock } = dependencies(child)
const inheritedCredentials = {
ANTHROPIC_API_KEY: 'inherited-anthropic',
OPENAI_API_KEY: 'inherited-openai',
GOOGLE_GENERATIVE_AI_API_KEY: 'inherited-google',
GEMINI_API_KEY: 'inherited-gemini',
AWS_ACCESS_KEY_ID: 'inherited-aws-access',
AWS_SECRET_ACCESS_KEY: 'inherited-aws-secret',
AWS_SESSION_TOKEN: 'inherited-aws-session',
AWS_PROFILE: 'inherited-aws-profile',
OPENROUTER_API_KEY: 'inherited-openrouter'
}
const previousEnvironment = Object.fromEntries(
Object.keys(inheritedCredentials).map((name) => [
name,
process.env[name]
])
)
Object.assign(process.env, inheritedCredentials)
setTimeout(() => {
stdoutOf(child).write(
'opencode server listening on http://127.0.0.1:3013\n'
)
}, 0)
const runtime = new OpenCodeRuntime(
options({
modelProfile: {
id: '00000000-0000-4000-8000-000000000014',
name: 'Explicit OpenAI profile',
baseUrl: 'https://model.example/v1',
modelName: 'private-model',
protocol: 'openai-responses',
authentication: 'api-key',
apiKey: 'selected-openai-key'
}
}),
deps
)
try {
await expect(runtime.getStatus()).resolves.toMatchObject({
available: true
})
const environment = (
spawnMock.mock.calls[0]?.[2] as
| { env?: NodeJS.ProcessEnv }
| undefined
)?.env
expect(environment?.OPENAI_API_KEY).toBe('selected-openai-key')
for (const name of Object.keys(inheritedCredentials)) {
if (name !== 'OPENAI_API_KEY') {
expect(environment).not.toHaveProperty(name)
}
}
} finally {
await runtime.dispose()
for (const [name, value] of Object.entries(
previousEnvironment
)) {
if (value === undefined) {
delete process.env[name]
} else {
process.env[name] = value
}
}
}
})
it.each([
{
label: 'Chat Completions',
protocol: 'openai-chat-completions' as const,
expectedPath: '/v1/chat/completions',
unexpectedPath: '/v1/responses'
},
{
label: 'Responses',
protocol: 'openai-responses' as const,
expectedPath: '/v1/responses',
unexpectedPath: '/v1/chat/completions'
}
])(
'routes a custom-base $label profile through the bundled OpenCode provider',
async ({
protocol,
expectedPath,
unexpectedPath
}) => {
const root = await mkdtemp(
join(tmpdir(), 'goodbuddy-opencode-routing-')
)
const requestPaths: string[] = []
const server = createServer((request, response) => {
requestPaths.push(request.url ?? '')
request.resume()
response.writeHead(400, {
'content-type': 'application/json'
})
response.end(
JSON.stringify({
error: {
message: 'Intentional local routing probe'
}
})
)
})
await new Promise<void>((resolveListen, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', () => resolveListen())
})
const address = server.address()
if (!address || typeof address === 'string') {
throw new Error('Failed to bind local routing probe')
}
const isolatedEnvironment = {
APPDATA: join(root, 'appdata'),
HOME: root,
LOCALAPPDATA: join(root, 'localappdata'),
USERPROFILE: root
} as const
const previousEnvironment = Object.fromEntries(
Object.keys(isolatedEnvironment).map((name) => [
name,
process.env[name]
])
)
Object.assign(process.env, isolatedEnvironment)
const runtime = new OpenCodeRuntime(
options({
binaryPath: join(
process.cwd(),
'node_modules',
'opencode-ai',
'bin',
process.platform === 'win32'
? 'opencode.exe'
: 'opencode'
),
defaultWorkspace: root,
modelProfile: {
id: '00000000-0000-4000-8000-000000000013',
name: 'Local endpoint probe',
baseUrl: `http://127.0.0.1:${address.port}/v1`,
modelName: 'probe-model',
protocol,
authentication: 'api-key',
apiKey: 'local-probe-key'
}
})
)
const controller = new AbortController()
const timeout = setTimeout(
() => controller.abort(new Error('Routing probe timed out')),
20_000
)
try {
let failure = ''
await (async () => {
for await (const _event of runtime.run(
{
requestId:
'3f496642-f47d-4e0a-8944-a32c77b0d6ef',
conversationId: 'routing-probe',
prompt: 'Reply with OK',
workMode: 'execute'
},
controller.signal
)) {
// The local probe intentionally returns an upstream error.
void _event
}
})().catch((error) => {
failure =
error instanceof Error ? error.message : String(error)
})
if (requestPaths.length === 0) {
throw new Error(`OpenCode routing probe failed: ${failure}`)
}
expect(requestPaths).toContain(expectedPath)
expect(requestPaths).not.toContain(unexpectedPath)
} finally {
clearTimeout(timeout)
await runtime.dispose()
for (const [name, value] of Object.entries(
previousEnvironment
)) {
if (value === undefined) {
delete process.env[name]
} else {
process.env[name] = value
}
}
await new Promise<void>((resolveClose, reject) => {
server.close((error) =>
error ? reject(error) : resolveClose()
)
})
await rm(root, { recursive: true, force: true })
}
},
30_000
)
it.each([
{
protocol: 'openai-chat-completions' as const,
authentication: 'none' as const,
providerId: 'goodbuddy-openai-chat',
providerPackage: '@ai-sdk/openai-compatible'
},
{
protocol: 'openai-responses' as const,
authentication: 'api-key' as const,
providerId: 'goodbuddy-openai-responses',
providerPackage: '@ai-sdk/openai'
}
])(
'generates an explicit $protocol provider configuration',
async ({
protocol,
authentication,
providerId,
providerPackage
}) => {
const child = fakeChild()
const { deps, spawnMock } = dependencies(child)
setTimeout(() => {
stdoutOf(child).write(
'opencode server listening on http://127.0.0.1:3012\n'
)
}, 0)
const runtime = new OpenCodeRuntime(
options({
modelProfile: {
id: '00000000-0000-4000-8000-000000000012',
name: 'OpenAI 独立模型',
baseUrl: 'https://model.example/v1',
modelName: 'custom-model',
protocol,
authentication,
...(authentication === 'api-key'
? { apiKey: 'private-key' }
: {})
}
}),
deps
)
await expect(runtime.getStatus()).resolves.toMatchObject({
available: true
})
const spawnOptions = spawnMock.mock.calls[0]?.[2] as
| { env?: NodeJS.ProcessEnv }
| undefined
const config = JSON.parse(
spawnOptions?.env?.OPENCODE_CONFIG_CONTENT ?? '{}'
) as {
model?: string
provider?: Record<
string,
{
npm?: string
options?: Record<string, unknown>
models?: Record<
string,
{ provider?: { npm?: string } }
>
}
>
}
expect(config.model).toBe(`${providerId}/custom-model`)
expect(config.provider?.[providerId]).toMatchObject({
npm: providerPackage,
options: {
baseURL: 'https://model.example/v1'
},
models: {
'custom-model': {
provider: {
npm: providerPackage
}
}
}
})
if (authentication === 'api-key') {
expect(
config.provider?.[providerId]?.options?.apiKey
).toBe('private-key')
} else {
expect(
config.provider?.[providerId]?.options
).not.toHaveProperty('apiKey')
}
await runtime.dispose()
}
)
it('isolates embedded server configuration from inherited env', async () => {
const child = fakeChild()
const { deps, spawnMock } = dependencies(child)
@@ -658,6 +978,350 @@ describe('OpenCodeRuntime embedded launcher', () => {
})
describe('OpenCodeRuntime embedded permission mediation', () => {
it('adds only the request-scoped knowledge MCP tool for Ask and disconnects it', async () => {
const setup = runClient([
{
id: 'idle',
type: 'session.idle',
properties: { sessionID: 'session-1' }
}
])
const toolIds = setup.tool.ids as unknown as ReturnType<typeof vi.fn>
toolIds
.mockResolvedValueOnce({
data: ['read', 'write', 'bash'],
error: undefined
})
.mockResolvedValueOnce({
data: [
'read',
'write',
'bash',
'goodbuddy_knowledge_search'
],
error: undefined
})
.mockResolvedValue({
data: [
'read',
'write',
'bash',
'goodbuddy_knowledge_search'
],
error: undefined
})
const gateway = {
getEndpoint: () => 'http://127.0.0.1:4567/mcp'
} as unknown as KnowledgeMcpGateway
const child = fakeChild()
const { deps } = dependencies(child, {
createClient: vi.fn(
() => setup.client
) as unknown as typeof createOpencodeClient
})
setTimeout(() => {
stdoutOf(child).write(
'opencode server listening on http://127.0.0.1:4010\n'
)
}, 0)
const runtime = new OpenCodeRuntime(
options({ knowledgeGateway: gateway }),
deps
)
const events = []
for await (const event of runtime.run(
{
requestId: '3f496642-f47d-4e0a-8944-a32c77b0d6ef',
conversationId: 'conversation-1',
prompt: 'search',
workMode: 'ask',
knowledgeCapabilityToken: 'secret-capability'
},
new AbortController().signal
)) {
events.push(event)
}
expect(setup.client.mcp.add).toHaveBeenCalledWith({
directory: process.cwd(),
name: expect.stringMatching(/^goodbuddy-knowledge-[a-f0-9]{20}$/u),
config: {
type: 'remote',
url: 'http://127.0.0.1:4567/mcp',
enabled: true,
headers: {
Authorization: 'Bearer secret-capability'
},
oauth: false
}
})
const knowledgeMcpName = (
(
setup.client.mcp.add as unknown as ReturnType<typeof vi.fn>
).mock.calls[0]?.[0] as { name: string }
).name
const knowledgeToolId = `${knowledgeMcpName}_knowledge_search`
expect(setup.session.create).toHaveBeenCalledWith(
expect.objectContaining({
permission: [
{ permission: '*', pattern: '*', action: 'deny' },
{
permission: knowledgeToolId,
pattern: '*',
action: 'allow'
}
]
})
)
expect(setup.session.promptAsync).toHaveBeenCalledWith(
expect.objectContaining({
tools: {
read: false,
write: false,
bash: false,
[knowledgeToolId]: true
}
}),
expect.anything()
)
expect(setup.client.mcp.disconnect).toHaveBeenCalledWith({
name: expect.stringMatching(/^goodbuddy-knowledge-/u),
directory: process.cwd()
})
expect(events.at(-1)).toMatchObject({ type: 'done' })
await runtime.dispose()
})
it('enables the deterministic MCP tool name when tool ids omit dynamic tools', async () => {
const setup = runClient([
{
id: 'idle',
type: 'session.idle',
properties: { sessionID: 'session-1' }
}
])
const baseline = {
data: ['read', 'write', 'bash'],
error: undefined
}
const toolIds = setup.tool.ids as unknown as ReturnType<typeof vi.fn>
toolIds.mockResolvedValue(baseline)
const child = fakeChild()
const { deps } = dependencies(child, {
createClient: vi.fn(
() => setup.client
) as unknown as typeof createOpencodeClient
})
setTimeout(() => {
stdoutOf(child).write(
'opencode server listening on http://127.0.0.1:4010\n'
)
}, 0)
const runtime = new OpenCodeRuntime(
options({
knowledgeGateway: {
getEndpoint: () => 'http://127.0.0.1:4567/mcp'
} as unknown as KnowledgeMcpGateway
}),
deps
)
for await (const _event of runtime.run(
{
requestId: '3f496642-f47d-4e0a-8944-a32c77b0d6ef',
conversationId: 'conversation-1',
prompt: 'search',
workMode: 'ask',
knowledgeCapabilityToken: 'secret-capability'
},
new AbortController().signal
)) {
void _event
}
const knowledgeMcpName = (
(
setup.client.mcp.add as unknown as ReturnType<typeof vi.fn>
).mock.calls[0]?.[0] as { name: string }
).name
const knowledgeToolId = `${knowledgeMcpName}_knowledge_search`
expect(toolIds).toHaveBeenCalledTimes(1)
expect(setup.session.promptAsync).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
read: false,
write: false,
bash: false,
[knowledgeToolId]: true
})
}),
expect.anything()
)
await runtime.dispose()
})
it('serializes overlapping embedded MCP registration and discovery', async () => {
const setup = runClient([
{
id: 'idle',
type: 'session.idle',
properties: { sessionID: 'session-1' }
}
])
const toolIds = setup.tool.ids as unknown as ReturnType<typeof vi.fn>
const baseline = {
data: ['read', 'write'],
error: undefined
}
const withKnowledge = {
data: ['read', 'write', 'goodbuddy_knowledge_search'],
error: undefined
}
for (const response of [
baseline,
withKnowledge,
withKnowledge,
baseline,
withKnowledge,
withKnowledge
]) {
toolIds.mockResolvedValueOnce(response)
}
let resolveFirstAdd!: () => void
const firstAdd = new Promise<void>((resolve) => {
resolveFirstAdd = resolve
})
const mcpAdd = setup.client.mcp.add as unknown as ReturnType<typeof vi.fn>
mcpAdd
.mockImplementationOnce(async (input: { name: string }) => {
await firstAdd
return {
data: {
[input.name]: { status: 'connected' }
},
error: undefined
}
})
.mockImplementation(async (input: { name: string }) => ({
data: {
[input.name]: { status: 'connected' }
},
error: undefined
}))
const child = fakeChild()
const { deps } = dependencies(child, {
createClient: vi.fn(
() => setup.client
) as unknown as typeof createOpencodeClient
})
setTimeout(() => {
stdoutOf(child).write(
'opencode server listening on http://127.0.0.1:4010\n'
)
}, 0)
const runtime = new OpenCodeRuntime(
options({
knowledgeGateway: {
getEndpoint: () => 'http://127.0.0.1:4567/mcp'
} as unknown as KnowledgeMcpGateway
}),
deps
)
const collect = async (
requestId: string,
conversationId: string,
token: string
): Promise<void> => {
for await (const _event of runtime.run(
{
requestId,
conversationId,
prompt: 'search',
workMode: 'ask',
knowledgeCapabilityToken: token
},
new AbortController().signal
)) {
void _event
}
}
const first = collect(
'3f496642-f47d-4e0a-8944-a32c77b0d6e1',
'conversation-one',
'first-token'
)
await vi.waitFor(() => expect(mcpAdd).toHaveBeenCalledTimes(1))
const second = collect(
'3f496642-f47d-4e0a-8944-a32c77b0d6e2',
'conversation-two',
'second-token'
)
await new Promise((resolve) => setTimeout(resolve, 10))
expect(mcpAdd).toHaveBeenCalledTimes(1)
resolveFirstAdd()
await first
await vi.waitFor(() => expect(mcpAdd).toHaveBeenCalledTimes(2))
await second
expect(
mcpAdd.mock.calls.map(
([input]) =>
(input as {
config: { headers: { Authorization: string } }
}).config.headers.Authorization
)
).toEqual(['Bearer first-token', 'Bearer second-token'])
expect(setup.client.mcp.disconnect).toHaveBeenCalledTimes(2)
await runtime.dispose()
})
it('does not send a knowledge capability to external OpenCode', async () => {
const setup = runClient([
{
id: 'idle',
type: 'session.idle',
properties: { sessionID: 'session-1' }
}
])
const runtime = new OpenCodeRuntime(
options({
embedded: false,
baseUrl: 'http://127.0.0.1:4096',
knowledgeGateway: {
getEndpoint: () => 'http://127.0.0.1:4567/mcp'
} as unknown as KnowledgeMcpGateway
}),
{
createClient: vi.fn(
() => setup.client
) as unknown as typeof createOpencodeClient
}
)
for await (const _event of runtime.run(
{
requestId: '3f496642-f47d-4e0a-8944-a32c77b0d6ef',
conversationId: 'conversation-1',
prompt: 'search',
workMode: 'ask',
knowledgeCapabilityToken: 'must-not-leave-main'
},
new AbortController().signal
)) {
void _event
}
expect(setup.client.mcp.add).not.toHaveBeenCalled()
expect(
JSON.stringify(
(
setup.session.promptAsync as unknown as ReturnType<typeof vi.fn>
).mock.calls
)
).not.toContain('must-not-leave-main')
await runtime.dispose()
})
it('subscribes before prompting and auto-allows a tool request', async () => {
const {
client,