fix: resolve confirmed character hidden bug, remove online font dependency, improve UI/UX experience

This commit is contained in:
saturn
2026-03-21 14:35:32 +08:00
parent f364bbc9e4
commit a6ad11b9c4
42 changed files with 1189 additions and 553 deletions

View File

@@ -11,7 +11,10 @@ const prismaMock = vi.hoisted(() => ({
novelPromotionEpisode: { findFirst: vi.fn() },
novelPromotionCharacter: { create: vi.fn(async () => ({ id: 'char-new-1' })) },
novelPromotionLocation: { create: vi.fn(async () => ({ id: 'loc-new-1' })) },
locationImage: { create: vi.fn(async () => ({})) },
locationImage: {
create: vi.fn(async () => ({})),
createMany: vi.fn(async () => ({ count: 1 })),
},
}))
const llmMock = vi.hoisted(() => ({
@@ -49,6 +52,7 @@ vi.mock('@/lib/prompt-i18n', () => ({
PROMPT_IDS: {
NP_AGENT_CHARACTER_PROFILE: 'char',
NP_SELECT_LOCATION: 'loc',
NP_SELECT_PROP: 'prop',
},
buildPrompt: vi.fn(() => 'analysis-prompt'),
}))
@@ -75,6 +79,10 @@ describe('worker analyze-novel behavior', () => {
beforeEach(() => {
vi.clearAllMocks()
prismaMock.novelPromotionLocation.create
.mockResolvedValueOnce({ id: 'loc-new-1' })
.mockResolvedValueOnce({ id: 'prop-new-1' })
prismaMock.project.findUnique.mockResolvedValue({
id: 'project-1',
mode: 'novel-promotion',
@@ -114,6 +122,14 @@ describe('worker analyze-novel behavior', () => {
},
],
}))
.mockReturnValueOnce(JSON.stringify({
props: [
{
name: '金箍棒',
summary: '一根两头包裹金片的黑铁长棍',
},
],
}))
})
it('no global text and no episode text -> explicit error', async () => {
@@ -137,10 +153,10 @@ describe('worker analyze-novel behavior', () => {
success: true,
characters: [{ id: 'char-new-1' }],
locations: [{ id: 'loc-new-1' }],
props: [],
props: [{ id: 'prop-new-1' }],
characterCount: 1,
locationCount: 1,
propCount: 0,
propCount: 1,
})
expect(prismaMock.novelPromotionCharacter.create).toHaveBeenCalledWith(
@@ -163,12 +179,24 @@ describe('worker analyze-novel behavior', () => {
}),
)
expect(prismaMock.locationImage.create).toHaveBeenCalledWith({
data: {
locationId: 'loc-new-1',
imageIndex: 0,
description: '雨夜街道',
},
expect(prismaMock.locationImage.create).not.toHaveBeenCalled()
expect(prismaMock.locationImage.createMany).toHaveBeenNthCalledWith(1, {
data: [
{
locationId: 'loc-new-1',
imageIndex: 0,
description: '雨夜街道',
},
],
})
expect(prismaMock.locationImage.createMany).toHaveBeenNthCalledWith(2, {
data: [
{
locationId: 'prop-new-1',
imageIndex: 0,
description: '一根两头包裹金片的黑铁长棍',
},
],
})
expect(prismaMock.novelPromotionProject.update).toHaveBeenCalledWith({

View File

@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { TASK_TYPE, type TaskJobData } from '@/lib/task/types'
const prismaMock = vi.hoisted(() => ({
$transaction: vi.fn(),
novelPromotionCharacter: {
findFirst: vi.fn(),
findMany: vi.fn(),
@@ -10,6 +11,7 @@ const prismaMock = vi.hoisted(() => ({
},
characterAppearance: {
create: vi.fn(async () => ({})),
deleteMany: vi.fn(async () => ({ count: 1 })),
},
}))
@@ -89,6 +91,9 @@ function buildJob(type: TaskJobData['type'], payload: Record<string, unknown>):
describe('worker character-profile behavior', () => {
beforeEach(() => {
vi.clearAllMocks()
prismaMock.$transaction.mockImplementation(async (callback: (tx: typeof prismaMock) => Promise<unknown>) => {
return await callback(prismaMock)
})
llmMock.getCompletionContent.mockReturnValue(
JSON.stringify({
@@ -134,10 +139,13 @@ describe('worker character-profile behavior', () => {
await expect(handleCharacterProfileTask(job)).rejects.toThrow('Unsupported character profile task type')
})
it('confirm profile success -> creates appearance and marks profileConfirmed', async () => {
it('confirm profile success -> rebuilds appearances and marks profileConfirmed', async () => {
const job = buildJob(TASK_TYPE.CHARACTER_PROFILE_CONFIRM, { characterId: 'character-1' })
const result = await handleCharacterProfileTask(job)
expect(prismaMock.characterAppearance.deleteMany).toHaveBeenCalledWith({
where: { characterId: 'character-1' },
})
expect(prismaMock.characterAppearance.create).toHaveBeenCalledWith({
data: expect.objectContaining({
characterId: 'character-1',
@@ -149,7 +157,10 @@ describe('worker character-profile behavior', () => {
expect(prismaMock.novelPromotionCharacter.update).toHaveBeenCalledWith({
where: { id: 'character-1' },
data: { profileConfirmed: true },
data: {
profileData: JSON.stringify({ archetype: 'lead' }),
profileConfirmed: true,
},
})
expect(result).toEqual(expect.objectContaining({
@@ -171,4 +182,18 @@ describe('worker character-profile behavior', () => {
})
expect(prismaMock.characterAppearance.create).toHaveBeenCalledTimes(2)
})
it('reconfirm with existing appearances -> replaces old rows instead of colliding on unique index', async () => {
const job = buildJob(TASK_TYPE.CHARACTER_PROFILE_CONFIRM, { characterId: 'character-1' })
await expect(handleCharacterProfileTask(job)).resolves.toEqual(expect.objectContaining({
success: true,
}))
expect(prismaMock.$transaction).toHaveBeenCalledTimes(1)
expect(prismaMock.characterAppearance.deleteMany).toHaveBeenCalledWith({
where: { characterId: 'character-1' },
})
expect(prismaMock.characterAppearance.create).toHaveBeenCalledTimes(1)
})
})