Files
waoowaoo/tests/unit/task/publisher.replay.test.ts

208 lines
5.6 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const taskEventFindManyMock = vi.hoisted(() => vi.fn(async () => []))
const taskEventCreateMock = vi.hoisted(() => vi.fn(async () => null))
const taskFindManyMock = vi.hoisted(() => vi.fn(async () => []))
const redisPublishMock = vi.hoisted(() => vi.fn(async () => 1))
vi.mock('@/lib/prisma', () => ({
prisma: {
taskEvent: {
findMany: taskEventFindManyMock,
create: taskEventCreateMock,
},
task: {
findMany: taskFindManyMock,
},
},
}))
vi.mock('@/lib/redis', () => ({
redis: {
publish: redisPublishMock,
},
}))
import { listEventsAfter, listTaskLifecycleEvents, publishTaskStreamEvent } from '@/lib/task/publisher'
describe('task publisher replay', () => {
beforeEach(() => {
taskEventFindManyMock.mockReset()
taskEventCreateMock.mockReset()
taskFindManyMock.mockReset()
redisPublishMock.mockReset()
})
it('replays persisted lifecycle + stream rows in chronological order', async () => {
taskEventFindManyMock.mockResolvedValueOnce([
{
id: 12,
taskId: 'task-1',
projectId: 'project-1',
userId: 'user-1',
eventType: 'task.stream',
payload: {
stepId: 'step-1',
stream: {
kind: 'text',
seq: 2,
lane: 'main',
delta: 'world',
},
},
createdAt: new Date('2026-02-27T00:00:02.000Z'),
},
{
id: 11,
taskId: 'task-1',
projectId: 'project-1',
userId: 'user-1',
eventType: 'task.processing',
payload: {
lifecycleType: 'task.processing',
stepId: 'step-1',
stepTitle: '阶段1',
},
createdAt: new Date('2026-02-27T00:00:01.000Z'),
},
{
id: 10,
taskId: 'task-1',
projectId: 'project-1',
userId: 'user-1',
eventType: 'task.ignored',
payload: {},
createdAt: new Date('2026-02-27T00:00:00.000Z'),
},
])
taskFindManyMock.mockResolvedValueOnce([
{
id: 'task-1',
type: 'script_to_storyboard_run',
targetType: 'episode',
targetId: 'episode-1',
episodeId: 'episode-1',
},
])
const events = await listTaskLifecycleEvents('task-1', 50)
expect(taskEventFindManyMock).toHaveBeenCalledWith(expect.objectContaining({
where: { taskId: 'task-1' },
orderBy: { id: 'desc' },
take: 50,
}))
expect(events).toHaveLength(2)
expect(events.map((event) => event.id)).toEqual(['11', '12'])
expect(events.map((event) => event.type)).toEqual(['task.lifecycle', 'task.stream'])
expect((events[1]?.payload as { stream?: { delta?: string } }).stream?.delta).toBe('world')
})
it('persists stream rows when persist=true', async () => {
taskEventCreateMock.mockResolvedValueOnce({
id: 99,
taskId: 'task-1',
projectId: 'project-1',
userId: 'user-1',
eventType: 'task.stream',
payload: {
stream: {
kind: 'text',
seq: 1,
lane: 'main',
delta: 'hello',
},
},
createdAt: new Date('2026-02-27T00:00:03.000Z'),
})
redisPublishMock.mockResolvedValueOnce(1)
const message = await publishTaskStreamEvent({
taskId: 'task-1',
projectId: 'project-1',
userId: 'user-1',
taskType: 'story_to_script_run',
targetType: 'episode',
targetId: 'episode-1',
episodeId: 'episode-1',
payload: {
stepId: 'step-1',
stream: {
kind: 'text',
seq: 1,
lane: 'main',
delta: 'hello',
},
},
persist: true,
})
expect(taskEventCreateMock).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
taskId: 'task-1',
eventType: 'task.stream',
}),
}))
expect(redisPublishMock).toHaveBeenCalledTimes(1)
expect(message?.id).toBe('99')
expect(message?.type).toBe('task.stream')
})
it('replays lifecycle + stream rows in listEventsAfter', async () => {
taskEventFindManyMock.mockResolvedValueOnce([
{
id: 101,
taskId: 'task-1',
projectId: 'project-1',
userId: 'user-1',
eventType: 'task.stream',
payload: {
stepId: 'step-1',
stream: {
kind: 'text',
seq: 3,
lane: 'main',
delta: 'chunk',
},
},
createdAt: new Date('2026-02-27T00:00:03.000Z'),
},
{
id: 102,
taskId: 'task-1',
projectId: 'project-1',
userId: 'user-1',
eventType: 'task.processing',
payload: {
lifecycleType: 'task.processing',
stepId: 'step-1',
},
createdAt: new Date('2026-02-27T00:00:04.000Z'),
},
])
taskFindManyMock.mockResolvedValueOnce([
{
id: 'task-1',
type: 'story_to_script_run',
targetType: 'episode',
targetId: 'episode-1',
episodeId: 'episode-1',
},
])
const events = await listEventsAfter('project-1', 100, 20)
expect(taskEventFindManyMock).toHaveBeenCalledWith(expect.objectContaining({
where: {
projectId: 'project-1',
id: { gt: 100 },
},
orderBy: { id: 'asc' },
}))
expect(events).toHaveLength(2)
expect(events.map((event) => event.id)).toEqual(['101', '102'])
expect(events.map((event) => event.type)).toEqual(['task.stream', 'task.lifecycle'])
expect((events[0]?.payload as { stream?: { delta?: string } }).stream?.delta).toBe('chunk')
})
})