release: opensource snapshot 2026-02-27 19:25:00

This commit is contained in:
saturn
2026-02-27 19:25:00 +08:00
commit 5de9622c8b
1055 changed files with 164772 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
# Behavior Test Standard
## Scope
- `tests/integration/api/contract/**/*.test.ts`
- `tests/integration/chain/**/*.test.ts`
- `tests/unit/worker/**/*.test.ts`
## Must-have
- Assert observable results: response payload/status, persisted fields, or queue/job payload.
- Include at least one concrete-value assertion for each key business branch.
- Cover at least one failure branch for each critical route/handler.
## Forbidden patterns
- Source-text contract assertions (for example checking route code contains `apiHandler`, `submitTask`, `maybeSubmitLLMTask`).
- Using only weak call assertions like `toHaveBeenCalled()` as the primary proof.
- Structural tests that pass without executing route/worker logic.
## Minimum assertion quality
- Prefer `toHaveBeenCalledWith(...)` with `objectContaining(...)` on critical fields.
- Validate exact business fields (`description`, `imageUrl`, `referenceImages`, `aspectRatio`, `taskId`, `async`).
- For async task chains, validate queue selection and job metadata (`jobId`, `priority`, `type`).
## Regression rule
- One historical bug must map to at least one dedicated regression test case.
- Bug fix without matching behavior regression test is incomplete.

View File

@@ -0,0 +1,24 @@
import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { REQUIREMENTS_MATRIX } from './requirements-matrix'
function fileExists(repoPath: string) {
return fs.existsSync(path.resolve(process.cwd(), repoPath))
}
describe('requirements matrix integrity', () => {
it('requirement ids are unique', () => {
const ids = REQUIREMENTS_MATRIX.map((entry) => entry.id)
expect(new Set(ids).size).toBe(ids.length)
})
it('all declared test files exist', () => {
for (const entry of REQUIREMENTS_MATRIX) {
expect(entry.tests.length, entry.id).toBeGreaterThan(0)
for (const testPath of entry.tests) {
expect(fileExists(testPath), `${entry.id} -> ${testPath}`).toBe(true)
}
}
})
})

View File

@@ -0,0 +1,84 @@
export type RequirementPriority = 'P0' | 'P1' | 'P2'
export type RequirementCoverageEntry = {
id: string
feature: string
userValue: string
risk: string
priority: RequirementPriority
tests: ReadonlyArray<string>
}
export const REQUIREMENTS_MATRIX: ReadonlyArray<RequirementCoverageEntry> = [
{
id: 'REQ-ASSETHUB-CHARACTER-EDIT',
feature: 'Asset Hub character edit',
userValue: '角色信息编辑后立即可见并正确保存',
risk: '字段映射漂移导致保存失败或误写',
priority: 'P0',
tests: [
'tests/integration/api/contract/crud-routes.test.ts',
'tests/integration/chain/text.chain.test.ts',
],
},
{
id: 'REQ-ASSETHUB-REFERENCE-TO-CHARACTER',
feature: 'Asset Hub reference-to-character',
userValue: '上传参考图后生成角色形象且使用参考图',
risk: 'referenceImages 丢失或分支走错',
priority: 'P0',
tests: [
'tests/unit/helpers/reference-to-character-helpers.test.ts',
'tests/unit/worker/reference-to-character.test.ts',
'tests/integration/chain/text.chain.test.ts',
],
},
{
id: 'REQ-NP-GENERATE-IMAGE',
feature: 'Novel promotion image generation',
userValue: '角色/场景/分镜图可稳定生成并回写',
risk: '任务 payload 漂移、worker 写回错误实体',
priority: 'P0',
tests: [
'tests/integration/api/contract/direct-submit-routes.test.ts',
'tests/unit/worker/image-task-handlers-core.test.ts',
'tests/integration/chain/image.chain.test.ts',
],
},
{
id: 'REQ-NP-GENERATE-VIDEO',
feature: 'Novel promotion video generation',
userValue: '面板视频可生成并可追踪状态',
risk: 'panel 定位错误、model 能力判断错误、状态错乱',
priority: 'P0',
tests: [
'tests/integration/api/contract/direct-submit-routes.test.ts',
'tests/unit/worker/video-worker.test.ts',
'tests/integration/chain/video.chain.test.ts',
],
},
{
id: 'REQ-NP-TEXT-ANALYSIS',
feature: 'Text analysis and storyboard orchestration',
userValue: '文本分析链路稳定并可回放结果',
risk: 'step 编排变化导致结果结构损坏',
priority: 'P1',
tests: [
'tests/integration/api/contract/llm-observe-routes.test.ts',
'tests/unit/worker/script-to-storyboard.test.ts',
'tests/integration/chain/text.chain.test.ts',
],
},
{
id: 'REQ-TASK-STATE-CONSISTENCY',
feature: 'Task state and SSE consistency',
userValue: '前端状态与任务真实状态一致',
risk: 'target-state 与 SSE 失配导致误提示',
priority: 'P0',
tests: [
'tests/unit/helpers/task-state-service.test.ts',
'tests/integration/api/contract/task-infra-routes.test.ts',
'tests/unit/optimistic/sse-invalidation.test.ts',
],
},
]

