feat: M1-01~03 — AI Gateway deepening, Vector module, Task Queue deepening
M1-01 AI Gateway:
- DB-driven ModelRoute/ProviderConfig/FallbackEvent tables
- ModelRouter rewrite with loadFromDb() hot-reload
- Fallback event recording + AIUsageRecorded event publishing
- Admin AAPI: routes CRUD, provider enable/disable, fallback events log
M1-02 Vector & Retrieval:
- VectorService with Qdrant client (upsert/delete/search/rerank)
- Admin AAPI: collection status, vector count, reindex trigger
M1-03 Task Queue:
- 16 task types with default retry/timeout configs
- Task stats dashboard, worker status panel, batch retry endpoint
M0 audit fixes:
- ApiMetric retention policy (30-day cleanup)
- Content Safety integration in Files module
- Queue registration centralized (domain-events)
- SECRET_MASTER_KEY production validation
E2E tests:
- M0: 28 smoke tests covering all 14 M0 issues
- M1: 16 tests covering M1-01/02/03
- Mock infrastructure: prisma, ioredis, jose, bullmq, qdrant
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:18:07 +08:00
|
|
|
// Mock @prisma/client for E2E tests.
|
|
|
|
|
// PrismaService extends PrismaClient → this must be a plain class.
|
|
|
|
|
// Model access (prisma.user.findMany()) is supported via prototype delegates.
|
|
|
|
|
|
|
|
|
|
function modelMethods(): Record<string, Function> {
|
|
|
|
|
return {
|
|
|
|
|
findUnique: () => Promise.resolve(null),
|
|
|
|
|
findFirst: () => Promise.resolve(null),
|
|
|
|
|
findMany: () => Promise.resolve([]),
|
|
|
|
|
findRaw: () => Promise.resolve([]),
|
|
|
|
|
create: (args: any) => Promise.resolve({ id: 1, ...args?.data }),
|
|
|
|
|
update: (args: any) => Promise.resolve({ id: 1, ...args?.data }),
|
|
|
|
|
delete: () => Promise.resolve({ id: 1 }),
|
|
|
|
|
upsert: (args: any) => Promise.resolve({ id: 1, ...args?.create }),
|
|
|
|
|
count: () => Promise.resolve(0),
|
|
|
|
|
aggregate: () => Promise.resolve({}),
|
|
|
|
|
groupBy: () => Promise.resolve([]),
|
|
|
|
|
createMany: () => Promise.resolve({ count: 1 }),
|
|
|
|
|
deleteMany: () => Promise.resolve({ count: 0 }),
|
|
|
|
|
updateMany: () => Promise.resolve({ count: 0 }),
|
|
|
|
|
aggregateRaw: () => Promise.resolve([]),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createModelDelegate(): any {
|
|
|
|
|
const methods = modelMethods()
|
|
|
|
|
return new Proxy(methods, {
|
|
|
|
|
get(target: any, prop: string) {
|
|
|
|
|
if (prop === 'then') return undefined
|
|
|
|
|
if (prop in target) return target[prop]
|
|
|
|
|
return () => Promise.resolve(undefined)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// admin user fixture so login tests can get a real JWT
|
|
|
|
|
const ADMIN_USER = {
|
|
|
|
|
id: 'admin-test-001',
|
|
|
|
|
email: 'admin@zhixi.app',
|
|
|
|
|
displayName: 'Test Admin',
|
|
|
|
|
passwordHash: '$2b$10$mp8kF.PwWBjb0fp/5d0nZ.VNofYcVm7jhJYtswxLfGU/EJW5K8qCm', // bcrypt hash of "admin123"
|
|
|
|
|
role: 'SUPER_ADMIN',
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
twoFactorEnabled: false,
|
|
|
|
|
failedLoginCount: 0,
|
|
|
|
|
lockedUntil: null,
|
|
|
|
|
deletedAt: null,
|
|
|
|
|
lastLoginAt: null,
|
|
|
|
|
lastLoginIp: null,
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ADMIN_SESSION = {
|
|
|
|
|
id: 1,
|
|
|
|
|
adminUserId: 'admin-test-001',
|
|
|
|
|
refreshTokenHash: 'test-hash',
|
|
|
|
|
ip: null,
|
|
|
|
|
userAgent: null,
|
|
|
|
|
revokedAt: null,
|
|
|
|
|
expiresAt: new Date(Date.now() + 7 * 86400000),
|
|
|
|
|
createdAt: new Date(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class PrismaClient {
|
|
|
|
|
$connect() { return Promise.resolve() }
|
|
|
|
|
$disconnect() { return Promise.resolve() }
|
|
|
|
|
$on() {}
|
|
|
|
|
$transaction(fn: any) {
|
|
|
|
|
const delegate = createModelDelegate()
|
|
|
|
|
return typeof fn === 'function' ? fn(delegate) : Promise.resolve([])
|
|
|
|
|
}
|
|
|
|
|
$executeRaw() { return Promise.resolve(0) }
|
|
|
|
|
$queryRaw() { return Promise.resolve([]) }
|
|
|
|
|
$runCommandRaw() { return Promise.resolve({}) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const modelNames = [
|
|
|
|
|
'user', 'authAccount', 'refreshToken', 'userProfile', 'userPreference',
|
|
|
|
|
'userConsent', 'knowledgeBase', 'knowledgeItem', 'knowledgeItemRelation',
|
|
|
|
|
'tag', 'knowledgeItemTag', 'uploadedFile', 'documentImport',
|
|
|
|
|
'learningSession', 'learningRecord', 'activeRecallQuestion',
|
|
|
|
|
'activeRecallAnswer', 'aiAnalysisJob', 'aiAnalysisResult', 'focusItem',
|
|
|
|
|
'reviewCard', 'reviewLog', 'reviewPlan', 'dailyLearningActivity',
|
|
|
|
|
'notification', 'feedback', 'aiUsageLog', 'waitlistEntry', 'appChangelog',
|
|
|
|
|
'knowledgeSource', 'knowledgeChunk', 'importCandidate', 'backupJob',
|
|
|
|
|
'adminUser', 'adminSession', 'adminAuditLog', 'membershipPlan',
|
|
|
|
|
'adminConversation', 'adminMessage', 'adminCostItem', 'appConfig',
|
|
|
|
|
'featureFlag', 'configChangeLog', 'securityEvent', 'sensitiveWord',
|
|
|
|
|
'contentSafetyCheck', 'contentReport', 'apiMetric', 'taskLog',
|
|
|
|
|
'userMembership', 'quotaUsage', 'costDailySummary', 'secretRecord',
|
|
|
|
|
'secretAccessLog', 'modelRoute', 'providerConfig', 'fallbackEvent',
|
2026-05-24 11:18:56 +08:00
|
|
|
'violationRecord', 'contentReport', 'userDevice', 'accountDeletionRequest',
|
2026-05-24 13:12:16 +08:00
|
|
|
'workspace', 'knowledgeFolder', 'sourceReference', 'importStepLog',
|
feat: M3-04/05/06 — Workspace Experience, Notification, Cache Module
M3-04: RecentItem/Favorite/SearchHistory models, Tag CRUD, global search, workspace dashboard
M3-05: NotificationPreference/PushToken/Template models, preferences, push tokens, admin templates
M3-06: CacheService with wrap() penetration protection, key naming conventions, admin cache management
E2E: 27 new tests for M3-04/05/06 (35/36 passing overall)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 16:01:34 +08:00
|
|
|
'recentItem', 'favorite', 'searchHistory',
|
|
|
|
|
'chatSession', 'chatMessage', 'chatCitation',
|
|
|
|
|
'artifact', 'learningGoal', 'streakRecord',
|
|
|
|
|
'notificationPreference', 'pushToken', 'notificationTemplate',
|
2026-05-24 16:17:34 +08:00
|
|
|
'learningGoal', 'streakRecord', 'learningStats',
|
feat: M1-01~03 — AI Gateway deepening, Vector module, Task Queue deepening
M1-01 AI Gateway:
- DB-driven ModelRoute/ProviderConfig/FallbackEvent tables
- ModelRouter rewrite with loadFromDb() hot-reload
- Fallback event recording + AIUsageRecorded event publishing
- Admin AAPI: routes CRUD, provider enable/disable, fallback events log
M1-02 Vector & Retrieval:
- VectorService with Qdrant client (upsert/delete/search/rerank)
- Admin AAPI: collection status, vector count, reindex trigger
M1-03 Task Queue:
- 16 task types with default retry/timeout configs
- Task stats dashboard, worker status panel, batch retry endpoint
M0 audit fixes:
- ApiMetric retention policy (30-day cleanup)
- Content Safety integration in Files module
- Queue registration centralized (domain-events)
- SECRET_MASTER_KEY production validation
E2E tests:
- M0: 28 smoke tests covering all 14 M0 issues
- M1: 16 tests covering M1-01/02/03
- Mock infrastructure: prisma, ioredis, jose, bullmq, qdrant
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:18:07 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for (const name of modelNames) {
|
|
|
|
|
;(PrismaClient.prototype as any)[name] = createModelDelegate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Patch adminUser.findUnique so login tests can succeed
|
|
|
|
|
const origAdminUser = (PrismaClient.prototype as any).adminUser
|
|
|
|
|
;(PrismaClient.prototype as any).adminUser = new Proxy(origAdminUser, {
|
|
|
|
|
get(target: any, prop: string) {
|
|
|
|
|
if (prop === 'findUnique') {
|
|
|
|
|
return (args: any) => {
|
|
|
|
|
if (args?.where?.email === ADMIN_USER.email) return Promise.resolve(ADMIN_USER)
|
|
|
|
|
if (args?.where?.id === ADMIN_USER.id) return Promise.resolve(ADMIN_USER)
|
|
|
|
|
return target.findUnique(args)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (prop === 'findFirst') {
|
|
|
|
|
return (args: any) => {
|
|
|
|
|
if (args?.where?.email === ADMIN_USER.email) return Promise.resolve(ADMIN_USER)
|
|
|
|
|
return target.findFirst(args)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return target[prop]
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Patch adminSession so admin auth guard doesn't reject
|
|
|
|
|
const origAdminSession = (PrismaClient.prototype as any).adminSession
|
|
|
|
|
;(PrismaClient.prototype as any).adminSession = new Proxy(origAdminSession, {
|
|
|
|
|
get(target: any, prop: string) {
|
|
|
|
|
if (prop === 'findUnique' || prop === 'findFirst') {
|
|
|
|
|
return (_args?: any) => Promise.resolve(ADMIN_SESSION)
|
|
|
|
|
}
|
|
|
|
|
return target[prop]
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-25 16:55:04 +08:00
|
|
|
// Patch user so JwtAuthGuard status check passes for test users
|
|
|
|
|
const TEST_USER = {
|
|
|
|
|
id: 'test-user',
|
|
|
|
|
email: 'test@test.com',
|
|
|
|
|
role: 'USER',
|
|
|
|
|
status: 'active',
|
|
|
|
|
deletedAt: null,
|
|
|
|
|
}
|
|
|
|
|
const origUser = (PrismaClient.prototype as any).user
|
|
|
|
|
;(PrismaClient.prototype as any).user = new Proxy(origUser, {
|
|
|
|
|
get(target: any, prop: string) {
|
|
|
|
|
if (prop === 'findUnique') {
|
|
|
|
|
return (args: any) => {
|
|
|
|
|
if (args?.where?.id === 'test-user') return Promise.resolve(TEST_USER)
|
|
|
|
|
if (args?.where?.id === 'disabled-user') return Promise.resolve({ ...TEST_USER, id: 'disabled-user', status: 'disabled' })
|
|
|
|
|
if (args?.where?.id === 'deleted-user') return Promise.resolve({ ...TEST_USER, id: 'deleted-user', deletedAt: new Date() })
|
|
|
|
|
return target.findUnique(args)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return target[prop]
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Patch refreshToken so the refresh flow works in tests
|
|
|
|
|
const knownRefreshHashes = new Set<string>()
|
|
|
|
|
const origRefreshToken = (PrismaClient.prototype as any).refreshToken
|
|
|
|
|
;(PrismaClient.prototype as any).refreshToken = new Proxy(origRefreshToken, {
|
|
|
|
|
get(target: any, prop: string) {
|
|
|
|
|
if (prop === 'findFirst') {
|
|
|
|
|
return (args: any) => {
|
|
|
|
|
const hash = args?.where?.tokenHash
|
|
|
|
|
if (hash && knownRefreshHashes.has(hash)) {
|
|
|
|
|
return Promise.resolve({
|
|
|
|
|
id: 'rt-test-001',
|
|
|
|
|
userId: 'test-user',
|
|
|
|
|
tokenHash: hash,
|
|
|
|
|
deviceId: null,
|
|
|
|
|
deviceName: null,
|
|
|
|
|
expiresAt: new Date(Date.now() + 7 * 86400000),
|
|
|
|
|
revokedAt: null,
|
|
|
|
|
user: { ...TEST_USER },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return Promise.resolve(null)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (prop === 'update') {
|
|
|
|
|
return () => Promise.resolve({ id: 'rt-test-001' })
|
|
|
|
|
}
|
|
|
|
|
if (prop === 'updateMany') {
|
|
|
|
|
return () => Promise.resolve({ count: 1 })
|
|
|
|
|
}
|
|
|
|
|
if (prop === 'create') {
|
|
|
|
|
return (args: any) => {
|
|
|
|
|
if (args?.data?.tokenHash) {
|
|
|
|
|
knownRefreshHashes.add(args.data.tokenHash)
|
|
|
|
|
}
|
|
|
|
|
return Promise.resolve({ id: 'rt-test-new', ...args?.data })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return target[prop]
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
feat: M1-01~03 — AI Gateway deepening, Vector module, Task Queue deepening
M1-01 AI Gateway:
- DB-driven ModelRoute/ProviderConfig/FallbackEvent tables
- ModelRouter rewrite with loadFromDb() hot-reload
- Fallback event recording + AIUsageRecorded event publishing
- Admin AAPI: routes CRUD, provider enable/disable, fallback events log
M1-02 Vector & Retrieval:
- VectorService with Qdrant client (upsert/delete/search/rerank)
- Admin AAPI: collection status, vector count, reindex trigger
M1-03 Task Queue:
- 16 task types with default retry/timeout configs
- Task stats dashboard, worker status panel, batch retry endpoint
M0 audit fixes:
- ApiMetric retention policy (30-day cleanup)
- Content Safety integration in Files module
- Queue registration centralized (domain-events)
- SECRET_MASTER_KEY production validation
E2E tests:
- M0: 28 smoke tests covering all 14 M0 issues
- M1: 16 tests covering M1-01/02/03
- Mock infrastructure: prisma, ioredis, jose, bullmq, qdrant
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:18:07 +08:00
|
|
|
export const Prisma = {
|
|
|
|
|
ModelName: {},
|
|
|
|
|
PrismaClientKnownRequestError: class extends Error {
|
|
|
|
|
code: string
|
|
|
|
|
constructor(message: string, opts: { code: string; clientVersion: string }) {
|
|
|
|
|
super(message)
|
|
|
|
|
this.code = opts.code
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
PrismaClientValidationError: class extends Error {},
|
|
|
|
|
PrismaClientInitializationError: class extends Error {},
|
|
|
|
|
}
|