This is an automated email from the ASF dual-hosted git repository.

wangjian pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git


The following commit(s) were added to refs/heads/2.x by this push:
     new bd5afab901 test: add unit tests for saga-engine low coverage 
components (#7915)
bd5afab901 is described below

commit bd5afab9013e589752a9b85ed7d9ac77a6efda9f
Author: Eric Wang <[email protected]>
AuthorDate: Wed Mar 4 15:01:26 2026 +0800

    test: add unit tests for saga-engine low coverage components (#7915)
---
 changes/en-us/2.x.md                               |   1 +
 changes/zh-cn/2.x.md                               |   1 +
 .../exception/EngineExecutionExceptionTest.java    | 111 ++++++++
 .../exception/ForwardInvalidExceptionTest.java     |  89 ++++++
 .../exception/ExceptionMatchExpressionTest.java    | 118 ++++++++
 .../impl/ProcessCtrlStateMachineEngineTest.java    | 148 ++++++++++
 .../pcext/handlers/LoopStartStateHandlerTest.java  | 240 ++++++++++++++++
 .../pcext/handlers/SubStateMachineHandlerTest.java | 312 +++++++++++++++++++++
 .../LoopTaskHandlerInterceptorTest.java            | 261 +++++++++++++++++
 .../engine/pcext/utils/LoopContextHolderTest.java  | 141 ++++++++++
 .../saga/engine/pcext/utils/LoopTaskUtilsTest.java | 298 ++++++++++++++++++++
 .../repo/impl/StateLogRepositoryImplTest.java      | 132 +++++++++
 .../saga/engine/utils/ExceptionUtilsTest.java      | 144 ++++++++++
 13 files changed, 1996 insertions(+)

diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 192d97de1e..b55f16e1cf 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -52,6 +52,7 @@ Add changes here for all PR submitted to the 2.x branch.
 
 - [[#7962](https://github.com/apache/incubator-seata/pull/7962)] add unit 
tests for NacosRegistryProvider and NacosRegistryServiceImpl
 - [[#8003](https://github.com/apache/incubator-seata/pull/8003)] Enhance 
NacosRegistryServiceImplTest with additional mocks for service name group and 
cluster
+- [[#7915](https://github.com/apache/incubator-seata/pull/7915)] add unit 
tests for saga-engine low coverage components
 
 ### refactor:
 
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 3da705d6b3..cea60b183a 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -57,6 +57,7 @@
 
 - [[#7962](https://github.com/apache/incubator-seata/pull/7962)] 为 
NacosRegistryProvider 和 NacosRegistryServiceImpl 添加单元测试用例
 - [[#8003](https://github.com/apache/incubator-seata/pull/8003)] 为 
NacosRegistryServiceImplTest 增加服务名称、分组和集群的额外模拟
+- [[#7915](https://github.com/apache/incubator-seata/pull/7915)] 为 saga-engine 
添加单元测试用例
 
 
 ### refactor:
diff --git 
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/EngineExecutionExceptionTest.java
 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/EngineExecutionExceptionTest.java
new file mode 100644
index 0000000000..6b6471afb0
--- /dev/null
+++ 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/EngineExecutionExceptionTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.exception;
+
+import org.apache.seata.common.exception.FrameworkErrorCode;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Test class for {@link EngineExecutionException}
+ */
+public class EngineExecutionExceptionTest {
+
+    @Test
+    public void defaultConstructorTest() {
+        EngineExecutionException e = new EngineExecutionException();
+        assertNotNull(e);
+        // Parent class FrameworkException default constructor sets the 
default message
+        assertNotNull(e.getMessage());
+    }
+
+    @Test
+    public void constructorWithErrorCodeTest() {
+        EngineExecutionException e = new 
EngineExecutionException(FrameworkErrorCode.UnknownAppError);
+        assertNotNull(e);
+        assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+    }
+
+    @Test
+    public void constructorWithMessageTest() {
+        EngineExecutionException e = new EngineExecutionException("Test error 
message");
+        assertEquals("Test error message", e.getMessage());
+    }
+
+    @Test
+    public void constructorWithMessageAndErrorCodeTest() {
+        EngineExecutionException e = new EngineExecutionException("Test 
error", FrameworkErrorCode.UnknownAppError);
+        assertEquals("Test error", e.getMessage());
+        assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+    }
+
+    @Test
+    public void constructorWithCauseTest() {
+        Throwable cause = new RuntimeException("root cause");
+        EngineExecutionException e = new EngineExecutionException(cause);
+        assertSame(cause, e.getCause());
+    }
+
+    @Test
+    public void constructorWithCauseAndMessageTest() {
+        Throwable cause = new RuntimeException("root cause");
+        EngineExecutionException e = new EngineExecutionException(cause, 
"wrapper message");
+        assertEquals("wrapper message", e.getMessage());
+        assertSame(cause, e.getCause());
+    }
+
+    @Test
+    public void constructorWithCauseMessageAndErrorCodeTest() {
+        Throwable cause = new RuntimeException("root cause");
+        EngineExecutionException e =
+                new EngineExecutionException(cause, "wrapper message", 
FrameworkErrorCode.UnknownAppError);
+        assertEquals("wrapper message", e.getMessage());
+        assertSame(cause, e.getCause());
+        assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+    }
+
+    @Test
+    public void stateNameGetterSetterTest() {
+        EngineExecutionException e = new EngineExecutionException();
+        e.setStateName("TestState");
+        assertEquals("TestState", e.getStateName());
+    }
+
+    @Test
+    public void stateMachineNameGetterSetterTest() {
+        EngineExecutionException e = new EngineExecutionException();
+        e.setStateMachineName("TestMachine");
+        assertEquals("TestMachine", e.getStateMachineName());
+    }
+
+    @Test
+    public void stateMachineInstanceIdGetterSetterTest() {
+        EngineExecutionException e = new EngineExecutionException();
+        e.setStateMachineInstanceId("instance-123");
+        assertEquals("instance-123", e.getStateMachineInstanceId());
+    }
+
+    @Test
+    public void stateInstanceIdGetterSetterTest() {
+        EngineExecutionException e = new EngineExecutionException();
+        e.setStateInstanceId("state-456");
+        assertEquals("state-456", e.getStateInstanceId());
+    }
+}
diff --git 
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/ForwardInvalidExceptionTest.java
 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/ForwardInvalidExceptionTest.java
new file mode 100644
index 0000000000..824f5bca5e
--- /dev/null
+++ 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/ForwardInvalidExceptionTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.exception;
+
+import org.apache.seata.common.exception.FrameworkErrorCode;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Test class for {@link ForwardInvalidException}
+ */
+public class ForwardInvalidExceptionTest {
+
+    @Test
+    public void defaultConstructorTest() {
+        ForwardInvalidException e = new ForwardInvalidException();
+        assertNotNull(e);
+    }
+
+    @Test
+    public void constructorWithErrorCodeTest() {
+        ForwardInvalidException e = new 
ForwardInvalidException(FrameworkErrorCode.UnknownAppError);
+        assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+    }
+
+    @Test
+    public void constructorWithMessageTest() {
+        ForwardInvalidException e = new ForwardInvalidException("forward 
invalid");
+        assertEquals("forward invalid", e.getMessage());
+    }
+
+    @Test
+    public void constructorWithMessageAndErrorCodeTest() {
+        ForwardInvalidException e = new ForwardInvalidException("forward 
invalid", FrameworkErrorCode.UnknownAppError);
+        assertEquals("forward invalid", e.getMessage());
+        assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+    }
+
+    @Test
+    public void constructorWithCauseMessageAndErrorCodeTest() {
+        Throwable cause = new RuntimeException("root");
+        ForwardInvalidException e =
+                new ForwardInvalidException(cause, "forward invalid", 
FrameworkErrorCode.UnknownAppError);
+        assertSame(cause, e.getCause());
+        assertEquals("forward invalid", e.getMessage());
+        assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+    }
+
+    @Test
+    public void constructorWithCauseTest() {
+        Throwable cause = new RuntimeException("root");
+        ForwardInvalidException e = new ForwardInvalidException(cause);
+        assertSame(cause, e.getCause());
+    }
+
+    @Test
+    public void constructorWithCauseAndMessageTest() {
+        Throwable cause = new RuntimeException("root");
+        ForwardInvalidException e = new ForwardInvalidException(cause, 
"forward invalid");
+        assertEquals("forward invalid", e.getMessage());
+        assertSame(cause, e.getCause());
+    }
+
+    @Test
+    public void inheritanceFromEngineExecutionExceptionTest() {
+        ForwardInvalidException e = new ForwardInvalidException("test");
+        e.setStateName("TestState");
+        e.setStateMachineName("TestMachine");
+        assertEquals("TestState", e.getStateName());
+        assertEquals("TestMachine", e.getStateMachineName());
+    }
+}
diff --git 
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/expression/exception/ExceptionMatchExpressionTest.java
 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/expression/exception/ExceptionMatchExpressionTest.java
new file mode 100644
index 0000000000..71279637c9
--- /dev/null
+++ 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/expression/exception/ExceptionMatchExpressionTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.expression.exception;
+
+import org.apache.seata.saga.engine.exception.EngineExecutionException;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Test class for {@link ExceptionMatchExpression}
+ */
+public class ExceptionMatchExpressionTest {
+
+    @Test
+    public void getValueWhenExactClassNameMatchReturnTrueTest() {
+        ExceptionMatchExpression expr = new ExceptionMatchExpression();
+        expr.setExpressionString("java.lang.RuntimeException");
+        Object result = expr.getValue(new RuntimeException("test"));
+        assertEquals(true, result);
+    }
+
+    @Test
+    public void getValueWhenSubclassMatchReturnTrueTest() {
+        ExceptionMatchExpression expr = new ExceptionMatchExpression();
+        expr.setExpressionString("java.lang.Exception");
+        Object result = expr.getValue(new RuntimeException("test"));
+        assertEquals(true, result);
+    }
+
+    @Test
+    public void getValueWhenNoMatchReturnFalseTest() {
+        ExceptionMatchExpression expr = new ExceptionMatchExpression();
+        expr.setExpressionString("java.io.IOException");
+        Object result = expr.getValue(new RuntimeException("test"));
+        assertEquals(false, result);
+    }
+
+    @Test
+    public void getValueWhenContextIsNotExceptionReturnFalseTest() {
+        ExceptionMatchExpression expr = new ExceptionMatchExpression();
+        expr.setExpressionString("java.lang.RuntimeException");
+        Object result = expr.getValue("not an exception");
+        assertEquals(false, result);
+    }
+
+    @Test
+    public void getValueWhenExpressionStringEmptyReturnFalseTest() {
+        ExceptionMatchExpression expr = new ExceptionMatchExpression();
+        // expressionString is null by default
+        Object result = expr.getValue(new RuntimeException());
+        assertEquals(false, result);
+    }
+
+    @Test
+    public void getValueWhenContextIsNullReturnFalseTest() {
+        ExceptionMatchExpression expr = new ExceptionMatchExpression();
+        expr.setExpressionString("java.lang.RuntimeException");
+        Object result = expr.getValue(null);
+        assertEquals(false, result);
+    }
+
+    @Test
+    public void setExpressionStringWithValidExceptionClassSetClassTest() {
+        ExceptionMatchExpression expr = new ExceptionMatchExpression();
+        expr.setExpressionString("java.lang.RuntimeException");
+        assertEquals("java.lang.RuntimeException", expr.getExpressionString());
+    }
+
+    @Test
+    public void setExpressionStringWithIOExceptionClassTest() {
+        ExceptionMatchExpression expr = new ExceptionMatchExpression();
+        expr.setExpressionString("java.io.IOException");
+        assertEquals("java.io.IOException", expr.getExpressionString());
+
+        // Test matching
+        Object result = expr.getValue(new IOException("test"));
+        assertEquals(true, result);
+    }
+
+    @Test
+    public void 
setExpressionStringWithInvalidClassThrowEngineExecutionExceptionTest() {
+        ExceptionMatchExpression expr = new ExceptionMatchExpression();
+        assertThrows(EngineExecutionException.class, () -> 
expr.setExpressionString("com.nonexistent.NotAnException"));
+    }
+
+    @Test
+    public void setValueDoNothingTest() {
+        ExceptionMatchExpression expr = new ExceptionMatchExpression();
+        // setValue is a no-op, just verify it doesn't throw
+        assertDoesNotThrow(() -> expr.setValue("value", "context"));
+    }
+
+    @Test
+    public void getExpressionStringWhenNotSetReturnNullTest() {
+        ExceptionMatchExpression expr = new ExceptionMatchExpression();
+        assertNull(expr.getExpressionString());
+    }
+}
diff --git 
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/impl/ProcessCtrlStateMachineEngineTest.java
 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/impl/ProcessCtrlStateMachineEngineTest.java
new file mode 100644
index 0000000000..ed2b2438be
--- /dev/null
+++ 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/impl/ProcessCtrlStateMachineEngineTest.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.impl;
+
+import org.apache.seata.saga.engine.StateMachineConfig;
+import org.apache.seata.saga.engine.exception.ForwardInvalidException;
+import org.apache.seata.saga.engine.store.StateLogStore;
+import org.apache.seata.saga.statelang.domain.ExecutionStatus;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.apache.seata.saga.statelang.domain.StateType;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link ProcessCtrlStateMachineEngine}
+ */
+public class ProcessCtrlStateMachineEngineTest {
+
+    private ProcessCtrlStateMachineEngine engine;
+    private StateMachineConfig config;
+
+    @BeforeEach
+    public void setUp() {
+        engine = new ProcessCtrlStateMachineEngine();
+        config = mock(StateMachineConfig.class);
+        engine.setStateMachineConfig(config);
+    }
+
+    @Test
+    public void getStateMachineConfigTest() {
+        assertSame(config, engine.getStateMachineConfig());
+    }
+
+    @Test
+    public void setStateMachineConfigTest() {
+        StateMachineConfig newConfig = mock(StateMachineConfig.class);
+        engine.setStateMachineConfig(newConfig);
+        assertSame(newConfig, engine.getStateMachineConfig());
+    }
+
+    @Test
+    public void findOutLastForwardStateInstanceWithEmptyListReturnNullTest() {
+        List<StateInstance> emptyList = Collections.emptyList();
+        StateInstance result = 
engine.findOutLastForwardStateInstance(emptyList);
+        assertNull(result);
+    }
+
+    @Test
+    public void findOutLastForwardStateInstanceWithCompensationStateSkipTest() 
{
+        StateInstance stateInstance = mock(StateInstance.class);
+        when(stateInstance.isForCompensation()).thenReturn(true);
+
+        List<StateInstance> stateList = new ArrayList<>();
+        stateList.add(stateInstance);
+
+        StateInstance result = 
engine.findOutLastForwardStateInstance(stateList);
+        assertNull(result);
+    }
+
+    @Test
+    public void 
findOutLastForwardStateInstanceWithSuccessfulCompensationSkipTest() {
+        StateInstance stateInstance = mock(StateInstance.class);
+        when(stateInstance.isForCompensation()).thenReturn(false);
+        
when(stateInstance.getCompensationStatus()).thenReturn(ExecutionStatus.SU);
+
+        List<StateInstance> stateList = new ArrayList<>();
+        stateList.add(stateInstance);
+
+        StateInstance result = 
engine.findOutLastForwardStateInstance(stateList);
+        assertNull(result);
+    }
+
+    @Test
+    public void 
findOutLastForwardStateInstanceReturnLastNonCompensationStateTest() {
+        StateInstance state1 = mock(StateInstance.class);
+        StateInstance state2 = mock(StateInstance.class);
+
+        when(state1.isForCompensation()).thenReturn(false);
+        when(state1.getCompensationStatus()).thenReturn(null);
+        when(state1.getType()).thenReturn(StateType.SERVICE_TASK);
+
+        when(state2.isForCompensation()).thenReturn(false);
+        when(state2.getCompensationStatus()).thenReturn(null);
+        when(state2.getType()).thenReturn(StateType.SERVICE_TASK);
+
+        List<StateInstance> stateList = new ArrayList<>();
+        stateList.add(state1);
+        stateList.add(state2);
+
+        StateInstance result = 
engine.findOutLastForwardStateInstance(stateList);
+        assertSame(state2, result);
+    }
+
+    @Test
+    public void 
findOutLastForwardStateInstanceWithUNCompensationStatusThrowExceptionTest() {
+        StateInstance stateInstance = mock(StateInstance.class);
+        when(stateInstance.isForCompensation()).thenReturn(false);
+        
when(stateInstance.getCompensationStatus()).thenReturn(ExecutionStatus.UN);
+        when(stateInstance.getType()).thenReturn(StateType.SERVICE_TASK);
+        when(stateInstance.getId()).thenReturn("state-123");
+
+        List<StateInstance> stateList = new ArrayList<>();
+        stateList.add(stateInstance);
+
+        assertThrows(ForwardInvalidException.class, () -> 
engine.findOutLastForwardStateInstance(stateList));
+    }
+
+    @Test
+    public void reloadStateMachineInstanceWhenNullReturnNullTest() {
+        StateLogStore stateLogStore = mock(StateLogStore.class);
+        when(config.getStateLogStore()).thenReturn(stateLogStore);
+        
when(stateLogStore.getStateMachineInstance("non-existent")).thenReturn(null);
+
+        StateMachineInstance result = 
engine.reloadStateMachineInstance("non-existent");
+        assertNull(result);
+    }
+
+    @Test
+    public void engineNotNullTest() {
+        assertNotNull(engine);
+    }
+}
diff --git 
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/LoopStartStateHandlerTest.java
 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/LoopStartStateHandlerTest.java
new file mode 100644
index 0000000000..d1cffed1c1
--- /dev/null
+++ 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/LoopStartStateHandlerTest.java
@@ -0,0 +1,240 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.pcext.handlers;
+
+import org.apache.seata.saga.engine.StateMachineConfig;
+import org.apache.seata.saga.engine.expression.Expression;
+import org.apache.seata.saga.engine.expression.ExpressionResolver;
+import org.apache.seata.saga.engine.pcext.StateInstruction;
+import org.apache.seata.saga.engine.pcext.utils.LoopContextHolder;
+import org.apache.seata.saga.proctrl.HierarchicalProcessContext;
+import org.apache.seata.saga.proctrl.ProcessContext;
+import org.apache.seata.saga.proctrl.eventing.impl.ProcessCtrlEventPublisher;
+import org.apache.seata.saga.statelang.domain.DomainConstants;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.apache.seata.saga.statelang.domain.StateType;
+import org.apache.seata.saga.statelang.domain.impl.AbstractTaskState;
+import org.apache.seata.saga.statelang.domain.impl.AbstractTaskState.LoopImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link LoopStartStateHandler}
+ */
+public class LoopStartStateHandlerTest {
+
+    private LoopStartStateHandler handler;
+
+    @BeforeEach
+    public void setUp() {
+        handler = new LoopStartStateHandler();
+    }
+
+    @Test
+    public void processWhenLoopConfigNullSetTemporaryStateTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        StateInstruction instruction = mock(StateInstruction.class);
+        StateMachineInstance smInstance = mock(StateMachineInstance.class);
+        StateMachineConfig config = mock(StateMachineConfig.class);
+        AbstractTaskState state = mock(AbstractTaskState.class);
+
+        
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+        when(instruction.getState(context)).thenReturn(state);
+        when(instruction.getStateName()).thenReturn("testState");
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(smInstance);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+
+        when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+        when(state.getLoop()).thenReturn(null);
+
+        handler.process(context);
+
+        verify(instruction).setTemporaryState(null);
+        verify(instruction).setTemporaryState(state);
+    }
+
+    @Test
+    public void processHandlerNotNullTest() {
+        assertNotNull(handler);
+    }
+
+    @Test
+    public void processCleanupContextVariablesInFinallyBlockTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        StateInstruction instruction = mock(StateInstruction.class);
+        StateMachineInstance smInstance = mock(StateMachineInstance.class);
+        StateMachineConfig config = mock(StateMachineConfig.class);
+        AbstractTaskState state = mock(AbstractTaskState.class);
+
+        
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+        when(instruction.getState(context)).thenReturn(state);
+        when(instruction.getStateName()).thenReturn("testState");
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(smInstance);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+        when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+        when(state.getLoop()).thenReturn(null);
+
+        handler.process(context);
+
+        verify(context).removeVariable(DomainConstants.LOOP_SEMAPHORE);
+        verify(context).removeVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE);
+        
verify(context).removeVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER);
+    }
+
+    @Test
+    public void processWhenLoopContextHolderIsNullNotThrowExceptionTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        StateInstruction instruction = mock(StateInstruction.class);
+        StateMachineInstance smInstance = mock(StateMachineInstance.class);
+        StateMachineConfig config = mock(StateMachineConfig.class);
+        AbstractTaskState state = mock(AbstractTaskState.class);
+
+        
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+        when(instruction.getState(context)).thenReturn(state);
+        when(instruction.getStateName()).thenReturn("testState");
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(smInstance);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(null);
+        when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+        when(state.getLoop()).thenReturn(null);
+
+        assertDoesNotThrow(() -> handler.process(context));
+    }
+
+    @Test
+    public void processWhenLoopContextHolderExistsWithFailEndFalseTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        StateInstruction instruction = mock(StateInstruction.class);
+        StateMachineInstance smInstance = mock(StateMachineInstance.class);
+        StateMachineConfig config = mock(StateMachineConfig.class);
+        AbstractTaskState state = mock(AbstractTaskState.class);
+
+        LoopContextHolder holder = new LoopContextHolder();
+        holder.setFailEnd(false);
+
+        
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+        when(instruction.getState(context)).thenReturn(state);
+        when(instruction.getStateName()).thenReturn("testState");
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(smInstance);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(holder);
+        when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+        when(state.getLoop()).thenReturn(null);
+
+        handler.process(context);
+
+        assertFalse(holder.isFailEnd());
+    }
+
+    @Test
+    public void processAsyncLoopExecutionHappyPathTest() {
+        HierarchicalProcessContext context = 
mock(HierarchicalProcessContext.class);
+        StateInstruction instruction = mock(StateInstruction.class);
+        StateMachineInstance smInstance = mock(StateMachineInstance.class);
+        StateMachineConfig config = mock(StateMachineConfig.class);
+        AbstractTaskState state = mock(AbstractTaskState.class);
+        ProcessCtrlEventPublisher publisher = 
mock(ProcessCtrlEventPublisher.class);
+        ExpressionResolver resolver = mock(ExpressionResolver.class);
+
+        LoopImpl loop = new LoopImpl();
+        loop.setParallel(2);
+        loop.setCollection("$.users");
+        loop.setElementVariableName("user");
+
+        
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+        when(instruction.getState(context)).thenReturn(state);
+        when(instruction.getStateName()).thenReturn("loopState");
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(smInstance);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONTEXT)).thenReturn(new
 HashMap<>());
+
+        LoopContextHolder holder = new LoopContextHolder();
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(holder);
+
+        when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+        when(state.getLoop()).thenReturn(loop);
+
+        Expression expression = mock(Expression.class);
+        List<String> userList = Arrays.asList("user1", "user2", "user3");
+        when(expression.getValue(any())).thenReturn(userList);
+        when(resolver.getExpression(any())).thenReturn(expression);
+        when(config.getExpressionResolver()).thenReturn(resolver);
+
+        when(config.isEnableAsync()).thenReturn(true);
+        when(config.getAsyncProcessCtrlEventPublisher()).thenReturn(publisher);
+
+        final Semaphore[] capturedSemaphore = new Semaphore[1];
+        doAnswer(invocation -> {
+                    String key = invocation.getArgument(0);
+                    Object value = invocation.getArgument(1);
+                    if (DomainConstants.LOOP_SEMAPHORE.equals(key)) {
+                        capturedSemaphore[0] = (Semaphore) value;
+                    }
+                    return null;
+                })
+                .when(context)
+                .setVariable(eq(DomainConstants.LOOP_SEMAPHORE), any());
+
+        // Merged doAnswer handling both StateInstance injection AND Semaphore 
release
+        doAnswer(invocation -> {
+                    ProcessContext ctx = invocation.getArgument(0);
+
+                    StateInstance mockStateInst = mock(StateInstance.class);
+                    when(mockStateInst.getOutputParams()).thenReturn(new 
HashMap<>());
+
+                    // Crucial Fix: Use setVariableLocally to prevent 
delegation to mocked parent
+                    if (ctx instanceof HierarchicalProcessContext) {
+                        ((HierarchicalProcessContext) ctx)
+                                
.setVariableLocally(DomainConstants.VAR_NAME_STATE_INST, mockStateInst);
+                    } else {
+                        ctx.setVariable(DomainConstants.VAR_NAME_STATE_INST, 
mockStateInst);
+                    }
+
+                    if (capturedSemaphore[0] != null) {
+                        capturedSemaphore[0].release();
+                    }
+                    return null;
+                })
+                .when(publisher)
+                .publish(any());
+
+        handler.process(context);
+
+        verify(publisher, times(3)).publish(any());
+        verify(context).removeVariable(DomainConstants.LOOP_SEMAPHORE);
+    }
+}
diff --git 
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/SubStateMachineHandlerTest.java
 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/SubStateMachineHandlerTest.java
new file mode 100644
index 0000000000..be2a386017
--- /dev/null
+++ 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/SubStateMachineHandlerTest.java
@@ -0,0 +1,312 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.pcext.handlers;
+
+import org.apache.seata.saga.engine.StateMachineConfig;
+import org.apache.seata.saga.engine.StateMachineEngine;
+import org.apache.seata.saga.engine.pcext.StateHandlerInterceptor;
+import org.apache.seata.saga.engine.pcext.StateInstruction;
+import org.apache.seata.saga.engine.store.StateLogStore;
+import org.apache.seata.saga.proctrl.HierarchicalProcessContext;
+import org.apache.seata.saga.statelang.domain.DomainConstants;
+import org.apache.seata.saga.statelang.domain.ExecutionStatus;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.apache.seata.saga.statelang.domain.impl.SubStateMachineImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+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;
+
+/**
+ * Test class for {@link SubStateMachineHandler}
+ */
+public class SubStateMachineHandlerTest {
+
+    private SubStateMachineHandler handler;
+
+    @BeforeEach
+    public void setUp() {
+        handler = new SubStateMachineHandler();
+    }
+
+    @Test
+    public void addInterceptorAddToListTest() {
+        StateHandlerInterceptor interceptor = 
mock(StateHandlerInterceptor.class);
+        handler.addInterceptor(interceptor);
+        assertTrue(handler.getInterceptors().contains(interceptor));
+    }
+
+    @Test
+    public void addInterceptorDuplicateInterceptorNotAddTest() {
+        StateHandlerInterceptor interceptor = 
mock(StateHandlerInterceptor.class);
+        handler.addInterceptor(interceptor);
+        handler.addInterceptor(interceptor);
+        assertEquals(1, handler.getInterceptors().size());
+    }
+
+    @Test
+    public void addInterceptorMultipleDifferentInterceptorsTest() {
+        StateHandlerInterceptor interceptor1 = 
mock(StateHandlerInterceptor.class);
+        StateHandlerInterceptor interceptor2 = 
mock(StateHandlerInterceptor.class);
+        handler.addInterceptor(interceptor1);
+        handler.addInterceptor(interceptor2);
+        assertEquals(2, handler.getInterceptors().size());
+        assertTrue(handler.getInterceptors().contains(interceptor1));
+        assertTrue(handler.getInterceptors().contains(interceptor2));
+    }
+
+    @Test
+    public void getInterceptorsReturnListTest() {
+        List<StateHandlerInterceptor> interceptors = handler.getInterceptors();
+        assertNotNull(interceptors);
+    }
+
+    @Test
+    public void setInterceptorsTest() {
+        List<StateHandlerInterceptor> interceptors = new ArrayList<>();
+        StateHandlerInterceptor interceptor = 
mock(StateHandlerInterceptor.class);
+        interceptors.add(interceptor);
+
+        handler.setInterceptors(interceptors);
+
+        assertSame(interceptors, handler.getInterceptors());
+    }
+
+    @Test
+    public void setInterceptorsWithNullTest() {
+        handler.setInterceptors(null);
+        assertNull(handler.getInterceptors());
+    }
+
+    @Test
+    public void addInterceptorWhenInterceptorsIsNullDoNothingTest() {
+        handler.setInterceptors(null);
+        StateHandlerInterceptor interceptor = 
mock(StateHandlerInterceptor.class);
+
+        // Should not throw
+        assertDoesNotThrow(() -> handler.addInterceptor(interceptor));
+    }
+
+    @Test
+    public void handlerNotNullTest() {
+        assertNotNull(handler);
+    }
+
+    // ========== Existing Static Method Tests ==========
+
+    @Test
+    public void decideStatusForwardWithSuccessReturnSUTest() throws Exception {
+        StateMachineInstance instance = mock(StateMachineInstance.class);
+        when(instance.getStatus()).thenReturn(ExecutionStatus.SU);
+
+        ExecutionStatus result = invokeDecideStatus(instance, true);
+
+        assertEquals(ExecutionStatus.SU, result);
+    }
+
+    @Test
+    public void decideStatusForwardWithFailureReturnInstanceStatusTest() 
throws Exception {
+        StateMachineInstance instance = mock(StateMachineInstance.class);
+        when(instance.getStatus()).thenReturn(ExecutionStatus.FA);
+        when(instance.getCompensationStatus()).thenReturn(null);
+
+        ExecutionStatus result = invokeDecideStatus(instance, true);
+
+        assertEquals(ExecutionStatus.FA, result);
+    }
+
+    @Test
+    public void 
decideStatusWhenCompensationStatusIsNullReturnInstanceStatusTest() throws 
Exception {
+        StateMachineInstance instance = mock(StateMachineInstance.class);
+        when(instance.getStatus()).thenReturn(ExecutionStatus.FA);
+        when(instance.getCompensationStatus()).thenReturn(null);
+
+        ExecutionStatus result = invokeDecideStatus(instance, false);
+
+        assertEquals(ExecutionStatus.FA, result);
+    }
+
+    @Test
+    public void 
decideStatusWhenCompensationStatusIsFAReturnInstanceStatusTest() throws 
Exception {
+        StateMachineInstance instance = mock(StateMachineInstance.class);
+        when(instance.getStatus()).thenReturn(ExecutionStatus.FA);
+        when(instance.getCompensationStatus()).thenReturn(ExecutionStatus.FA);
+
+        ExecutionStatus result = invokeDecideStatus(instance, false);
+
+        assertEquals(ExecutionStatus.FA, result);
+    }
+
+    @Test
+    public void decideStatusWhenCompensationStatusIsSUReturnFATest() throws 
Exception {
+        StateMachineInstance instance = mock(StateMachineInstance.class);
+        when(instance.getCompensationStatus()).thenReturn(ExecutionStatus.SU);
+
+        ExecutionStatus result = invokeDecideStatus(instance, false);
+
+        assertEquals(ExecutionStatus.FA, result);
+    }
+
+    @Test
+    public void decideStatusWhenCompensationStatusIsUNReturnUNTest() throws 
Exception {
+        StateMachineInstance instance = mock(StateMachineInstance.class);
+        when(instance.getCompensationStatus()).thenReturn(ExecutionStatus.UN);
+
+        ExecutionStatus result = invokeDecideStatus(instance, false);
+
+        assertEquals(ExecutionStatus.UN, result);
+    }
+
+    @Test
+    public void decideStatusWhenCompensationStatusIsRUReturnUNTest() throws 
Exception {
+        StateMachineInstance instance = mock(StateMachineInstance.class);
+        when(instance.getCompensationStatus()).thenReturn(ExecutionStatus.RU);
+
+        ExecutionStatus result = invokeDecideStatus(instance, false);
+
+        assertEquals(ExecutionStatus.UN, result);
+    }
+
+    /**
+     * Invoke private static method decideStatus via reflection
+     */
+    private ExecutionStatus invokeDecideStatus(StateMachineInstance instance, 
boolean isForward) throws Exception {
+        Method method = SubStateMachineHandler.class.getDeclaredMethod(
+                "decideStatus", StateMachineInstance.class, boolean.class);
+        method.setAccessible(true);
+        return (ExecutionStatus) method.invoke(null, instance, isForward);
+    }
+
+    // ========== New Process Tests ==========
+
+    @Test
+    public void processStartNewSubMachineTest() {
+        HierarchicalProcessContext context = 
mock(HierarchicalProcessContext.class);
+        StateInstruction instruction = mock(StateInstruction.class);
+        SubStateMachineImpl subStateMachine = mock(SubStateMachineImpl.class);
+        StateMachineEngine engine = mock(StateMachineEngine.class);
+        StateMachineInstance parentSmInstance = 
mock(StateMachineInstance.class);
+        StateInstance stateInstance = mock(StateInstance.class);
+        StateMachineInstance subSmInstance = mock(StateMachineInstance.class);
+
+        
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+        when(instruction.getState(context)).thenReturn(subStateMachine);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_ENGINE)).thenReturn(engine);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(parentSmInstance);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+
+        // Input params
+        Map<String, Object> inputParams = new HashMap<>();
+        inputParams.put("a", 1);
+        
when(context.getVariable(DomainConstants.VAR_NAME_INPUT_PARAMS)).thenReturn(inputParams);
+
+        // Mock SubStateMachine
+        when(subStateMachine.getStateMachineName()).thenReturn("subMachine");
+        when(subStateMachine.getName()).thenReturn("subState");
+
+        // Mock Engine Start
+        when(engine.start(eq("subMachine"), any(), 
any())).thenReturn(subSmInstance);
+
+        // Mock SubStateMachineInstance Result
+        Map<String, Object> endParams = new HashMap<>();
+        endParams.put("result", "success");
+        when(subSmInstance.getEndParams()).thenReturn(endParams);
+        when(subSmInstance.getStatus()).thenReturn(ExecutionStatus.SU);
+        when(subSmInstance.getCompensationStatus()).thenReturn(null);
+
+        // Mock StateInstance
+        
when(stateInstance.getStateMachineInstance()).thenReturn(parentSmInstance);
+
+        handler.process(context);
+
+        verify(engine).start(eq("subMachine"), any(), any());
+        verify(stateInstance).setOutputParams(endParams);
+        
verify(context).setVariable(eq(DomainConstants.VAR_NAME_OUTPUT_PARAMS), 
eq(endParams));
+        verify(stateInstance).setStatus(ExecutionStatus.SU);
+    }
+
+    @Test
+    public void processForwardExistingSubMachineTest() {
+        HierarchicalProcessContext context = 
mock(HierarchicalProcessContext.class);
+        StateInstruction instruction = mock(StateInstruction.class);
+        SubStateMachineImpl subStateMachine = mock(SubStateMachineImpl.class);
+        StateMachineEngine engine = mock(StateMachineEngine.class);
+        StateMachineInstance parentSmInstance = 
mock(StateMachineInstance.class);
+        StateInstance stateInstance = mock(StateInstance.class);
+        StateMachineInstance subSmInstance = mock(StateMachineInstance.class);
+        StateMachineConfig config = mock(StateMachineConfig.class);
+        StateLogStore stateLogStore = mock(StateLogStore.class);
+
+        
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+        when(instruction.getState(context)).thenReturn(subStateMachine);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_ENGINE)).thenReturn(engine);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(parentSmInstance);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+
+        // Use Mockito to mock boolean check for forward flag
+        
when(context.getVariable(DomainConstants.VAR_NAME_IS_FOR_SUB_STATMACHINE_FORWARD))
+                .thenReturn(true);
+
+        when(config.getStateLogStore()).thenReturn(stateLogStore);
+
+        // Correctly set up the retry scenario so validation passes
+        
when(stateInstance.getStateIdRetriedFor()).thenReturn("originalStateId");
+        when(stateInstance.getMachineInstanceId()).thenReturn("machineId1");
+
+        StateInstance originalStateInst = mock(StateInstance.class);
+        when(originalStateInst.getId()).thenReturn("originalStateId");
+        
when(originalStateInst.getMachineInstanceId()).thenReturn("machineId1");
+        // Must return null to break the do-while loop
+        when(originalStateInst.getStateIdRetriedFor()).thenReturn(null);
+
+        when(stateLogStore.getStateInstance(eq("originalStateId"), 
any())).thenReturn(originalStateInst);
+
+        List<StateMachineInstance> existingSubInsts = 
Collections.singletonList(subSmInstance);
+        
when(stateLogStore.queryStateMachineInstanceByParentId(any())).thenReturn(existingSubInsts);
+
+        when(subSmInstance.getId()).thenReturn("subInstId");
+        when(subSmInstance.getEndParams()).thenReturn(new HashMap<>());
+        when(subSmInstance.getStatus()).thenReturn(ExecutionStatus.SU);
+
+        // Mock Engine Forward
+        when(engine.forward(eq("subInstId"), any())).thenReturn(subSmInstance);
+
+        handler.process(context);
+
+        verify(engine).forward(eq("subInstId"), any());
+        
verify(context).removeVariable(DomainConstants.VAR_NAME_IS_FOR_SUB_STATMACHINE_FORWARD);
+    }
+}
diff --git 
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/interceptors/LoopTaskHandlerInterceptorTest.java
 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/interceptors/LoopTaskHandlerInterceptorTest.java
new file mode 100644
index 0000000000..01a82be4b7
--- /dev/null
+++ 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/interceptors/LoopTaskHandlerInterceptorTest.java
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.pcext.interceptors;
+
+import org.apache.seata.saga.engine.pcext.StateInstruction;
+import org.apache.seata.saga.engine.pcext.handlers.ServiceTaskStateHandler;
+import org.apache.seata.saga.engine.pcext.handlers.SubStateMachineHandler;
+import org.apache.seata.saga.engine.pcext.utils.LoopContextHolder;
+import org.apache.seata.saga.proctrl.HierarchicalProcessContext;
+import org.apache.seata.saga.proctrl.ProcessContext;
+import org.apache.seata.saga.statelang.domain.DomainConstants;
+import org.apache.seata.saga.statelang.domain.ExecutionStatus;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.TaskState.Loop;
+import org.apache.seata.saga.statelang.domain.impl.AbstractTaskState;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+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;
+
+/**
+ * Test class for {@link LoopTaskHandlerInterceptor}
+ */
+public class LoopTaskHandlerInterceptorTest {
+
+    private LoopTaskHandlerInterceptor interceptor;
+
+    @BeforeEach
+    public void setUp() {
+        interceptor = new LoopTaskHandlerInterceptor();
+    }
+
+    @Test
+    public void matchForServiceTaskHandlerReturnTrueTest() {
+        assertTrue(interceptor.match(ServiceTaskStateHandler.class));
+    }
+
+    @Test
+    public void matchForSubStateMachineHandlerReturnTrueTest() {
+        assertTrue(interceptor.match(SubStateMachineHandler.class));
+    }
+
+    @Test
+    public void matchForNullClassReturnFalseTest() {
+        assertFalse(interceptor.match(null));
+    }
+
+    @Test
+    public void preProcessWhenNotLoopStateDoNothingTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(false);
+
+        // Should not throw and do nothing when not a loop state
+        assertDoesNotThrow(() -> interceptor.preProcess(context));
+    }
+
+    @Test
+    public void postProcessWhenNotLoopStateDoNothingTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(false);
+
+        // Should not throw and do nothing when not a loop state
+        assertDoesNotThrow(() -> interceptor.postProcess(context, null));
+    }
+
+    @Test
+    public void interceptorNotNullTest() {
+        assertNotNull(interceptor);
+    }
+
+    // ========== Additional Tests: Coverage for preProcess Core Logic 
==========
+
+    @Test
+    public void preProcessWhenIsLoopStateSetContextVariablesTest() {
+        HierarchicalProcessContext context = 
mock(HierarchicalProcessContext.class);
+        StateInstruction instruction = mock(StateInstruction.class);
+        AbstractTaskState taskState = mock(AbstractTaskState.class);
+        Loop loop = mock(Loop.class);
+
+        
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+        
when(context.hasVariable(DomainConstants.VAR_NAME_CURRENT_COMPEN_TRIGGER_STATE))
+                .thenReturn(false);
+        
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+        when(instruction.getState(context)).thenReturn(taskState);
+        when(taskState.getLoop()).thenReturn(loop);
+        when(context.getVariable(DomainConstants.LOOP_COUNTER)).thenReturn(0);
+
+        // Set loop configuration
+        when(loop.getElementIndexName()).thenReturn("loopIndex");
+        when(loop.getElementVariableName()).thenReturn("loopElement");
+
+        // Set LoopContextHolder
+        LoopContextHolder holder = new LoopContextHolder();
+        List<String> collection = Arrays.asList("item1", "item2", "item3");
+        holder.setCollection(collection);
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(holder);
+
+        // Set stateMachineContext
+        Map<String, Object> contextVariables = new HashMap<>();
+        contextVariables.put("existingKey", "existingValue");
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONTEXT)).thenReturn(contextVariables);
+
+        // Execute
+        assertDoesNotThrow(() -> interceptor.preProcess(context));
+
+        // Verify setVariableLocally is called
+        
verify(context).setVariableLocally(eq(DomainConstants.VAR_NAME_STATEMACHINE_CONTEXT),
 any(Map.class));
+    }
+
+    // ========== Additional Tests: Coverage for postProcess Core Logic 
==========
+
+    @Test
+    public void postProcessWhenStateSuccessIncrementCompletedInstancesTest() {
+        HierarchicalProcessContext context = 
mock(HierarchicalProcessContext.class);
+        StateInstance stateInstance = mock(StateInstance.class);
+
+        
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+        when(stateInstance.getStatus()).thenReturn(ExecutionStatus.SU);
+
+        LoopContextHolder holder = new LoopContextHolder();
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(holder);
+        
when(context.getVariableLocally(DomainConstants.VAR_NAME_CURRENT_EXCEPTION))
+                .thenReturn(null);
+
+        // Execute
+        interceptor.postProcess(context, null);
+
+        // Verify nrOfCompletedInstances has been incremented
+        assertEquals(1, holder.getNrOfCompletedInstances().get());
+        // Verify nrOfActiveInstances has been decremented
+        assertEquals(-1, holder.getNrOfActiveInstances().get());
+    }
+
+    @Test
+    public void postProcessWhenStateFailedSetFailEndTrueTest() {
+        HierarchicalProcessContext context = 
mock(HierarchicalProcessContext.class);
+        StateInstance stateInstance = mock(StateInstance.class);
+
+        
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+        when(stateInstance.getStatus()).thenReturn(ExecutionStatus.FA);
+
+        LoopContextHolder holder = new LoopContextHolder();
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(holder);
+        
when(context.getVariableLocally(DomainConstants.VAR_NAME_CURRENT_EXCEPTION))
+                .thenReturn(null);
+
+        // Execute
+        interceptor.postProcess(context, null);
+
+        // Verify failEnd is set to true
+        assertTrue(holder.isFailEnd());
+    }
+
+    @Test
+    public void postProcessWhenExceptionOccursReleaseSemaphoreTest() {
+        HierarchicalProcessContext context = 
mock(HierarchicalProcessContext.class);
+        StateInstance stateInstance = mock(StateInstance.class);
+        Semaphore semaphore = new Semaphore(0);
+
+        
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+        when(stateInstance.getStatus()).thenReturn(ExecutionStatus.SU);
+
+        LoopContextHolder holder = new LoopContextHolder();
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(holder);
+        
when(context.getVariableLocally(DomainConstants.VAR_NAME_CURRENT_EXCEPTION))
+                .thenReturn(null);
+        
when(context.hasVariable(DomainConstants.LOOP_SEMAPHORE)).thenReturn(true);
+        
when(context.getVariable(DomainConstants.LOOP_SEMAPHORE)).thenReturn(semaphore);
+
+        Exception testException = new RuntimeException("test exception");
+
+        // Execute
+        interceptor.postProcess(context, testException);
+
+        // Verify semaphore is released
+        assertEquals(1, semaphore.availablePermits());
+        // Verify failEnd is set to true (due to exception)
+        assertTrue(holder.isFailEnd());
+    }
+
+    @Test
+    public void postProcessWhenStateInstanceIsNullDoNotSetFailEndTest() {
+        HierarchicalProcessContext context = 
mock(HierarchicalProcessContext.class);
+
+        
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(null);
+
+        LoopContextHolder holder = new LoopContextHolder();
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(holder);
+        
when(context.getVariableLocally(DomainConstants.VAR_NAME_CURRENT_EXCEPTION))
+                .thenReturn(null);
+
+        // Execute
+        interceptor.postProcess(context, null);
+
+        // Verify failEnd remains false
+        assertFalse(holder.isFailEnd());
+    }
+
+    @Test
+    public void postProcessWhenLocalExceptionExistsSetFailEndTrueTest() {
+        HierarchicalProcessContext context = 
mock(HierarchicalProcessContext.class);
+        StateInstance stateInstance = mock(StateInstance.class);
+
+        
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+        
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+        when(stateInstance.getStatus()).thenReturn(ExecutionStatus.SU);
+
+        LoopContextHolder holder = new LoopContextHolder();
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(holder);
+
+        // Local exception exists
+        Exception localException = new RuntimeException("local exception");
+        
when(context.getVariableLocally(DomainConstants.VAR_NAME_CURRENT_EXCEPTION))
+                .thenReturn(localException);
+
+        // Execute
+        interceptor.postProcess(context, null);
+
+        // Verify failEnd is set to true
+        assertTrue(holder.isFailEnd());
+    }
+}
diff --git 
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopContextHolderTest.java
 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopContextHolderTest.java
