196 lines
6.4 KiB
TypeScript
196 lines
6.4 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest'
|
|
import { reconcileActiveRunsFromTasks } from '@/lib/run-runtime/reconcile'
|
|
import { RUN_STATUS, RUN_STEP_STATUS } from '@/lib/run-runtime/types'
|
|
import { TASK_STATUS, TASK_TYPE } from '@/lib/task/types'
|
|
import { prisma } from '../../helpers/prisma'
|
|
import { resetBillingState } from '../../helpers/db-reset'
|
|
import { createTestUser } from '../../helpers/billing-fixtures'
|
|
|
|
describe('run runtime reconcileActiveRunsFromTasks', () => {
|
|
beforeEach(async () => {
|
|
await resetBillingState()
|
|
})
|
|
|
|
it('marks a running run completed when the linked task already completed', async () => {
|
|
const user = await createTestUser()
|
|
const finishedAt = new Date('2026-03-30T08:00:00.000Z')
|
|
const task = await prisma.task.create({
|
|
data: {
|
|
userId: user.id,
|
|
projectId: 'project-run-complete',
|
|
episodeId: 'episode-run-complete',
|
|
type: TASK_TYPE.STORY_TO_SCRIPT_RUN,
|
|
targetType: 'NovelPromotionEpisode',
|
|
targetId: 'episode-run-complete',
|
|
status: TASK_STATUS.COMPLETED,
|
|
progress: 100,
|
|
payload: { episodeId: 'episode-run-complete' },
|
|
result: {
|
|
episodeId: 'episode-run-complete',
|
|
persistedClips: 12,
|
|
},
|
|
queuedAt: new Date('2026-03-30T07:55:00.000Z'),
|
|
startedAt: new Date('2026-03-30T07:56:00.000Z'),
|
|
finishedAt,
|
|
},
|
|
})
|
|
const run = await prisma.graphRun.create({
|
|
data: {
|
|
userId: user.id,
|
|
projectId: 'project-run-complete',
|
|
episodeId: 'episode-run-complete',
|
|
workflowType: TASK_TYPE.STORY_TO_SCRIPT_RUN,
|
|
taskType: TASK_TYPE.STORY_TO_SCRIPT_RUN,
|
|
taskId: task.id,
|
|
targetType: 'NovelPromotionEpisode',
|
|
targetId: 'episode-run-complete',
|
|
status: RUN_STATUS.RUNNING,
|
|
leaseOwner: 'worker:story-to-script',
|
|
leaseExpiresAt: new Date('2026-03-30T08:05:00.000Z'),
|
|
heartbeatAt: new Date('2026-03-30T07:59:30.000Z'),
|
|
queuedAt: new Date('2026-03-30T07:55:00.000Z'),
|
|
startedAt: new Date('2026-03-30T07:56:00.000Z'),
|
|
},
|
|
})
|
|
await prisma.graphStep.create({
|
|
data: {
|
|
runId: run.id,
|
|
stepKey: 'story_to_script_persist',
|
|
stepTitle: 'Persist screenplay',
|
|
status: RUN_STEP_STATUS.RUNNING,
|
|
currentAttempt: 1,
|
|
stepIndex: 4,
|
|
stepTotal: 4,
|
|
startedAt: new Date('2026-03-30T07:58:00.000Z'),
|
|
},
|
|
})
|
|
|
|
const reconciled = await reconcileActiveRunsFromTasks()
|
|
|
|
expect(reconciled).toEqual([{
|
|
runId: run.id,
|
|
taskId: task.id,
|
|
nextStatus: 'completed',
|
|
reason: 'linked task already completed',
|
|
}])
|
|
|
|
const refreshedRun = await prisma.graphRun.findUnique({ where: { id: run.id } })
|
|
expect(refreshedRun).toMatchObject({
|
|
status: RUN_STATUS.COMPLETED,
|
|
output: {
|
|
episodeId: 'episode-run-complete',
|
|
persistedClips: 12,
|
|
},
|
|
errorCode: null,
|
|
errorMessage: null,
|
|
leaseOwner: null,
|
|
leaseExpiresAt: null,
|
|
heartbeatAt: null,
|
|
})
|
|
expect(refreshedRun?.finishedAt?.toISOString()).toBe(finishedAt.toISOString())
|
|
|
|
const refreshedStep = await prisma.graphStep.findUnique({
|
|
where: {
|
|
runId_stepKey: {
|
|
runId: run.id,
|
|
stepKey: 'story_to_script_persist',
|
|
},
|
|
},
|
|
})
|
|
expect(refreshedStep).toMatchObject({
|
|
status: RUN_STEP_STATUS.COMPLETED,
|
|
lastErrorCode: null,
|
|
lastErrorMessage: null,
|
|
})
|
|
expect(refreshedStep?.finishedAt?.toISOString()).toBe(finishedAt.toISOString())
|
|
})
|
|
|
|
it('marks a running run failed when the linked task already failed', async () => {
|
|
const user = await createTestUser()
|
|
const finishedAt = new Date('2026-03-30T09:00:00.000Z')
|
|
const task = await prisma.task.create({
|
|
data: {
|
|
userId: user.id,
|
|
projectId: 'project-run-failed',
|
|
episodeId: 'episode-run-failed',
|
|
type: TASK_TYPE.STORY_TO_SCRIPT_RUN,
|
|
targetType: 'NovelPromotionEpisode',
|
|
targetId: 'episode-run-failed',
|
|
status: TASK_STATUS.FAILED,
|
|
progress: 72,
|
|
payload: { episodeId: 'episode-run-failed' },
|
|
errorCode: 'WATCHDOG_TIMEOUT',
|
|
errorMessage: 'Task heartbeat timeout',
|
|
queuedAt: new Date('2026-03-30T08:50:00.000Z'),
|
|
startedAt: new Date('2026-03-30T08:51:00.000Z'),
|
|
finishedAt,
|
|
},
|
|
})
|
|
const run = await prisma.graphRun.create({
|
|
data: {
|
|
userId: user.id,
|
|
projectId: 'project-run-failed',
|
|
episodeId: 'episode-run-failed',
|
|
workflowType: TASK_TYPE.STORY_TO_SCRIPT_RUN,
|
|
taskType: TASK_TYPE.STORY_TO_SCRIPT_RUN,
|
|
taskId: task.id,
|
|
targetType: 'NovelPromotionEpisode',
|
|
targetId: 'episode-run-failed',
|
|
status: RUN_STATUS.RUNNING,
|
|
leaseOwner: 'worker:story-to-script',
|
|
leaseExpiresAt: new Date('2026-03-30T08:55:00.000Z'),
|
|
heartbeatAt: new Date('2026-03-30T08:54:00.000Z'),
|
|
queuedAt: new Date('2026-03-30T08:50:00.000Z'),
|
|
startedAt: new Date('2026-03-30T08:51:00.000Z'),
|
|
},
|
|
})
|
|
await prisma.graphStep.create({
|
|
data: {
|
|
runId: run.id,
|
|
stepKey: 'screenplay_clip-1',
|
|
stepTitle: 'Screenplay clip 1',
|
|
status: RUN_STEP_STATUS.RUNNING,
|
|
currentAttempt: 1,
|
|
stepIndex: 3,
|
|
stepTotal: 6,
|
|
startedAt: new Date('2026-03-30T08:52:00.000Z'),
|
|
},
|
|
})
|
|
|
|
const reconciled = await reconcileActiveRunsFromTasks()
|
|
|
|
expect(reconciled).toEqual([{
|
|
runId: run.id,
|
|
taskId: task.id,
|
|
nextStatus: 'failed',
|
|
reason: 'linked task already failed',
|
|
}])
|
|
|
|
const refreshedRun = await prisma.graphRun.findUnique({ where: { id: run.id } })
|
|
expect(refreshedRun).toMatchObject({
|
|
status: RUN_STATUS.FAILED,
|
|
errorCode: 'WATCHDOG_TIMEOUT',
|
|
errorMessage: 'Task heartbeat timeout',
|
|
leaseOwner: null,
|
|
leaseExpiresAt: null,
|
|
heartbeatAt: null,
|
|
})
|
|
expect(refreshedRun?.finishedAt?.toISOString()).toBe(finishedAt.toISOString())
|
|
|
|
const refreshedStep = await prisma.graphStep.findUnique({
|
|
where: {
|
|
runId_stepKey: {
|
|
runId: run.id,
|
|
stepKey: 'screenplay_clip-1',
|
|
},
|
|
},
|
|
})
|
|
expect(refreshedStep).toMatchObject({
|
|
status: RUN_STEP_STATUS.FAILED,
|
|
lastErrorCode: 'WATCHDOG_TIMEOUT',
|
|
lastErrorMessage: 'Task heartbeat timeout',
|
|
})
|
|
expect(refreshedStep?.finishedAt?.toISOString()).toBe(finishedAt.toISOString())
|
|
})
|
|
})
|