feat: expand multi-runtime workflows for 0.10.0

GoodBuddy previously exposed Runtime capabilities, MCP assignments, plugin controls, context compaction, usage reporting, and task-completion behavior through incomplete or inconsistent paths. This release unifies managed OpenCode, Continue, and DeepSeek Harness controls; adds DSH plugin and image workflows; strengthens MCP and Runtime lifecycle bounds; fixes Windows notification activation; validates cross-architecture packages; and presents the approved bilingual four-section release notes.

The DSH plugin marketplace remains a default-off preview whose trusted third-party code runs with current-user permissions. Ask remains read-only, Execute keeps approval controls, and context compaction may use the selected model without deleting GoodBuddy chat history.

Release note: GoodBuddy 0.10.0 重点完善多 Runtime 工作流,统一 OpenCode、Continue 与 DeepSeek Harness 的能力、MCP、插件和上下文管理,并提升长对话、多会话与任务通知的连贯性。
This commit is contained in:
mesalogo
2026-08-17 12:57:12 +08:00
parent f5ee9b2198
commit cbbe30896e
50 changed files with 4077 additions and 605 deletions
+128
View File
@@ -490,6 +490,134 @@ describe('ModelToolProvider', () => {
await overflowingProvider.dispose()
})
it('loads every paginated MCP tool before exposing the catalog', async () => {
const workspace = await createWorkspace()
mocks.client.listTools.mockImplementation(
async (params?: { cursor?: string }) =>
params?.cursor === ''
? {
tools: [
{
name: 'second',
inputSchema: {
type: 'object',
properties: {}
}
}
]
}
: {
tools: [
{
name: 'first',
inputSchema: {
type: 'object',
properties: {}
}
}
],
nextCursor: ''
}
)
const provider = new ModelToolProvider(workspace, [
createMcpServer()
])
const tools = await provider.listTools(
toolContext,
new AbortController().signal
)
expect(tools).toEqual(
expect.arrayContaining([
expect.objectContaining({ displayName: 'Search MCP / first' }),
expect.objectContaining({ displayName: 'Search MCP / second' })
])
)
expect(mocks.client.listTools).toHaveBeenNthCalledWith(
2,
{ cursor: '' },
expect.objectContaining({ timeout: expect.any(Number) })
)
await provider.dispose()
})
it('refreshes a dynamic MCP change announced during initial listing', async () => {
const workspace = await createWorkspace()
let resolveInitialList:
| ((value: {
tools: Array<{
name: string
inputSchema: {
type: 'object'
properties: Record<string, never>
}
}>
}) => void)
| undefined
mocks.client.getServerCapabilities.mockReturnValue({
tools: { listChanged: true }
})
mocks.client.listTools
.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveInitialList = resolve
})
)
.mockResolvedValueOnce({
tools: [
{
name: 'current',
inputSchema: {
type: 'object',
properties: {}
}
}
]
})
const provider = new ModelToolProvider(workspace, [
createMcpServer(true)
])
const listing = provider.listTools(
toolContext,
new AbortController().signal
)
await vi.waitFor(() => {
expect(mocks.client.listTools).toHaveBeenCalledOnce()
})
const options = mocks.Client.mock.calls[0]?.[1] as
| {
listChanged?: {
tools?: {
onChanged?: (error?: Error) => void
}
}
}
| undefined
options?.listChanged?.tools?.onChanged?.()
resolveInitialList?.({
tools: [
{
name: 'stale',
inputSchema: {
type: 'object',
properties: {}
}
}
]
})
await expect(listing).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({ displayName: 'Search MCP / current' })
])
)
expect(mocks.client.listTools).toHaveBeenCalledTimes(2)
await provider.dispose()
})
it('rejects workspace traversal before accessing the filesystem', async () => {
const workspace = await createWorkspace()
const provider = new ModelToolProvider(workspace)