release: opensource snapshot 2026-02-27 19:25:00
This commit is contained in:
25
tests/contracts/behavior-test-standard.md
Normal file
25
tests/contracts/behavior-test-standard.md
Normal 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.
|
||||
24
tests/contracts/requirements-matrix.test.ts
Normal file
24
tests/contracts/requirements-matrix.test.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
84
tests/contracts/requirements-matrix.ts
Normal file
84
tests/contracts/requirements-matrix.ts
Normal 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',
|
||||
],
|
||||
},
|
||||
]
|
||||
50
tests/contracts/route-behavior-matrix.ts
Normal file
50
tests/contracts/route-behavior-matrix.ts
Normal 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
|
||||
213
tests/contracts/route-catalog.ts
Normal file
213
tests/contracts/route-catalog.ts
Normal 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
|
||||
58
tests/contracts/task-type-catalog.ts
Normal file
58
tests/contracts/task-type-catalog.ts
Normal 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
|
||||
105
tests/contracts/tasktype-behavior-matrix.ts
Normal file
105
tests/contracts/tasktype-behavior-matrix.ts
Normal 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
|
||||
Reference in New Issue
Block a user