View File

@@ -0,0 +1,50 @@
import { ROUTE_CATALOG, type RouteCatalogEntry } from './route-catalog'
export type RouteBehaviorMatrixEntry = {
routeFile: string
contractGroup: RouteCatalogEntry['contractGroup']
caseId: string
tests: ReadonlyArray<string>
}
const CONTRACT_TEST_BY_GROUP: Record<RouteCatalogEntry['contractGroup'], string> = {
'llm-observe-routes': 'tests/integration/api/contract/llm-observe-routes.test.ts',
'direct-submit-routes': 'tests/integration/api/contract/direct-submit-routes.test.ts',
'crud-asset-hub-routes': 'tests/integration/api/contract/crud-routes.test.ts',
'crud-novel-promotion-routes': 'tests/integration/api/contract/crud-routes.test.ts',
'task-infra-routes': 'tests/integration/api/contract/task-infra-routes.test.ts',
'user-project-routes': 'tests/integration/api/contract/crud-routes.test.ts',
'auth-routes': 'tests/integration/api/contract/crud-routes.test.ts',
'infra-routes': 'tests/integration/api/contract/crud-routes.test.ts',
}
function resolveChainTest(routeFile: string): string {
if (routeFile.includes('/generate-video/') || routeFile.includes('/lip-sync/')) {
return 'tests/integration/chain/video.chain.test.ts'
}
if (routeFile.includes('/voice-') || routeFile.includes('/voice/')) {
return 'tests/integration/chain/voice.chain.test.ts'
}
if (
routeFile.includes('/analyze')
|| routeFile.includes('/story-to-script')
|| routeFile.includes('/script-to-storyboard')
|| routeFile.includes('/screenplay-conversion')
|| routeFile.includes('/reference-to-character')
) {
return 'tests/integration/chain/text.chain.test.ts'
}
return 'tests/integration/chain/image.chain.test.ts'
}
export const ROUTE_BEHAVIOR_MATRIX: ReadonlyArray<RouteBehaviorMatrixEntry> = ROUTE_CATALOG.map((entry) => ({
routeFile: entry.routeFile,
contractGroup: entry.contractGroup,
caseId: `ROUTE:${entry.routeFile.replace(/^src\/app\/api\//, '').replace(/\/route\.ts$/, '')}`,
tests: [
CONTRACT_TEST_BY_GROUP[entry.contractGroup],
resolveChainTest(entry.routeFile),
],
}))
export const ROUTE_BEHAVIOR_COUNT = ROUTE_BEHAVIOR_MATRIX.length

View File

