Files
svn-log-tool/src/test/java/com/svnlog/web/service/AiWorkflowServiceTest.java
T
liumangmang 1b182c2930 feat: v2 Vue3 frontend + multiple optimizations
- New Vue 3 + Vite frontend at /v2/ (OLED dark theme, Fira Sans/Code)
- Date selector: support day/week/month range (backend unchanged)
- SSE auto-reconnect (up to 3 retries)
- Visibility polling pause (dashboard pauses when tab hidden)
- Friendly Chinese HTTP error messages
- Cancel task with confirmation in Dashboard
- Split AiWorkflowService (1700->845 lines):
  - AiApiService: AI API calls + streaming
  - ExcelExportService: POI Excel generation
- Dockerfile: 3-stage build (Node frontend -> Maven -> JRE)
- WebApplication.java: System.out -> Logger
- .gitignore: v2 build output, backup dirs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 15:12:52 +08:00

187 lines
7.3 KiB
Java

package com.svnlog.web.service;
import java.nio.file.Path;
import java.time.Instant;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import com.svnlog.web.model.TaskInfo;
import com.svnlog.web.model.TaskStatus;
import okio.Buffer;
class AiWorkflowServiceTest {
@TempDir
Path tempDir;
private AiApiService buildAiApiService(SettingsService settingsService) {
return new AiApiService(settingsService);
}
private ExcelExportService buildExcelExportService() {
return new ExcelExportService();
}
@Test
void shouldResolveDeepSeekProviderByDefault() {
final OutputFileService outputFileService = buildOutputFileService();
final SettingsService settingsService = buildSettingsService(outputFileService);
final AiApiService aiApiService = buildAiApiService(settingsService);
final AiWorkflowService service = new AiWorkflowService(
outputFileService,
new AiInputValidator(),
aiApiService,
buildExcelExportService()
);
final AiApiService.AiProviderContext context = aiApiService.resolveProviderContext(null);
Assertions.assertEquals(SettingsService.PROVIDER_DEEPSEEK, context.getProvider());
Assertions.assertEquals("deepseek-chat", context.getStageOneModel());
Assertions.assertEquals("deepseek-reasoner", context.getStageTwoModel());
}
@Test
void shouldResolveOpenAiCompatibleModelsAndUrl() {
final OutputFileService outputFileService = buildOutputFileService();
final SettingsService settingsService = buildSettingsService(outputFileService);
settingsService.updateSettings(
null,
SettingsService.PROVIDER_OPENAI_COMPATIBLE,
"http://127.0.0.1:5001/v1/",
"sk-openai-test",
"deepseek-v4-flash",
"deepseek-v4-pro",
null,
null,
null,
null
);
final AiApiService aiApiService = buildAiApiService(settingsService);
final AiWorkflowService service = new AiWorkflowService(
outputFileService, new AiInputValidator(), aiApiService, buildExcelExportService()
);
final AiApiService.AiProviderContext context = aiApiService.resolveProviderContext(null);
Assertions.assertEquals(SettingsService.PROVIDER_OPENAI_COMPATIBLE, context.getProvider());
Assertions.assertEquals("http://127.0.0.1:5001/v1/chat/completions", context.getApiUrl());
Assertions.assertEquals("deepseek-v4-flash", context.getStageOneModel());
Assertions.assertEquals("deepseek-v4-pro", context.getStageTwoModel());
}
@Test
void shouldFailFastWhenOpenAiCompatibleBaseUrlMissing() {
final OutputFileService outputFileService = buildOutputFileService();
final AiApiService aiApiService = buildAiApiService(
new StubSettingsService(outputFileService, " ", "sk-openai-test")
);
final IllegalStateException error = Assertions.assertThrows(
IllegalStateException.class,
() -> aiApiService.resolveProviderContext(null)
);
Assertions.assertTrue(error.getMessage().contains("OpenAI兼容 Base URL"));
}
@Test
void shouldParseCompatibleStreamWhenOnlyContentIsReturned() throws Exception {
final OutputFileService outputFileService = buildOutputFileService();
final SettingsService settingsService = buildSettingsService(outputFileService);
settingsService.updateSettings(
null,
SettingsService.PROVIDER_OPENAI_COMPATIBLE,
"http://127.0.0.1:5001/v1",
"sk-openai-test",
"deepseek-v4-flash",
"deepseek-v4-pro",
null,
null,
null,
null
);
final AiApiService aiApiService = buildAiApiService(settingsService);
final AiApiService.AiProviderContext providerContext = aiApiService.resolveProviderContext(null);
final TaskContext taskContext = new TaskContext(buildTaskInfo(), null, null);
final Buffer buffer = new Buffer()
.writeUtf8("data: {\"choices\":[{\"delta\":{\"content\":\"{\\\"items\\\":[\"}}]}\n")
.writeUtf8("data: {\"choices\":[{\"delta\":{\"content\":\"{\\\"summary\\\":\\\"done\\\"}\"}}]}\n")
.writeUtf8("data: {\"choices\":[{\"delta\":{\"content\":\"]}\"},\"finish_reason\":\"stop\"}]}\n")
.writeUtf8("data: [DONE]\n");
final AiApiService.AiStreamResult result = aiApiService.readStreamingResponse(
buffer,
taskContext,
providerContext,
"deepseek-v4-flash",
8000
);
Assertions.assertEquals("{\"items\":[{\"summary\":\"done\"}]}", result.getAnswer());
Assertions.assertEquals("stop", result.getFinishReason());
}
private OutputFileService buildOutputFileService() {
final OutputFileService outputFileService = new OutputFileService();
outputFileService.setOutputRoot(tempDir.resolve("outputs").toString());
return outputFileService;
}
private TaskInfo buildTaskInfo() {
final TaskInfo taskInfo = new TaskInfo();
taskInfo.setTaskId("task-1");
taskInfo.setStatus(TaskStatus.RUNNING);
taskInfo.setCreatedAt(Instant.now());
taskInfo.setUpdatedAt(Instant.now());
return taskInfo;
}
private SettingsService buildSettingsService(OutputFileService outputFileService) {
final RepositoryConfigService repositoryConfigService = buildRepositoryConfigService(outputFileService);
final SvnPresetService svnPresetService = new SvnPresetService(repositoryConfigService);
svnPresetService.init();
return new SettingsService(outputFileService, new SettingsPersistenceService(), svnPresetService, repositoryConfigService);
}
private RepositoryConfigService buildRepositoryConfigService(OutputFileService outputFileService) {
final RepositoryConfigService repositoryConfigService = new RepositoryConfigService(outputFileService);
repositoryConfigService.init();
return repositoryConfigService;
}
private static final class StubSettingsService extends SettingsService {
private final String openaiBaseUrl;
private final String openaiApiKey;
private StubSettingsService(OutputFileService outputFileService, String openaiBaseUrl, String openaiApiKey) {
super(
outputFileService,
new SettingsPersistenceService(),
new SvnPresetService(new RepositoryConfigService(outputFileService)),
new RepositoryConfigService(outputFileService)
);
this.openaiBaseUrl = openaiBaseUrl;
this.openaiApiKey = openaiApiKey;
}
@Override
public String getProvider() {
return SettingsService.PROVIDER_OPENAI_COMPATIBLE;
}
@Override
public String getOpenaiBaseUrl() {
return openaiBaseUrl;
}
@Override
public String getOpenaiApiKey() {
return openaiApiKey;
}
}
}