fix: 完善自动导入异常中断与恢复

- 自动导入基础设施异常触发批次熔断,保留完整日志并输出安全错误信息

- 增加恢复令牌与租约围栏、无文档失败项重建及消息退避机制

- 前端展示中断状态并在状态请求失败后自动恢复轮询

- 补充批次中断迁移、配置与并发异常路径测试
This commit is contained in:
2026-08-10 11:26:00 +08:00
parent 54d85ae460
commit bae7b18977
33 changed files with 3725 additions and 221 deletions

View File

@@ -153,4 +153,95 @@ describe('documentImportBatchStatus', () => {
expect(wrapper.find('.batch-status__continue').exists()).toBe(false);
wrapper.unmount();
});
it('中断批次展示异常原因、错误标识和继续入口', async () => {
apiMocks.get.mockResolvedValue({
data: {
...createBatch('RUNNING', 0),
failedCount: 2,
interruptCode: 'redis_unavailable',
interruptedAt: '2026-08-07T09:25:28+08:00',
interruptMessage: '缓存与消息服务异常,自动导入已中断',
pendingCount: 0,
status: 'INTERRUPTED',
},
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: {
knowledgeId: 'knowledge-1',
manageable: true,
},
});
await flushPromises();
const alert = wrapper.find('.batch-status__alert');
expect(alert.exists()).toBe(true);
expect(alert.text()).toContain('缓存与消息服务异常,自动导入已中断');
expect(alert.text()).toContain('redis_unavailable');
expect(wrapper.find('.batch-status__count').text()).toContain('2 / 2');
expect(wrapper.find('.batch-status__continue').exists()).toBe(true);
wrapper.unmount();
});
it('状态接口暂时失败时展示错误并自动退避恢复轮询', async () => {
vi.useFakeTimers();
apiMocks.get
.mockRejectedValueOnce(new Error('network unavailable'))
.mockResolvedValueOnce({
data: createBatch('RUNNING', 1),
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
expect(wrapper.find('.batch-status--error').exists()).toBe(true);
await vi.advanceTimersByTimeAsync(3000);
await flushPromises();
expect(apiMocks.get).toHaveBeenCalledTimes(2);
expect(wrapper.find('.batch-status--error').exists()).toBe(false);
expect(wrapper.find('.batch-status').exists()).toBe(true);
wrapper.unmount();
});
it('运行批次轮询失败时保留旧状态、展示错误并自动恢复', async () => {
vi.useFakeTimers();
apiMocks.get
.mockResolvedValueOnce({
data: createBatch('RUNNING', 1),
errorCode: 0,
})
.mockRejectedValueOnce(new Error('network unavailable'))
.mockResolvedValueOnce({
data: createBatch('COMPLETED', 2),
errorCode: 0,
});
const wrapper = mount(DocumentImportBatchStatus, {
props: { knowledgeId: 'knowledge-1' },
});
await flushPromises();
await vi.advanceTimersByTimeAsync(3000);
await flushPromises();
expect(wrapper.find('.batch-status__summary').exists()).toBe(true);
expect(wrapper.find('.batch-status__load-error').exists()).toBe(true);
await vi.advanceTimersByTimeAsync(3000);
await flushPromises();
expect(apiMocks.get).toHaveBeenCalledTimes(3);
expect(wrapper.find('.batch-status__load-error').exists()).toBe(false);
expect(wrapper.text()).toContain(
'documentCollection.importDoc.batchCompleted',
);
wrapper.unmount();
});
});