@@ -0,0 +1,213 @@
export type RouteCategory =
| 'asset-hub'
| 'novel-promotion'
| 'projects'
| 'tasks'
| 'user'
| 'auth'
| 'infra'
| 'system'
export type RouteContractGroup =
| 'llm-observe-routes'
| 'direct-submit-routes'
| 'crud-asset-hub-routes'
| 'crud-novel-promotion-routes'
| 'task-infra-routes'
| 'user-project-routes'
| 'auth-routes'
| 'infra-routes'
export type RouteCatalogEntry = {
routeFile: string
category: RouteCategory
contractGroup: RouteContractGroup
}
const ROUTE_FILES = [
'src/app/api/asset-hub/ai-design-character/route.ts',
'src/app/api/asset-hub/ai-design-location/route.ts',
'src/app/api/asset-hub/ai-modify-character/route.ts',
'src/app/api/asset-hub/ai-modify-location/route.ts',
'src/app/api/asset-hub/appearances/route.ts',
'src/app/api/asset-hub/character-voice/route.ts',
'src/app/api/asset-hub/characters/[characterId]/appearances/[appearanceIndex]/route.ts',
'src/app/api/asset-hub/characters/[characterId]/route.ts',
'src/app/api/asset-hub/characters/route.ts',
'src/app/api/asset-hub/folders/[folderId]/route.ts',
'src/app/api/asset-hub/folders/route.ts',
'src/app/api/asset-hub/generate-image/route.ts',
'src/app/api/asset-hub/locations/[locationId]/route.ts',
'src/app/api/asset-hub/locations/route.ts',
'src/app/api/asset-hub/modify-image/route.ts',
'src/app/api/asset-hub/picker/route.ts',
'src/app/api/asset-hub/reference-to-character/route.ts',
'src/app/api/asset-hub/select-image/route.ts',
'src/app/api/asset-hub/undo-image/route.ts',
'src/app/api/asset-hub/update-asset-label/route.ts',
'src/app/api/asset-hub/upload-image/route.ts',
'src/app/api/asset-hub/upload-temp/route.ts',
'src/app/api/asset-hub/voice-design/route.ts',
'src/app/api/asset-hub/voices/[id]/route.ts',
'src/app/api/asset-hub/voices/route.ts',
'src/app/api/asset-hub/voices/upload/route.ts',
'src/app/api/auth/[...nextauth]/route.ts',
'src/app/api/auth/register/route.ts',
'src/app/api/cos/image/route.ts',
'src/app/api/files/[...path]/route.ts',
'src/app/api/novel-promotion/[projectId]/ai-create-character/route.ts',
'src/app/api/novel-promotion/[projectId]/ai-create-location/route.ts',
'src/app/api/novel-promotion/[projectId]/ai-modify-appearance/route.ts',
'src/app/api/novel-promotion/[projectId]/ai-modify-location/route.ts',
'src/app/api/novel-promotion/[projectId]/ai-modify-shot-prompt/route.ts',
'src/app/api/novel-promotion/[projectId]/analyze-global/route.ts',
'src/app/api/novel-promotion/[projectId]/analyze-shot-variants/route.ts',
'src/app/api/novel-promotion/[projectId]/analyze/route.ts',
'src/app/api/novel-promotion/[projectId]/assets/route.ts',
'src/app/api/novel-promotion/[projectId]/character-profile/batch-confirm/route.ts',
'src/app/api/novel-promotion/[projectId]/character-profile/confirm/route.ts',
'src/app/api/novel-promotion/[projectId]/character-voice/route.ts',
'src/app/api/novel-promotion/[projectId]/character/appearance/route.ts',
'src/app/api/novel-promotion/[projectId]/character/confirm-selection/route.ts',
'src/app/api/novel-promotion/[projectId]/character/route.ts',
'src/app/api/novel-promotion/[projectId]/cleanup-unselected-images/route.ts',
'src/app/api/novel-promotion/[projectId]/clips/[clipId]/route.ts',
'src/app/api/novel-promotion/[projectId]/clips/route.ts',
'src/app/api/novel-promotion/[projectId]/copy-from-global/route.ts',
'src/app/api/novel-promotion/[projectId]/download-images/route.ts',
'src/app/api/novel-promotion/[projectId]/download-videos/route.ts',
'src/app/api/novel-promotion/[projectId]/download-voices/route.ts',
'src/app/api/novel-promotion/[projectId]/editor/route.ts',
'src/app/api/novel-promotion/[projectId]/episodes/[episodeId]/route.ts',
'src/app/api/novel-promotion/[projectId]/episodes/batch/route.ts',
'src/app/api/novel-promotion/[projectId]/episodes/route.ts',
'src/app/api/novel-promotion/[projectId]/episodes/split-by-markers/route.ts',
'src/app/api/novel-promotion/[projectId]/episodes/split/route.ts',
'src/app/api/novel-promotion/[projectId]/generate-character-image/route.ts',
'src/app/api/novel-promotion/[projectId]/generate-image/route.ts',
'src/app/api/novel-promotion/[projectId]/generate-video/route.ts',
'src/app/api/novel-promotion/[projectId]/insert-panel/route.ts',
'src/app/api/novel-promotion/[projectId]/lip-sync/route.ts',
'src/app/api/novel-promotion/[projectId]/location/confirm-selection/route.ts',
'src/app/api/novel-promotion/[projectId]/location/route.ts',
'src/app/api/novel-promotion/[projectId]/modify-asset-image/route.ts',
'src/app/api/novel-promotion/[projectId]/modify-storyboard-image/route.ts',
'src/app/api/novel-promotion/[projectId]/panel-link/route.ts',
'src/app/api/novel-promotion/[projectId]/panel-variant/route.ts',
'src/app/api/novel-promotion/[projectId]/panel/route.ts',
'src/app/api/novel-promotion/[projectId]/panel/select-candidate/route.ts',
'src/app/api/novel-promotion/[projectId]/photography-plan/route.ts',
'src/app/api/novel-promotion/[projectId]/reference-to-character/route.ts',
'src/app/api/novel-promotion/[projectId]/regenerate-group/route.ts',
'src/app/api/novel-promotion/[projectId]/regenerate-panel-image/route.ts',
'src/app/api/novel-promotion/[projectId]/regenerate-single-image/route.ts',
'src/app/api/novel-promotion/[projectId]/regenerate-storyboard-text/route.ts',
'src/app/api/novel-promotion/[projectId]/route.ts',
'src/app/api/novel-promotion/[projectId]/screenplay-conversion/route.ts',
'src/app/api/novel-promotion/[projectId]/script-to-storyboard-stream/route.ts',
'src/app/api/novel-promotion/[projectId]/select-character-image/route.ts',
'src/app/api/novel-promotion/[projectId]/select-location-image/route.ts',
'src/app/api/novel-promotion/[projectId]/speaker-voice/route.ts',
'src/app/api/novel-promotion/[projectId]/story-to-script-stream/route.ts',
'src/app/api/novel-promotion/[projectId]/storyboard-group/route.ts',
'src/app/api/novel-promotion/[projectId]/storyboards/route.ts',
'src/app/api/novel-promotion/[projectId]/undo-regenerate/route.ts',
'src/app/api/novel-promotion/[projectId]/update-appearance/route.ts',
'src/app/api/novel-promotion/[projectId]/update-asset-label/route.ts',
'src/app/api/novel-promotion/[projectId]/update-location/route.ts',
'src/app/api/novel-promotion/[projectId]/update-prompt/route.ts',
'src/app/api/novel-promotion/[projectId]/upload-asset-image/route.ts',
'src/app/api/novel-promotion/[projectId]/video-proxy/route.ts',
'src/app/api/novel-promotion/[projectId]/video-urls/route.ts',
'src/app/api/novel-promotion/[projectId]/voice-analyze/route.ts',
'src/app/api/novel-promotion/[projectId]/voice-design/route.ts',
'src/app/api/novel-promotion/[projectId]/voice-generate/route.ts',
'src/app/api/novel-promotion/[projectId]/voice-lines/route.ts',
'src/app/api/projects/[projectId]/assets/route.ts',
'src/app/api/projects/[projectId]/costs/route.ts',
'src/app/api/projects/[projectId]/data/route.ts',
'src/app/api/projects/[projectId]/route.ts',
'src/app/api/projects/route.ts',
'src/app/api/sse/route.ts',
'src/app/api/system/boot-id/route.ts',
'src/app/api/task-target-states/route.ts',
'src/app/api/tasks/[taskId]/route.ts',
'src/app/api/tasks/dismiss/route.ts',
'src/app/api/tasks/route.ts',
'src/app/api/user-preference/route.ts',
'src/app/api/user/api-config/route.ts',
'src/app/api/user/api-config/test-connection/route.ts',
'src/app/api/user/balance/route.ts',
'src/app/api/user/costs/details/route.ts',
'src/app/api/user/costs/route.ts',
'src/app/api/user/models/route.ts',
'src/app/api/user/transactions/route.ts',
] as const
function resolveCategory(routeFile: string): RouteCategory {
if (routeFile.startsWith('src/app/api/asset-hub/')) return 'asset-hub'
if (routeFile.startsWith('src/app/api/novel-promotion/')) return 'novel-promotion'
if (routeFile.startsWith('src/app/api/projects/')) return 'projects'
if (routeFile.startsWith('src/app/api/tasks/') || routeFile === 'src/app/api/task-target-states/route.ts') return 'tasks'
if (routeFile.startsWith('src/app/api/user/') || routeFile === 'src/app/api/user-preference/route.ts') return 'user'
if (routeFile.startsWith('src/app/api/auth/')) return 'auth'
if (routeFile.startsWith('src/app/api/system/')) return 'system'
return 'infra'
}
function resolveContractGroup(routeFile: string): RouteContractGroup {
if (
routeFile.includes('/ai-')
|| routeFile.includes('/analyze')
|| routeFile.includes('/story-to-script-stream/')
|| routeFile.includes('/script-to-storyboard-stream/')
|| routeFile.includes('/screenplay-conversion/')
|| routeFile.includes('/reference-to-character/')
|| routeFile.includes('/character-profile/')
|| routeFile.endsWith('/clips/route.ts')
|| routeFile.endsWith('/episodes/split/route.ts')
|| routeFile.endsWith('/voice-analyze/route.ts')
) {
return 'llm-observe-routes'
}
if (
routeFile.endsWith('/generate-image/route.ts')
|| routeFile.endsWith('/generate-video/route.ts')
|| routeFile.endsWith('/modify-image/route.ts')
|| routeFile.endsWith('/voice-design/route.ts')
|| routeFile.endsWith('/insert-panel/route.ts')
|| routeFile.endsWith('/lip-sync/route.ts')
|| routeFile.endsWith('/modify-asset-image/route.ts')
|| routeFile.endsWith('/modify-storyboard-image/route.ts')
|| routeFile.endsWith('/panel-variant/route.ts')
|| routeFile.endsWith('/regenerate-group/route.ts')
|| routeFile.endsWith('/regenerate-panel-image/route.ts')
|| routeFile.endsWith('/regenerate-single-image/route.ts')
|| routeFile.endsWith('/regenerate-storyboard-text/route.ts')
|| routeFile.endsWith('/voice-generate/route.ts')
) {
return 'direct-submit-routes'
}
if (routeFile.startsWith('src/app/api/asset-hub/')) return 'crud-asset-hub-routes'
if (routeFile.startsWith('src/app/api/novel-promotion/')) return 'crud-novel-promotion-routes'
if (
routeFile.startsWith('src/app/api/tasks/')
|| routeFile === 'src/app/api/task-target-states/route.ts'
|| routeFile === 'src/app/api/sse/route.ts'
) {
return 'task-infra-routes'
}
if (routeFile.startsWith('src/app/api/projects/') || routeFile.startsWith('src/app/api/user/')) {
return 'user-project-routes'
}
if (routeFile.startsWith('src/app/api/auth/')) return 'auth-routes'
return 'infra-routes'
}
export const ROUTE_CATALOG: ReadonlyArray<RouteCatalogEntry> = ROUTE_FILES.map((routeFile) => ({
routeFile,
category: resolveCategory(routeFile),
contractGroup: resolveContractGroup(routeFile),
}))
export const ROUTE_COUNT = ROUTE_CATALOG.length