new file mode 100644
index 0000000000..c1c29db2b5
--- /dev/null
+++ 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopContextHolderTest.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.pcext.utils;
+
+import org.apache.seata.saga.proctrl.ProcessContext;
+import org.apache.seata.saga.statelang.domain.DomainConstants;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+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;
+
+/**
+ * Test class for {@link LoopContextHolder}
+ */
+public class LoopContextHolderTest {
+
+    @Test
+    public void getCurrentWhenNotExistAndForceCreateFalseReturnNullTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(null);
+
+        LoopContextHolder result = LoopContextHolder.getCurrent(context, 
false);
+
+        assertNull(result);
+    }
+
+    @Test
+    public void getCurrentWhenNotExistAndForceCreateTrueCreateNewTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(null);
+
+        LoopContextHolder result = LoopContextHolder.getCurrent(context, true);
+
+        assertNotNull(result);
+        verify(context)
+                
.setVariable(eq(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER), 
any(LoopContextHolder.class));
+    }
+
+    @Test
+    public void getCurrentWhenExistReturnExistingTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        LoopContextHolder existing = new LoopContextHolder();
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(existing);
+
+        LoopContextHolder result = LoopContextHolder.getCurrent(context, true);
+
+        assertSame(existing, result);
+    }
+
+    @Test
+    public void clearCurrentRemoveVariableTest() {
+        ProcessContext context = mock(ProcessContext.class);
+
+        LoopContextHolder.clearCurrent(context);
+
+        
verify(context).removeVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER);
+    }
+
+    @Test
+    public void atomicCountersThreadSafeTest() {
+        LoopContextHolder holder = new LoopContextHolder();
+
+        holder.getNrOfInstances().set(10);
+        holder.getNrOfActiveInstances().incrementAndGet();
+        holder.getNrOfCompletedInstances().addAndGet(5);
+
+        assertEquals(10, holder.getNrOfInstances().get());
+        assertEquals(1, holder.getNrOfActiveInstances().get());
+        assertEquals(5, holder.getNrOfCompletedInstances().get());
+    }
+
+    @Test
+    public void stackOperationsTest() {
+        LoopContextHolder holder = new LoopContextHolder();
+
+        holder.getLoopCounterStack().push(1);
+        holder.getLoopCounterStack().push(2);
+        holder.getForwardCounterStack().push(10);
+
+        assertEquals(Integer.valueOf(2), holder.getLoopCounterStack().pop());
+        assertEquals(Integer.valueOf(1), holder.getLoopCounterStack().pop());
+        assertEquals(Integer.valueOf(10), 
holder.getForwardCounterStack().pop());
+    }
+
+    @Test
+    public void collectionGetterSetterTest() {
+        LoopContextHolder holder = new LoopContextHolder();
+        List<String> collection = Arrays.asList("a", "b", "c");
+
+        holder.setCollection(collection);
+
+        assertSame(collection, holder.getCollection());
+    }
+
+    @Test
+    public void failEndFlagTest() {
+        LoopContextHolder holder = new LoopContextHolder();
+
+        assertFalse(holder.isFailEnd());
+        holder.setFailEnd(true);
+        assertTrue(holder.isFailEnd());
+    }
+
+    @Test
+    public void completionConditionSatisfiedFlagTest() {
+        LoopContextHolder holder = new LoopContextHolder();
+
+        assertFalse(holder.isCompletionConditionSatisfied());
+        holder.setCompletionConditionSatisfied(true);
+        assertTrue(holder.isCompletionConditionSatisfied());
+    }
+}
diff --git 
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopTaskUtilsTest.java
 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopTaskUtilsTest.java
