Files
WechatExplorer/tests/unit/scheduled-report-api-service.test.ts
电摇小子 4d5c6459fe feat: 定时日报并入自动化,统一规则管理与发送链路
发送目标与退群通知统一为 来源群 / 自己 / 文件传输助手 / 指定好友
日报页补「定时日报已并入自动化」指引条
2026-09-28 17:11:50 +07:00

252 lines
8.5 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import type { PersonalWechatSendCapability } from '../../src/shared/personal-wechat'
import type {
ScheduledReportCreateInput,
ScheduledReportExecution,
ScheduledReportResult,
ScheduledReportTask,
ScheduledReportUpdateInput
} from '../../src/shared/scheduled-report'
import {
ScheduledReportApiService,
type ScheduledReportApiDependencies
} from '../../src/main/services/scheduled-report-api-service'
type ApiTestService = ScheduledReportApiDependencies['service'] & {
createTask: ReturnType<typeof vi.fn>
updateTask: ReturnType<typeof vi.fn>
}
type ApiTestResult = {
api: ScheduledReportApiService
service: ApiTestService
}
const contacts = [
{
md5: 'tech-md5',
m_nsUsrName: 'tech@chatroom',
m_nsNickName: '技术交流群',
type: 'group' as const
},
{
md5: 'product-md5',
m_nsUsrName: 'product@chatroom',
m_nsNickName: '产品交流群',
type: 'group' as const
}
]
const task = (overrides: Partial<ScheduledReportTask> = {}): ScheduledReportTask => ({
id: 'task-1',
name: '技术交流群 · 每日日报',
group: 'tech-md5',
target: 'tech@chatroom',
scheduleTime: '09:00',
reportRange: 'yesterday',
enabled: true,
createdAt: '2026-08-27T00:00:00.000Z',
updatedAt: '2026-08-27T00:00:00.000Z',
nextRunAt: '2026-08-28T01:00:00.000Z',
...overrides
})
const capability = (
status: PersonalWechatSendCapability['status']
): PersonalWechatSendCapability => ({
supported: status !== 'unsupported',
ready: status === 'ready',
status,
capabilities: { text: status === 'ready', image: status === 'ready', voice: status === 'ready' },
senderStatus: {} as PersonalWechatSendCapability['senderStatus'],
message: status === 'ready' ? 'ready' : '请先配置微信'
})
function makeApi(
options: { capability?: PersonalWechatSendCapability; tasks?: ScheduledReportTask[] } = {}
): ApiTestResult {
const tasks = options.tasks || []
const service = {
listTasks: vi.fn(async (): Promise<ScheduledReportTask[]> => tasks),
listExecutions: vi.fn(
async (_taskId?: string): Promise<ScheduledReportExecution[]> => []
),
createTask: vi.fn(
async (input: ScheduledReportCreateInput): Promise<ScheduledReportResult<ScheduledReportTask>> => {
const created = task({
id: `task-${tasks.length + 1}`,
name: String(input.name),
group: String(input.group),
target: String(input.target),
scheduleTime: String(input.scheduleTime),
reportRange: input.reportRange as ScheduledReportTask['reportRange'],
enabled: Boolean(input.enabled)
})
tasks.push(created)
return { success: true, data: created }
}
),
updateTask: vi.fn(
async (
_taskId: string,
_input: ScheduledReportUpdateInput
): Promise<ScheduledReportResult<ScheduledReportTask>> => ({
success: true,
data: tasks[0]
})
),
deleteTask: vi.fn(
async (id: string): Promise<ScheduledReportResult<{ deletedId: string }>> => ({
success: true,
data: { deletedId: id }
})
),
setTaskEnabled: vi.fn(
async (
id: string,
enabled: boolean
): Promise<ScheduledReportResult<ScheduledReportTask>> => ({
success: true,
data: task({ id, enabled })
})
),
runScheduledReportNow: vi.fn(
async (id: string): Promise<ScheduledReportResult<ScheduledReportExecution>> => ({
success: true,
data: {
id: 'execution-1',
taskId: id,
startedAt: '2026-08-27T01:00:00.000Z',
status: 'success' as const
}
})
)
}
return {
api: new ScheduledReportApiService({
service,
getCapability: async () => options.capability || capability('ready'),
listContacts: () => contacts,
isDatabaseReady: () => true,
platform: 'darwin'
}),
service
}
}
describe('ScheduledReportApiService', () => {
it('resolves a unique group and maps the public create request to the existing service', async () => {
const { api, service } = makeApi()
const result = await api.create({
group: { name: '技术交流群' },
schedule: { type: 'daily', time: '09:00' },
reportRange: 'yesterday'
})
expect(result).toMatchObject({
name: '技术交流群 · 每日日报',
group: { talker: 'tech@chatroom', name: '技术交流群' },
schedule: { type: 'daily', time: '09:00' },
target: { type: 'wechat_group', talker: 'tech@chatroom' }
})
expect(service.createTask).toHaveBeenCalledWith(
expect.objectContaining({ group: 'tech-md5', target: 'tech@chatroom' })
)
})
it.each(['unsupported', 'unconfigured', 'needs_binding', 'initializing', 'error'] as const)(
'allows creation and leaves a send warning for capability state %s',
async (status) => {
const { api, service } = makeApi({ capability: capability(status) })
await expect(
api.create({
group: '技术交流群',
schedule: { type: 'daily', time: '09:00' }
})
).resolves.toMatchObject({ id: 'task-1' })
expect(service.createTask).toHaveBeenCalledOnce()
}
)
it('allows Windows when the image send capability is ready', async () => {
const { service } = makeApi()
const api = new ScheduledReportApiService({
service,
getCapability: async () => capability('ready'),
listContacts: () => contacts,
isDatabaseReady: () => true,
platform: 'win32'
})
const created = await api.create({
group: '技术交流群',
schedule: { type: 'daily', time: '09:00' }
})
expect(created.group).toMatchObject({ talker: 'tech@chatroom' })
expect(service.createTask).toHaveBeenCalledOnce()
})
it('allows task creation on an unsupported platform for later recovery', async () => {
const { service } = makeApi()
const api = new ScheduledReportApiService({
service,
getCapability: async () => capability('ready'),
listContacts: () => contacts,
isDatabaseReady: () => true,
platform: 'linux'
})
await expect(
api.create({ group: '技术交流群', schedule: { type: 'daily', time: '09:00' } })
).resolves.toMatchObject({ id: 'task-1' })
expect(service.createTask).toHaveBeenCalledOnce()
})
it('returns candidates for ambiguous group names and rejects duplicate tasks', async () => {
const ambiguousContacts = contacts.map((contact) => ({ ...contact, m_nsNickName: '重复群' }))
const { service: ambiguousService } = makeApi()
const ambiguousApi = new ScheduledReportApiService({
service: ambiguousService,
getCapability: async () => capability('ready'),
listContacts: () => ambiguousContacts,
isDatabaseReady: () => true,
platform: 'darwin'
})
await expect(
ambiguousApi.create({ group: '重复群', schedule: { type: 'daily', time: '09:00' } })
).rejects.toMatchObject({ code: 'group_ambiguous', status: 409 })
const duplicateApi = makeApi({ tasks: [task()] }).api
await expect(
duplicateApi.create({ group: '技术交流群', schedule: { type: 'daily', time: '09:00' } })
).rejects.toMatchObject({ code: 'duplicate', status: 409 })
})
it('updates a changed group target together when target is omitted', async () => {
const existing = task()
const { api, service } = makeApi({ tasks: [existing] })
vi.mocked(service.updateTask).mockResolvedValue({
success: true,
data: task({ group: 'product-md5', target: 'product@chatroom', name: '产品日报' })
})
const result = await api.update('task-1', {
group: { name: '产品交流群' },
name: '产品日报'
})
expect(result.group).toMatchObject({ talker: 'product@chatroom', name: '产品交流群' })
expect(service.updateTask).toHaveBeenCalledWith(
'task-1',
expect.objectContaining({ group: 'product-md5', target: 'product@chatroom' })
)
})
it('serializes concurrent creates so only one identical task is created', async () => {
const { api, service } = makeApi()
const request = {
group: '技术交流群',
schedule: { type: 'daily' as const, time: '09:00' },
reportRange: 'yesterday' as const
}
const results = await Promise.allSettled([api.create(request), api.create(request)])
expect(results.filter((result) => result.status === 'fulfilled')).toHaveLength(1)
expect(results.filter((result) => result.status === 'rejected')).toHaveLength(1)
expect(service.createTask).toHaveBeenCalledOnce()
})
})