View File

@@ -0,0 +1,58 @@
import { TASK_TYPE, type TaskType } from '@/lib/task/types'
export type TaskTestLayer = 'unit-helper' | 'worker-unit' | 'api-contract' | 'chain'
export type TaskTypeCoverageEntry = {
taskType: TaskType
owner: string
layers: ReadonlyArray<TaskTestLayer>
}
const TASK_TYPE_OWNER_MAP = {
[TASK_TYPE.IMAGE_PANEL]: 'tests/unit/worker/panel-image-task-handler.test.ts',
[TASK_TYPE.IMAGE_CHARACTER]: 'tests/unit/worker/character-image-task-handler.test.ts',
[TASK_TYPE.IMAGE_LOCATION]: 'tests/unit/worker/location-image-task-handler.test.ts',
[TASK_TYPE.VIDEO_PANEL]: 'tests/unit/worker/video-worker.test.ts',
[TASK_TYPE.LIP_SYNC]: 'tests/unit/worker/video-worker.test.ts',
[TASK_TYPE.VOICE_LINE]: 'tests/unit/worker/voice-worker.test.ts',
[TASK_TYPE.VOICE_DESIGN]: 'tests/unit/worker/voice-worker.test.ts',
[TASK_TYPE.ASSET_HUB_VOICE_DESIGN]: 'tests/unit/worker/voice-worker.test.ts',
[TASK_TYPE.REGENERATE_STORYBOARD_TEXT]: 'tests/unit/worker/script-to-storyboard.test.ts',
[TASK_TYPE.INSERT_PANEL]: 'tests/unit/worker/script-to-storyboard.test.ts',
[TASK_TYPE.PANEL_VARIANT]: 'tests/unit/worker/panel-variant-task-handler.test.ts',
[TASK_TYPE.MODIFY_ASSET_IMAGE]: 'tests/unit/worker/image-task-handlers-core.test.ts',
[TASK_TYPE.REGENERATE_GROUP]: 'tests/unit/worker/image-task-handlers-core.test.ts',
[TASK_TYPE.ASSET_HUB_IMAGE]: 'tests/unit/worker/asset-hub-image-suffix.test.ts',
[TASK_TYPE.ASSET_HUB_MODIFY]: 'tests/unit/worker/modify-image-reference-description.test.ts',
[TASK_TYPE.ANALYZE_NOVEL]: 'tests/unit/worker/analyze-novel.test.ts',
[TASK_TYPE.STORY_TO_SCRIPT_RUN]: 'tests/unit/worker/story-to-script.test.ts',
[TASK_TYPE.SCRIPT_TO_STORYBOARD_RUN]: 'tests/unit/worker/script-to-storyboard.test.ts',
[TASK_TYPE.CLIPS_BUILD]: 'tests/unit/worker/clips-build.test.ts',
[TASK_TYPE.SCREENPLAY_CONVERT]: 'tests/unit/worker/screenplay-convert.test.ts',
[TASK_TYPE.VOICE_ANALYZE]: 'tests/unit/worker/voice-analyze.test.ts',
[TASK_TYPE.ANALYZE_GLOBAL]: 'tests/unit/worker/analyze-global.test.ts',
[TASK_TYPE.AI_MODIFY_APPEARANCE]: 'tests/unit/worker/shot-ai-prompt-appearance.test.ts',
[TASK_TYPE.AI_MODIFY_LOCATION]: 'tests/unit/worker/shot-ai-prompt-location.test.ts',
[TASK_TYPE.AI_MODIFY_SHOT_PROMPT]: 'tests/unit/worker/shot-ai-prompt-shot.test.ts',
[TASK_TYPE.ANALYZE_SHOT_VARIANTS]: 'tests/unit/worker/shot-ai-variants.test.ts',
[TASK_TYPE.AI_CREATE_CHARACTER]: 'tests/unit/worker/shot-ai-tasks.test.ts',
[TASK_TYPE.AI_CREATE_LOCATION]: 'tests/unit/worker/shot-ai-tasks.test.ts',
[TASK_TYPE.REFERENCE_TO_CHARACTER]: 'tests/unit/worker/reference-to-character.test.ts',
[TASK_TYPE.CHARACTER_PROFILE_CONFIRM]: 'tests/unit/worker/character-profile.test.ts',
[TASK_TYPE.CHARACTER_PROFILE_BATCH_CONFIRM]: 'tests/unit/worker/character-profile.test.ts',
[TASK_TYPE.EPISODE_SPLIT_LLM]: 'tests/unit/worker/episode-split.test.ts',
[TASK_TYPE.ASSET_HUB_AI_DESIGN_CHARACTER]: 'tests/unit/worker/asset-hub-ai-design.test.ts',
[TASK_TYPE.ASSET_HUB_AI_DESIGN_LOCATION]: 'tests/unit/worker/asset-hub-ai-design.test.ts',
[TASK_TYPE.ASSET_HUB_AI_MODIFY_CHARACTER]: 'tests/unit/worker/asset-hub-ai-modify.test.ts',
[TASK_TYPE.ASSET_HUB_AI_MODIFY_LOCATION]: 'tests/unit/worker/asset-hub-ai-modify.test.ts',
[TASK_TYPE.ASSET_HUB_REFERENCE_TO_CHARACTER]: 'tests/unit/worker/reference-to-character.test.ts',
} as const satisfies Record<TaskType, string>
export const TASK_TYPE_CATALOG: ReadonlyArray<TaskTypeCoverageEntry> = (Object.values(TASK_TYPE) as TaskType[])
.map((taskType) => ({
taskType,
owner: TASK_TYPE_OWNER_MAP[taskType],
layers: ['worker-unit', 'api-contract', 'chain'],
}))
export const TASK_TYPE_COUNT = TASK_TYPE_CATALOG.length

