refactor: analysis workflow architecture

fix: NEXTAUTH_URL

fix: prevent project model edits from affecting default model
This commit is contained in:
saturn
2026-03-16 21:48:57 +08:00
parent ecbd183a77
commit 9aff44e37a
58 changed files with 2753 additions and 7985 deletions

View File

@@ -1,8 +1,9 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ApiError } from '@/lib/api-errors'
import { buildDefaultTaskBillingInfo } from '@/lib/billing/task-policy'
import { createRun } from '@/lib/run-runtime/service'
import { submitTask } from '@/lib/task/submitter'
import { TASK_TYPE } from '@/lib/task/types'
import { TASK_STATUS, TASK_TYPE } from '@/lib/task/types'
import { prisma } from '../../helpers/prisma'
import { resetBillingState } from '../../helpers/db-reset'
import { createTestUser, seedBalance } from '../../helpers/billing-fixtures'
@@ -11,19 +12,23 @@ const queueState = vi.hoisted(() => ({
mode: 'success' as 'success' | 'fail',
errorMessage: 'queue add failed',
}))
const addTaskJobMock = vi.hoisted(() => vi.fn(async () => ({ id: 'mock-job' })))
const publishTaskEventMock = vi.hoisted(() => vi.fn(async () => ({})))
vi.mock('@/lib/task/queues', () => ({
addTaskJob: vi.fn(async () => {
addTaskJob: addTaskJobMock,
}))
vi.mock('@/lib/task/publisher', () => ({
publishTaskEvent: publishTaskEventMock,
}))
addTaskJobMock.mockImplementation(async () => {
if (queueState.mode === 'fail') {
throw new Error(queueState.errorMessage)
}
return { id: 'mock-job' }
}),
}))
vi.mock('@/lib/task/publisher', () => ({
publishTaskEvent: vi.fn(async () => ({})),
}))
})
describe('billing/submitter integration', () => {
beforeEach(async () => {
@@ -31,6 +36,7 @@ describe('billing/submitter integration', () => {
process.env.BILLING_MODE = 'ENFORCE'
queueState.mode = 'success'
queueState.errorMessage = 'queue add failed'
vi.clearAllMocks()
})
it('builds billing info server-side for billable task submission', async () => {
@@ -181,4 +187,140 @@ describe('billing/submitter integration', () => {
const freeze = await prisma.balanceFreeze.findFirst({ orderBy: { createdAt: 'desc' } })
expect(freeze?.status).toBe('rolled_back')
})
it('reuses the active core analysis run instead of creating a second run', async () => {
process.env.BILLING_MODE = 'OFF'
const user = await createTestUser()
const existingTask = await prisma.task.create({
data: {
userId: user.id,
projectId: 'project-core',
episodeId: 'episode-core',
type: TASK_TYPE.STORY_TO_SCRIPT_RUN,
targetType: 'NovelPromotionEpisode',
targetId: 'episode-core',
status: TASK_STATUS.QUEUED,
payload: {
episodeId: 'episode-core',
analysisModel: 'model-core',
meta: { locale: 'zh' },
},
queuedAt: new Date(),
},
})
const run = await createRun({
userId: user.id,
projectId: 'project-core',
episodeId: 'episode-core',
workflowType: TASK_TYPE.STORY_TO_SCRIPT_RUN,
taskType: TASK_TYPE.STORY_TO_SCRIPT_RUN,
taskId: existingTask.id,
targetType: 'NovelPromotionEpisode',
targetId: 'episode-core',
input: {
episodeId: 'episode-core',
analysisModel: 'model-core',
meta: { locale: 'zh' },
},
})
await prisma.task.update({
where: { id: existingTask.id },
data: {
payload: {
episodeId: 'episode-core',
analysisModel: 'model-core',
runId: run.id,
meta: { locale: 'zh', runId: run.id },
},
},
})
const result = await submitTask({
userId: user.id,
locale: 'zh',
projectId: 'project-core',
episodeId: 'episode-core',
type: TASK_TYPE.STORY_TO_SCRIPT_RUN,
targetType: 'NovelPromotionEpisode',
targetId: 'episode-core',
payload: {
episodeId: 'episode-core',
analysisModel: 'model-core',
},
dedupeKey: 'story_to_script:episode-core',
})
expect(result.deduped).toBe(true)
expect(result.taskId).toBe(existingTask.id)
expect(result.runId).toBe(run.id)
expect(await prisma.graphRun.count()).toBe(1)
expect(addTaskJobMock).not.toHaveBeenCalled()
})
it('reattaches a new task to the existing active run when the old task is already terminal', async () => {
process.env.BILLING_MODE = 'OFF'
const user = await createTestUser()
const failedTask = await prisma.task.create({
data: {
userId: user.id,
projectId: 'project-core-retry',
episodeId: 'episode-core-retry',
type: TASK_TYPE.SCRIPT_TO_STORYBOARD_RUN,
targetType: 'NovelPromotionEpisode',
targetId: 'episode-core-retry',
status: TASK_STATUS.FAILED,
errorCode: 'TEST_FAILED',
errorMessage: 'old task already failed',
payload: {
episodeId: 'episode-core-retry',
analysisModel: 'model-core',
meta: { locale: 'zh' },
},
queuedAt: new Date(),
finishedAt: new Date(),
},
})
const run = await createRun({
userId: user.id,
projectId: 'project-core-retry',
episodeId: 'episode-core-retry',
workflowType: TASK_TYPE.SCRIPT_TO_STORYBOARD_RUN,
taskType: TASK_TYPE.SCRIPT_TO_STORYBOARD_RUN,
taskId: failedTask.id,
targetType: 'NovelPromotionEpisode',
targetId: 'episode-core-retry',
input: {
episodeId: 'episode-core-retry',
analysisModel: 'model-core',
meta: { locale: 'zh' },
},
})
const result = await submitTask({
userId: user.id,
locale: 'zh',
projectId: 'project-core-retry',
episodeId: 'episode-core-retry',
type: TASK_TYPE.SCRIPT_TO_STORYBOARD_RUN,
targetType: 'NovelPromotionEpisode',
targetId: 'episode-core-retry',
payload: {
episodeId: 'episode-core-retry',
analysisModel: 'model-core',
},
dedupeKey: 'script_to_storyboard:episode-core-retry',
})
expect(result.deduped).toBe(false)
expect(result.runId).toBe(run.id)
expect(result.taskId).not.toBe(failedTask.id)
const refreshedRun = await prisma.graphRun.findUnique({ where: { id: run.id } })
const newTask = await prisma.task.findUnique({ where: { id: result.taskId } })
expect(refreshedRun?.taskId).toBe(result.taskId)
expect(newTask?.status).toBe(TASK_STATUS.QUEUED)
expect(newTask?.payload).toMatchObject({
runId: run.id,
})
})
})