feat: 完善多模型配置与运行切换
将模型连接统一持久化管理,并支持 Agent 运行中切换模型以及中断后选择模型继续。补充配置校验、事务与前后端交互测试。
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package tech.easyflow.manuagent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -373,7 +374,7 @@ class DatabaseAndEventIntegrationTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证模型新增、保留旧密钥更新、默认分配及密钥解密读取。
|
||||
* 验证多个模型持久化、首模型自动默认、保留旧密钥更新以及默认模型切换。
|
||||
*
|
||||
* @throws Exception Mapper XML 初始化失败时抛出
|
||||
*/
|
||||
@@ -386,24 +387,19 @@ class DatabaseAndEventIntegrationTest {
|
||||
when(users.requireUserId("admin")).thenReturn(userId);
|
||||
AppProperties properties = new AppProperties(
|
||||
temporaryDirectory,
|
||||
temporaryDirectory.resolve("deepseek.key"),
|
||||
temporaryDirectory.resolve("dashscope.key"),
|
||||
"integration-master-key",
|
||||
"admin",
|
||||
"admin",
|
||||
"https://default.example.test",
|
||||
"default-model",
|
||||
131_072,
|
||||
"runtime:test",
|
||||
"bridge",
|
||||
Duration.ofMinutes(1));
|
||||
ModelService service = new ModelService(
|
||||
modelConfigMapper(),
|
||||
modelAssignmentMapper(),
|
||||
mock(AppUserMapper.class),
|
||||
agentRunMapper(),
|
||||
users,
|
||||
new KeyCipher(properties),
|
||||
properties,
|
||||
new ObjectMapper());
|
||||
Map<String, Object> capabilities = Map.of(
|
||||
"toolCalling", true, "reasoning", true, "contextWindow", 65_536);
|
||||
@@ -420,16 +416,61 @@ class DatabaseAndEventIntegrationTest {
|
||||
"测试模型更新", "https://model.example.test", "model-v2", "",
|
||||
Map.of("timeoutSeconds", 120), capabilities),
|
||||
() -> "admin");
|
||||
service.setDefault(created.id(), () -> "admin");
|
||||
ModelService.ModelSecret secret = service.defaultModelSecret();
|
||||
ModelService.ModelView second = service.save(
|
||||
null,
|
||||
new ModelService.ModelInput(
|
||||
"第二测试模型", "https://second-model.example.test", "model-second", "secret-5678",
|
||||
Map.of("timeoutSeconds", 90), capabilities),
|
||||
() -> "admin");
|
||||
|
||||
assertThat(created.defaultModel()).isTrue();
|
||||
assertThat(second.defaultModel()).isFalse();
|
||||
|
||||
service.setDefault(second.id(), () -> "admin");
|
||||
ModelService.ModelSecret firstSecret = service.requireRuntimeModel(created.id());
|
||||
ModelService.ModelSecret defaultSecret = service.defaultModelSecret();
|
||||
|
||||
assertThat(updated.name()).isEqualTo("测试模型更新");
|
||||
assertThat(updated.apiKeyHint()).endsWith("1234");
|
||||
assertThat(secret.apiKey()).isEqualTo("secret-1234");
|
||||
assertThat(secret.modelId()).isEqualTo("model-v2");
|
||||
assertThat(secret.contextWindow()).isEqualTo(65_536);
|
||||
assertThat(firstSecret.apiKey()).isEqualTo("secret-1234");
|
||||
assertThat(firstSecret.modelId()).isEqualTo("model-v2");
|
||||
assertThat(defaultSecret.apiKey()).isEqualTo("secret-5678");
|
||||
assertThat(defaultSecret.modelId()).isEqualTo("model-second");
|
||||
assertThat(defaultSecret.contextWindow()).isEqualTo(65_536);
|
||||
assertThat(jdbc().sql("SELECT COUNT(*) FROM app.model_assignment WHERE model_config_id = :id")
|
||||
.param("id", created.id()).query(Long.class).single()).isEqualTo(3L);
|
||||
.param("id", second.id()).query(Long.class).single()).isEqualTo(3L);
|
||||
|
||||
assertThatThrownBy(() -> service.setEnabled(second.id(), false))
|
||||
.isInstanceOfSatisfying(tech.easyflow.manuagent.common.ApiException.class, exception ->
|
||||
assertThat(exception.code()).isEqualTo("DEFAULT_MODEL_REQUIRED"));
|
||||
ModelService.ModelView disabled = service.setEnabled(created.id(), false);
|
||||
assertThat(disabled.enabled()).isFalse();
|
||||
|
||||
// 已完成 Run 仍属于历史审计事实;即使模型已经停用,也必须阻止真删除。
|
||||
UUID historyProjectId = UUID.randomUUID();
|
||||
UUID historyRunId = UUID.randomUUID();
|
||||
jdbc().sql("""
|
||||
INSERT INTO app.project(id, company_name, project_name, agui_thread_id, application_level, created_by)
|
||||
VALUES (:id, '模型历史企业', '模型历史测试', :threadId, 'ADVANCED', :userId)
|
||||
""").param("id", historyProjectId).param("threadId", "thread-" + historyProjectId)
|
||||
.param("userId", userId).update();
|
||||
jdbc().sql("""
|
||||
INSERT INTO app.agent_run(
|
||||
id, project_id, model_config_id, trigger_type, status, trace_id, ended_at)
|
||||
VALUES (:id, :projectId, :modelId, 'INITIAL', 'COMPLETED', :traceId, CURRENT_TIMESTAMP)
|
||||
""").param("id", historyRunId).param("projectId", historyProjectId)
|
||||
.param("modelId", created.id()).param("traceId", "trace-" + historyRunId).update();
|
||||
|
||||
assertThatThrownBy(() -> service.delete(created.id()))
|
||||
.isInstanceOfSatisfying(tech.easyflow.manuagent.common.ApiException.class, exception ->
|
||||
assertThat(exception.code()).isEqualTo("MODEL_HISTORY_EXISTS"));
|
||||
|
||||
// 清理本测试创建的引用后,再验证从未被历史 Run 使用的模型仍可按原有规则删除。
|
||||
jdbc().sql("DELETE FROM app.agent_run WHERE id = :id").param("id", historyRunId).update();
|
||||
jdbc().sql("DELETE FROM app.project WHERE id = :id").param("id", historyProjectId).update();
|
||||
service.delete(created.id());
|
||||
assertThat(jdbc().sql("SELECT COUNT(*) FROM app.model_config WHERE id = :id")
|
||||
.param("id", created.id()).query(Long.class).single()).isZero();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,14 +483,10 @@ class DatabaseAndEventIntegrationTest {
|
||||
SkillConfigMapper mapper = skillConfigMapper();
|
||||
AppProperties properties = new AppProperties(
|
||||
temporaryDirectory,
|
||||
temporaryDirectory.resolve("deepseek.key"),
|
||||
temporaryDirectory.resolve("dashscope.key"),
|
||||
"integration-master-key",
|
||||
"admin",
|
||||
"admin",
|
||||
"https://default.example.test",
|
||||
"default-model",
|
||||
131_072,
|
||||
"runtime:test",
|
||||
"bridge",
|
||||
Duration.ofMinutes(1));
|
||||
|
||||
@@ -28,8 +28,8 @@ class KeyCipherAndShellTest {
|
||||
|
||||
private AppProperties properties() {
|
||||
return new AppProperties(
|
||||
Path.of("data"), Path.of("deepseek"), Path.of("dashscope"),
|
||||
"unit-test-master", "admin", "admin123", "https://api.example.test", "model",
|
||||
131_072, "smart-factory-agent-runtime:test", "bridge", Duration.ofMinutes(1));
|
||||
Path.of("data"), Path.of("dashscope"),
|
||||
"unit-test-master", "admin", "admin123",
|
||||
"smart-factory-agent-runtime:test", "bridge", Duration.ofMinutes(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import tech.easyflow.manuagent.config.MyBatisFlexConfiguration;
|
||||
import tech.easyflow.manuagent.mapper.AppUserMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.model.KeyCipher;
|
||||
import tech.easyflow.manuagent.model.ModelService;
|
||||
|
||||
@@ -91,7 +92,7 @@ class MyBatisFlexTransactionIntegrationTest {
|
||||
UUID userId = UUID.randomUUID();
|
||||
UUID missingUserId = UUID.randomUUID();
|
||||
UUID currentDefaultId = UUID.randomUUID();
|
||||
UUID disabledTargetId = UUID.randomUUID();
|
||||
UUID enabledTargetId = UUID.randomUUID();
|
||||
jdbc.sql("""
|
||||
INSERT INTO app.app_user(id, username, password_hash, display_name)
|
||||
VALUES (:id, :username, 'encoded', '事务测试用户')
|
||||
@@ -106,12 +107,12 @@ class MyBatisFlexTransactionIntegrationTest {
|
||||
(:currentId, :currentName, 'OPENAI_COMPATIBLE', 'https://current.test',
|
||||
'current-model', TRUE, TRUE),
|
||||
(:targetId, :targetName, 'OPENAI_COMPATIBLE', 'https://target.test',
|
||||
'target-model', FALSE, FALSE)
|
||||
'target-model', TRUE, FALSE)
|
||||
""")
|
||||
.param("currentId", currentDefaultId)
|
||||
.param("currentName", "current-" + currentDefaultId)
|
||||
.param("targetId", disabledTargetId)
|
||||
.param("targetName", "target-" + disabledTargetId)
|
||||
.param("targetId", enabledTargetId)
|
||||
.param("targetName", "target-" + enabledTargetId)
|
||||
.update();
|
||||
UserService users = context.getBean(UserService.class);
|
||||
when(users.requireUserId("admin")).thenReturn(missingUserId);
|
||||
@@ -119,7 +120,7 @@ class MyBatisFlexTransactionIntegrationTest {
|
||||
assertThat(context.getBeansOfType(PlatformTransactionManager.class)).hasSize(1);
|
||||
assertThat(AopUtils.isAopProxy(modelService)).isTrue();
|
||||
|
||||
assertThatThrownBy(() -> modelService.setDefault(disabledTargetId, () -> "admin"))
|
||||
assertThatThrownBy(() -> modelService.setDefault(enabledTargetId, () -> "admin"))
|
||||
.hasRootCauseInstanceOf(PSQLException.class);
|
||||
|
||||
assertThat(jdbc.sql("SELECT is_default FROM app.model_config WHERE id = :id")
|
||||
@@ -127,7 +128,7 @@ class MyBatisFlexTransactionIntegrationTest {
|
||||
.query(Boolean.class)
|
||||
.single()).isTrue();
|
||||
assertThat(jdbc.sql("SELECT is_default FROM app.model_config WHERE id = :id")
|
||||
.param("id", disabledTargetId)
|
||||
.param("id", enabledTargetId)
|
||||
.query(Boolean.class)
|
||||
.single()).isFalse();
|
||||
});
|
||||
@@ -152,15 +153,14 @@ class MyBatisFlexTransactionIntegrationTest {
|
||||
ModelService modelService(
|
||||
ModelConfigMapper modelMapper,
|
||||
ModelAssignmentMapper assignmentMapper,
|
||||
AppUserMapper userMapper,
|
||||
AgentRunMapper runMapper,
|
||||
UserService userService) {
|
||||
return new ModelService(
|
||||
modelMapper,
|
||||
assignmentMapper,
|
||||
userMapper,
|
||||
runMapper,
|
||||
userService,
|
||||
mock(KeyCipher.class),
|
||||
mock(AppProperties.class),
|
||||
new ObjectMapper());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,62 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.agentscope.core.agui.adapter.AguiAgentAdapter;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import tech.easyflow.manuagent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.project.ProjectService;
|
||||
import tech.easyflow.manuagent.skill.SkillService;
|
||||
|
||||
/**
|
||||
* 验证 Agent 事件持久化的精简规则。
|
||||
*/
|
||||
class AgentExecutionServiceTest {
|
||||
|
||||
/**
|
||||
* Run 在创建时已经固化模型配置 ID;执行和模型重连都必须沿用该 ID,
|
||||
* 不能在工厂内部重新读取可能已经变化的全局默认模型。
|
||||
*/
|
||||
@Test
|
||||
void shouldCreateAgentWithModelBoundToRun() {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
UUID runId = UUID.randomUUID();
|
||||
UUID modelId = UUID.randomUUID();
|
||||
AgentFactory factory = mock(AgentFactory.class);
|
||||
AgentFactory.AgentHandle handle = mock(AgentFactory.AgentHandle.class);
|
||||
AguiAgentAdapter adapter = mock(AguiAgentAdapter.class);
|
||||
SkillService skillService = mock(SkillService.class);
|
||||
when(handle.adapter()).thenReturn(adapter);
|
||||
when(adapter.run(any())).thenReturn(Flux.empty());
|
||||
when(skillService.enabledNames()).thenReturn(new String[] {"document"});
|
||||
when(factory.create(eq(projectId), eq(modelId), any(String[].class))).thenReturn(handle);
|
||||
|
||||
AgentExecutionService service = new AgentExecutionService(
|
||||
new ObjectMapper(),
|
||||
factory,
|
||||
mock(AgentEventService.class),
|
||||
mock(ProjectFileService.class),
|
||||
skillService);
|
||||
ProjectService.ProjectView project = mock(ProjectService.ProjectView.class);
|
||||
when(project.id()).thenReturn(projectId);
|
||||
when(project.threadId()).thenReturn("project-" + projectId);
|
||||
AgentRunService.RunView run = new AgentRunService.RunView(
|
||||
runId, projectId, modelId, "INITIAL", "RUNNING", null, null, null, null);
|
||||
|
||||
service.execute(project, run, "执行测试", Mono.never(), () -> { }, () -> false);
|
||||
|
||||
verify(factory).create(eq(projectId), eq(modelId), any(String[].class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文档视觉结果保留图片路径元数据并丢弃 Base64 正文。
|
||||
*/
|
||||
|
||||
@@ -1,20 +1,87 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.agentscope.core.model.transport.HttpTransportException;
|
||||
import io.agentscope.core.skill.AgentSkill;
|
||||
import java.nio.file.Path;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import tech.easyflow.manuagent.artifact.ArtifactService;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.project.ProjectService;
|
||||
|
||||
/**
|
||||
* 验证 Agent Run 的模型重连边界。
|
||||
*/
|
||||
class AgentRunServiceTest {
|
||||
|
||||
/**
|
||||
* 运行中模型配置被编辑后,用户应能选择同一模型 ID 创建新的恢复 Run,
|
||||
* 使新 Agent 客户端重新读取数据库中的最新地址、模型标识和密钥。
|
||||
*/
|
||||
@Test
|
||||
void shouldRestartRunningTaskWhenSameModelConfigurationWasUpdated() {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
UUID modelId = UUID.randomUUID();
|
||||
UUID currentRunId = UUID.randomUUID();
|
||||
UUID replacementRunId = UUID.randomUUID();
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
ProjectService.ProjectView project = new ProjectService.ProjectView(
|
||||
projectId, "测试企业", "测试项目", "thread-1", "ADVANCED", "MATERIAL_CHECK", 0L, now, now);
|
||||
AgentRunService.RunView current = new AgentRunService.RunView(
|
||||
currentRunId, projectId, modelId, "INITIAL", "RUNNING", null, null, now, null);
|
||||
AgentRunService.RunView replacement = new AgentRunService.RunView(
|
||||
replacementRunId, projectId, modelId, "RESUME", "RUNNING", null, null, now, null);
|
||||
|
||||
AgentRunMapper runMapper = mock(AgentRunMapper.class);
|
||||
AgentRunStore runStore = mock(AgentRunStore.class);
|
||||
ProjectService projectService = mock(ProjectService.class);
|
||||
UserService userService = mock(UserService.class);
|
||||
when(projectService.require(projectId)).thenReturn(project);
|
||||
when(userService.requireUserId("admin")).thenReturn(UUID.randomUUID());
|
||||
when(runStore.latest(projectId)).thenReturn(current);
|
||||
when(runStore.interruptedPhase(current, project)).thenReturn("MATERIAL_CHECK");
|
||||
when(runMapper.interruptRunning(currentRunId)).thenReturn(1);
|
||||
when(runStore.create(projectId, "RESUME", currentRunId, modelId)).thenReturn(replacement);
|
||||
|
||||
AgentRunService service = new AgentRunService(
|
||||
runMapper,
|
||||
new ObjectMapper(),
|
||||
mock(AgentExecutionService.class),
|
||||
mock(AgentOutputService.class),
|
||||
runStore,
|
||||
mock(AgentEventService.class),
|
||||
projectService,
|
||||
mock(ProjectFileService.class),
|
||||
userService,
|
||||
mock(ArtifactService.class),
|
||||
mock(ExecutorService.class),
|
||||
mock(TransactionTemplate.class));
|
||||
|
||||
// 服务会注册“提交后取消旧流并启动新流”的回调;测试只验证注册前的事务内状态转换。
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
try {
|
||||
assertThat(service.switchModel(projectId, modelId, () -> "admin")).isEqualTo(replacement);
|
||||
verify(runMapper).interruptRunning(currentRunId);
|
||||
verify(runStore).create(projectId, "RESUME", currentRunId, modelId);
|
||||
} finally {
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络故障和服务端错误允许重连,参数错误保持原始失败。
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import tech.easyflow.manuagent.entity.AgentRunEntity;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.entity.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.mapper.AgentEventMapper;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
@@ -49,10 +50,11 @@ class AgentRunStoreQueryTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认模型缺失属于数据库配置异常,不应在 ORM 迁移中新增 409 业务错误。
|
||||
* 数据库允许在首次启动时没有模型,因此启动 Run 时应返回可操作的业务错误,
|
||||
* 不能把正常的“尚未配置”状态暴露为服务端技术异常。
|
||||
*/
|
||||
@Test
|
||||
void shouldKeepMissingDefaultModelAsUnexpectedTechnicalFailure() {
|
||||
void shouldReportMissingDefaultModelAsConfigurationConflict() {
|
||||
AgentRunMapper runMapper = mock(AgentRunMapper.class);
|
||||
when(runMapper.selectCountByQuery(any(QueryWrapper.class))).thenReturn(0L);
|
||||
AgentRunStore store = new AgentRunStore(
|
||||
@@ -62,8 +64,49 @@ class AgentRunStoreQueryTest {
|
||||
new ObjectMapper());
|
||||
|
||||
assertThatThrownBy(() -> store.create(UUID.randomUUID(), "INITIAL", null))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.isNotInstanceOf(ApiException.class);
|
||||
.isInstanceOfSatisfying(ApiException.class, exception -> {
|
||||
assertThat(exception.status().value()).isEqualTo(409);
|
||||
assertThat(exception.code()).isEqualTo("MODEL_NOT_CONFIGURED");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户恢复任务时可以显式选择替代模型;新 Run 必须保存该模型 ID,
|
||||
* 不能再次回退到随后可能变化的全局默认模型。
|
||||
*/
|
||||
@Test
|
||||
void shouldCreateRunWithExplicitEnabledModel() {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
UUID modelId = UUID.randomUUID();
|
||||
AgentRunMapper runMapper = mock(AgentRunMapper.class);
|
||||
ModelConfigMapper modelMapper = mock(ModelConfigMapper.class);
|
||||
when(runMapper.selectCountByQuery(any(QueryWrapper.class))).thenReturn(0L);
|
||||
|
||||
ModelConfigEntity selectedModel = new ModelConfigEntity();
|
||||
selectedModel.setId(modelId);
|
||||
selectedModel.setEnabled(true);
|
||||
when(modelMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(selectedModel);
|
||||
|
||||
AgentRunEntity storedRun = new AgentRunEntity();
|
||||
storedRun.setId(UUID.randomUUID());
|
||||
storedRun.setProjectId(projectId);
|
||||
storedRun.setModelConfigId(modelId);
|
||||
storedRun.setTriggerType("RESUME");
|
||||
storedRun.setStatus("RUNNING");
|
||||
when(runMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(storedRun);
|
||||
|
||||
AgentRunStore store = new AgentRunStore(
|
||||
runMapper,
|
||||
mock(AgentEventMapper.class),
|
||||
modelMapper,
|
||||
new ObjectMapper());
|
||||
|
||||
AgentRunService.RunView run = store.create(projectId, "RESUME", null, modelId);
|
||||
|
||||
ArgumentCaptor<AgentRunEntity> entityCaptor = ArgumentCaptor.forClass(AgentRunEntity.class);
|
||||
verify(runMapper).insertSelective(entityCaptor.capture());
|
||||
assertThat(entityCaptor.getValue().getModelConfigId()).isEqualTo(modelId);
|
||||
assertThat(run.modelConfigId()).isEqualTo(modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package tech.easyflow.manuagent.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 验证应用主密钥沿用本地配置文件的读取方式。
|
||||
*/
|
||||
class AppPropertiesValidationTest {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withUserConfiguration(PropertiesConfiguration.class)
|
||||
.withPropertyValues(
|
||||
"app.data-root=file:../data",
|
||||
"app.dashscope-key-file=./dashscope_key.txt",
|
||||
"app.master-key=unit-test-master-key",
|
||||
"app.admin-username=admin",
|
||||
"app.admin-password=admin123",
|
||||
"app.sandbox-image=runtime:test",
|
||||
"app.sandbox-network=bridge",
|
||||
"app.run-timeout=1m");
|
||||
|
||||
/**
|
||||
* 默认应用配置必须直接提供主密钥,使本地启动不依赖额外环境变量。
|
||||
*
|
||||
* @throws IOException application.yml 无法读取时抛出
|
||||
*/
|
||||
@Test
|
||||
void shouldProvideMasterKeyInApplicationConfiguration() throws IOException {
|
||||
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
|
||||
List<PropertySource<?>> sources = loader.load(
|
||||
"application.yml",
|
||||
new ClassPathResource("application.yml"));
|
||||
|
||||
assertThat(sources)
|
||||
.extracting(source -> source.getProperty("app.master-key"))
|
||||
.singleElement()
|
||||
.isInstanceOf(String.class)
|
||||
.asString()
|
||||
.isNotBlank()
|
||||
.doesNotContain("APP_MASTER_KEY");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供非空加密主密钥后,配置属性应可以正常绑定并供密钥组件使用。
|
||||
*/
|
||||
@Test
|
||||
void shouldBindConfiguredMasterKey() {
|
||||
contextRunner.run(context -> {
|
||||
assertThat(context).hasNotFailed();
|
||||
assertThat(context.getBean(AppProperties.class).masterKey())
|
||||
.isEqualTo("unit-test-master-key");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册应用配置属性,复用生产环境的 Spring Boot 配置绑定流程。
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(AppProperties.class)
|
||||
static class PropertiesConfiguration {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package tech.easyflow.manuagent.model;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
/**
|
||||
* 验证模型管理页面依赖的 HTTP 接口契约。
|
||||
*
|
||||
* <p>这些测试刻意放在控制器边界,防止前端请求方法或路径与后端映射再次发生漂移。</p>
|
||||
*/
|
||||
class ModelControllerTest {
|
||||
|
||||
/**
|
||||
* 测试连接必须接收当前表单草稿,使用户无需先持久化可能无效的配置。
|
||||
*
|
||||
* @throws Exception MockMvc 执行失败时抛出
|
||||
*/
|
||||
@Test
|
||||
void shouldTestCurrentModelDraftWithoutSavingIt() throws Exception {
|
||||
ModelService service = mock(ModelService.class);
|
||||
when(service.test(any(ModelService.ConnectionTestInput.class)))
|
||||
.thenReturn(new ModelService.ConnectionResult(true, 12L, "连接正常"));
|
||||
MockMvc mvc = MockMvcBuilders.standaloneSetup(new ModelController(service)).build();
|
||||
|
||||
mvc.perform(post("/api/models/test")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"id": "01cdd509-a79a-4503-839f-160b32d518e2",
|
||||
"baseUrl": "https://draft.example.test/v1",
|
||||
"modelId": "draft-model",
|
||||
"apiKey": "draft-secret"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.message").value("连接正常"));
|
||||
|
||||
verify(service).test(new ModelService.ConnectionTestInput(
|
||||
UUID.fromString("01cdd509-a79a-4503-839f-160b32d518e2"),
|
||||
"https://draft.example.test/v1",
|
||||
"draft-model",
|
||||
"draft-secret"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用操作必须由 PATCH 状态接口处理,不能落入静态资源处理器并返回 404。
|
||||
*
|
||||
* @throws Exception MockMvc 执行失败时抛出
|
||||
*/
|
||||
@Test
|
||||
void shouldRouteDisableRequestToModelService() throws Exception {
|
||||
UUID id = UUID.fromString("01cdd509-a79a-4503-839f-160b32d518e2");
|
||||
ModelService service = mock(ModelService.class);
|
||||
MockMvc mvc = MockMvcBuilders.standaloneSetup(new ModelController(service)).build();
|
||||
|
||||
mvc.perform(patch("/api/models/{id}/enabled", id)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"enabled\":false}"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(service).setEnabled(id, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package tech.easyflow.manuagent.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.entity.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
|
||||
/**
|
||||
* 验证模型草稿连接测试的密钥选择和外部请求边界。
|
||||
*/
|
||||
class ModelServiceConnectionTest {
|
||||
|
||||
/**
|
||||
* Spring 容器必须能够在生产构造器和包内测试构造器之间选择生产构造器。
|
||||
*
|
||||
* <p>该测试防止新增辅助构造器后,应用启动阶段退化为查找不存在的无参构造器。</p>
|
||||
*/
|
||||
@Test
|
||||
void shouldCreateModelServiceBeanWithProductionConstructor() {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
|
||||
context.registerBean(ModelConfigMapper.class, () -> mock(ModelConfigMapper.class));
|
||||
context.registerBean(ModelAssignmentMapper.class, () -> mock(ModelAssignmentMapper.class));
|
||||
context.registerBean(AgentRunMapper.class, () -> mock(AgentRunMapper.class));
|
||||
context.registerBean(UserService.class, () -> mock(UserService.class));
|
||||
context.registerBean(KeyCipher.class, () -> mock(KeyCipher.class));
|
||||
context.registerBean(ObjectMapper.class, () -> new ObjectMapper());
|
||||
context.register(ModelService.class);
|
||||
|
||||
context.refresh();
|
||||
|
||||
assertThat(context.getBean(ModelService.class)).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单提供新密钥时,应直接测试草稿地址,且不能读取或修改数据库模型配置。
|
||||
*
|
||||
* @throws Exception HTTP 客户端桩配置或调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
void shouldTestDraftWithSubmittedKeyWithoutReadingDatabase() throws Exception {
|
||||
ModelConfigMapper modelMapper = mock(ModelConfigMapper.class);
|
||||
HttpClient httpClient = successfulHttpClient();
|
||||
ModelService service = service(modelMapper, mock(KeyCipher.class), httpClient);
|
||||
|
||||
ModelService.ConnectionResult result = service.test(new ModelService.ConnectionTestInput(
|
||||
UUID.randomUUID(),
|
||||
"https://draft.example.test/v1/",
|
||||
"draft-model",
|
||||
"draft-secret"));
|
||||
|
||||
assertThat(result.success()).isTrue();
|
||||
HttpRequest request = sentRequest(httpClient);
|
||||
assertThat(request.uri()).isEqualTo(URI.create("https://draft.example.test/v1/chat/completions"));
|
||||
assertThat(request.headers().firstValue("Authorization")).contains("Bearer draft-secret");
|
||||
verifyNoInteractions(modelMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新模型没有数据库 ID 和保存密钥,API Key 留空时应在发起网络请求前返回明确错误。
|
||||
*/
|
||||
@Test
|
||||
void shouldRequireKeyWhenTestingNewModelDraft() {
|
||||
ModelConfigMapper modelMapper = mock(ModelConfigMapper.class);
|
||||
HttpClient httpClient = mock(HttpClient.class);
|
||||
ModelService service = service(modelMapper, mock(KeyCipher.class), httpClient);
|
||||
|
||||
assertThatThrownBy(() -> service.test(new ModelService.ConnectionTestInput(
|
||||
null,
|
||||
"https://draft.example.test/v1",
|
||||
"draft-model",
|
||||
"")))
|
||||
.isInstanceOf(ApiException.class)
|
||||
.hasMessage("测试新模型需要 API Key");
|
||||
|
||||
verifyNoInteractions(modelMapper, httpClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* 草稿改到其他 API 主机时不能转发数据库中的隐藏密钥,必须要求用户重新输入 Key。
|
||||
*/
|
||||
@Test
|
||||
void shouldNotForwardStoredKeyToChangedBaseUrl() {
|
||||
UUID modelId = UUID.randomUUID();
|
||||
ModelConfigEntity saved = new ModelConfigEntity();
|
||||
saved.setId(modelId);
|
||||
saved.setBaseUrl("https://saved.example.test/v1");
|
||||
saved.setApiKeyCiphertext("encrypted-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
ModelConfigMapper modelMapper = mock(ModelConfigMapper.class);
|
||||
when(modelMapper.selectOneByQuery(any())).thenReturn(saved);
|
||||
HttpClient httpClient = mock(HttpClient.class);
|
||||
ModelService service = service(modelMapper, mock(KeyCipher.class), httpClient);
|
||||
|
||||
assertThatThrownBy(() -> service.test(new ModelService.ConnectionTestInput(
|
||||
modelId,
|
||||
"https://attacker.example.test/v1",
|
||||
"draft-model",
|
||||
"")))
|
||||
.isInstanceOf(ApiException.class)
|
||||
.hasMessage("API 地址变更后需要重新输入 API Key");
|
||||
|
||||
verifyNoInteractions(httpClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* 草稿地址与保存地址一致时,允许安全复用保存密钥,避免用户为普通模型参数调整重复输入 Key。
|
||||
*
|
||||
* @throws Exception HTTP 客户端桩配置或调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
void shouldReuseStoredKeyWhenBaseUrlIsUnchanged() throws Exception {
|
||||
UUID modelId = UUID.randomUUID();
|
||||
byte[] ciphertext = "encrypted-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
ModelConfigEntity saved = new ModelConfigEntity();
|
||||
saved.setId(modelId);
|
||||
saved.setBaseUrl("https://saved.example.test/v1");
|
||||
saved.setApiKeyCiphertext(ciphertext);
|
||||
ModelConfigMapper modelMapper = mock(ModelConfigMapper.class);
|
||||
when(modelMapper.selectOneByQuery(any())).thenReturn(saved);
|
||||
KeyCipher keyCipher = mock(KeyCipher.class);
|
||||
when(keyCipher.decrypt(ciphertext)).thenReturn("stored-secret");
|
||||
HttpClient httpClient = successfulHttpClient();
|
||||
ModelService service = service(modelMapper, keyCipher, httpClient);
|
||||
|
||||
ModelService.ConnectionResult result = service.test(new ModelService.ConnectionTestInput(
|
||||
modelId,
|
||||
"https://saved.example.test/v1/",
|
||||
"updated-model-id",
|
||||
""));
|
||||
|
||||
assertThat(result.success()).isTrue();
|
||||
assertThat(sentRequest(httpClient).headers().firstValue("Authorization"))
|
||||
.contains("Bearer stored-secret");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建仅替换外部依赖的模型服务,生产逻辑仍由真实 {@link ModelService} 执行。
|
||||
*
|
||||
* @param modelMapper 模型配置 Mapper 桩
|
||||
* @param keyCipher 密钥组件桩
|
||||
* @param httpClient HTTP 客户端桩
|
||||
* @return 待测试模型服务
|
||||
*/
|
||||
private ModelService service(ModelConfigMapper modelMapper, KeyCipher keyCipher, HttpClient httpClient) {
|
||||
return new ModelService(
|
||||
modelMapper,
|
||||
mock(ModelAssignmentMapper.class),
|
||||
mock(AgentRunMapper.class),
|
||||
mock(UserService.class),
|
||||
keyCipher,
|
||||
new ObjectMapper(),
|
||||
httpClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建固定返回 HTTP 200 的客户端,避免测试访问真实模型服务。
|
||||
*
|
||||
* @return HTTP 客户端桩
|
||||
* @throws Exception 配置泛型 send 方法桩时抛出
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private HttpClient successfulHttpClient() throws Exception {
|
||||
HttpClient client = mock(HttpClient.class);
|
||||
HttpResponse<String> response = mock(HttpResponse.class);
|
||||
when(response.statusCode()).thenReturn(200);
|
||||
when(client.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(response);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获服务发送的请求,以结果状态而非内部调用顺序验证连接目标和认证头。
|
||||
*
|
||||
* @param client HTTP 客户端桩
|
||||
* @return 捕获到的请求
|
||||
* @throws Exception Mockito 验证泛型 send 方法时抛出
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private HttpRequest sentRequest(HttpClient client) throws Exception {
|
||||
ArgumentCaptor<HttpRequest> captor = ArgumentCaptor.forClass(HttpRequest.class);
|
||||
org.mockito.Mockito.verify(client).send(captor.capture(), any(HttpResponse.BodyHandler.class));
|
||||
return captor.getValue();
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.config.AppProperties;
|
||||
@@ -19,12 +20,21 @@ import tech.easyflow.manuagent.entity.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.mapper.AppUserMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
|
||||
/**
|
||||
* 验证模型管理接口使用最小字段投影,不把加密 API Key 读入普通请求内存。
|
||||
*/
|
||||
class ModelServiceQueryTest {
|
||||
|
||||
/**
|
||||
* 模型必须完全由管理接口和数据库维护,服务启动不得再通过 Key 文件自动灌入默认模型。
|
||||
*/
|
||||
@Test
|
||||
void shouldNotInitializeModelConfigurationFromApplicationFiles() {
|
||||
assertThat(ApplicationRunner.class.isAssignableFrom(ModelService.class)).isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型列表只需要页面展示字段,查询 SQL 不得包含密文和密钥版本列。
|
||||
*/
|
||||
@@ -36,10 +46,9 @@ class ModelServiceQueryTest {
|
||||
ModelService service = new ModelService(
|
||||
modelMapper,
|
||||
mock(ModelAssignmentMapper.class),
|
||||
mock(AppUserMapper.class),
|
||||
mock(AgentRunMapper.class),
|
||||
mock(UserService.class),
|
||||
mock(KeyCipher.class),
|
||||
mock(AppProperties.class),
|
||||
new ObjectMapper());
|
||||
|
||||
List<ModelService.ModelView> models = service.list();
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package tech.easyflow.manuagent.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelAssignmentMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
|
||||
/**
|
||||
* 验证模型配置在进入数据库和外部 HTTP 客户端之前具有明确、可审计的输入边界。
|
||||
*/
|
||||
class ModelServiceValidationTest {
|
||||
|
||||
/**
|
||||
* 验证名称、地址、模型标识和 API Key 的长度限制,防止超长请求占用内存或触发数据库截断异常。
|
||||
*/
|
||||
@Test
|
||||
void shouldBoundModelInputTextFields() {
|
||||
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||
ModelService.ModelInput input = new ModelService.ModelInput(
|
||||
"名".repeat(101),
|
||||
"https://example.test/" + "a".repeat(500),
|
||||
"m".repeat(256),
|
||||
"k".repeat(4097),
|
||||
Map.of(),
|
||||
Map.of("contextWindow", 8_192));
|
||||
|
||||
// 一次构造四个越界字段,并按属性名断言,确保每个 HTTP 入参都真正受到约束。
|
||||
Set<String> invalidProperties = validator.validate(input).stream()
|
||||
.map(violation -> violation.getPropertyPath().toString())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
assertThat(invalidProperties).containsExactlyInAnyOrder("name", "baseUrl", "modelId", "apiKey");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证模型地址拒绝非 HTTP(S) 协议、缺失主机以及可能改变请求语义的用户信息、查询和片段。
|
||||
*/
|
||||
@Test
|
||||
void shouldRejectUnsafeOrAmbiguousBaseUrls() {
|
||||
ModelService service = service();
|
||||
|
||||
for (String baseUrl : Set.of(
|
||||
"file:///etc/passwd",
|
||||
"https:///v1",
|
||||
"https://user:password@example.test/v1",
|
||||
"https://example.test/v1?tenant=other",
|
||||
"https://example.test/v1#fragment")) {
|
||||
ModelService.ModelInput input = new ModelService.ModelInput(
|
||||
"测试模型",
|
||||
baseUrl,
|
||||
"model-v1",
|
||||
"secret",
|
||||
Map.of(),
|
||||
Map.of("contextWindow", 8_192));
|
||||
|
||||
assertThatThrownBy(() -> service.save(null, input, () -> "admin"))
|
||||
.as("地址应被拒绝:%s", baseUrl)
|
||||
.isInstanceOfSatisfying(ApiException.class, exception ->
|
||||
assertThat(exception.code()).isEqualTo("MODEL_BASE_URL_INVALID"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建只执行输入校验所需路径的模型服务;无效 URL 应在任何持久化写入之前被拒绝。
|
||||
*
|
||||
* @return 配置了当前用户的模型服务
|
||||
*/
|
||||
private ModelService service() {
|
||||
UserService userService = mock(UserService.class);
|
||||
when(userService.requireUserId("admin")).thenReturn(UUID.randomUUID());
|
||||
return new ModelService(
|
||||
mock(ModelConfigMapper.class),
|
||||
mock(ModelAssignmentMapper.class),
|
||||
mock(AgentRunMapper.class),
|
||||
userService,
|
||||
mock(KeyCipher.class),
|
||||
new ObjectMapper());
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,9 @@ class ProjectFileServiceTest {
|
||||
@Test
|
||||
void shouldDeleteProjectWorkspace() throws Exception {
|
||||
AppProperties properties = new AppProperties(
|
||||
temporaryDirectory, Path.of("deepseek"), Path.of("dashscope"),
|
||||
"test-master", "admin", "admin", "https://example.test", "model",
|
||||
131_072, "runtime:test", "bridge", Duration.ofMinutes(1));
|
||||
temporaryDirectory, Path.of("dashscope"),
|
||||
"test-master", "admin", "admin",
|
||||
"runtime:test", "bridge", Duration.ofMinutes(1));
|
||||
ProjectFileService service = new ProjectFileService(
|
||||
mock(ProjectFileMapper.class), mock(UserService.class), mock(ProjectService.class), properties);
|
||||
UUID projectId = UUID.randomUUID();
|
||||
@@ -121,8 +121,8 @@ class ProjectFileServiceTest {
|
||||
*/
|
||||
private AppProperties properties() {
|
||||
return new AppProperties(
|
||||
temporaryDirectory, Path.of("deepseek"), Path.of("dashscope"),
|
||||
"test-master", "admin", "admin", "https://example.test", "model",
|
||||
131_072, "runtime:test", "bridge", Duration.ofMinutes(1));
|
||||
temporaryDirectory, Path.of("dashscope"),
|
||||
"test-master", "admin", "admin",
|
||||
"runtime:test", "bridge", Duration.ofMinutes(1));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user