new file mode 100644
index 0000000000..98fc71e570
--- /dev/null
+++ 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopTaskUtilsTest.java
@@ -0,0 +1,298 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.pcext.utils;
+
+import org.apache.seata.saga.engine.pcext.StateInstruction;
+import org.apache.seata.saga.proctrl.ProcessContext;
+import org.apache.seata.saga.proctrl.impl.ProcessContextImpl;
+import org.apache.seata.saga.statelang.domain.DomainConstants;
+import org.apache.seata.saga.statelang.domain.State;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.apache.seata.saga.statelang.domain.StateType;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Stack;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link LoopTaskUtils}
+ */
+public class LoopTaskUtilsTest {
+
+    @Test
+    public void matchLoopWhenStateTypeIsServiceTaskReturnTrueTest() {
+        State state = mock(State.class);
+        when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+
+        assertTrue(LoopTaskUtils.matchLoop(state));
+    }
+
+    @Test
+    public void matchLoopWhenStateTypeIsScriptTaskReturnTrueTest() {
+        State state = mock(State.class);
+        when(state.getType()).thenReturn(StateType.SCRIPT_TASK);
+
+        assertTrue(LoopTaskUtils.matchLoop(state));
+    }
+
+    @Test
+    public void matchLoopWhenStateTypeIsSubStateMachineReturnTrueTest() {
+        State state = mock(State.class);
+        when(state.getType()).thenReturn(StateType.SUB_STATE_MACHINE);
+
+        assertTrue(LoopTaskUtils.matchLoop(state));
+    }
+
+    @Test
+    public void matchLoopWhenStateTypeIsChoiceReturnFalseTest() {
+        State state = mock(State.class);
+        when(state.getType()).thenReturn(StateType.CHOICE);
+
+        assertFalse(LoopTaskUtils.matchLoop(state));
+    }
+
+    @Test
+    public void matchLoopWhenStateIsNullReturnFalseTest() {
+        assertFalse(LoopTaskUtils.matchLoop(null));
+    }
+
+    @Test
+    public void reloadLoopCounterFromValidStateNameExtractCounterTest() {
+        String stateName = "myState" + LoopTaskUtils.LOOP_STATE_NAME_PATTERN + 
"10";
+        int counter = LoopTaskUtils.reloadLoopCounter(stateName);
+
+        assertEquals(10, counter);
+    }
+
+    @Test
+    public void reloadLoopCounterFromInvalidStateNameReturnNegativeOneTest() {
+        String stateName = "myState";
+        int counter = LoopTaskUtils.reloadLoopCounter(stateName);
+
+        // Source code returns -1 when pattern is not found
+        assertEquals(-1, counter);
+    }
+
+    @Test
+    public void reloadLoopCounterWithZeroCounterTest() {
+        String stateName = "myState" + LoopTaskUtils.LOOP_STATE_NAME_PATTERN + 
"0";
+        int counter = LoopTaskUtils.reloadLoopCounter(stateName);
+
+        assertEquals(0, counter);
+    }
+
+    @Test
+    public void reloadLoopCounterWithNullStateNameReturnNegativeOneTest() {
+        int counter = LoopTaskUtils.reloadLoopCounter(null);
+
+        assertEquals(-1, counter);
+    }
+
+    @Test
+    public void reloadLoopCounterWithEmptyStateNameReturnNegativeOneTest() {
+        int counter = LoopTaskUtils.reloadLoopCounter("");
+
+        assertEquals(-1, counter);
+    }
+
+    @Test
+    public void loopStateNamePatternConstantTest() {
+        assertEquals("-loop-", LoopTaskUtils.LOOP_STATE_NAME_PATTERN);
+    }
+
+    // ========== Additional Tests: Coverage for createLoopCounterContext 
==========
+
+    @Test
+    public void createLoopCounterContextPushCountersToStackTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        LoopContextHolder holder = new LoopContextHolder();
+        List<String> collection = Arrays.asList("a", "b", "c");
+        holder.setCollection(collection);
+
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(holder);
+
+        LoopTaskUtils.createLoopCounterContext(context);
+
+        assertEquals(3, holder.getNrOfInstances().get());
+        // Stack should contain 2, 1, 0 (pushed in descending order)
+        Stack<Integer> stack = holder.getLoopCounterStack();
+        assertEquals(3, stack.size());
+        assertEquals(Integer.valueOf(0), stack.pop());
+        assertEquals(Integer.valueOf(1), stack.pop());
+        assertEquals(Integer.valueOf(2), stack.pop());
+    }
+
+    // ========== Additional Tests: Coverage for generateLoopStateName 
==========
+
+    @Test
+    public void generateLoopStateNameWithValidNameAppendPatternTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        when(context.getVariable(DomainConstants.LOOP_COUNTER)).thenReturn(5);
+
+        String result = LoopTaskUtils.generateLoopStateName(context, 
"testState");
+
+        assertEquals("testState-loop-5", result);
+    }
+
+    @Test
+    public void generateLoopStateNameWithBlankNameReturnOriginalTest() {
+        ProcessContext context = mock(ProcessContext.class);
+
+        String result = LoopTaskUtils.generateLoopStateName(context, "");
+
+        assertEquals("", result);
+    }
+
+    @Test
+    public void generateLoopStateNameWithNullNameReturnNullTest() {
+        ProcessContext context = mock(ProcessContext.class);
+
+        String result = LoopTaskUtils.generateLoopStateName(context, null);
+
+        assertNull(result);
+    }
+
+    // ========== Additional Tests: Coverage for acquireNextLoopCounter 
==========
+
+    @Test
+    public void acquireNextLoopCounterPopFromStackTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        LoopContextHolder holder = new LoopContextHolder();
+        holder.getLoopCounterStack().push(10);
+        holder.getLoopCounterStack().push(20);
+
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(holder);
+
+        int counter = LoopTaskUtils.acquireNextLoopCounter(context);
+
+        assertEquals(20, counter);
+    }
+
+    @Test
+    public void acquireNextLoopCounterWhenStackEmptyReturnNegativeOneTest() {
+        ProcessContext context = mock(ProcessContext.class);
+        LoopContextHolder holder = new LoopContextHolder();
+        // Empty stack
+
+        
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+                .thenReturn(holder);
+
+        int counter = LoopTaskUtils.acquireNextLoopCounter(context);
+
+        assertEquals(-1, counter);
+    }
+
+    // ========== Additional Tests: Coverage for createLoopEventContext 
==========
+
+    @Test
+    public void 
createLoopEventContextWithPositiveCounterSetCounterDirectlyTest() {
+        ProcessContext parentContext = mock(ProcessContext.class);
+        StateInstruction instruction = mock(StateInstruction.class);
+        
when(parentContext.getInstruction(StateInstruction.class)).thenReturn(instruction);
+        when(instruction.getStateMachineName()).thenReturn("testMachine");
+        when(instruction.getTenantId()).thenReturn("tenant1");
+        when(instruction.getStateName()).thenReturn("testState");
+        when(instruction.getTemporaryState()).thenReturn(null);
+
+        ProcessContext childContext = 
LoopTaskUtils.createLoopEventContext(parentContext, 5);
+
+        assertNotNull(childContext);
+        assertEquals(5, 
childContext.getVariable(DomainConstants.LOOP_COUNTER));
+    }
+
+    @Test
+    public void 
createLoopEventContextWithNegativeCounterAcquireFromStackTest() {
+        ProcessContextImpl parentContext = new ProcessContextImpl();
+        StateInstruction instruction = new StateInstruction();
+        instruction.setStateMachineName("testMachine");
+        instruction.setTenantId("tenant1");
+        instruction.setStateName("testState");
+        parentContext.setInstruction(instruction);
+
+        LoopContextHolder holder = new LoopContextHolder();
+        holder.getLoopCounterStack().push(7);
+        
parentContext.setVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER, 
holder);
+
+        ProcessContext childContext = 
LoopTaskUtils.createLoopEventContext(parentContext, -1);
+
+        assertNotNull(childContext);
+        assertEquals(7, 
childContext.getVariable(DomainConstants.LOOP_COUNTER));
+    }
+
+    // ========== Additional Tests: Coverage for 
findOutLastRetriedStateInstance ==========
+
+    @Test
+    public void findOutLastRetriedStateInstanceWhenFoundReturnStateTest() {
+        StateMachineInstance smInstance = mock(StateMachineInstance.class);
+        StateInstance state1 = mock(StateInstance.class);
+        StateInstance state2 = mock(StateInstance.class);
+
+        when(state1.getName()).thenReturn("state-loop-0");
+        when(state2.getName()).thenReturn("state-loop-1");
+
+        List<StateInstance> stateList = new ArrayList<>();
+        stateList.add(state1);
+        stateList.add(state2);
+
+        when(smInstance.getStateList()).thenReturn(stateList);
+
+        StateInstance result = 
LoopTaskUtils.findOutLastRetriedStateInstance(smInstance, "state-loop-1");
+
+        assertSame(state2, result);
+    }
+
+    @Test
+    public void findOutLastRetriedStateInstanceWhenNotFoundReturnNullTest() {
+        StateMachineInstance smInstance = mock(StateMachineInstance.class);
+        StateInstance state1 = mock(StateInstance.class);
+
+        when(state1.getName()).thenReturn("state-loop-0");
+
+        List<StateInstance> stateList = new ArrayList<>();
+        stateList.add(state1);
+
+        when(smInstance.getStateList()).thenReturn(stateList);
+
+        StateInstance result = 
LoopTaskUtils.findOutLastRetriedStateInstance(smInstance, "nonexistent");
+
+        assertNull(result);
+    }
+
+    @Test
+    public void findOutLastRetriedStateInstanceWithEmptyListReturnNullTest() {
+        StateMachineInstance smInstance = mock(StateMachineInstance.class);
+        when(smInstance.getStateList()).thenReturn(new ArrayList<>());
+
+        StateInstance result = 
LoopTaskUtils.findOutLastRetriedStateInstance(smInstance, "anyState");
+
+        assertNull(result);
+    }
+}
diff --git 
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/repo/impl/StateLogRepositoryImplTest.java
 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/repo/impl/StateLogRepositoryImplTest.java
