import { beforeEach, describe, expect, it, vi } from 'vitest'
import { render } from '@testing-library/react'
import { useScheduledTaskDesktopNotifications } from './useScheduledTaskDesktopNotifications'
const { listMock, getRecentRunsMock, notifyDesktopMock } = vi.hoisted(() => ({
listMock: vi.fn(),
getRecentRunsMock: vi.fn(),
notifyDesktopMock: vi.fn(),
}))
vi.mock('../api/tasks', () => ({
tasksApi: {
list: listMock,
getRecentRuns: getRecentRunsMock,
},
}))
vi.mock('../lib/desktopNotifications', () => ({
notifyDesktop: notifyDesktopMock,
}))
function Harness() {
useScheduledTaskDesktopNotifications()
return null
}
describe('useScheduledTaskDesktopNotifications', () => {
beforeEach(() => {
vi.useFakeTimers()
localStorage.clear()
listMock.mockReset()
getRecentRunsMock.mockReset()
notifyDesktopMock.mockReset()
notifyDesktopMock.mockResolvedValue(true)
})
it('does not notify old runs on first poll and notifies new desktop-enabled task runs later', async () => {
listMock.mockResolvedValue({
tasks: [{
id: 'task-1',
name: 'Daily review',
cron: '* * * * *',
prompt: 'review',
enabled: true,
createdAt: 1,
notification: { enabled: true, channels: ['desktop'] },
}],
})
getRecentRunsMock
.mockResolvedValueOnce({
runs: [{
id: 'run-old',
taskId: 'task-1',
taskName: 'Daily review',
startedAt: '2026-05-03T00:00:00.000Z',
completedAt: '2026-05-03T00:00:01.000Z',
status: 'completed',
prompt: 'review',
output: 'old result',
}],
})
.mockResolvedValueOnce({
runs: [
{
id: 'run-old',
taskId: 'task-1',
taskName: 'Daily review',
startedAt: '2026-05-03T00:00:00.000Z',
completedAt: '2026-05-03T00:00:01.000Z',
status: 'completed',
prompt: 'review',
output: 'old result',
},
{
id: 'run-new',
taskId: 'task-1',
taskName: 'Daily review',
startedAt: '2026-05-03T00:01:00.000Z',
completedAt: '2026-05-03T00:01:01.000Z',
status: 'failed',
prompt: 'review',
error: 'provider timeout',
},
],
})
render()
await vi.waitFor(() => expect(getRecentRunsMock).toHaveBeenCalledTimes(1))
expect(notifyDesktopMock).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(30_000)
await vi.waitFor(() => expect(notifyDesktopMock).toHaveBeenCalledTimes(1))
expect(notifyDesktopMock).toHaveBeenCalledWith({
dedupeKey: 'scheduled-task:run-new',
title: '定时任务 Daily review',
body: '失败: provider timeout',
target: { type: 'scheduled' },
})
})
it('targets the run session when a scheduled task run has a session id', async () => {
listMock.mockResolvedValue({
tasks: [{
id: 'task-1',
name: 'Daily review',
cron: '* * * * *',
prompt: 'review',
enabled: true,
createdAt: 1,
notification: { enabled: true, channels: ['desktop'] },
}],
})
getRecentRunsMock
.mockResolvedValueOnce({ runs: [] })
.mockResolvedValueOnce({
runs: [{
id: 'run-new',
taskId: 'task-1',
taskName: 'Daily review',
startedAt: '2026-05-03T00:01:00.000Z',
completedAt: '2026-05-03T00:01:01.000Z',
status: 'completed',
prompt: 'review',
output: 'done',
sessionId: 'session-task-run',
}],
})
render()
await vi.waitFor(() => expect(getRecentRunsMock).toHaveBeenCalledTimes(1))
await vi.advanceTimersByTimeAsync(30_000)
await vi.waitFor(() => expect(notifyDesktopMock).toHaveBeenCalledTimes(1))
expect(notifyDesktopMock).toHaveBeenCalledWith({
dedupeKey: 'scheduled-task:run-new',
title: '定时任务 Daily review',
body: '完成: done',
target: {
type: 'session',
sessionId: 'session-task-run',
title: 'Daily review',
},
})
})
it('ignores task runs without the desktop notification channel', async () => {
listMock.mockResolvedValue({
tasks: [{
id: 'task-1',
name: 'IM only',
cron: '* * * * *',
prompt: 'review',
enabled: true,
createdAt: 1,
notification: { enabled: true, channels: ['telegram'] },
}],
})
getRecentRunsMock.mockResolvedValue({
runs: [{
id: 'run-1',
taskId: 'task-1',
taskName: 'IM only',
startedAt: '2026-05-03T00:00:00.000Z',
completedAt: '2026-05-03T00:00:01.000Z',
status: 'completed',
prompt: 'review',
}],
})
render()
await vi.waitFor(() => expect(getRecentRunsMock).toHaveBeenCalledTimes(1))
await vi.advanceTimersByTimeAsync(30_000)
expect(notifyDesktopMock).not.toHaveBeenCalled()
})
it('does not mark a run as notified when desktop notification delivery fails', async () => {
notifyDesktopMock
.mockResolvedValueOnce(false)
.mockResolvedValueOnce(true)
listMock.mockResolvedValue({
tasks: [{
id: 'task-1',
name: 'Daily review',
cron: '* * * * *',
prompt: 'review',
enabled: true,
createdAt: 1,
notification: { enabled: true, channels: ['desktop'] },
}],
})
getRecentRunsMock
.mockResolvedValueOnce({ runs: [] })
.mockResolvedValue({
runs: [{
id: 'run-new',
taskId: 'task-1',
taskName: 'Daily review',
startedAt: '2026-05-03T00:01:00.000Z',
completedAt: '2026-05-03T00:01:01.000Z',
status: 'completed',
prompt: 'review',
}],
})
render()
await vi.waitFor(() => expect(getRecentRunsMock).toHaveBeenCalledTimes(1))
await vi.advanceTimersByTimeAsync(30_000)
await vi.waitFor(() => expect(notifyDesktopMock).toHaveBeenCalledTimes(1))
await vi.advanceTimersByTimeAsync(30_000)
await vi.waitFor(() => expect(notifyDesktopMock).toHaveBeenCalledTimes(2))
})
})