test: 补充 7 个缺失测试用例,覆盖所有 CAS 失败和只读路径
Some checks failed
Deploy API Server / build-and-unit (push) Successful in 33s
Deploy API Server / backward-compat (push) Has been cancelled
Deploy API Server / deploy (push) Has been cancelled
Deploy API Server / current-integration (push) Has been cancelled

新增用例:
- 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 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-20 17:21:22 +08:00
parent 38d1c4f414
commit 333632dc5b

View File

@ -240,6 +240,16 @@ describe('AiJobLifecycleRepository', () => {
await expect(repo.extendLock('job-001', 'worker-1')).rejects.toThrow(JobExtendLockError); 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', () => { describe('markSucceeded', () => {
@ -285,6 +295,15 @@ describe('AiJobLifecycleRepository', () => {
expect(tx.aiJob.updateMany).toHaveBeenCalled(); expect(tx.aiJob.updateMany).toHaveBeenCalled();
// 不应该调用 this.prisma // 不应该调用 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', () => { describe('markFailed', () => {
@ -316,6 +335,29 @@ describe('AiJobLifecycleRepository', () => {
); );
expect(result.lifecycleStatus).toBe('failed'); 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', () => { describe('markCancelled', () => {
@ -405,6 +447,12 @@ describe('AiJobLifecycleRepository', () => {
await expect(repo.requestCancellation('job-001')).rejects.toThrow(JobNotCancellableError); 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', () => { 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);
});
});
}); });