View File

@@ -0,0 +1,105 @@
import { TASK_TYPE_CATALOG } from './task-type-catalog'
import type { TaskType } from '@/lib/task/types'
export type TaskTypeBehaviorMatrixEntry = {
taskType: TaskType
caseId: string
workerTest: string
chainTest: string
apiContractTest: string
}
function resolveChainTestByTaskType(taskType: TaskType): string {
if (taskType === 'video_panel' || taskType === 'lip_sync') {
return 'tests/integration/chain/video.chain.test.ts'
}
if (taskType === 'voice_line' || taskType === 'voice_design' || taskType === 'asset_hub_voice_design') {
return 'tests/integration/chain/voice.chain.test.ts'
}
if (
taskType === 'analyze_novel'
|| taskType === 'story_to_script_run'
|| taskType === 'script_to_storyboard_run'
|| taskType === 'clips_build'
|| taskType === 'screenplay_convert'
|| taskType === 'voice_analyze'
|| taskType === 'analyze_global'
|| taskType === 'ai_modify_appearance'
|| taskType === 'ai_modify_location'
|| taskType === 'ai_modify_shot_prompt'
|| taskType === 'analyze_shot_variants'
|| taskType === 'ai_create_character'
|| taskType === 'ai_create_location'
|| taskType === 'reference_to_character'
|| taskType === 'character_profile_confirm'
|| taskType === 'character_profile_batch_confirm'
|| taskType === 'episode_split_llm'
|| taskType === 'asset_hub_ai_design_character'
|| taskType === 'asset_hub_ai_design_location'
|| taskType === 'asset_hub_ai_modify_character'
|| taskType === 'asset_hub_ai_modify_location'
|| taskType === 'asset_hub_reference_to_character'
) {
return 'tests/integration/chain/text.chain.test.ts'
}
return 'tests/integration/chain/image.chain.test.ts'
}
function resolveApiContractByTaskType(taskType: TaskType): string {
if (
taskType === 'analyze_novel'
|| taskType === 'story_to_script_run'
|| taskType === 'script_to_storyboard_run'
|| taskType === 'clips_build'
|| taskType === 'screenplay_convert'
|| taskType === 'voice_analyze'
|| taskType === 'analyze_global'
|| taskType === 'ai_modify_appearance'
|| taskType === 'ai_modify_location'
|| taskType === 'ai_modify_shot_prompt'
|| taskType === 'analyze_shot_variants'
|| taskType === 'ai_create_character'
|| taskType === 'ai_create_location'
|| taskType === 'reference_to_character'
|| taskType === 'character_profile_confirm'
|| taskType === 'character_profile_batch_confirm'
|| taskType === 'episode_split_llm'
|| taskType === 'asset_hub_ai_design_character'
|| taskType === 'asset_hub_ai_design_location'
|| taskType === 'asset_hub_ai_modify_character'
|| taskType === 'asset_hub_ai_modify_location'
|| taskType === 'asset_hub_reference_to_character'
) {
return 'tests/integration/api/contract/llm-observe-routes.test.ts'
}
if (
taskType === 'image_panel'
|| taskType === 'image_character'
|| taskType === 'image_location'
|| taskType === 'video_panel'
|| taskType === 'lip_sync'
|| taskType === 'voice_line'
|| taskType === 'voice_design'
|| taskType === 'asset_hub_voice_design'
|| taskType === 'insert_panel'
|| taskType === 'panel_variant'
|| taskType === 'modify_asset_image'
|| taskType === 'regenerate_group'
|| taskType === 'asset_hub_image'
|| taskType === 'asset_hub_modify'
|| taskType === 'regenerate_storyboard_text'
) {
return 'tests/integration/api/contract/direct-submit-routes.test.ts'
}
return 'tests/integration/api/contract/task-infra-routes.test.ts'
}
export const TASKTYPE_BEHAVIOR_MATRIX: ReadonlyArray<TaskTypeBehaviorMatrixEntry> = TASK_TYPE_CATALOG.map((entry) => ({
taskType: entry.taskType,
caseId: `TASKTYPE:${entry.taskType}`,
workerTest: entry.owner,
chainTest: resolveChainTestByTaskType(entry.taskType),
apiContractTest: resolveApiContractByTaskType(entry.taskType),
}))
export const TASKTYPE_BEHAVIOR_COUNT = TASKTYPE_BEHAVIOR_MATRIX.length