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; import tech.easyflow.manuagent.agent.AgentEventService; import tech.easyflow.manuagent.agent.AgentRunService; import tech.easyflow.manuagent.agent.AgentRunStore; import tech.easyflow.manuagent.artifact.ArtifactService; import tech.easyflow.manuagent.artifact.DocxValidator; import tech.easyflow.manuagent.auth.UserService; import tech.easyflow.manuagent.entity.AgentEventEntity; import tech.easyflow.manuagent.entity.AppUserEntity; import tech.easyflow.manuagent.mapper.AgentEventMapper; import tech.easyflow.manuagent.mapper.AgentRunMapper; import tech.easyflow.manuagent.mapper.AppUserMapper; import tech.easyflow.manuagent.mapper.ArtifactMapper; import tech.easyflow.manuagent.mapper.ProjectMapper; import tech.easyflow.manuagent.mapper.ProjectPlanMapper; import tech.easyflow.manuagent.mapper.ModelAssignmentMapper; import tech.easyflow.manuagent.mapper.ModelConfigMapper; import tech.easyflow.manuagent.mapper.SkillConfigMapper; import tech.easyflow.manuagent.model.KeyCipher; import tech.easyflow.manuagent.model.ModelService; import tech.easyflow.manuagent.config.AppProperties; import tech.easyflow.manuagent.skill.SkillPackageReader; import tech.easyflow.manuagent.skill.SkillService; import io.agentscope.core.skill.repository.postgresql.PostgresSkillRepository; import tech.easyflow.manuagent.typehandler.JsonbStringTypeHandler; import tech.easyflow.manuagent.typehandler.UuidTypeHandler; import tech.easyflow.manuagent.project.ProjectService; import tech.easyflow.manuagent.project.ProjectFileService; import com.fasterxml.jackson.databind.ObjectMapper; import com.mybatisflex.core.MybatisFlexBootstrap; import com.mybatisflex.core.datasource.FlexDataSource; import com.mybatisflex.core.mybatis.FlexConfiguration; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.table.TableInfo; import com.mybatisflex.core.table.TableInfoFactory; import java.nio.file.Files; import java.nio.file.Path; import java.io.InputStream; import java.sql.DriverManager; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.time.Duration; import org.flywaydb.core.Flyway; import org.apache.ibatis.builder.xml.XMLMapperBuilder; import org.apache.ibatis.io.Resources; import org.apache.ibatis.mapping.Environment; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.postgresql.ds.PGSimpleDataSource; import org.springframework.jdbc.core.simple.JdbcClient; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; /** * 验证 PostgreSQL 17 全量迁移及事件游标回放。 */ @Testcontainers class DatabaseAndEventIntegrationTest { @Container private static final PostgreSQLContainer> POSTGRES = new PostgreSQLContainer<>("postgres:17-alpine"); @TempDir private Path temporaryDirectory; /** * 在干净 PostgreSQL 17 实例执行并校验全部 Flyway 迁移。 */ @BeforeAll static void migrate() { Flyway flyway = Flyway.configure() .dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword()) .locations("classpath:db/migration") .load(); flyway.migrate(); flyway.validate(); assertThat(flyway.migrate().migrationsExecuted).isZero(); } /** * 验证核心表、索引和约束已建立。 * * @throws Exception 数据库访问失败时抛出 */ @Test void shouldCreateCoreSchemaOnPostgres17() throws Exception { try (var connection = DriverManager.getConnection( POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword()); var statement = connection.createStatement(); var result = statement.executeQuery(""" SELECT count(*) FROM information_schema.tables WHERE table_schema IN ('app', 'agentscope') """)) { assertThat(result.next()).isTrue(); assertThat(result.getInt(1)).isEqualTo(12); } } /** * 验证事件按项目全局 ID 增量回放且不重复。 */ @Test void shouldReplayEventsAfterCursorInOrder() throws Exception { JdbcClient jdbc = jdbc(); UUID userId = UUID.randomUUID(); UUID modelId = UUID.randomUUID(); UUID projectId = UUID.randomUUID(); UUID runId = UUID.randomUUID(); jdbc.sql("INSERT INTO app.app_user(id, username, password_hash, display_name) VALUES (:id, :name, 'x', 'test')") .param("id", userId).param("name", "u-" + userId).update(); jdbc.sql(""" INSERT INTO app.model_config(id, name, provider, base_url, model_id, is_default) VALUES (:id, :name, 'OPENAI_COMPATIBLE', 'https://example.test', 'model', FALSE) """).param("id", modelId).param("name", "m-" + modelId).update(); jdbc.sql(""" INSERT INTO app.project(id, company_name, project_name, agui_thread_id, application_level, created_by) VALUES (:id, '企业', '项目', :thread, 'ADVANCED', :userId) """).param("id", projectId).param("thread", "t-" + projectId).param("userId", userId).update(); jdbc.sql(""" INSERT INTO app.agent_run(id, project_id, model_config_id, trigger_type, status, trace_id) VALUES (:id, :projectId, :modelId, 'INITIAL', 'RUNNING', :trace) """).param("id", runId).param("projectId", projectId).param("modelId", modelId) .param("trace", UUID.randomUUID().toString()).update(); AgentEventService service = new AgentEventService(agentEventMapper(), new ObjectMapper()); long first = service.append(projectId, runId, "RUN_STARTED", Map.of("phase", "MATERIAL_CHECK")).id(); long second = service.append(projectId, runId, "TEXT_MESSAGE_CONTENT", Map.of("delta", "分析")).id(); long third = service.append(projectId, runId, "TEXT_MESSAGE_CONTENT", Map.of("delta", "完成")).id(); assertThat(service.listAfter(projectId, first, 100)) .extracting(AgentEventService.EventView::id) .containsExactly(second, third); var next = service.streamAfter(projectId, third) .filter(event -> !"HEARTBEAT".equals(event.type())) .next() .toFuture(); long pushed = service.append(projectId, runId, "TEXT_MESSAGE_CONTENT", Map.of("delta", "推送")).id(); assertThat(next.orTimeout(2, TimeUnit.SECONDS).join().id()).isEqualTo(pushed); } /** * 验证重试生成同一路径产物时更新登记信息,避免唯一约束导致成功 Run 被标记失败。 * * @throws Exception 临时文件写入失败时抛出 */ @Test void shouldReplaceArtifactMetadataForSameProjectPath() throws Exception { JdbcClient jdbc = jdbc(); UUID userId = UUID.randomUUID(); UUID modelId = UUID.randomUUID(); UUID projectId = UUID.randomUUID(); UUID firstRunId = UUID.randomUUID(); UUID secondRunId = UUID.randomUUID(); jdbc.sql("INSERT INTO app.app_user(id, username, password_hash, display_name) VALUES (:id, :name, 'x', 'test')") .param("id", userId).param("name", "u-" + userId).update(); jdbc.sql(""" INSERT INTO app.model_config(id, name, provider, base_url, model_id) VALUES (:id, :name, 'OPENAI_COMPATIBLE', 'https://example.test', 'model') """).param("id", modelId).param("name", "m-" + modelId).update(); jdbc.sql(""" INSERT INTO app.project(id, company_name, project_name, agui_thread_id, application_level, created_by) VALUES (:id, '企业', '项目', :thread, 'ADVANCED', :userId) """).param("id", projectId).param("thread", "t-" + projectId).param("userId", userId).update(); for (UUID runId : List.of(firstRunId, secondRunId)) { jdbc.sql(""" INSERT INTO app.agent_run( id, project_id, model_config_id, trigger_type, status, trace_id, ended_at) VALUES (:id, :projectId, :modelId, 'RETRY', 'COMPLETED', :trace, CURRENT_TIMESTAMP) """).param("id", runId).param("projectId", projectId).param("modelId", modelId) .param("trace", UUID.randomUUID().toString()).update(); } Path document = temporaryDirectory.resolve("draft.docx"); Files.writeString(document, "first"); ProjectFileService files = mock(ProjectFileService.class); when(files.safeProjectPath(projectId, "artifacts/draft.docx")).thenReturn(document); ArtifactService artifacts = new ArtifactService(artifactMapper(), files, new DocxValidator()); ObjectMapper mapper = new ObjectMapper(); ArtifactService.ArtifactView first = artifacts.publish( projectId, firstRunId, "DOCX", "draft.docx", "artifacts/draft.docx", mapper.createObjectNode().put("version", 1)); Files.writeString(document, "second version"); ArtifactService.ArtifactView second = artifacts.publish( projectId, secondRunId, "DOCX", "draft.docx", "artifacts/draft.docx", mapper.createObjectNode().put("version", 2)); assertThat(second.id()).isEqualTo(first.id()); assertThat(second.runId()).isEqualTo(secondRunId); assertThat(second.sizeBytes()).isEqualTo(Files.size(document)); assertThat(artifacts.list(projectId)).hasSize(1); } /** * 验证项目真删除会清除所有关联业务记录。 */ @Test void shouldDeleteProjectRecords() throws Exception { JdbcClient jdbc = jdbc(); UUID userId = UUID.randomUUID(); UUID projectId = UUID.randomUUID(); UUID runId = UUID.randomUUID(); jdbc.sql("INSERT INTO app.app_user(id, username, password_hash, display_name) VALUES (:id, :name, 'x', 'test')") .param("id", userId).param("name", "u-" + userId).update(); jdbc.sql(""" INSERT INTO app.project(id, company_name, project_name, agui_thread_id, application_level, created_by) VALUES (:id, '待删除企业', '待删除项目', :thread, 'ADVANCED', :userId) """).param("id", projectId).param("thread", "t-" + projectId).param("userId", userId).update(); jdbc.sql(""" INSERT INTO app.agent_run(id, project_id, trigger_type, status, trace_id, ended_at) VALUES (:id, :projectId, 'INITIAL', 'COMPLETED', :trace, CURRENT_TIMESTAMP) """).param("id", runId).param("projectId", projectId) .param("trace", UUID.randomUUID().toString()).update(); jdbc.sql(""" INSERT INTO app.agent_event(project_id, run_id, event_type, payload) VALUES (:projectId, :runId, 'RUN_FINISHED', '{}'::jsonb) """).param("projectId", projectId).param("runId", runId).update(); jdbc.sql(""" INSERT INTO app.project_plan(id, project_id, plan_version, status, plan_json, created_by) VALUES (:id, :projectId, 1, 'DRAFT', '{}'::jsonb, :userId) """).param("id", UUID.randomUUID()).param("projectId", projectId).param("userId", userId).update(); jdbc.sql(""" INSERT INTO app.project_file( id, project_id, original_name, stored_name, relative_path, mime_type, extension, size_bytes, sha256, uploaded_by) VALUES (:id, :projectId, 'input.txt', 'input.txt', 'inputs/input.txt', 'text/plain', 'txt', 1, :sha, :userId) """).param("id", UUID.randomUUID()).param("projectId", projectId) .param("sha", "0".repeat(64)).param("userId", userId).update(); jdbc.sql(""" INSERT INTO app.artifact( id, project_id, run_id, kind, name, relative_path, mime_type, size_bytes, sha256) VALUES (:id, :projectId, :runId, 'OTHER', 'result.txt', 'artifacts/result.txt', 'text/plain', 1, :sha) """).param("id", UUID.randomUUID()).param("projectId", projectId).param("runId", runId) .param("sha", "0".repeat(64)).update(); ProjectService service = new ProjectService( projectMapper(), mock(ProjectPlanMapper.class), mock(UserService.class), new ObjectMapper()); service.delete(projectId); for (String table : List.of("agent_event", "artifact", "project_plan", "project_file", "agent_run")) { Long count = jdbc.sql("SELECT COUNT(*) FROM app." + table + " WHERE project_id = :projectId") .param("projectId", projectId) .query(Long.class) .single(); assertThat(count).as(table).isZero(); } assertThat(jdbc.sql("SELECT COUNT(*) FROM app.project WHERE id = :projectId") .param("projectId", projectId) .query(Long.class) .single()).isZero(); } /** * 验证 Run 创建、等待确认、完成和中断均遵守数据库状态机条件。 * *
该测试直接覆盖 MyBatis-Flex BaseMapper 插入、XML 状态更新和实体结果映射, * 防止迁移后出现 UUID 主键未写入、JSONB Ask 丢失或终态被重复覆盖。
*/ @Test void shouldPersistAndTransitionAgentRunWithMybatisFlex() throws Exception { JdbcClient jdbc = jdbc(); UUID userId = UUID.randomUUID(); UUID modelId = UUID.randomUUID(); UUID projectId = UUID.randomUUID(); jdbc.sql("INSERT INTO app.app_user(id, username, password_hash, display_name) VALUES (:id, :name, 'x', 'test')") .param("id", userId).param("name", "run-u-" + userId).update(); // 测试类共用同一容器;先释放其他用例留下的唯一默认模型,再建立本用例的确定性前置条件。 jdbc.sql("UPDATE app.model_config SET is_default = FALSE WHERE is_default").update(); jdbc.sql(""" INSERT INTO app.model_config(id, name, provider, base_url, model_id, is_default) VALUES (:id, :name, 'OPENAI_COMPATIBLE', 'https://example.test', 'model', TRUE) """).param("id", modelId).param("name", "run-m-" + modelId).update(); jdbc.sql(""" INSERT INTO app.project(id, company_name, project_name, agui_thread_id, application_level, created_by) VALUES (:id, '企业', 'Run 迁移测试', :threadId, 'ADVANCED', :userId) """).param("id", projectId).param("threadId", "run-thread-" + projectId) .param("userId", userId).update(); AgentRunMapper mapper = agentRunMapper(); AgentEventMapper events = agentEventMapper(); AgentRunStore store = new AgentRunStore(mapper, events, modelConfigMapper(), new ObjectMapper()); AgentRunService.RunView initial = store.create(projectId, "INITIAL", null); assertThat(initial.status()).isEqualTo("RUNNING"); assertThat(store.latest(projectId).id()).isEqualTo(initial.id()); store.ensureRunning(initial.id()); String interrupt = "{\"kind\":\"material_check\",\"items\":[]}"; assertThat(mapper.waitForInput(initial.id(), interrupt)).isEqualTo(1); assertThat(new ObjectMapper().readTree( store.requireWaiting(projectId, "material_check").pendingInterrupt())) .isEqualTo(new ObjectMapper().readTree(interrupt)); store.completeWaiting(initial.id()); assertThat(store.require(initial.id()).status()).isEqualTo("COMPLETED"); AgentRunService.RunView resumed = store.create(projectId, "RESUME", initial.id()); AgentEventEntity started = new AgentEventEntity(); started.setProjectId(projectId); started.setRunId(resumed.id()); started.setEventType("RUN_STARTED"); started.setEventId(UUID.randomUUID().toString()); started.setPayloadJson("{\"phase\":\"PLANNING\"}"); events.insertReturning(started); AgentEventEntity response = new AgentEventEntity(); response.setProjectId(projectId); response.setRunId(resumed.id()); response.setEventType("ASK_RESPONDED"); response.setEventId(UUID.randomUUID().toString()); response.setPayloadJson("{\"decisions\":[]}"); events.insertReturning(response); assertThat(events.selectLatestStartedPhase(resumed.id())).isEqualTo("PLANNING"); assertThat(new ObjectMapper().readTree(events.selectLatestMaterialResponseJson(projectId))) .isEqualTo(new ObjectMapper().readTree("{\"decisions\":[]}")); assertThat(mapper.interruptRunning(resumed.id())).isEqualTo(1); assertThat(mapper.interruptRunning(resumed.id())).isZero(); assertThat(store.isInterrupted(resumed.id())).isTrue(); AgentRunService.RunView restartCandidate = store.create(projectId, "RETRY", resumed.id()); assertThat(mapper.interruptRunningAfterRestart()).isGreaterThanOrEqualTo(1); assertThat(mapper.selectOneById(restartCandidate.id()).getErrorCode()).isEqualTo("PROCESS_RESTARTED"); } /** * 验证规划草稿使用递增版本写入 JSONB,并且只有 DRAFT 可以原子确认。 * * @throws Exception Mapper XML 初始化失败时抛出 */ @Test void shouldSaveAndConfirmProjectPlanWithMyBatisFlex() throws Exception { JdbcClient jdbc = jdbc(); UUID userId = UUID.randomUUID(); UUID projectId = UUID.randomUUID(); jdbc.sql("INSERT INTO app.app_user(id, username, password_hash, display_name) VALUES (:id, :name, 'x', 'test')") .param("id", userId).param("name", "plan-u-" + userId).update(); jdbc.sql(""" INSERT INTO app.project(id, company_name, project_name, agui_thread_id, application_level, created_by) VALUES (:id, '规划企业', '规划项目', :thread, 'ADVANCED', :userId) """).param("id", projectId).param("thread", "plan-t-" + projectId).param("userId", userId).update(); UserService users = mock(UserService.class); when(users.requireUserId("admin")).thenReturn(userId); ProjectService service = new ProjectService( projectMapper(), projectPlanMapper(), users, new ObjectMapper()); ObjectMapper json = new ObjectMapper(); ProjectService.PlanView draft = service.saveDraftPlan( projectId, json.createObjectNode().put("title", "第一版"), userId); ProjectService.PlanView confirmed = service.confirmPlan( projectId, draft.id(), json.createObjectNode().put("title", "确认版"), () -> "admin"); assertThat(draft.version()).isEqualTo(1); assertThat(service.currentPlan(projectId).id()).isEqualTo(draft.id()); assertThat(confirmed.status()).isEqualTo("CONFIRMED"); assertThat(confirmed.plan().path("title").asText()).isEqualTo("确认版"); assertThat(service.require(projectId).status()).isEqualTo("WRITING"); assertThat(service.require(projectId).version()).isEqualTo(2L); } /** * 验证多个模型持久化、首模型自动默认、保留旧密钥更新以及默认模型切换。 * * @throws Exception Mapper XML 初始化失败时抛出 */ @Test void shouldManageEncryptedModelConfigurationWithMyBatisFlex() throws Exception { UUID userId = UUID.randomUUID(); jdbc().sql("INSERT INTO app.app_user(id, username, password_hash, display_name) VALUES (:id, :name, 'x', 'test')") .param("id", userId).param("name", "model-u-" + userId).update(); UserService users = mock(UserService.class); when(users.requireUserId("admin")).thenReturn(userId); AppProperties properties = new AppProperties( temporaryDirectory, temporaryDirectory.resolve("dashscope.key"), "integration-master-key", "admin", "admin", "runtime:test", "bridge", Duration.ofMinutes(1)); ModelService service = new ModelService( modelConfigMapper(), modelAssignmentMapper(), agentRunMapper(), users, new KeyCipher(properties), new ObjectMapper()); Map生产环境由 Spring Boot 扫描类型处理器与 mapper-locations;此处使用轻量 Bootstrap, * 因而需要显式复现相同配置。
* * @return 可访问 Testcontainers PostgreSQL 的产物 Mapper * @throws Exception XML 资源读取或 Mapper 初始化失败时抛出 */ private ArtifactMapper artifactMapper() throws Exception { PGSimpleDataSource source = dataSource(); FlexDataSource flexDataSource = new FlexDataSource("artifact-integration-test", source); Environment environment = new Environment( "artifact-integration-test", new JdbcTransactionFactory(), flexDataSource); FlexConfiguration configuration = new FlexConfiguration(environment); configuration.getTypeHandlerRegistry().register(UuidTypeHandler.class); configuration.getTypeHandlerRegistry().register(JsonbStringTypeHandler.class); MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap() .setConfiguration(configuration) .setDataSource(flexDataSource) .addMapper(ArtifactMapper.class) .start(); String resource = "mapper/ArtifactMapper.xml"; try (InputStream input = Resources.getResourceAsStream(resource)) { new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse(); } return bootstrap.getMapper(ArtifactMapper.class); } /** * 创建加载事件原子写入和游标回放 SQL 的 Agent 事件 Mapper。 * * @return Agent 事件 Mapper * @throws Exception XML 资源读取或 Mapper 初始化失败时抛出 */ private AgentEventMapper agentEventMapper() throws Exception { PGSimpleDataSource source = dataSource(); FlexDataSource flexDataSource = new FlexDataSource("agent-event-integration-test", source); Environment environment = new Environment( "agent-event-integration-test", new JdbcTransactionFactory(), flexDataSource); FlexConfiguration configuration = new FlexConfiguration(environment); configuration.getTypeHandlerRegistry().register(UuidTypeHandler.class); configuration.getTypeHandlerRegistry().register(JsonbStringTypeHandler.class); MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap() .setConfiguration(configuration) .setDataSource(flexDataSource) .addMapper(AgentEventMapper.class) .start(); String resource = "mapper/AgentEventMapper.xml"; try (InputStream input = Resources.getResourceAsStream(resource)) { new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse(); } return bootstrap.getMapper(AgentEventMapper.class); } /** * 创建加载 Run 状态机 SQL 及 UUID、JSONB 类型处理器的 Agent Run Mapper。 * * @return Agent Run Mapper * @throws Exception XML 资源读取或 Mapper 初始化失败时抛出 */ private AgentRunMapper agentRunMapper() throws Exception { PGSimpleDataSource source = dataSource(); FlexDataSource flexDataSource = new FlexDataSource("agent-run-integration-test", source); Environment environment = new Environment( "agent-run-integration-test", new JdbcTransactionFactory(), flexDataSource); FlexConfiguration configuration = new FlexConfiguration(environment); configuration.getTypeHandlerRegistry().register(UuidTypeHandler.class); configuration.getTypeHandlerRegistry().register(JsonbStringTypeHandler.class); MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap() .setConfiguration(configuration) .setDataSource(flexDataSource) .addMapper(AgentRunMapper.class) .start(); String resource = "mapper/AgentRunMapper.xml"; try (InputStream input = Resources.getResourceAsStream(resource)) { new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse(); } return bootstrap.getMapper(AgentRunMapper.class); } /** * 创建加载了项目级联删除 XML 的项目 Mapper。 * * @return 项目 Mapper * @throws Exception XML 资源读取或 Mapper 初始化失败时抛出 */ private ProjectMapper projectMapper() throws Exception { PGSimpleDataSource source = dataSource(); FlexDataSource flexDataSource = new FlexDataSource("project-integration-test", source); Environment environment = new Environment( "project-integration-test", new JdbcTransactionFactory(), flexDataSource); FlexConfiguration configuration = new FlexConfiguration(environment); configuration.getTypeHandlerRegistry().register(UuidTypeHandler.class); MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap() .setConfiguration(configuration) .setDataSource(flexDataSource) .addMapper(ProjectMapper.class) .start(); String resource = "mapper/ProjectMapper.xml"; try (InputStream input = Resources.getResourceAsStream(resource)) { new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse(); } return bootstrap.getMapper(ProjectMapper.class); } /** * 创建加载了规划版本 SQL 与 JSONB 处理器的规划 Mapper。 * * @return 规划 Mapper * @throws Exception XML 资源读取或 Mapper 初始化失败时抛出 */ private ProjectPlanMapper projectPlanMapper() throws Exception { PGSimpleDataSource source = dataSource(); FlexDataSource flexDataSource = new FlexDataSource("plan-integration-test", source); Environment environment = new Environment( "plan-integration-test", new JdbcTransactionFactory(), flexDataSource); FlexConfiguration configuration = new FlexConfiguration(environment); configuration.getTypeHandlerRegistry().register(UuidTypeHandler.class); configuration.getTypeHandlerRegistry().register(JsonbStringTypeHandler.class); MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap() .setConfiguration(configuration) .setDataSource(flexDataSource) .addMapper(ProjectPlanMapper.class) .start(); String resource = "mapper/ProjectPlanMapper.xml"; try (InputStream input = Resources.getResourceAsStream(resource)) { new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse(); } return bootstrap.getMapper(ProjectPlanMapper.class); } /** * 创建使用 MyBatis-Flex Wrapper,并加载 JSONB 显式写入 SQL 的模型配置 Mapper。 * * @return 模型配置 Mapper * @throws Exception XML 资源读取或 Mapper 初始化失败时抛出 */ private ModelConfigMapper modelConfigMapper() throws Exception { PGSimpleDataSource source = dataSource(); FlexDataSource flexDataSource = new FlexDataSource("model-config-integration-test", source); Environment environment = new Environment( "model-config-integration-test", new JdbcTransactionFactory(), flexDataSource); FlexConfiguration configuration = new FlexConfiguration(environment); configuration.getTypeHandlerRegistry().register(UuidTypeHandler.class); configuration.getTypeHandlerRegistry().register(JsonbStringTypeHandler.class); MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap() .setConfiguration(configuration) .setDataSource(flexDataSource) .addMapper(ModelConfigMapper.class) .start(); String resource = "mapper/ModelConfigMapper.xml"; try (InputStream input = Resources.getResourceAsStream(resource)) { new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse(); } return bootstrap.getMapper(ModelConfigMapper.class); } /** 创建加载角色 upsert SQL 的模型分配 Mapper。 */ private ModelAssignmentMapper modelAssignmentMapper() throws Exception { PGSimpleDataSource source = dataSource(); FlexDataSource flexDataSource = new FlexDataSource("model-assignment-integration-test", source); Environment environment = new Environment( "model-assignment-integration-test", new JdbcTransactionFactory(), flexDataSource); FlexConfiguration configuration = new FlexConfiguration(environment); configuration.getTypeHandlerRegistry().register(UuidTypeHandler.class); MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap() .setConfiguration(configuration) .setDataSource(flexDataSource) .addMapper(ModelAssignmentMapper.class) .start(); String resource = "mapper/ModelAssignmentMapper.xml"; try (InputStream input = Resources.getResourceAsStream(resource)) { new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse(); } return bootstrap.getMapper(ModelAssignmentMapper.class); } /** 创建加载 AgentScope 只读联查 SQL 的 Skill 配置 Mapper。 */ private SkillConfigMapper skillConfigMapper() throws Exception { PGSimpleDataSource source = dataSource(); FlexDataSource flexDataSource = new FlexDataSource("skill-config-integration-test", source); Environment environment = new Environment( "skill-config-integration-test", new JdbcTransactionFactory(), flexDataSource); FlexConfiguration configuration = new FlexConfiguration(environment); configuration.getTypeHandlerRegistry().register(UuidTypeHandler.class); MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap() .setConfiguration(configuration) .setDataSource(flexDataSource) .addMapper(SkillConfigMapper.class) .start(); String resource = "mapper/SkillConfigMapper.xml"; try (InputStream input = Resources.getResourceAsStream(resource)) { new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse(); } return bootstrap.getMapper(SkillConfigMapper.class); } }