feat(web): 增强任务治理与系统诊断能力
新增任务持久化、筛选分页、取消任务、健康检查与 AI 输入校验,并完善前端历史管理交互与容错重试机制。补充对应单元测试,提升系统稳定性和可运维性。
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package com.svnlog.web.service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class AiInputValidatorTest {
|
||||
|
||||
@Test
|
||||
public void shouldRejectEmptyListWhenValidatingInputFiles() {
|
||||
AiInputValidator validator = new AiInputValidator();
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> validator.validate(Collections.<Path>emptyList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRejectNonMarkdownFileWhenValidatingInputFiles() throws Exception {
|
||||
AiInputValidator validator = new AiInputValidator();
|
||||
Path temp = Files.createTempFile("ai-input", ".txt");
|
||||
Files.write(temp, "abc".getBytes(StandardCharsets.UTF_8));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> validator.validate(Arrays.asList(temp)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAcceptSmallMarkdownFilesWhenValidatingInputFiles() throws Exception {
|
||||
AiInputValidator validator = new AiInputValidator();
|
||||
Path temp = Files.createTempFile("ai-input", ".md");
|
||||
Files.write(temp, "# title".getBytes(StandardCharsets.UTF_8));
|
||||
validator.validate(Arrays.asList(temp));
|
||||
Assertions.assertTrue(true);
|
||||
}
|
||||
}
|
||||
36
src/test/java/com/svnlog/web/service/HealthServiceTest.java
Normal file
36
src/test/java/com/svnlog/web/service/HealthServiceTest.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.svnlog.web.service;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class HealthServiceTest {
|
||||
|
||||
@Test
|
||||
public void shouldReturnDetailedHealthWhenDependenciesAvailable() throws Exception {
|
||||
OutputFileService outputFileService = Mockito.mock(OutputFileService.class);
|
||||
SettingsService settingsService = Mockito.mock(SettingsService.class);
|
||||
TaskService taskService = Mockito.mock(TaskService.class);
|
||||
|
||||
Path outputDir = Files.createTempDirectory("health-service-test");
|
||||
Mockito.when(outputFileService.getOutputRoot()).thenReturn(outputDir);
|
||||
|
||||
Map<String, Object> settings = new HashMap<String, Object>();
|
||||
settings.put("apiKeyConfigured", true);
|
||||
Mockito.when(settingsService.getSettings()).thenReturn(settings);
|
||||
Mockito.when(taskService.getTasks()).thenReturn(new java.util.ArrayList<>());
|
||||
|
||||
HealthService healthService = new HealthService(outputFileService, settingsService, taskService);
|
||||
Map<String, Object> details = healthService.detailedHealth();
|
||||
|
||||
Assertions.assertEquals("ok", details.get("status"));
|
||||
Assertions.assertEquals(true, details.get("outputDirWritable"));
|
||||
Assertions.assertEquals(true, details.get("apiKeyConfigured"));
|
||||
Assertions.assertEquals(0, details.get("taskTotal"));
|
||||
}
|
||||
}
|
||||
39
src/test/java/com/svnlog/web/service/RetrySupportTest.java
Normal file
39
src/test/java/com/svnlog/web/service/RetrySupportTest.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package com.svnlog.web.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class RetrySupportTest {
|
||||
|
||||
@Test
|
||||
public void shouldRetryAndSucceedWhenExceptionIsRetryable() throws Exception {
|
||||
RetrySupport retrySupport = new RetrySupport();
|
||||
AtomicInteger attempts = new AtomicInteger(0);
|
||||
|
||||
String result = retrySupport.execute(() -> {
|
||||
if (attempts.incrementAndGet() < 3) {
|
||||
throw new IOException("temporary error");
|
||||
}
|
||||
return "ok";
|
||||
}, 3, 1L);
|
||||
|
||||
Assertions.assertEquals("ok", result);
|
||||
Assertions.assertEquals(3, attempts.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFailImmediatelyWhenExceptionIsNotRetryable() {
|
||||
RetrySupport retrySupport = new RetrySupport();
|
||||
AtomicInteger attempts = new AtomicInteger(0);
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> retrySupport.execute(() -> {
|
||||
attempts.incrementAndGet();
|
||||
throw new IllegalArgumentException("bad request");
|
||||
}, 3, 1L));
|
||||
|
||||
Assertions.assertEquals(1, attempts.get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.svnlog.web.service;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.svnlog.web.model.TaskInfo;
|
||||
import com.svnlog.web.model.TaskStatus;
|
||||
|
||||
public class TaskPersistenceServiceTest {
|
||||
|
||||
@Test
|
||||
public void shouldSaveAndLoadTaskHistoryWhenStoreFileExists() throws Exception {
|
||||
TaskPersistenceService service = new TaskPersistenceService();
|
||||
Path tempDir = Files.createTempDirectory("task-persistence-test");
|
||||
Path storePath = tempDir.resolve("task-history.json");
|
||||
|
||||
TaskInfo task = new TaskInfo();
|
||||
task.setTaskId("task-1");
|
||||
task.setType("SVN_FETCH");
|
||||
task.setStatus(TaskStatus.SUCCESS);
|
||||
task.setProgress(100);
|
||||
task.setMessage("ok");
|
||||
task.setError("");
|
||||
task.setCreatedAt(Instant.parse("2026-03-01T10:00:00Z"));
|
||||
task.setUpdatedAt(Instant.parse("2026-03-01T10:05:00Z"));
|
||||
task.getFiles().add("md/a.md");
|
||||
|
||||
service.save(storePath, Arrays.asList(task));
|
||||
|
||||
List<TaskInfo> loaded = service.load(storePath);
|
||||
Assertions.assertEquals(1, loaded.size());
|
||||
Assertions.assertEquals("task-1", loaded.get(0).getTaskId());
|
||||
Assertions.assertEquals(TaskStatus.SUCCESS, loaded.get(0).getStatus());
|
||||
Assertions.assertEquals(1, loaded.get(0).getFiles().size());
|
||||
Assertions.assertEquals("md/a.md", loaded.get(0).getFiles().get(0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.svnlog.web.service;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import com.svnlog.web.model.TaskInfo;
|
||||
import com.svnlog.web.model.TaskStatus;
|
||||
|
||||
public class TaskServiceCancelTest {
|
||||
|
||||
@Test
|
||||
public void shouldCancelRunningTaskWhenCancelEndpointInvoked() throws Exception {
|
||||
TaskPersistenceService persistenceService = Mockito.mock(TaskPersistenceService.class);
|
||||
OutputFileService outputFileService = Mockito.mock(OutputFileService.class);
|
||||
|
||||
Path tempDir = Files.createTempDirectory("task-cancel-test");
|
||||
Mockito.when(outputFileService.getOutputRoot()).thenReturn(tempDir);
|
||||
Mockito.when(persistenceService.load(tempDir.resolve("task-history.json"))).thenReturn(new ArrayList<TaskInfo>());
|
||||
|
||||
TaskService taskService = new TaskService(persistenceService, outputFileService);
|
||||
String taskId = taskService.submit("SVN_FETCH", context -> {
|
||||
for (int i = 0; i < 50; i++) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new InterruptedException("cancelled");
|
||||
}
|
||||
Thread.sleep(20);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
boolean cancelled = taskService.cancelTask(taskId);
|
||||
Assertions.assertTrue(cancelled);
|
||||
|
||||
TaskInfo task = taskService.getTask(taskId);
|
||||
Assertions.assertNotNull(task);
|
||||
Assertions.assertEquals(TaskStatus.CANCELLED, task.getStatus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.svnlog.web.service;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import com.svnlog.web.model.TaskInfo;
|
||||
import com.svnlog.web.model.TaskPageResult;
|
||||
import com.svnlog.web.model.TaskStatus;
|
||||
|
||||
public class TaskServiceQueryTest {
|
||||
|
||||
@Test
|
||||
public void shouldFilterAndPaginateTasksWhenQuerying() throws Exception {
|
||||
TaskPersistenceService persistenceService = Mockito.mock(TaskPersistenceService.class);
|
||||
OutputFileService outputFileService = Mockito.mock(OutputFileService.class);
|
||||
|
||||
Path tempDir = Files.createTempDirectory("task-query-test");
|
||||
Mockito.when(outputFileService.getOutputRoot()).thenReturn(tempDir);
|
||||
|
||||
TaskInfo t1 = buildTask("1", "SVN_FETCH", TaskStatus.SUCCESS, "抓取完成", Instant.parse("2026-03-01T10:00:00Z"));
|
||||
TaskInfo t2 = buildTask("2", "AI_ANALYZE", TaskStatus.FAILED, "AI失败", Instant.parse("2026-03-01T10:10:00Z"));
|
||||
TaskInfo t3 = buildTask("3", "SVN_FETCH", TaskStatus.SUCCESS, "导出成功", Instant.parse("2026-03-01T10:20:00Z"));
|
||||
|
||||
Mockito.when(persistenceService.load(tempDir.resolve("task-history.json")))
|
||||
.thenReturn(Arrays.asList(t1, t2, t3));
|
||||
|
||||
TaskService taskService = new TaskService(persistenceService, outputFileService);
|
||||
|
||||
TaskPageResult page1 = taskService.queryTasks("SUCCESS", "SVN_FETCH", "", 1, 1);
|
||||
Assertions.assertEquals(2, page1.getTotal());
|
||||
Assertions.assertEquals(1, page1.getItems().size());
|
||||
Assertions.assertEquals("3", page1.getItems().get(0).getTaskId());
|
||||
|
||||
TaskPageResult page2 = taskService.queryTasks("SUCCESS", "SVN_FETCH", "", 2, 1);
|
||||
Assertions.assertEquals(1, page2.getItems().size());
|
||||
Assertions.assertEquals("1", page2.getItems().get(0).getTaskId());
|
||||
|
||||
TaskPageResult keyword = taskService.queryTasks("", "", "ai", 1, 10);
|
||||
Assertions.assertEquals(1, keyword.getTotal());
|
||||
Assertions.assertEquals("2", keyword.getItems().get(0).getTaskId());
|
||||
}
|
||||
|
||||
private TaskInfo buildTask(String id, String type, TaskStatus status, String message, Instant createdAt) {
|
||||
TaskInfo task = new TaskInfo();
|
||||
task.setTaskId(id);
|
||||
task.setType(type);
|
||||
task.setStatus(status);
|
||||
task.setMessage(message);
|
||||
task.setProgress(status == TaskStatus.SUCCESS ? 100 : 0);
|
||||
task.setCreatedAt(createdAt);
|
||||
task.setUpdatedAt(createdAt);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user