test: 补充 7 个缺失测试用例,覆盖所有 CAS 失败和只读路径
新增用例: - 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:
parent
38d1c4f414
commit
333632dc5b
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user