Files
goodbuddy/src/main/desktop-notification.test.ts
T
mesalogo cbbe30896e 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、插件和上下文管理,并提升长对话、多会话与任务通知的连贯性。
2026-08-17 12:57:12 +08:00

268 lines
7.6 KiB
TypeScript

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(),
instances: [] as Array<{
emit: (event: string, ...args: unknown[]) => boolean
listenerCount: (event: string) => number
}>
}))
vi.mock('electron', async () => {
const { EventEmitter } = await import('node:events')
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', () => {
const shown = showDesktopNotificationWhenUnfocused(
{
isDestroyed: vi.fn(() => false),
isFocused: vi.fn(() => true)
} as never,
{ title: '任务已完成' }
)
expect(shown).toBe(false)
expect(notificationMocks.show).not.toHaveBeenCalled()
})
it('shows desktop notifications while GoodBuddy is unfocused', () => {
const shown = showDesktopNotificationWhenUnfocused(
{
isDestroyed: vi.fn(() => false),
isFocused: vi.fn(() => false)
} as never,
{ title: '任务已完成' }
)
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)
})
})