diff --git a/backend/boot/build.gradle.kts b/backend/boot/build.gradle.kts index 7a96775..6d74c99 100644 --- a/backend/boot/build.gradle.kts +++ b/backend/boot/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testRuntimeOnly("com.h2database:h2") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/backend/boot/src/test/kotlin/com/all8ai/aioa/admin/configuration/ProcessTemplateExecutionTest.kt b/backend/boot/src/test/kotlin/com/all8ai/aioa/admin/configuration/ProcessTemplateExecutionTest.kt new file mode 100644 index 0000000..008cb25 --- /dev/null +++ b/backend/boot/src/test/kotlin/com/all8ai/aioa/admin/configuration/ProcessTemplateExecutionTest.kt @@ -0,0 +1,65 @@ +package com.all8ai.aioa.admin.configuration + +import org.flowable.engine.ProcessEngine +import org.flowable.engine.ProcessEngineConfiguration +import org.flowable.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class ProcessTemplateExecutionTest { + private lateinit var engine:ProcessEngine + private val manager=ApprovalStepCommand("主管审批","approverId") + private val oa=ApprovalStepCommand("OA 复核","oaAdministratorId") + private val hr=ApprovalStepCommand("HR 复核","hrReviewerId") + private val variables=mapOf( + "approverId" to "manager", "oaAdministratorId" to "oa", "hrReviewerId" to "hr", + "businessId" to "00000000-0000-7000-8000-000000000001", + ) + + @BeforeEach fun startEngine() { + engine=StandaloneInMemProcessEngineConfiguration() + .setJdbcUrl("jdbc:h2:mem:aioa-${System.nanoTime()};DB_CLOSE_DELAY=-1") + .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE) + .buildProcessEngine() + } + @AfterEach fun stopEngine(){engine.close()} + + @Test fun `conditional template skips or creates second approval by duration`() { + deploy(ProcessTemplateCommand("conditionalRuntime","条件流程","CONDITIONAL",listOf(manager,oa),conditionThreshold=960)) + val short=start("conditionalRuntime",variables+mapOf("durationMinutes" to 480L)) + completeOnlyTask(short,"manager",true) + assertNull(engine.runtimeService.createProcessInstanceQuery().processInstanceId(short).singleResult()) + + val long=start("conditionalRuntime",variables+mapOf("durationMinutes" to 1440L)) + completeOnlyTask(long,"manager",true) + assertNotNull(engine.taskService.createTaskQuery().processInstanceId(long).taskAssignee("oa").singleResult()) + completeOnlyTask(long,"oa",true) + assertNull(engine.runtimeService.createProcessInstanceQuery().processInstanceId(long).singleResult()) + } + + @Test fun `parallel template waits for every approval and terminates on rejection`() { + deploy(ProcessTemplateCommand("parallelRuntime","并行流程","PARALLEL",listOf(oa,hr))) + val approved=start("parallelRuntime",variables) + assertEquals(2,engine.taskService.createTaskQuery().processInstanceId(approved).count()) + completeOnlyTask(approved,"oa",true) + assertNotNull(engine.runtimeService.createProcessInstanceQuery().processInstanceId(approved).singleResult()) + completeOnlyTask(approved,"hr",true) + assertNull(engine.runtimeService.createProcessInstanceQuery().processInstanceId(approved).singleResult()) + + val rejected=start("parallelRuntime",variables) + completeOnlyTask(rejected,"oa",false) + assertNull(engine.runtimeService.createProcessInstanceQuery().processInstanceId(rejected).singleResult()) + assertEquals(0,engine.taskService.createTaskQuery().processInstanceId(rejected).count()) + } + + private fun deploy(command:ProcessTemplateCommand){engine.repositoryService.createDeployment().addString("${command.key}.bpmn20.xml",generateProcessTemplate(command)).deploy()} + private fun start(key:String,vars:Map)=engine.runtimeService.startProcessInstanceByKey(key,vars).id + private fun completeOnlyTask(instanceId:String,assignee:String,approved:Boolean){ + val task=engine.taskService.createTaskQuery().processInstanceId(instanceId).taskAssignee(assignee).singleResult() + assertNotNull(task);engine.taskService.complete(task.id,mapOf("approved" to approved)) + } +} diff --git a/docs/engineering/workflow-capabilities.md b/docs/engineering/workflow-capabilities.md index e372707..a370aee 100644 --- a/docs/engineering/workflow-capabilities.md +++ b/docs/engineering/workflow-capabilities.md @@ -60,3 +60,5 @@ Web 管理端提供受约束的流程设计器,可生成并部署以下 Flowab - 条件审批:首个审批通过后按请假时长阈值决定直接结束或增加复核节点。 流程 Key、节点数量、审批人变量、条件变量和阈值均由后端校验。管理端不能上传任意 BPMN XML、任意表达式或任意审批人脚本。部署成功后,新定义立即出现在流程绑定工作台中。 + +自动化执行测试使用独立内存 Flowable 引擎验证:短条件路径直接结束、长条件路径创建复核任务、并行会签等待全部审批、任一并行节点驳回时终止其余任务。