new file mode 100644
index 0000000000..6fa2edb3cd
--- /dev/null
+++ 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/repo/impl/StateLogRepositoryImplTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.repo.impl;
+
+import org.apache.seata.saga.engine.store.StateLogStore;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link StateLogRepositoryImpl}
+ */
+public class StateLogRepositoryImplTest {
+
+    @Test
+    public void getStateMachineInstanceWhenStoreIsNullReturnNullTest() {
+        StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+        assertNull(repo.getStateMachineInstance("any-id"));
+    }
+
+    @Test
+    public void getStateMachineInstanceWhenStoreExistsDelegateToStoreTest() {
+        StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+        StateLogStore store = mock(StateLogStore.class);
+        StateMachineInstance expected = mock(StateMachineInstance.class);
+        when(store.getStateMachineInstance("test-id")).thenReturn(expected);
+
+        repo.setStateLogStore(store);
+
+        assertSame(expected, repo.getStateMachineInstance("test-id"));
+        verify(store).getStateMachineInstance("test-id");
+    }
+
+    @Test
+    public void 
getStateMachineInstanceByBusinessKeyWhenStoreIsNullReturnNullTest() {
+        StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+        assertNull(repo.getStateMachineInstanceByBusinessKey("key", "tenant"));
+    }
+
+    @Test
+    public void 
getStateMachineInstanceByBusinessKeyWhenStoreExistsDelegateToStoreTest() {
+        StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+        StateLogStore store = mock(StateLogStore.class);
+        StateMachineInstance expected = mock(StateMachineInstance.class);
+        when(store.getStateMachineInstanceByBusinessKey("key", 
"tenant")).thenReturn(expected);
+
+        repo.setStateLogStore(store);
+
+        assertSame(expected, repo.getStateMachineInstanceByBusinessKey("key", 
"tenant"));
+        verify(store).getStateMachineInstanceByBusinessKey("key", "tenant");
+    }
+
+    @Test
+    public void 
queryStateMachineInstanceByParentIdWhenStoreIsNullReturnNullTest() {
+        StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+        assertNull(repo.queryStateMachineInstanceByParentId("parent-id"));
+    }
+
+    @Test
+    public void 
queryStateMachineInstanceByParentIdWhenStoreExistsDelegateToStoreTest() {
+        StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+        StateLogStore store = mock(StateLogStore.class);
+        List<StateMachineInstance> expected = 
Arrays.asList(mock(StateMachineInstance.class));
+        
when(store.queryStateMachineInstanceByParentId("parent-id")).thenReturn(expected);
+
+        repo.setStateLogStore(store);
+
+        assertSame(expected, 
repo.queryStateMachineInstanceByParentId("parent-id"));
+        verify(store).queryStateMachineInstanceByParentId("parent-id");
+    }
+
+    @Test
+    public void getStateInstanceWhenStoreIsNullReturnNullTest() {
+        StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+        assertNull(repo.getStateInstance("state-id", "machine-id"));
+    }
+
+    @Test
+    public void getStateInstanceWhenStoreExistsDelegateToStoreTest() {
+        StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+        StateLogStore store = mock(StateLogStore.class);
+        StateInstance expected = mock(StateInstance.class);
+        when(store.getStateInstance("state-id", 
"machine-id")).thenReturn(expected);
+
+        repo.setStateLogStore(store);
+
+        assertSame(expected, repo.getStateInstance("state-id", "machine-id"));
+        verify(store).getStateInstance("state-id", "machine-id");
+    }
+
+    @Test
+    public void 
queryStateInstanceListByMachineInstanceIdWhenStoreIsNullReturnNullTest() {
+        StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+        
assertNull(repo.queryStateInstanceListByMachineInstanceId("machine-id"));
+    }
+
+    @Test
+    public void 
queryStateInstanceListByMachineInstanceIdWhenStoreExistsDelegateToStoreTest() {
+        StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+        StateLogStore store = mock(StateLogStore.class);
+        List<StateInstance> expected = 
Arrays.asList(mock(StateInstance.class));
+        
when(store.queryStateInstanceListByMachineInstanceId("machine-id")).thenReturn(expected);
+
+        repo.setStateLogStore(store);
+
+        assertSame(expected, 
repo.queryStateInstanceListByMachineInstanceId("machine-id"));
+        verify(store).queryStateInstanceListByMachineInstanceId("machine-id");
+    }
+}
diff --git 
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/utils/ExceptionUtilsTest.java
 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/utils/ExceptionUtilsTest.java
