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); new AiWorkflowService( outputFileService, new AiInputValidator(), aiApiService, buildExcelExportService() ); // DeepSeek provider:通过请求临时传入 key 来解析 context(不依赖内置默认 key) final AiApiService.AiProviderContext context = aiApiService.resolveProviderContext("sk-test-key"); 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); 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; } } }