Fix prop confirmation bug, add Wan 2.7 model, refine multiple UI details, improve prop generation quality and aspect ratio, remove text overlays from Asset Center created images, and optimize prop filtering logic

This commit is contained in:
saturn
2026-04-03 22:36:41 +08:00
parent 854b932e67
commit 78b93331b4
136 changed files with 3393 additions and 875 deletions

View File

@@ -54,6 +54,7 @@ describe('location-backed assets service', () => {
novelPromotionProjectId: 'novel-project-1',
name: 'Bronze Dagger',
summary: 'Old bronze dagger',
initialDescription: 'A bronze dagger with a carved handle and weathered blade',
kind: 'prop',
})
@@ -62,7 +63,7 @@ describe('location-backed assets service', () => {
{
locationId: result.id,
imageIndex: 0,
description: 'Old bronze dagger',
description: 'A bronze dagger with a carved handle and weathered blade',
availableSlots: '[]',
},
],

View File

@@ -2,13 +2,13 @@ import { describe, expect, it } from 'vitest'
import { canGenerateLocationBackedAsset, resolveLocationBackedGenerateType } from '@/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/location-backed-asset'
describe('location-backed asset generation rules', () => {
it('allows props to generate from summary even before any image slot exists', () => {
it('requires props to have a visual description before generation', () => {
expect(canGenerateLocationBackedAsset({
id: 'prop-1',
name: '金箍棒',
summary: '一根两头包裹金片的黑铁长棍',
images: [],
})).toBe(true)
}, 'prop')).toBe(false)
})
it('allows locations to generate from seeded image descriptions', () => {
@@ -27,7 +27,7 @@ describe('location-backed asset generation rules', () => {
isSelected: false,
},
],
})).toBe(true)
}, 'location')).toBe(true)
})
it('routes prop generation through the prop branch', () => {

View File

@@ -0,0 +1,127 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const deleteObjectMock = vi.hoisted(() => vi.fn())
const resolveStorageKeyFromMediaValueMock = vi.hoisted(() => vi.fn())
const prismaMock = vi.hoisted(() => ({
novelPromotionLocation: {
findUnique: vi.fn(),
update: vi.fn(),
},
locationImage: {
update: vi.fn(),
deleteMany: vi.fn(),
},
$transaction: vi.fn(),
}))
vi.mock('@/lib/prisma', () => ({
prisma: prismaMock,
}))
vi.mock('@/lib/storage', () => ({
deleteObject: deleteObjectMock,
}))
vi.mock('@/lib/media/service', () => ({
resolveStorageKeyFromMediaValue: resolveStorageKeyFromMediaValueMock,
}))
describe('project location-backed selection service', () => {
beforeEach(() => {
vi.clearAllMocks()
prismaMock.$transaction.mockImplementation(async (
callback: (tx: {
locationImage: {
update: typeof prismaMock.locationImage.update
deleteMany: typeof prismaMock.locationImage.deleteMany
}
novelPromotionLocation: {
update: typeof prismaMock.novelPromotionLocation.update
}
}) => Promise<void>,
) => callback({
locationImage: prismaMock.locationImage,
novelPromotionLocation: prismaMock.novelPromotionLocation,
}))
resolveStorageKeyFromMediaValueMock.mockImplementation(async (value: string) => `key:${value}`)
deleteObjectMock.mockResolvedValue(undefined)
prismaMock.locationImage.deleteMany.mockResolvedValue({ count: 1 })
prismaMock.locationImage.update.mockResolvedValue(undefined)
prismaMock.novelPromotionLocation.update.mockResolvedValue(undefined)
})
it('confirms a prop selection by keeping only the selected render', async () => {
prismaMock.novelPromotionLocation.findUnique.mockResolvedValue({
id: 'prop-1',
selectedImageId: 'prop-image-2',
images: [
{
id: 'prop-image-1',
imageIndex: 0,
imageUrl: 'https://example.com/prop-1.png',
isSelected: false,
},
{
id: 'prop-image-2',
imageIndex: 1,
imageUrl: 'https://example.com/prop-2.png',
isSelected: true,
},
],
})
const mod = await import('@/lib/assets/services/project-location-backed-selection')
const result = await mod.confirmProjectLocationBackedSelection('prop-1')
expect(result).toEqual({ success: true })
expect(resolveStorageKeyFromMediaValueMock).toHaveBeenCalledWith('https://example.com/prop-1.png')
expect(deleteObjectMock).toHaveBeenCalledWith('key:https://example.com/prop-1.png')
expect(prismaMock.locationImage.deleteMany).toHaveBeenCalledWith({
where: {
locationId: 'prop-1',
id: { not: 'prop-image-2' },
},
})
expect(prismaMock.locationImage.update).toHaveBeenCalledWith({
where: { id: 'prop-image-2' },
data: {
imageIndex: 0,
isSelected: true,
},
})
expect(prismaMock.novelPromotionLocation.update).toHaveBeenCalledWith({
where: { id: 'prop-1' },
data: { selectedImageId: 'prop-image-2' },
})
})
it('fails explicitly when confirming without a selected prop render', async () => {
prismaMock.novelPromotionLocation.findUnique.mockResolvedValue({
id: 'prop-1',
selectedImageId: null,
images: [
{
id: 'prop-image-1',
imageIndex: 0,
imageUrl: 'https://example.com/prop-1.png',
isSelected: false,
},
{
id: 'prop-image-2',
imageIndex: 1,
imageUrl: 'https://example.com/prop-2.png',
isSelected: false,
},
],
})
const mod = await import('@/lib/assets/services/project-location-backed-selection')
await expect(mod.confirmProjectLocationBackedSelection('prop-1')).rejects.toMatchObject({
code: 'INVALID_PARAMS',
})
expect(prismaMock.locationImage.deleteMany).not.toHaveBeenCalled()
expect(deleteObjectMock).not.toHaveBeenCalled()
})
})