From 333632dc5bf362b589cdacde849a084188b5a4db Mon Sep 17 00:00:00 2001 From: wangdl Date: Sat, 20 Jun 2026 17:21:22 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E8=A1=A5=E5=85=85=207=20=E4=B8=AA?= =?UTF-8?q?=E7=BC=BA=E5=A4=B1=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=EF=BC=8C?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E6=89=80=E6=9C=89=20CAS=20=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E5=92=8C=E5=8F=AA=E8=AF=BB=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增用例: - markSucceeded CAS 失败(非终态 queued)→ JobNotActiveError - markFailed CAS 失败(已终态)→ JobAlreadyTerminalError - markFailed 接受可选 tx 参数 - extendLock CAS 失败(Job 不在 running)→ JobNotActiveError - requestCancellation 终态 Job → JobNotCancellableError - findExpiredLocks 过期锁查询 - findById 按 ID 查询 总测试数:43 → 50 Co-Authored-By: Claude --- .../ai-job-lifecycle.repository.spec.ts | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/modules/ai-job/ai-job-lifecycle.repository.spec.ts b/src/modules/ai-job/ai-job-lifecycle.repository.spec.ts index f275d07..f8851d6 100644 --- a/src/modules/ai-job/ai-job-lifecycle.repository.spec.ts +++ b/src/modules/ai-job/ai-job-lifecycle.repository.spec.ts @@ -240,6 +240,16 @@ describe('AiJobLifecycleRepository', () => { await expect(repo.extendLock('job-001', 'worker-1')).rejects.toThrow(JobExtendLockError); }); + + it('续约失败(Job 不在 running 状态)抛出 JobNotActiveError', async () => { + (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 }); + (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({ + lifecycleStatus: 'succeeded', // terminal, not running + lockedBy: null, + }); + + await expect(repo.extendLock('job-001', 'worker-1')).rejects.toThrow(JobNotActiveError); + }); }); describe('markSucceeded', () => { @@ -285,6 +295,15 @@ describe('AiJobLifecycleRepository', () => { expect(tx.aiJob.updateMany).toHaveBeenCalled(); // 不应该调用 this.prisma }); + + it('CAS 失败(非终态,如 queued)抛出 JobNotActiveError', async () => { + (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 }); + (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({ + lifecycleStatus: 'queued', // not running, not terminal + }); + + await expect(repo.markSucceeded('job-001')).rejects.toThrow(JobNotActiveError); + }); }); describe('markFailed', () => { @@ -316,6 +335,29 @@ describe('AiJobLifecycleRepository', () => { ); expect(result.lifecycleStatus).toBe('failed'); }); + + it('CAS 失败(已终态)抛出 JobAlreadyTerminalError', async () => { + (prisma.aiJob.updateMany as jest.Mock).mockResolvedValue({ count: 0 }); + (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({ + lifecycleStatus: 'succeeded', + }); + + await expect( + repo.markFailed('job-001', { errorCode: 'TEST' }), + ).rejects.toThrow(JobAlreadyTerminalError); + }); + + it('接受可选 tx 参数,复用调用方事务', async () => { + tx.aiJob.updateMany.mockResolvedValue({ count: 1 }); + tx.aiJob.findUniqueOrThrow.mockResolvedValue({ + id: 'job-001', + lifecycleStatus: 'failed', + errorCode: 'PROVIDER_TIMEOUT', + }); + + await repo.markFailed('job-001', { errorCode: 'PROVIDER_TIMEOUT' }, tx); + expect(tx.aiJob.updateMany).toHaveBeenCalled(); + }); }); describe('markCancelled', () => { @@ -405,6 +447,12 @@ describe('AiJobLifecycleRepository', () => { await expect(repo.requestCancellation('job-001')).rejects.toThrow(JobNotCancellableError); }); + + it('终态 Job 不可取消 → 抛出 JobNotCancellableError', async () => { + (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue({ lifecycleStatus: 'succeeded' }); + + await expect(repo.requestCancellation('job-001')).rejects.toThrow(JobNotCancellableError); + }); }); describe('findByLifecycleStatus', () => { @@ -424,4 +472,31 @@ describe('AiJobLifecycleRepository', () => { ); }); }); + + describe('findExpiredLocks', () => { + it('查找锁过期的 running Job', async () => { + (prisma.aiJob.findMany as jest.Mock).mockResolvedValue([]); + + await repo.findExpiredLocks(90000); + expect(prisma.aiJob.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { + lifecycleStatus: 'running', + lockUntil: { lt: expect.any(Date) }, + }, + }), + ); + }); + }); + + describe('findById', () => { + it('按 ID 查找 Job', async () => { + const mockJob = { id: 'job-001', lifecycleStatus: 'queued' }; + (prisma.aiJob.findUnique as jest.Mock).mockResolvedValue(mockJob); + + const result = await repo.findById('job-001'); + expect(prisma.aiJob.findUnique).toHaveBeenCalledWith({ where: { id: 'job-001' } }); + expect(result).toEqual(mockJob); + }); + }); });