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:
@@ -1,23 +1,55 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { showDesktopNotificationWhenUnfocused } from './desktop-notification'
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
registerDesktopNotificationActivation,
|
||||
showDesktopNotificationWhenUnfocused
|
||||
} from './desktop-notification'
|
||||
|
||||
const notificationMocks = vi.hoisted(() => ({
|
||||
activationHandler: undefined as (() => void) | undefined,
|
||||
handleActivation: vi.fn((callback: () => void) => {
|
||||
notificationMocks.activationHandler = callback
|
||||
}),
|
||||
close: vi.fn(),
|
||||
isSupported: vi.fn(() => true),
|
||||
show: vi.fn()
|
||||
show: vi.fn(),
|
||||
instances: [] as Array<{
|
||||
emit: (event: string, ...args: unknown[]) => boolean
|
||||
listenerCount: (event: string) => number
|
||||
}>
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
Notification: class {
|
||||
static isSupported = notificationMocks.isSupported
|
||||
vi.mock('electron', async () => {
|
||||
const { EventEmitter } = await import('node:events')
|
||||
|
||||
show = notificationMocks.show
|
||||
return {
|
||||
Notification: class extends EventEmitter {
|
||||
static handleActivation = notificationMocks.handleActivation
|
||||
static isSupported = notificationMocks.isSupported
|
||||
|
||||
close = notificationMocks.close
|
||||
show = notificationMocks.show
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
notificationMocks.instances.push(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
describe('showDesktopNotificationWhenUnfocused', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
notificationMocks.activationHandler = undefined
|
||||
notificationMocks.isSupported.mockReturnValue(true)
|
||||
notificationMocks.instances.length = 0
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const notification of notificationMocks.instances) {
|
||||
notification.emit('close', {})
|
||||
}
|
||||
})
|
||||
|
||||
it('suppresses desktop notifications while GoodBuddy is focused', () => {
|
||||
@@ -45,4 +77,191 @@ describe('showDesktopNotificationWhenUnfocused', () => {
|
||||
expect(shown).toBe(true)
|
||||
expect(notificationMocks.show).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('registers Windows notification activation to restore GoodBuddy', () => {
|
||||
const window = {
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isMinimized: vi.fn(() => true),
|
||||
restore: vi.fn(),
|
||||
show: vi.fn(),
|
||||
focus: vi.fn()
|
||||
}
|
||||
|
||||
registerDesktopNotificationActivation(window as never, 'win32')
|
||||
notificationMocks.activationHandler?.()
|
||||
|
||||
expect(notificationMocks.handleActivation).toHaveBeenCalledOnce()
|
||||
expect(window.restore).toHaveBeenCalledOnce()
|
||||
expect(window.show).toHaveBeenCalledOnce()
|
||||
expect(window.focus).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('waits for the renderer before showing a cold-start activation', () => {
|
||||
const webContents = new EventEmitter() as EventEmitter & {
|
||||
getURL: ReturnType<typeof vi.fn>
|
||||
isLoadingMainFrame: ReturnType<typeof vi.fn>
|
||||
}
|
||||
webContents.getURL = vi.fn(() => '')
|
||||
webContents.isLoadingMainFrame = vi.fn(() => true)
|
||||
const window = {
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isMinimized: vi.fn(() => false),
|
||||
restore: vi.fn(),
|
||||
show: vi.fn(),
|
||||
focus: vi.fn(),
|
||||
webContents
|
||||
}
|
||||
|
||||
registerDesktopNotificationActivation(window as never, 'win32')
|
||||
notificationMocks.activationHandler?.()
|
||||
|
||||
expect(window.show).not.toHaveBeenCalled()
|
||||
|
||||
webContents.getURL.mockReturnValue(
|
||||
'file:///D:/goodbuddy/out/renderer/index.html'
|
||||
)
|
||||
webContents.isLoadingMainFrame.mockReturnValue(false)
|
||||
webContents.emit('did-finish-load')
|
||||
|
||||
expect(window.show).toHaveBeenCalledOnce()
|
||||
expect(window.focus).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not register native activation outside Windows', () => {
|
||||
registerDesktopNotificationActivation(
|
||||
{
|
||||
isDestroyed: vi.fn(() => false)
|
||||
} as never,
|
||||
'linux'
|
||||
)
|
||||
|
||||
expect(notificationMocks.handleActivation).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('restores, shows, and focuses GoodBuddy when a notification is clicked', () => {
|
||||
const window = {
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isFocused: vi.fn(() => false),
|
||||
isMinimized: vi.fn(() => true),
|
||||
restore: vi.fn(),
|
||||
show: vi.fn(),
|
||||
focus: vi.fn()
|
||||
}
|
||||
|
||||
showDesktopNotificationWhenUnfocused(window as never, {
|
||||
title: '任务已完成'
|
||||
})
|
||||
|
||||
notificationMocks.instances[0]!.emit('click', {})
|
||||
|
||||
expect(window.restore).toHaveBeenCalledOnce()
|
||||
expect(window.show).toHaveBeenCalledOnce()
|
||||
expect(window.focus).toHaveBeenCalledOnce()
|
||||
expect(
|
||||
notificationMocks.instances[0]!.listenerCount('click')
|
||||
).toBe(0)
|
||||
})
|
||||
|
||||
it('does not operate on a window destroyed before the click', () => {
|
||||
const window = {
|
||||
isDestroyed: vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValue(true),
|
||||
isFocused: vi.fn(() => false),
|
||||
isMinimized: vi.fn(),
|
||||
restore: vi.fn(),
|
||||
show: vi.fn(),
|
||||
focus: vi.fn()
|
||||
}
|
||||
|
||||
showDesktopNotificationWhenUnfocused(window as never, {
|
||||
title: '任务已完成'
|
||||
})
|
||||
notificationMocks.instances[0]!.emit('click', {})
|
||||
|
||||
expect(window.isMinimized).not.toHaveBeenCalled()
|
||||
expect(window.restore).not.toHaveBeenCalled()
|
||||
expect(window.show).not.toHaveBeenCalled()
|
||||
expect(window.focus).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not let window activation errors escape the native callback', () => {
|
||||
const window = {
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isFocused: vi.fn(() => false),
|
||||
isMinimized: vi.fn(() => false),
|
||||
restore: vi.fn(),
|
||||
show: vi.fn(() => {
|
||||
throw new Error('window closed')
|
||||
}),
|
||||
focus: vi.fn()
|
||||
}
|
||||
|
||||
showDesktopNotificationWhenUnfocused(window as never, {
|
||||
title: '任务已完成'
|
||||
})
|
||||
|
||||
expect(() =>
|
||||
notificationMocks.instances[0]!.emit('click', {})
|
||||
).not.toThrow()
|
||||
expect(
|
||||
notificationMocks.instances[0]!.listenerCount('click')
|
||||
).toBe(0)
|
||||
})
|
||||
|
||||
it.each(['close', 'failed'])(
|
||||
'releases retained notifications after %s',
|
||||
(event) => {
|
||||
showDesktopNotificationWhenUnfocused(
|
||||
{
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isFocused: vi.fn(() => false)
|
||||
} as never,
|
||||
{ title: '任务已完成' }
|
||||
)
|
||||
const notification = notificationMocks.instances[0]!
|
||||
|
||||
notification.emit(event, {})
|
||||
|
||||
expect(notification.listenerCount('click')).toBe(0)
|
||||
}
|
||||
)
|
||||
|
||||
it('releases an unhandled notification after a bounded retention period', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
showDesktopNotificationWhenUnfocused(
|
||||
{
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isFocused: vi.fn(() => false)
|
||||
} as never,
|
||||
{ title: '任务已完成' }
|
||||
)
|
||||
const notification = notificationMocks.instances[0]!
|
||||
|
||||
expect(notification.listenerCount('click')).toBe(1)
|
||||
vi.advanceTimersByTime(15 * 60_000)
|
||||
expect(notificationMocks.close).toHaveBeenCalledOnce()
|
||||
expect(notification.listenerCount('click')).toBe(0)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('bounds retained notification instances during long-running sessions', () => {
|
||||
for (let index = 0; index < 65; index += 1) {
|
||||
showDesktopNotificationWhenUnfocused(
|
||||
{
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isFocused: vi.fn(() => false)
|
||||
} as never,
|
||||
{ title: `任务已完成 ${index}` }
|
||||
)
|
||||
}
|
||||
|
||||
expect(notificationMocks.close).toHaveBeenCalledOnce()
|
||||
expect(notificationMocks.instances[0]!.listenerCount('click')).toBe(0)
|
||||
expect(notificationMocks.instances[64]!.listenerCount('click')).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user