new file mode 100644
index 0000000000..0c5b37d1b9
--- /dev/null
+++ 
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/utils/ExceptionUtilsTest.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.saga.engine.utils;
+
+import org.apache.seata.common.exception.FrameworkErrorCode;
+import org.apache.seata.saga.engine.exception.EngineExecutionException;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachine;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.junit.jupiter.api.Test;
+
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link ExceptionUtils}
+ */
+public class ExceptionUtilsTest {
+
+    @Test
+    public void createEngineExecutionExceptionWithAllParamsTest() {
+        StateMachineInstance smInstance = mock(StateMachineInstance.class);
+        StateMachine sm = mock(StateMachine.class);
+        StateInstance stateInstance = mock(StateInstance.class);
+
+        when(smInstance.getStateMachine()).thenReturn(sm);
+        when(sm.getAppName()).thenReturn("testApp");
+        when(smInstance.getId()).thenReturn("sm-123");
+        when(stateInstance.getName()).thenReturn("testState");
+        when(stateInstance.getId()).thenReturn("state-456");
+
+        Exception cause = new RuntimeException("test");
+        EngineExecutionException result = 
ExceptionUtils.createEngineExecutionException(
+                cause, FrameworkErrorCode.UnknownAppError, "test message", 
smInstance, stateInstance);
+
+        assertEquals("test message", result.getMessage());
+        assertEquals("testApp", result.getStateMachineName());
+        assertEquals("sm-123", result.getStateMachineInstanceId());
+        assertEquals("testState", result.getStateName());
+        assertEquals("state-456", result.getStateInstanceId());
+    }
+
+    @Test
+    public void 
createEngineExecutionExceptionWithNullStateMachineInstanceTest() {
+        EngineExecutionException result = 
ExceptionUtils.createEngineExecutionException(
+                null, FrameworkErrorCode.UnknownAppError, "test", null, 
(StateInstance) null);
+
+        assertNull(result.getStateMachineName());
+        assertNull(result.getStateMachineInstanceId());
+    }
+
+    @Test
+    public void createEngineExecutionExceptionWithStateNameTest() {
+        StateMachineInstance smInstance = mock(StateMachineInstance.class);
+        StateMachine sm = mock(StateMachine.class);
+
+        when(smInstance.getStateMachine()).thenReturn(sm);
+        when(sm.getAppName()).thenReturn("testApp");
+        when(smInstance.getId()).thenReturn("sm-123");
+
+        Exception cause = new RuntimeException("test");
+        EngineExecutionException result = 
ExceptionUtils.createEngineExecutionException(
+                cause, FrameworkErrorCode.UnknownAppError, "test message", 
smInstance, "testStateName");
+
+        assertEquals("test message", result.getMessage());
+        assertEquals("testApp", result.getStateMachineName());
+        assertEquals("sm-123", result.getStateMachineInstanceId());
+        assertEquals("testStateName", result.getStateName());
+    }
+
+    @Test
+    public void getNetExceptionTypeSocketTimeoutExceptionConnectTimeoutTest() {
+        SocketTimeoutException e = new SocketTimeoutException("connect timed 
out");
+        
assertEquals(ExceptionUtils.NetExceptionType.CONNECT_TIMEOUT_EXCEPTION, 
ExceptionUtils.getNetExceptionType(e));
+    }
+
+    @Test
+    public void getNetExceptionTypeSocketTimeoutExceptionReadTimeoutTest() {
+        SocketTimeoutException e = new SocketTimeoutException("read timed 
out");
+        assertEquals(ExceptionUtils.NetExceptionType.READ_TIMEOUT_EXCEPTION, 
ExceptionUtils.getNetExceptionType(e));
+    }
+
+    @Test
+    public void getNetExceptionTypeConnectExceptionTest() {
+        ConnectException e = new ConnectException("Connection refused");
+        assertEquals(ExceptionUtils.NetExceptionType.CONNECT_EXCEPTION, 
ExceptionUtils.getNetExceptionType(e));
+    }
+
+    @Test
+    public void getNetExceptionTypeNestedNetworkExceptionTest() {
+        ConnectException innerException = new ConnectException();
+        RuntimeException outerException = new RuntimeException("wrapper", 
innerException);
+        assertEquals(
+                ExceptionUtils.NetExceptionType.CONNECT_EXCEPTION, 
ExceptionUtils.getNetExceptionType(outerException));
+    }
+
+    @Test
+    public void getNetExceptionTypeNotNetExceptionTest() {
+        RuntimeException e = new RuntimeException("not a network error");
+        assertEquals(ExceptionUtils.NetExceptionType.NOT_NET_EXCEPTION, 
ExceptionUtils.getNetExceptionType(e));
+    }
+
+    @Test
+    public void getNetExceptionTypeMaxCauseDepthExceededTest() {
+        Exception current = new RuntimeException("base");
+        for (int i = 0; i < 25; i++) {
+            current = new RuntimeException("level-" + i, current);
+        }
+        assertEquals(ExceptionUtils.NetExceptionType.NOT_NET_EXCEPTION, 
ExceptionUtils.getNetExceptionType(current));
+    }
+
+    @Test
+    public void isNetExceptionWhenIsNetworkExceptionReturnTrueTest() {
+        ConnectException e = new ConnectException();
+        assertTrue(ExceptionUtils.isNetException(e));
+    }
+
+    @Test
+    public void isNetExceptionWhenNotNetworkExceptionReturnFalseTest() {
+        RuntimeException e = new RuntimeException("not network");
+        assertFalse(ExceptionUtils.isNetException(e));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to