From 2ff0633742b4065264c316b1da3827d6ee324132 Mon Sep 17 00:00:00 2001 From: contrueCT Date: Thu, 16 Jul 2026 16:00:00 +0800 Subject: [PATCH 1/4] refactor(server): upgrade TinkerPop to 3.7.6 and Groovy 4 (#142) * fix(server): resolve phase1 compatibility gaps --------- Co-authored-by: imbajin --- .github/workflows/cluster-test-ci.yml | 23 +- .github/workflows/commons-ci.yml | 4 +- .github/workflows/pd-store-ci.yml | 72 ++- .github/workflows/server-ci.yml | 29 +- README.md | 6 +- .../apache/hugegraph/ct/env/AbstractEnv.java | 70 ++- .../ct/node/AbstractNodeWrapper.java | 31 +- .../hugegraph/unit/util/DateUtilTest.java | 18 +- hugegraph-commons/pom.xml | 4 +- .../hugegraph/api/cypher/CypherClient.java | 102 ++- .../opencypher/CypherOpProcessor.java | 12 +- .../hugegraph/backend/query/Condition.java | 9 +- .../hugegraph/io/HugeGraphSONModule.java | 65 +- .../hugegraph/structure/HugeFeatures.java | 4 +- .../traversal/optimize/ConditionP.java | 13 +- .../traversal/optimize/HugeCountStep.java | 10 +- .../traversal/optimize/HugeCountStrategy.java | 8 +- .../traversal/optimize/TraversalUtil.java | 56 +- .../apache/hugegraph/version/CoreVersion.java | 2 +- .../static/conf/gremlin-driver-settings.yaml | 8 +- .../assembly/static/conf/gremlin-server.yaml | 31 +- .../assembly/static/conf/remote-objects.yaml | 8 +- .../src/assembly/static/conf/remote.yaml | 8 +- .../src/assembly/travis/ci-service-utils.sh | 180 ++++++ .../travis/conf-raft1/gremlin-server.yaml | 26 +- .../travis/conf-raft2/gremlin-server.yaml | 26 +- .../travis/conf-raft3/gremlin-server.yaml | 26 +- .../src/assembly/travis/start-pd.sh | 6 +- .../src/assembly/travis/start-store.sh | 6 +- hugegraph-server/hugegraph-test/pom.xml | 6 - .../apache/hugegraph/api/CypherApiTest.java | 149 ++++- .../api/cypher/CypherClientTest.java | 129 ++++ .../hugegraph/core/CountStrategyCoreTest.java | 45 ++ .../apache/hugegraph/core/VertexCoreTest.java | 18 + .../tinkerpop/ProcessBasicSuite.java | 23 +- .../tinkerpop/StructureBasicSuite.java | 5 +- .../apache/hugegraph/tinkerpop/TestGraph.java | 26 +- .../tinkerpop/TestGraphProvider.java | 163 ++++- .../optimize/TraversalUtilOptimizeTest.java | 39 ++ .../apache/hugegraph/unit/UnitTestSuite.java | 12 + .../GremlinConfigCompatibilityTest.java | 582 ++++++++++++++++++ .../hugegraph/unit/core/ConditionTest.java | 65 ++ .../GroovyScriptEngineCompatibilityTest.java | 49 ++ .../hugegraph/unit/core/HugeFeaturesTest.java | 37 ++ .../serializer/HugeGraphSONModuleTest.java | 148 +++++ .../src/main/resources/fast-methods.filter | 83 ++- .../src/main/resources/methods.filter | 78 ++- hugegraph-server/pom.xml | 8 +- hugegraph-store/hg-store-core/pom.xml | 8 +- hugegraph-struct/pom.xml | 11 +- .../org/apache/hugegraph/query/Condition.java | 9 +- pom.xml | 14 + 52 files changed, 2380 insertions(+), 190 deletions(-) create mode 100644 hugegraph-server/hugegraph-dist/src/assembly/travis/ci-service-utils.sh create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GroovyScriptEngineCompatibilityTest.java create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugeFeaturesTest.java create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.java diff --git a/.github/workflows/cluster-test-ci.yml b/.github/workflows/cluster-test-ci.yml index 3ef269e878..8e9f4d870f 100644 --- a/.github/workflows/cluster-test-ci.yml +++ b/.github/workflows/cluster-test-ci.yml @@ -45,8 +45,27 @@ jobs: - name: Run simple cluster test run: | - mvn test -pl hugegraph-cluster-test/hugegraph-clustertest-test -am -P simple-cluster-test + timeout 45m mvn test -pl hugegraph-cluster-test/hugegraph-clustertest-test \ + -am -P simple-cluster-test - name: Run multi cluster test run: | - mvn test -pl hugegraph-cluster-test/hugegraph-clustertest-test -am -P multi-cluster-test + timeout 45m mvn test -pl hugegraph-cluster-test/hugegraph-clustertest-test \ + -am -P multi-cluster-test + + - name: Show cluster diagnostics on failure + if: failure() + run: | + echo "[ci] java processes:" + ps -ef | grep -E "HugeGraph|hg-|java" | grep -v grep || true + echo "[ci] listening tcp ports:" + (ss -ltnp || netstat -ltnp || true) 2>&1 + find hugegraph-cluster-test -path '*/logs/*' -type f | sort | while read -r log; do + echo "--- tail -n 200 $log ---" + tail -n 200 "$log" || true + done + find . -path '*/surefire-reports/*' -type f \ + \( -name '*.txt' -o -name '*.xml' \) | sort | while read -r report; do + echo "--- tail -n 120 $report ---" + tail -n 120 "$report" || true + done diff --git a/.github/workflows/commons-ci.yml b/.github/workflows/commons-ci.yml index 5311ebeee0..5ba4c70f47 100644 --- a/.github/workflows/commons-ci.yml +++ b/.github/workflows/commons-ci.yml @@ -5,8 +5,8 @@ on: push: branches: - master - - /^release-.*$/ - - /^test-.*$/ + - 'release-*' + - 'test-*' pull_request: jobs: diff --git a/.github/workflows/pd-store-ci.yml b/.github/workflows/pd-store-ci.yml index 62006794c7..a12cd7a2a1 100644 --- a/.github/workflows/pd-store-ci.yml +++ b/.github/workflows/pd-store-ci.yml @@ -143,6 +143,23 @@ jobs: run: | mvn test -pl hugegraph-pd/hg-pd-test -am -P pd-rest-test + - name: Show PD diagnostics on failure + if: failure() + run: | + VERSION=$(grep -E '^VersionInBash=' \ + hugegraph-commons/hugegraph-common/src/main/resources/version.properties | cut -d'=' -f2-) + [[ "$VERSION" =~ ^[0-9A-Za-z._-]+$ ]] || { + echo "Invalid VersionInBash: $VERSION" + exit 1 + } + PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION + bash $TRAVIS_DIR/ci-service-utils.sh dump "$PD_DIR" HugeGraphPD || true + find . -path '*/surefire-reports/*' -type f \ + \( -name '*.txt' -o -name '*.xml' \) | sort | while read -r report; do + echo "--- tail -n 120 $report ---" + tail -n 120 "$report" || true + done + - name: Upload coverage to Codecov uses: codecov/codecov-action@v3.0.0 with: @@ -246,6 +263,25 @@ jobs: run: | mvn test -pl hugegraph-store/hg-store-test -am -P store-raftcore-test + - name: Show Store diagnostics on failure + if: failure() + run: | + VERSION=$(grep -E '^VersionInBash=' \ + hugegraph-commons/hugegraph-common/src/main/resources/version.properties | cut -d'=' -f2-) + [[ "$VERSION" =~ ^[0-9A-Za-z._-]+$ ]] || { + echo "Invalid VersionInBash: $VERSION" + exit 1 + } + PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION + STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION + bash $TRAVIS_DIR/ci-service-utils.sh dump "$PD_DIR" HugeGraphPD || true + bash $TRAVIS_DIR/ci-service-utils.sh dump "$STORE_DIR" HugeGraphStore || true + find . -path '*/surefire-reports/*' -type f \ + \( -name '*.txt' -o -name '*.xml' \) | sort | while read -r report; do + echo "--- tail -n 120 $report ---" + tail -n 120 "$report" || true + done + - name: Upload coverage to Codecov uses: codecov/codecov-action@v3.0.0 with: @@ -312,10 +348,38 @@ jobs: run: | $TRAVIS_DIR/run-api-test-for-raft.sh $BACKEND $REPORT_DIR - - name: Run TinkerPop test - if: ${{ env.RELEASE_BRANCH == 'true' }} - run: | - $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND tinkerpop + # HStore remains covered by unit/core/API here; TinkerPop compliance is + # exercised in Server CI on memory and rocksdb. + - name: Run TinkerPop structure test + if: ${{ env.RELEASE_BRANCH == 'true' && env.BACKEND != 'hstore' }} + timeout-minutes: 60 + run: | + $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND structure + + - name: Run TinkerPop process test + if: ${{ env.RELEASE_BRANCH == 'true' && env.BACKEND != 'hstore' }} + timeout-minutes: 60 + run: | + $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND process + + - name: Show HStore diagnostics on failure + if: failure() + run: | + VERSION=$(grep -E '^VersionInBash=' \ + hugegraph-commons/hugegraph-common/src/main/resources/version.properties | cut -d'=' -f2-) + [[ "$VERSION" =~ ^[0-9A-Za-z._-]+$ ]] || { + echo "Invalid VersionInBash: $VERSION" + exit 1 + } + PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION + STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION + bash $TRAVIS_DIR/ci-service-utils.sh dump "$PD_DIR" HugeGraphPD || true + bash $TRAVIS_DIR/ci-service-utils.sh dump "$STORE_DIR" HugeGraphStore || true + find . -path '*/surefire-reports/*' -type f \ + \( -name '*.txt' -o -name '*.xml' \) | sort | while read -r report; do + echo "--- tail -n 120 $report ---" + tail -n 120 "$report" || true + done - name: Upload coverage to Codecov uses: codecov/codecov-action@v3.0.0 diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml index 586e36da49..68177e397c 100644 --- a/.github/workflows/server-ci.yml +++ b/.github/workflows/server-ci.yml @@ -21,8 +21,14 @@ jobs: HEAD_BRANCH_NAME: ${{ github.head_ref }} BASE_BRANCH_NAME: ${{ github.base_ref }} TARGET_BRANCH_NAME: ${{ github.base_ref != '' && github.base_ref || github.ref_name }} - RELEASE_BRANCH: ${{ startsWith(github.ref_name, 'release-') || startsWith(github.ref_name, 'test-') }} - RAFT_MODE: ${{ startsWith(github.head_ref, 'test') || startsWith(github.head_ref, 'raft') }} + RELEASE_BRANCH: >- + ${{ + startsWith(github.ref_name, 'release-') || + startsWith(github.ref_name, 'test-') || + startsWith(github.head_ref, 'release-') || + startsWith(github.head_ref, 'test-') + }} + RAFT_MODE: ${{ startsWith(github.ref_name, 'raft-') || startsWith(github.head_ref, 'raft-') }} strategy: fail-fast: false @@ -115,14 +121,25 @@ jobs: # TODO: disable raft test in normal PR due to the always timeout problem - name: Run raft test if: ${{ env.RAFT_MODE == 'true' && env.BACKEND == 'rocksdb' }} + timeout-minutes: 45 run: | $TRAVIS_DIR/run-api-test-for-raft.sh $BACKEND $REPORT_DIR - - name: Run TinkerPop test - if: ${{ env.RELEASE_BRANCH == 'true' }} + # TinkerPop compliance is covered by memory and rocksdb in CI. + # HBase still runs compile/unit/core/API because its full suite exceeds the CI budget. + - name: Run TinkerPop structure test + if: ${{ env.RELEASE_BRANCH == 'true' && env.BACKEND != 'hbase' }} + timeout-minutes: 60 run: | - echo "[WARNING] Enter Tinkerpop Test, current 'github.ref_name' is ${{ github.ref_name }}" - $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND tinkerpop + echo "[WARNING] Enter Tinkerpop Structure Test, current 'github.ref_name' is ${{ github.ref_name }}" + $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND structure + + - name: Run TinkerPop process test + if: ${{ env.RELEASE_BRANCH == 'true' && env.BACKEND != 'hbase' }} + timeout-minutes: 60 + run: | + echo "[WARNING] Enter Tinkerpop Process Test, current 'github.ref_name' is ${{ github.ref_name }}" + $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND process - name: Upload coverage to Codecov # TODO: update to v5 later diff --git a/README.md b/README.md index f24f6f7bfd..462cbe14d0 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ HugeGraph supports both **standalone** and **distributed** deployments: │ HugeGraph Server (:8080) │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ REST API │ │ Gremlin │ │ Cypher Engine │ │ - │ │(Jersey 3)│ │ (TP 3.5) │ │ (OpenCypher) │ │ + │ │(Jersey 3)│ │ (TP 3.7) │ │ (OpenCypher) │ │ │ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │ │ └─────────────┼─────────────────┘ │ │ ┌────────▼────────┐ │ @@ -128,7 +128,7 @@ flowchart TB subgraph Server["HugeGraph Server :8080"] API[REST API
Jersey 3] - GS[Gremlin Server
TinkerPop 3.5] + GS[Gremlin Server
TinkerPop 3.7] CS[Cypher Engine
OpenCypher] CORE[Graph Engine
hugegraph-core] @@ -294,7 +294,7 @@ curl http://localhost:8080/versions # "versions": { # "version": "v1", # "core": "1.7.0", -# "gremlin": "3.5.1", +# "gremlin": "3.7.6", # "api": "1.7.0" # } # } diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java index 0c24860929..e85a138bfe 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import org.apache.hugegraph.ct.base.HGTestLogger; import org.apache.hugegraph.ct.config.ClusterConfig; @@ -28,6 +29,7 @@ import org.apache.hugegraph.ct.config.PDConfig; import org.apache.hugegraph.ct.config.ServerConfig; import org.apache.hugegraph.ct.config.StoreConfig; +import org.apache.hugegraph.ct.node.AbstractNodeWrapper; import org.apache.hugegraph.ct.node.PDNodeWrapper; import org.apache.hugegraph.ct.node.ServerNodeWrapper; import org.apache.hugegraph.ct.node.StoreNodeWrapper; @@ -40,6 +42,8 @@ public abstract class AbstractEnv implements BaseEnv { private static final Logger LOG = HGTestLogger.ENV_LOG; + private static final int NODE_START_TIMEOUT_SECONDS = 120; + private static final int NODE_START_POLL_MILLIS = 1000; protected ClusterConfig clusterConfig; protected List pdNodeWrappers; @@ -88,34 +92,54 @@ protected void init(int pdCnt, int storeCnt, int serverCnt) { public void startCluster() { for (PDNodeWrapper pdNodeWrapper : pdNodeWrappers) { - pdNodeWrapper.start(); - while (!pdNodeWrapper.isStarted()) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } + startNode(pdNodeWrapper); } for (StoreNodeWrapper storeNodeWrapper : storeNodeWrappers) { - storeNodeWrapper.start(); - while (!storeNodeWrapper.isStarted()) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } + startNode(storeNodeWrapper); } for (ServerNodeWrapper serverNodeWrapper : serverNodeWrappers) { - serverNodeWrapper.start(); - while (!serverNodeWrapper.isStarted()) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } + startNode(serverNodeWrapper); + } + } + + private void startNode(AbstractNodeWrapper nodeWrapper) { + System.out.printf("[cluster-test] starting %s in %s%n", + nodeWrapper.getID(), nodeWrapper.getNodePath()); + nodeWrapper.start(); + waitUntilStarted(nodeWrapper); + } + + private static void waitUntilStarted(AbstractNodeWrapper nodeWrapper) { + long deadline = System.nanoTime() + + TimeUnit.SECONDS.toNanos(NODE_START_TIMEOUT_SECONDS); + while (System.nanoTime() < deadline) { + if (nodeWrapper.isStarted()) { + System.out.printf("[cluster-test] %s started%n", + nodeWrapper.getID()); + return; + } + if (!nodeWrapper.isAlive()) { + nodeWrapper.dumpLog(); + throw new AssertionError(String.format( + "%s failed to start, process status: %s", + nodeWrapper.getID(), nodeWrapper.processStatus())); } + sleepBeforeRetry(); + } + + nodeWrapper.dumpLog(); + throw new AssertionError(String.format( + "%s did not start within %s seconds, process status: %s", + nodeWrapper.getID(), NODE_START_TIMEOUT_SECONDS, + nodeWrapper.processStatus())); + } + + private static void sleepBeforeRetry() { + try { + TimeUnit.MILLISECONDS.sleep(NODE_START_POLL_MILLIS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError("Interrupted while waiting node start", e); } } diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java index 8236bb1392..97585756a1 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java @@ -174,7 +174,36 @@ public void stop() { } public boolean isAlive() { - return this.instance.isAlive(); + return this.instance != null && this.instance.isAlive(); + } + + public String processStatus() { + if (this.instance == null) { + return "not started"; + } + if (this.instance.isAlive()) { + return "alive"; + } + return "exited with code " + this.instance.exitValue(); + } + + public void dumpLog() { + Path logPath = Paths.get(getLogPath()); + System.out.println("===== " + getID() + " log: " + logPath + " ====="); + if (!Files.exists(logPath)) { + System.out.println("Log file does not exist"); + return; + } + + try { + List lines = Files.readAllLines(logPath, StandardCharsets.UTF_8); + int start = Math.max(0, lines.size() - 200); + for (int i = start; i < lines.size(); i++) { + System.out.println(lines.get(i)); + } + } catch (IOException e) { + System.out.println("Failed to read log file: " + e.getMessage()); + } } protected ProcessBuilder runCmd(List startCmd, File stdoutFile) throws IOException { diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/DateUtilTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/DateUtilTest.java index 7c51406b36..6c8214e3fe 100644 --- a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/DateUtilTest.java +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/DateUtilTest.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; @@ -81,9 +82,11 @@ public void testNow() { @Test public void testParseCornerDateValue() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); + final Date expected = DateUtil.parse("0", "yyyy"); int threadCount = 10; List threads = new ArrayList<>(threadCount); AtomicInteger errorCount = new AtomicInteger(0); + AtomicReference firstError = new AtomicReference<>(); for (int t = 0; t < threadCount; t++) { Thread thread = new Thread(() -> { try { @@ -92,9 +95,9 @@ public void testParseCornerDateValue() throws InterruptedException { throw new RuntimeException(e); } try { - Assert.assertEquals(new Date(-62167248343000L), - DateUtil.parse("0", "yyyy")); - } catch (Exception e) { + Assert.assertEquals(expected, DateUtil.parse("0", "yyyy")); + } catch (Throwable e) { + firstError.compareAndSet(null, e); errorCount.incrementAndGet(); } }); @@ -109,6 +112,15 @@ public void testParseCornerDateValue() throws InterruptedException { thread.join(); } + Throwable error = firstError.get(); + if (error != null) { + AssertionError assertion = new AssertionError(String.format( + "Expected concurrent parses to match " + + "baseline result, but got %s failures", + errorCount.get())); + assertion.initCause(error); + throw assertion; + } Assert.assertEquals(0, errorCount.get()); } diff --git a/hugegraph-commons/pom.xml b/hugegraph-commons/pom.xml index b9e780bd32..b45a4d7eec 100644 --- a/hugegraph-commons/pom.xml +++ b/hugegraph-commons/pom.xml @@ -98,10 +98,10 @@ 1.8 2.18.0 1.10 - 2.8.0 + 2.10.1 1.9.4 3.2.2 - 3.12.0 + 3.18.0 2.7 1.13 30.0-jre diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java index 92ae18c54d..85d98a25ce 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java @@ -17,7 +17,10 @@ package org.apache.hugegraph.api.cypher; +import java.lang.reflect.Array; +import java.util.IdentityHashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -29,20 +32,23 @@ import javax.annotation.concurrent.ThreadSafe; import org.apache.commons.configuration2.Configuration; +import org.apache.hugegraph.backend.id.Id; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; import org.apache.tinkerpop.gremlin.driver.Client; import org.apache.tinkerpop.gremlin.driver.Cluster; import org.apache.tinkerpop.gremlin.driver.Result; import org.apache.tinkerpop.gremlin.driver.ResultSet; -import org.apache.tinkerpop.gremlin.driver.Tokens; -import org.apache.tinkerpop.gremlin.driver.message.RequestMessage; +import org.apache.tinkerpop.gremlin.process.traversal.Path; +import org.apache.tinkerpop.gremlin.util.Tokens; +import org.apache.tinkerpop.gremlin.util.message.RequestMessage; import org.slf4j.Logger; @ThreadSafe public final class CypherClient { private static final Logger LOG = Log.logger(CypherClient.class); + private static final int NORMALIZE_MAX_DEPTH = 32; private final Supplier configurationSupplier; private String userName; private String password; @@ -105,12 +111,102 @@ private List doQueryList(Client client, RequestMessage request) while (iter.hasNext()) { Result data = iter.next(); - list.add(data.getObject()); + list.add(normalize(data.getObject())); } return list; } + static Object normalize(Object value) { + return normalize(value, 0, new IdentityHashMap<>()); + } + + private static Object normalize(Object value, int depth, + IdentityHashMap seen) { + if (value == null) { + return null; + } + if (value instanceof Id) { + return ((Id) value).asObject(); + } + boolean composite = value instanceof Map || value instanceof Path || + value instanceof Iterable || + value.getClass().isArray(); + if (!composite) { + return value; + } + if (depth >= NORMALIZE_MAX_DEPTH) { + throw new IllegalArgumentException( + "Exceeded max normalization depth 32"); + } + if (value instanceof Map) { + if (seen.put(value, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "Detected cyclic Cypher result"); + } + Map normalized = new LinkedHashMap<>(); + try { + for (Map.Entry entry : ((Map) value).entrySet()) { + normalized.put(normalize(entry.getKey(), depth + 1, seen), + normalize(entry.getValue(), depth + 1, seen)); + } + } finally { + seen.remove(value); + } + return normalized; + } + if (value instanceof Path) { + if (seen.put(value, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "Detected cyclic Cypher result"); + } + Map normalized = new LinkedHashMap<>(); + try { + Path path = (Path) value; + normalized.put("labels", + normalize(path.labels(), depth + 1, seen)); + normalized.put("objects", + normalize(path.objects(), depth + 1, seen)); + } finally { + seen.remove(value); + } + return normalized; + } + if (value instanceof Iterable) { + if (seen.put(value, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "Detected cyclic Cypher result"); + } + List normalized = new LinkedList<>(); + try { + for (Object item : (Iterable) value) { + normalized.add(normalize(item, depth + 1, seen)); + } + } finally { + seen.remove(value); + } + return normalized; + } + if (value.getClass().isArray()) { + if (seen.put(value, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "Detected cyclic Cypher result"); + } + List normalized = new LinkedList<>(); + try { + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + normalized.add(normalize(Array.get(value, i), depth + 1, + seen)); + } + } finally { + seen.remove(value); + } + return normalized; + } + return value; + } + /** * As Sasl does not support a token, which is a coded string to indicate a legal user, * we had to use a trick to fix it. When the token is set, the password will be set to diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java index dfad9b9594..e3f0a60364 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java @@ -19,7 +19,7 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Optional.empty; -import static org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode.SERVER_ERROR; +import static org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode.SERVER_ERROR; import static org.opencypher.gremlin.translation.StatementOption.EXPLAIN; import static org.slf4j.LoggerFactory.getLogger; @@ -33,10 +33,6 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; -import org.apache.tinkerpop.gremlin.driver.Tokens; -import org.apache.tinkerpop.gremlin.driver.message.RequestMessage; -import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage; -import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode; import org.apache.tinkerpop.gremlin.process.traversal.P; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal; @@ -49,7 +45,11 @@ import org.apache.tinkerpop.gremlin.server.op.AbstractEvalOpProcessor; import org.apache.tinkerpop.gremlin.server.op.OpProcessorException; import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.util.Tokens; import org.apache.tinkerpop.gremlin.util.function.ThrowingConsumer; +import org.apache.tinkerpop.gremlin.util.message.RequestMessage; +import org.apache.tinkerpop.gremlin.util.message.ResponseMessage; +import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode; import org.opencypher.gremlin.translation.CypherAst; import org.opencypher.gremlin.translation.groovy.GroovyPredicate; import org.opencypher.gremlin.translation.ir.TranslationWriter; @@ -66,7 +66,7 @@ /** * Description of the modifications: *

- * 1) Changed the method signature to adopt the gremlin-server 3.5.1. + * 1) Changed the method signature to adopt the gremlin-server Context API. *

  * public Optional> selectOther(RequestMessage requestMessage)
  * -->
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Condition.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Condition.java
index 8d9ecd4332..4351c3379b 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Condition.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Condition.java
@@ -25,7 +25,6 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.function.BiFunction;
-import java.util.function.BiPredicate;
 
 import org.apache.commons.lang.ArrayUtils;
 import org.apache.hugegraph.backend.id.Id;
@@ -37,6 +36,7 @@
 import org.apache.hugegraph.util.DateUtil;
 import org.apache.hugegraph.util.E;
 import org.apache.hugegraph.util.NumericUtil;
+import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -51,7 +51,7 @@ public enum ConditionType {
         NOT
     }
 
-    public enum RelationType implements BiPredicate {
+    public enum RelationType implements PBiPredicate {
 
         EQ("==", RelationType::equals),
 
@@ -165,6 +165,11 @@ public String string() {
             return this.operator;
         }
 
+        @Override
+        public String getPredicateName() {
+            return this.operator;
+        }
+
         /**
          * Determine two values of any type equal
          *
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
index ddb7c1a981..7866e076a5 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
@@ -103,12 +103,14 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
         TYPE_DEFINITIONS = new ConcurrentHashMap<>();
 
         TYPE_DEFINITIONS.put(Optional.class, "Optional");
+        TYPE_DEFINITIONS.put(File.class, "File");
         TYPE_DEFINITIONS.put(Date.class, "Date");
         TYPE_DEFINITIONS.put(UUID.class, "UUID");
 
         // HugeGraph id serializer
         TYPE_DEFINITIONS.put(StringId.class, "StringId");
         TYPE_DEFINITIONS.put(LongId.class, "LongId");
+        TYPE_DEFINITIONS.put(UuidId.class, "UuidId");
         TYPE_DEFINITIONS.put(EdgeId.class, "EdgeId");
 
         // HugeGraph schema serializer
@@ -171,6 +173,7 @@ public static void registerCommonSerializers(SimpleModule module) {
         module.addSerializer(Shard.class, new ShardSerializer());
 
         module.addSerializer(File.class, new FileSerializer());
+        module.addDeserializer(File.class, new FileDeserializer());
 
         boolean useTimestamp = false;
         module.addSerializer(Date.class,
@@ -641,8 +644,8 @@ public T deserialize(JsonParser jsonParser,
                 String idValue = ctxt.readValue(jsonParser, String.class);
                 return (T) IdGenerator.of(idValue);
             } else if (clazz.equals(UuidId.class)) {
-                UUID idValue = ctxt.readValue(jsonParser, UUID.class);
-                return (T) IdGenerator.of(idValue);
+                String idValue = ctxt.readValue(jsonParser, String.class);
+                return (T) IdGenerator.of(UUID.fromString(idValue));
             } else {
                 assert clazz.equals(EdgeId.class);
                 String idValue = ctxt.readValue(jsonParser, String.class);
@@ -924,9 +927,65 @@ public FileSerializer() {
         public void serialize(File file, JsonGenerator jsonGenerator,
                               SerializerProvider provider) throws IOException {
             jsonGenerator.writeStartObject();
-            jsonGenerator.writeStringField("file", file.getName());
+            this.writeFields(file, jsonGenerator);
             jsonGenerator.writeEndObject();
         }
+
+        @Override
+        public void serializeWithType(File file,
+                                      JsonGenerator jsonGenerator,
+                                      SerializerProvider provider,
+                                      TypeSerializer typeSer)
+                throws IOException {
+            WritableTypeId typeId = typeSer.typeId(
+                    file, JsonToken.VALUE_EMBEDDED_OBJECT);
+            typeSer.writeTypePrefix(jsonGenerator, typeId);
+            this.serialize(file, jsonGenerator, provider);
+            typeSer.writeTypeSuffix(jsonGenerator, typeId);
+        }
+
+        private void writeFields(File file, JsonGenerator jsonGenerator)
+                throws IOException {
+            jsonGenerator.writeStringField("file", file.getName());
+        }
+    }
+
+    private static class FileDeserializer extends StdDeserializer {
+
+        public FileDeserializer() {
+            super(File.class);
+        }
+
+        @Override
+        public File deserialize(JsonParser jsonParser,
+                                DeserializationContext ctxt)
+                throws IOException {
+            JsonToken token = jsonParser.currentToken();
+            if (token == null) {
+                token = jsonParser.nextToken();
+            }
+            if (token == JsonToken.VALUE_STRING) {
+                return new File(jsonParser.getValueAsString());
+            }
+            if (token == JsonToken.START_OBJECT) {
+                String file = null;
+                while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
+                    String field = jsonParser.currentName();
+                    jsonParser.nextToken();
+                    if ("file".equals(field)) {
+                        file = jsonParser.getValueAsString();
+                    } else {
+                        jsonParser.skipChildren();
+                    }
+                }
+                if (file == null) {
+                    return (File) ctxt.handleUnexpectedToken(File.class,
+                                                             jsonParser);
+                }
+                return new File(file);
+            }
+            return (File) ctxt.handleUnexpectedToken(File.class, jsonParser);
+        }
     }
 
     private static class BlobSerializer extends StdSerializer {
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeFeatures.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeFeatures.java
index f8bdb8c75c..49be0ecbe9 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeFeatures.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeFeatures.java
@@ -186,7 +186,7 @@ public boolean supportsSerializableValues() {
 
         @Override
         public boolean supportsUniformListValues() {
-            return true;
+            return false;
         }
 
         @Override
@@ -225,7 +225,7 @@ public boolean supportsSerializableValues() {
 
         @Override
         public boolean supportsUniformListValues() {
-            return true;
+            return false;
         }
 
     }
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java
index e41a0df706..2ef93114f1 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java
@@ -17,16 +17,23 @@
 
 package org.apache.hugegraph.traversal.optimize;
 
-import java.util.function.BiPredicate;
-
 import org.apache.hugegraph.backend.query.Condition;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate;
 
+/**
+ * A HugeGraph-local predicate used by server-side traversal processing.
+ *
+ * This type relies on HugeGraph {@link Condition.RelationType} predicates and
+ * has no registered GraphSON or GraphBinary wire serializer. Remote clients
+ * should use supported TinkerPop predicates or server-side query APIs instead
+ * of sending {@code ConditionP} instances directly.
+ */
 public class ConditionP extends P {
 
     private static final long serialVersionUID = 9094970577400072902L;
 
-    private ConditionP(final BiPredicate predicate,
+    private ConditionP(final PBiPredicate predicate,
                        Object value) {
         super(predicate, value);
     }
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java
index 403bf5be83..f12a84e40e 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java
@@ -55,12 +55,18 @@ public boolean equals(Object obj) {
 
         HugeCountStep other = (HugeCountStep) obj;
         return Objects.equals(this.originGraphStep,
-                              other.originGraphStep) && this.done == other.done;
+                              other.originGraphStep);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(super.hashCode(), this.originGraphStep, this.done);
+        return Objects.hash(super.hashCode(), this.originGraphStep);
+    }
+
+    @Override
+    public void reset() {
+        super.reset();
+        this.done = false;
     }
 
     @Override
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStrategy.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStrategy.java
index ea6de76ac0..e8f9695c88 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStrategy.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStrategy.java
@@ -25,11 +25,11 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.function.BiPredicate;
 
 import org.apache.tinkerpop.gremlin.process.traversal.Compare;
 import org.apache.tinkerpop.gremlin.process.traversal.Contains;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate;
 import org.apache.tinkerpop.gremlin.process.traversal.Step;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
@@ -62,8 +62,8 @@ public final class HugeCountStrategy
         extends AbstractTraversalStrategy
         implements TraversalStrategy.OptimizationStrategy {
 
-    private static final Map RANGE_PREDICATES =
-            new HashMap() {{
+    private static final Map RANGE_PREDICATES =
+            new HashMap() {{
                 put(Contains.within, 1L);
                 put(Contains.without, 0L);
             }};
@@ -99,7 +99,7 @@ public void apply(final Traversal.Admin traversal) {
                            ((ConnectiveP) isStepPredicate).getPredicates() :
                            Collections.singletonList(isStepPredicate)) {
                     final Object value = p.getValue();
-                    final BiPredicate predicate = p.getBiPredicate();
+                    final PBiPredicate predicate = p.getBiPredicate();
                     if (value instanceof Number) {
                         final long highRangeOffset =
                                 INCREASED_OFFSET_SCALAR_PREDICATES.contains(predicate) ?
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
index e6a56027a1..a59be46a21 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
@@ -24,7 +24,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.function.BiPredicate;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -59,6 +58,7 @@
 import org.apache.tinkerpop.gremlin.process.traversal.Contains;
 import org.apache.tinkerpop.gremlin.process.traversal.Order;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate;
 import org.apache.tinkerpop.gremlin.process.traversal.Step;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
@@ -318,7 +318,7 @@ private static boolean collectPositiveLabelValues(
     private static void addPositiveLabelValues(HasContainer has,
                                                List labels) {
         P predicate = has.getPredicate();
-        BiPredicate bp = predicate.getBiPredicate();
+        PBiPredicate bp = predicate.getBiPredicate();
         if (bp == Compare.eq) {
             labels.add(predicate.getValue());
         } else {
@@ -385,12 +385,12 @@ private static boolean isLabelContainer(HasContainer has) {
     }
 
     static boolean isPositiveLabelContainer(HasContainer has) {
-        if (!isLabelContainer(has)) {
+        if (!isLabelContainer(has) || hasNullLabelValue(has)) {
             return false;
         }
 
         P predicate = has.getPredicate();
-        BiPredicate bp = predicate.getBiPredicate();
+        PBiPredicate bp = predicate.getBiPredicate();
         if (bp == Compare.eq) {
             return true;
         }
@@ -454,7 +454,7 @@ private static boolean hasMatchIndexSensitivePredicate(HasContainer has) {
         List> predicates = new ArrayList<>();
         collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
         for (P pred : predicates) {
-            BiPredicate bp = pred.getBiPredicate();
+            PBiPredicate bp = pred.getBiPredicate();
             if (bp == Compare.neq ||
                 bp == Compare.gt || bp == Compare.gte ||
                 bp == Compare.lt || bp == Compare.lte) {
@@ -541,6 +541,26 @@ private static boolean hasNullPredicate(HasContainer has) {
         return false;
     }
 
+    private static boolean hasNullLabelValue(HasContainer has) {
+        if (!isLabelContainer(has)) {
+            return false;
+        }
+
+        List> predicates = new ArrayList<>();
+        collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
+        for (P pred : predicates) {
+            Object value = pred.getValue();
+            if (value == null) {
+                return true;
+            }
+            if (value instanceof Collection &&
+                ((Collection) value).contains(null)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static boolean hasBooleanIndex(HugeGraph graph,
                                            SchemaLabel schemaLabel,
                                            PropertyKey pkey) {
@@ -591,7 +611,7 @@ private static boolean hasOnlyRangePredicates(HasContainer has) {
         List> predicates = new ArrayList<>();
         collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
         for (P pred : predicates) {
-            BiPredicate bp = pred.getBiPredicate();
+            PBiPredicate bp = pred.getBiPredicate();
             if (bp != Compare.gt && bp != Compare.gte &&
                 bp != Compare.lt && bp != Compare.lte) {
                 return false;
@@ -655,6 +675,9 @@ private static boolean canExtractHasContainers(HugeGraph graph,
 
     static boolean canExtractHasContainer(HugeGraph graph,
                                           HasContainer has) {
+        if (has.getKey() == null || hasNullLabelValue(has)) {
+            return false;
+        }
         if (isSysProp(has.getKey())) {
             return true;
         }
@@ -678,7 +701,7 @@ static boolean canExtractHasContainer(HugeGraph graph,
         List> predicates = new ArrayList<>();
         collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
         for (P pred : predicates) {
-            BiPredicate bp = pred.getBiPredicate();
+            PBiPredicate bp = pred.getBiPredicate();
             if (bp == Compare.gt || bp == Compare.gte ||
                 bp == Compare.lt || bp == Compare.lte) {
                 return false;
@@ -840,7 +863,7 @@ public static void fillConditionQuery(ConditionQuery query,
     public static Condition convHas2Condition(HasContainer has, HugeType type, HugeGraph graph) {
         P p = has.getPredicate();
         E.checkArgument(p != null, "The predicate of has(%s) is null", has);
-        BiPredicate bp = p.getBiPredicate();
+        PBiPredicate bp = p.getBiPredicate();
         Condition condition;
         if (keyForContainsKeyOrValue(has.getKey())) {
             condition = convContains2Relation(graph, has);
@@ -913,7 +936,7 @@ private static Condition convCompare2Relation(HugeGraph graph,
                                                   HugeType type,
                                                   HasContainer has) {
         assert type.isGraph();
-        BiPredicate bp = has.getPredicate().getBiPredicate();
+        PBiPredicate bp = has.getPredicate().getBiPredicate();
         assert bp instanceof Compare;
 
         return isSysProp(has.getKey()) ?
@@ -924,7 +947,7 @@ private static Condition convCompare2Relation(HugeGraph graph,
     private static Condition.Relation convCompare2SyspropRelation(HugeGraph graph,
                                                                   HugeType type,
                                                                   HasContainer has) {
-        BiPredicate bp = has.getPredicate().getBiPredicate();
+        PBiPredicate bp = has.getPredicate().getBiPredicate();
         assert bp instanceof Compare;
 
         HugeKeys key = token2HugeKey(has.getKey());
@@ -952,7 +975,7 @@ private static Condition.Relation convCompare2SyspropRelation(HugeGraph graph,
     private static Condition convCompare2UserpropRelation(HugeGraph graph,
                                                           HugeType type,
                                                           HasContainer has) {
-        BiPredicate bp = has.getPredicate().getBiPredicate();
+        PBiPredicate bp = has.getPredicate().getBiPredicate();
         assert bp instanceof Compare;
 
         String key = has.getKey();
@@ -1012,7 +1035,7 @@ private static Condition convRelationType2Relation(HugeGraph graph,
                                                        HugeType type,
                                                        HasContainer has) {
         assert type.isGraph();
-        BiPredicate bp = has.getPredicate().getBiPredicate();
+        PBiPredicate bp = has.getPredicate().getBiPredicate();
         assert bp instanceof Condition.RelationType;
 
         String key = has.getKey();
@@ -1025,7 +1048,7 @@ private static Condition convRelationType2Relation(HugeGraph graph,
     public static Condition convIn2Relation(HugeGraph graph,
                                             HugeType type,
                                             HasContainer has) {
-        BiPredicate bp = has.getPredicate().getBiPredicate();
+        PBiPredicate bp = has.getPredicate().getBiPredicate();
         assert bp instanceof Contains;
         Collection values = (Collection) has.getValue();
 
@@ -1068,7 +1091,7 @@ public static Condition convIn2Relation(HugeGraph graph,
     public static Condition convContains2Relation(HugeGraph graph,
                                                   HasContainer has) {
         // Convert contains-key or contains-value
-        BiPredicate bp = has.getPredicate().getBiPredicate();
+        PBiPredicate bp = has.getPredicate().getBiPredicate();
         E.checkArgument(bp == Compare.eq, "CONTAINS query with relation " +
                                           "'%s' is not supported", bp);
 
@@ -1097,6 +1120,9 @@ public static HugeKeys string2HugeKey(String key) {
     }
 
     public static HugeKeys token2HugeKey(String key) {
+        if (key == null) {
+            return null;
+        }
         if (key.equals(T.label.getAccessor())) {
             return HugeKeys.LABEL;
         } else if (key.equals(T.id.getAccessor())) {
@@ -1187,7 +1213,7 @@ public static void convHasStep(HugeGraph graph, HasStep step) {
     private static void convPredicateValue(HugeGraph graph,
                                            HasContainer has) {
         // No need to convert if key is sys-prop
-        if (isSysProp(has.getKey())) {
+        if (has.getKey() == null || isSysProp(has.getKey())) {
             return;
         }
         PropertyKey pkey = graph.propertyKey(has.getKey());
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/version/CoreVersion.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/version/CoreVersion.java
index 2eff71487a..195bf60376 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/version/CoreVersion.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/version/CoreVersion.java
@@ -31,7 +31,7 @@ public class CoreVersion {
     /**
      * Update it when the gremlin version changed, search "tinkerpop.version" in pom
      */
-    public static final String GREMLIN_VERSION = "3.5.1";
+    public static final String GREMLIN_VERSION = "3.7.6";
 
     static {
         // Check versions of the dependency packages
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-driver-settings.yaml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-driver-settings.yaml
index 55f38ab97d..e9a28f0aa9 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-driver-settings.yaml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-driver-settings.yaml
@@ -17,9 +17,15 @@
 hosts: [localhost]
 port: 8182
 serializer: {
-  className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0,
+  className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1,
   config: {
     serializeResultToString: false,
     ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
   }
 }
+# Typed GraphSON fallback for clients that require @type/@value for
+# File/Id/simple typed values:
+# use org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV1 in
+# place of GraphSONUntypedMessageSerializerV1, and keep HugeGraphIoRegistry.
+# Full HugeGraph internal object typed GraphSON for schema/Vertex/Edge/Path
+# remains follow-up scope; keep untyped serializers for normal remote objects.
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml
index 32135163fd..e61e02a469 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml
@@ -82,30 +82,53 @@ scriptEngines: {
   }
 }
 serializers:
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV2,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV3,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
+  # Keep untyped GraphSON before typed GraphSON so application/json stays
+  # mapped to the untyped V1 serializer while explicit typed MIME requests work.
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV1,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV2,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+# Typed GraphSON remains fallback scope for File/Id/simple typed values.
+# Full HugeGraph internal object typed GraphSON for schema/Vertex/Edge/Path
+# remains follow-up scope; keep untyped serializers for normal remote objects.
 metrics: {
   consoleReporter: {enabled: false, interval: 180000},
   csvReporter: {enabled: false, interval: 180000, fileName: ./metrics/gremlin-server-metrics.csv},
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml
index 39679d8c30..1ab52aa3c8 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml
@@ -17,7 +17,7 @@
 hosts: [localhost]
 port: 8182
 serializer: {
-  className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0,
+  className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1,
   config: {
     serializeResultToString: false,
     # The duplication of HugeGraphIoRegistry is meant to fix a bug in the
@@ -28,3 +28,9 @@ serializer: {
     ]
   }
 }
+# Typed GraphSON fallback for clients that require @type/@value for
+# File/Id/simple typed values:
+# use org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV1 in
+# place of GraphSONUntypedMessageSerializerV1, and keep HugeGraphIoRegistry.
+# Full HugeGraph internal object typed GraphSON for schema/Vertex/Edge/Path
+# remains follow-up scope; keep untyped serializers for normal remote objects.
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote.yaml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote.yaml
index 55f38ab97d..e9a28f0aa9 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote.yaml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote.yaml
@@ -17,9 +17,15 @@
 hosts: [localhost]
 port: 8182
 serializer: {
-  className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0,
+  className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1,
   config: {
     serializeResultToString: false,
     ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
   }
 }
+# Typed GraphSON fallback for clients that require @type/@value for
+# File/Id/simple typed values:
+# use org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV1 in
+# place of GraphSONUntypedMessageSerializerV1, and keep HugeGraphIoRegistry.
+# Full HugeGraph internal object typed GraphSON for schema/Vertex/Edge/Path
+# remains follow-up scope; keep untyped serializers for normal remote objects.
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/ci-service-utils.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/ci-service-utils.sh
new file mode 100644
index 0000000000..663feda30d
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/ci-service-utils.sh
@@ -0,0 +1,180 @@
+#!/bin/bash
+#
+# 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.
+#
+
+function dump_service_diagnostics() {
+    local service_dir="$1"
+    local service_name="$2"
+    local log_dir="${service_dir}/logs"
+
+    echo "::group::${service_name} diagnostics"
+    echo "[ci] service dir: ${service_dir}"
+    echo "[ci] java processes:"
+    ps -ef | grep -E "HugeGraph|hg-|java" | grep -v grep || true
+    echo "[ci] listening tcp ports:"
+    (ss -ltnp || netstat -ltnp || true) 2>&1
+
+    if [ -d "${log_dir}" ]; then
+        find "${log_dir}" -maxdepth 2 -type f | sort | while read -r log_file; do
+            echo "--- tail -n 200 ${log_file} ---"
+            tail -n 200 "${log_file}" || true
+        done
+    else
+        echo "[ci] log dir not found: ${log_dir}"
+    fi
+    echo "::endgroup::"
+}
+
+function wait_for_tcp_port() {
+    local service_name="$1"
+    local host="$2"
+    local port="$3"
+    local pid_file="$4"
+    local service_dir="$5"
+    local timeout_seconds="${6:-90}"
+
+    echo "[ci] waiting for ${service_name} at ${host}:${port}"
+    for second in $(seq 1 "${timeout_seconds}"); do
+        if bash -c "echo > /dev/tcp/${host}/${port}" >/dev/null 2>&1; then
+            echo "[ci] ${service_name} is listening on ${host}:${port}"
+            return 0
+        fi
+
+        if [ -f "${pid_file}" ]; then
+            local pid
+            pid="$(cat "${pid_file}")"
+            if [ -n "${pid}" ] && ! kill -0 "${pid}" >/dev/null 2>&1; then
+                echo "[ci] ${service_name} process ${pid} exited before readiness"
+                dump_service_diagnostics "${service_dir}" "${service_name}"
+                return 1
+            fi
+        fi
+
+        if [ "$((second % 10))" -eq 0 ]; then
+            echo "[ci] still waiting for ${service_name} (${second}s)"
+        fi
+        sleep 1
+    done
+
+    echo "[ci] timeout waiting for ${service_name} at ${host}:${port}"
+    dump_service_diagnostics "${service_dir}" "${service_name}"
+    return 1
+}
+
+function http_status_is_accepted() {
+    local status="$1"
+    local accepted_statuses="$2"
+
+    case ",${accepted_statuses}," in
+        *",${status},"*)
+            return 0
+            ;;
+        *)
+            return 1
+            ;;
+    esac
+}
+
+function wait_for_http_status() {
+    local service_name="$1"
+    local url="$2"
+    local pid_file="$3"
+    local service_dir="$4"
+    local timeout_seconds="${5:-90}"
+    local accepted_statuses="${6:-200}"
+    local connect_timeout_seconds=2
+    local max_request_seconds=5
+    local started_at="${SECONDS}"
+    local deadline=$((started_at + timeout_seconds))
+    local next_log_at=10
+
+    echo "[ci] waiting for ${service_name} HTTP readiness at ${url}"
+    echo "[ci] accepted HTTP statuses: ${accepted_statuses}"
+    while (( SECONDS < deadline )); do
+        local remaining=$((deadline - SECONDS))
+        local request_timeout="${max_request_seconds}"
+        if (( remaining < request_timeout )); then
+            request_timeout="${remaining}"
+        fi
+        if (( request_timeout < 1 )); then
+            break
+        fi
+
+        local status
+        status="$(curl -s -o /dev/null -w "%{http_code}" \
+                  --connect-timeout "${connect_timeout_seconds}" \
+                  --max-time "${request_timeout}" \
+                  "${url}" 2>/dev/null)" || status="000"
+        if http_status_is_accepted "${status}" "${accepted_statuses}"; then
+            echo "[ci] ${service_name} is HTTP ready at ${url}" \
+                 "(status ${status})"
+            return 0
+        fi
+
+        if [ -f "${pid_file}" ]; then
+            local pid
+            pid="$(cat "${pid_file}")"
+            if [ -n "${pid}" ] && ! kill -0 "${pid}" >/dev/null 2>&1; then
+                echo "[ci] ${service_name} process ${pid} exited before" \
+                     "HTTP readiness"
+                dump_service_diagnostics "${service_dir}" "${service_name}"
+                return 1
+            fi
+        fi
+
+        local elapsed=$((SECONDS - started_at))
+        if (( elapsed >= next_log_at )); then
+            echo "[ci] still waiting for ${service_name} HTTP readiness" \
+                 "(${elapsed}s, last status ${status})"
+            next_log_at=$((next_log_at + 10))
+        fi
+        if (( SECONDS >= deadline )); then
+            break
+        fi
+        sleep 1
+    done
+
+    echo "[ci] timeout waiting for ${service_name} HTTP readiness at ${url}"
+    dump_service_diagnostics "${service_dir}" "${service_name}"
+    return 1
+}
+
+if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+    command="$1"
+    shift || true
+    case "${command}" in
+        dump)
+            dump_service_diagnostics "$@"
+            exit $?
+            ;;
+        wait)
+            wait_for_tcp_port "$@"
+            exit $?
+            ;;
+        wait-http)
+            wait_for_http_status "$@"
+            exit $?
+            ;;
+        *)
+            echo "Usage: $0 dump SERVICE_DIR SERVICE_NAME"
+            echo "       $0 wait SERVICE_NAME HOST PORT PID_FILE SERVICE_DIR [TIMEOUT_SECONDS]"
+            echo "       $0 wait-http SERVICE_NAME URL PID_FILE SERVICE_DIR" \
+                 "[TIMEOUT_SECONDS] [ACCEPTED_STATUSES]"
+            exit 2
+            ;;
+    esac
+fi
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml
index 7e10eb52b0..7972e96bd6 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml
@@ -74,25 +74,43 @@ scriptEngines: {
   }
 }
 serializers:
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV2,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV3,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV1,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV2,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml
index 5f097f91bc..019f6a5ccd 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml
@@ -74,25 +74,43 @@ scriptEngines: {
   }
 }
 serializers:
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV2,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV3,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV1,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV2,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml
index 3f50c64778..96c37f94cd 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml
@@ -74,25 +74,43 @@ scriptEngines: {
   }
 }
 serializers:
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV2,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
-  - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0,
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV3,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV1,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV2,
+     config: {
+       serializeResultToString: false,
+       ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+     }
+  }
+  - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3,
      config: {
        serializeResultToString: false,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/start-pd.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/start-pd.sh
index 35e82ade40..5ebb6c53a5 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/start-pd.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/start-pd.sh
@@ -30,8 +30,12 @@ else
 fi
 
 PD_DIR=$HOME_DIR/hugegraph-pd/apache-hugegraph-pd-$VersionInBash
+TRAVIS_DIR=$(dirname "$0")
+
+source "$TRAVIS_DIR"/ci-service-utils.sh
 
 pushd $PD_DIR
 . bin/start-hugegraph-pd.sh
-sleep 10
+wait_for_http_status HugeGraphPD http://127.0.0.1:8620/v1/health \
+                     "$PD_DIR"/bin/pid "$PD_DIR" 90 200,401
 popd
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/start-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/start-store.sh
index 3e876ce9a0..cb54a03efe 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/start-store.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/start-store.sh
@@ -30,8 +30,12 @@ else
 fi
 
 STORE_DIR=$HOME_DIR/hugegraph-store/apache-hugegraph-store-$VersionInBash
+TRAVIS_DIR=$(dirname "$0")
+
+source "$TRAVIS_DIR"/ci-service-utils.sh
 
 pushd $STORE_DIR
 . bin/start-hugegraph-store.sh
-sleep 10
+wait_for_http_status HugeGraphStore http://127.0.0.1:8520/v1/health \
+                     "$STORE_DIR"/bin/pid "$STORE_DIR" 90 200,401
 popd
diff --git a/hugegraph-server/hugegraph-test/pom.xml b/hugegraph-server/hugegraph-test/pom.xml
index b3fc645f79..bef5496b5a 100644
--- a/hugegraph-server/hugegraph-test/pom.xml
+++ b/hugegraph-server/hugegraph-test/pom.xml
@@ -86,12 +86,6 @@
             gremlin-test
             ${tinkerpop.version}
         
-        
-            org.apache.tinkerpop
-            gremlin-groovy-test
-            3.2.11
-        
-
         
             org.glassfish.jersey.test-framework.providers
             jersey-test-framework-provider-grizzly2
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java
index 3c3e3049f3..3c111bae3a 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java
@@ -19,8 +19,11 @@
 
 import static org.apache.hugegraph.testutil.Assert.assertContains;
 
+import java.util.List;
 import java.util.Map;
 
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.util.JsonUtil;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -72,13 +75,153 @@ public void testRelationQuery() {
         this.testCypherQueryAndContains(cypher, "friend");
     }
 
-    private void testCypherQueryAndContains(String cypher, String containsText) {
+    @Test
+    public void testReturnNodeIdAsPrimitiveValue() {
+        String cypher = "MATCH (n:person) WHERE n.name = 'marko' " +
+                        "RETURN id(n) AS nodeId";
+
+        String content = this.testCypherQueryAndContains(cypher, "nodeId");
+        List data = assertCypherSuccessData(content);
+        Map row = assertSingleMapRow(data);
+        Object nodeId = row.get("nodeId");
+
+        Assert.assertNotNull(nodeId);
+        assertPrimitiveValue(nodeId);
+        assertNoHugeGraphIdLeak(content);
+    }
+
+    @Test
+    public void testReturnNodeDoesNotLeakInternalIdTypes() {
+        String cypher = "MATCH (n:person) WHERE n.name = 'marko' RETURN n";
+
+        String content = this.testCypherQueryAndContains(cypher, "marko");
+        assertNoHugeGraphIdLeak(content);
+    }
+
+    @Test
+    public void testReturnNestedIdDoesNotLeakInternalIdTypes() {
+        String cypher = "MATCH (n:person) WHERE n.name = 'marko' " +
+                        "RETURN {nodeId: id(n), values: [id(n), n.name]} " +
+                        "AS payload";
+
+        String content = this.testCypherQueryAndContains(cypher, "payload");
+        List data = assertCypherSuccessData(content);
+        Map row = assertSingleMapRow(data);
+        Map payload = assertMapValue(row, "payload");
+        List values = assertListValue(payload, "values");
+        Object nodeId = payload.get("nodeId");
+
+        Assert.assertNotNull(nodeId);
+        assertPrimitiveValue(nodeId);
+        Assert.assertEquals(2, values.size());
+        Assert.assertEquals(nodeId, values.get(0));
+        Assert.assertEquals("marko", values.get(1));
+        assertNoHugeGraphIdLeak(content);
+    }
+
+    @Test
+    public void testReturnRelationIdDoesNotLeakInternalIdTypes() {
+        String cypher = "MATCH (n:person)-[r:knows]->(friend:person) " +
+                        "WHERE n.name = 'marko' RETURN id(r) AS relationId";
+
+        String content = this.testCypherQueryAndContains(cypher, "relationId");
+        List data = assertCypherSuccessData(content);
+        Map row = assertSingleMapRow(data);
+        Object relationId = row.get("relationId");
+
+        Assert.assertNotNull(relationId);
+        assertPrimitiveValue(relationId);
+        assertNoHugeGraphIdLeak(content);
+    }
+
+    @Test
+    public void testReturnPathShape() {
+        String cypher = "MATCH p=(n:person)-[r:knows]->(friend:person) " +
+                        "WHERE n.name = 'marko' RETURN p AS path";
+
+        String content = this.testCypherQueryAndContains(cypher, "path");
+        List data = assertCypherSuccessData(content);
+        Map row = assertSingleMapRow(data);
+        List path = assertListValue(row, "path");
+
+        Assert.assertEquals(3, path.size());
+        Map source = assertMapValue(path, 0);
+        Map relation = assertMapValue(path, 1);
+        Map target = assertMapValue(path, 2);
+
+        Assert.assertEquals("node", source.get("_type"));
+        Assert.assertEquals("person", source.get("_label"));
+        Assert.assertEquals("marko", source.get("name"));
+        Assert.assertEquals("knows", relation.get("_label"));
+        Assert.assertEquals("node", target.get("_type"));
+        Assert.assertEquals("person", target.get("_label"));
+        Assert.assertEquals("peter", target.get("name"));
+        assertContains("marko", content);
+        assertContains("peter", content);
+        assertNoHugeGraphIdLeak(content);
+    }
+
+    private String testCypherQueryAndContains(String cypher,
+                                             String containsText) {
         Response r = client().post(PATH, cypher);
-        this.validStatusAndTextContains(containsText, r);
+        return this.validStatusAndTextContains(containsText, r);
     }
 
-    private void validStatusAndTextContains(String value, Response r) {
+    private String validStatusAndTextContains(String value, Response r) {
         String content = assertResponseStatus(200, r);
         assertContains(value, content);
+        return content;
+    }
+
+    private static void assertNoHugeGraphIdLeak(String content) {
+        Assert.assertFalse(content.contains("org.apache.hugegraph.backend.id"));
+        Assert.assertFalse(content.contains("StringId"));
+        Assert.assertFalse(content.contains("LongId"));
+        Assert.assertFalse(content.contains("UuidId"));
+        Assert.assertFalse(content.contains("EdgeId"));
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List assertCypherSuccessData(String content) {
+        Map response = JsonUtil.fromJson(content, Map.class);
+        Assert.assertTrue(response.containsKey("requestId"));
+
+        Map status = assertMapValue(response, "status");
+        Assert.assertEquals(200, ((Number) status.get("code")).intValue());
+        Assert.assertEquals("", status.get("message"));
+
+        Map result = assertMapValue(response, "result");
+        Assert.assertInstanceOf(List.class, result.get("data"));
+        Assert.assertInstanceOf(Map.class, result.get("meta"));
+        return (List) result.get("data");
+    }
+
+    private static Map assertSingleMapRow(List data) {
+        Assert.assertEquals(1, data.size());
+        Assert.assertInstanceOf(Map.class, data.get(0));
+        return (Map) data.get(0);
+    }
+
+    private static Map assertMapValue(Map map, String key) {
+        Assert.assertTrue(map.containsKey(key));
+        Assert.assertInstanceOf(Map.class, map.get(key));
+        return (Map) map.get(key);
+    }
+
+    private static Map assertMapValue(List list, int index) {
+        Assert.assertTrue(list.size() > index);
+        Assert.assertInstanceOf(Map.class, list.get(index));
+        return (Map) list.get(index);
+    }
+
+    private static List assertListValue(Map map, String key) {
+        Assert.assertTrue(map.containsKey(key));
+        Assert.assertInstanceOf(List.class, map.get(key));
+        return (List) map.get(key);
+    }
+
+    private static void assertPrimitiveValue(Object value) {
+        Assert.assertFalse(value instanceof Map);
+        Assert.assertFalse(value instanceof List);
     }
 }
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java
new file mode 100644
index 0000000000..d9d36638d5
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.hugegraph.api.cypher;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.MutablePath;
+import org.junit.Test;
+
+public class CypherClientTest extends BaseUnitTest {
+
+    @Test
+    public void testNormalizeHandlesNullMapAndArrayValues() {
+        Map value = new LinkedHashMap<>();
+        value.put(IdGenerator.of(1L),
+                  new Object[]{IdGenerator.of("marko"), null});
+
+        Object normalized = CypherClient.normalize(value);
+
+        Assert.assertInstanceOf(Map.class, normalized);
+        Map map = (Map) normalized;
+        Assert.assertTrue(map.containsKey(1L));
+        Assert.assertInstanceOf(List.class, map.get(1L));
+
+        List values = (List) map.get(1L);
+        Assert.assertEquals("marko", values.get(0));
+        Assert.assertNull(values.get(1));
+    }
+
+    @Test
+    public void testNormalizeHandlesCyclicReferences() {
+        Map value = new LinkedHashMap<>();
+        value.put("private-value", value);
+
+        Assert.assertThrows(IllegalArgumentException.class,
+                            () -> CypherClient.normalize(value), e -> {
+            Assert.assertContains("cyclic Cypher result", e.getMessage());
+            Assert.assertFalse(e.getMessage().contains("private-value"));
+        });
+    }
+
+    @Test
+    public void testNormalizePreservesThirtyTwoContainerLayers() {
+        Object value = "leaf";
+        for (int i = 0; i < 32; i++) {
+            value = new Object[]{value};
+        }
+
+        Object normalized = CypherClient.normalize(value);
+        Object current = normalized;
+        for (int i = 0; i < 32; i++) {
+            Assert.assertInstanceOf(List.class, current);
+            List list = (List) current;
+            Assert.assertEquals(1, list.size());
+            current = list.get(0);
+        }
+
+        Assert.assertEquals("leaf", current);
+    }
+
+    @Test
+    public void testNormalizeRejectsThirtyThirdContainerLayer() {
+        Object value = "leaf";
+        for (int i = 0; i < 33; i++) {
+            value = new Object[]{value};
+        }
+        Object deeplyNestedValue = value;
+
+        Assert.assertThrows(IllegalArgumentException.class,
+                            () -> CypherClient.normalize(deeplyNestedValue),
+                            e -> Assert.assertContains(
+                                    "max normalization depth 32",
+                                    e.getMessage()));
+    }
+
+    @Test
+    public void testNormalizePreservesPathLabelsAndObjects() {
+        Path path = MutablePath.make()
+                               .extend(IdGenerator.of("marko"),
+                                       Set.of("a"))
+                               .extend(IdGenerator.of("lop"),
+                                       Set.of("b", "software"));
+
+        Object normalized = CypherClient.normalize(path);
+
+        Assert.assertInstanceOf(Map.class, normalized);
+        Map map = (Map) normalized;
+        Assert.assertTrue(map.containsKey("labels"));
+        Assert.assertTrue(map.containsKey("objects"));
+
+        Assert.assertInstanceOf(List.class, map.get("labels"));
+        Assert.assertInstanceOf(List.class, map.get("objects"));
+
+        List labels = (List) map.get("labels");
+        List objects = (List) map.get("objects");
+        Assert.assertEquals(2, labels.size());
+        Assert.assertEquals(2, objects.size());
+
+        Assert.assertEquals("marko", objects.get(0));
+        Assert.assertEquals("lop", objects.get(1));
+        List firstLabels = (List) labels.get(0);
+        List secondLabels = (List) labels.get(1);
+        Assert.assertTrue(firstLabels.contains("a"));
+        Assert.assertTrue(secondLabels.contains("b"));
+        Assert.assertTrue(secondLabels.contains("software"));
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
index 660c2e040c..7e8bbdfb2d 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
@@ -17,9 +17,13 @@
 
 package org.apache.hugegraph.core;
 
+import java.util.HashSet;
+import java.util.Set;
+
 import org.apache.hugegraph.exception.NoIndexException;
 import org.apache.hugegraph.schema.SchemaManager;
 import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.traversal.optimize.HugeCountStep;
 import org.apache.hugegraph.traversal.optimize.HugeGraphStep;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.Step;
@@ -209,6 +213,47 @@ public void testWhereCountGteNegativeDoesNotBuildInvalidRange() {
         Assert.assertEquals(4L, count);
     }
 
+    @Test
+    public void testOptimizedGraphCountCanBeResetAndReused() {
+        this.initSchema();
+        this.initGraph();
+
+        GraphTraversal traversal = graph().traversal().V().count();
+
+        Assert.assertEquals(3L, traversal.next());
+
+        traversal.asAdmin().reset();
+
+        Assert.assertEquals(3L, traversal.next());
+    }
+
+    @Test
+    public void testOptimizedGraphCountEqualityIgnoresExecutionState() {
+        this.initSchema();
+        this.initGraph();
+
+        GraphTraversal first = graph().traversal().V().count();
+        GraphTraversal second = graph().traversal().V().count();
+        first.asAdmin().applyStrategies();
+        second.asAdmin().applyStrategies();
+
+        Step firstStep = first.asAdmin().getEndStep();
+        Step secondStep = second.asAdmin().getEndStep();
+        Assert.assertInstanceOf(HugeCountStep.class, firstStep);
+        Assert.assertInstanceOf(HugeCountStep.class, secondStep);
+        Assert.assertEquals(firstStep, secondStep);
+
+        int hashCode = firstStep.hashCode();
+        Set> steps = new HashSet<>();
+        steps.add(firstStep);
+
+        Assert.assertEquals(3L, first.next());
+
+        Assert.assertEquals(hashCode, firstStep.hashCode());
+        Assert.assertEquals(firstStep, secondStep);
+        Assert.assertTrue(steps.contains(firstStep));
+    }
+
     @Test
     public void testRepeatAfterTextRangeFilterWithEmptyResult() {
         this.initTextRangeSchema(true);
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java
index 4b7ec65bc3..05bc8f847c 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java
@@ -3270,6 +3270,24 @@ public void testQueryByLabel() {
                                            SplicingIdGenerator.splicing(bookId, "java-5")));
     }
 
+    @Test
+    public void testQueryByNullKeyAndLabel() {
+        HugeGraph graph = graph();
+        init10Vertices();
+
+        Assert.assertFalse(graph.traversal().V()
+                                .has((String) null, "test-null-key")
+                                .hasNext());
+        Assert.assertFalse(graph.traversal().V()
+                                .hasLabel((String) null)
+                                .hasNext());
+
+        List vertices = graph.traversal().V()
+                                     .hasLabel(null, "book")
+                                     .toList();
+        Assert.assertEquals(5, vertices.size());
+    }
+
     @Test
     public void testQueryByLabelWithLimit() {
         HugeGraph graph = graph();
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/ProcessBasicSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/ProcessBasicSuite.java
index e0fcba9832..d899c86f8f 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/ProcessBasicSuite.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/ProcessBasicSuite.java
@@ -26,6 +26,8 @@
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalEngine;
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalInterruptionTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.ComplexTest;
+import org.apache.tinkerpop.gremlin.process.traversal.step.OrderabilityTest;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TernaryBooleanLogicsTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.branch.BranchTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.branch.ChooseTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.branch.LocalTest;
@@ -51,6 +53,7 @@
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.CoalesceTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.ConstantTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountTest;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.ElementMapTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.FlatMapTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.FoldTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphTest;
@@ -61,6 +64,8 @@
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.MathTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.MaxTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.MeanTest;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeVertexTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.MinTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderTest;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.PathTest;
@@ -87,6 +92,7 @@
 import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest;
 import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest;
 import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest;
+import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SeedStrategyProcessTest;
 import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategyProcessTest;
 import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.TranslationStrategyProcessTest;
 import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.EarlyLimitStrategyProcessTest;
@@ -105,7 +111,9 @@ public class ProcessBasicSuite extends AbstractGremlinSuite {
 
     /**
      * This list of tests in the suite that will be executed
-     * as part of this suite.
+     * as part of this suite. It is synchronized with TinkerPop 3.7.6's
+     * official ProcessStandardSuite; HugeGraphWriteTest
+     * intentionally replaces WriteTest.Traversals.
      */
     private static final Class[] ALL_TESTS = new Class[]{
             // branch
@@ -138,6 +146,7 @@ public class ProcessBasicSuite extends AbstractGremlinSuite {
             CoalesceTest.Traversals.class,
             ConstantTest.Traversals.class,
             CountTest.Traversals.class,
+            ElementMapTest.Traversals.class,
             FlatMapTest.Traversals.class,
             FoldTest.Traversals.class,
             GraphTest.Traversals.class,
@@ -149,6 +158,8 @@ public class ProcessBasicSuite extends AbstractGremlinSuite {
             MathTest.Traversals.class,
             MaxTest.Traversals.class,
             MeanTest.Traversals.class,
+            MergeEdgeTest.Traversals.class,
+            MergeVertexTest.Traversals.class,
             MinTest.Traversals.class,
             SumTest.Traversals.class,
             OrderTest.Traversals.class,
@@ -161,7 +172,7 @@ public class ProcessBasicSuite extends AbstractGremlinSuite {
             VertexTest.Traversals.class,
             UnfoldTest.Traversals.class,
             ValueMapTest.Traversals.class,
-            // Override WriteTest.Traversals.class
+            // Intentionally replace WriteTest.Traversals.class
             HugeGraphWriteTest.class,
 
             // sideEffect
@@ -190,11 +201,16 @@ public class ProcessBasicSuite extends AbstractGremlinSuite {
             EventStrategyProcessTest.class,
             ReadOnlyStrategyProcessTest.class,
             PartitionStrategyProcessTest.class,
+            SeedStrategyProcessTest.class,
             SubgraphStrategyProcessTest.class,
 
             // optimizations
             IncidentToAdjacentStrategyProcessTest.class,
-            EarlyLimitStrategyProcessTest.class
+            EarlyLimitStrategyProcessTest.class,
+
+            // semantics
+            OrderabilityTest.Traversals.class,
+            TernaryBooleanLogicsTest.class
     };
 
     /**
@@ -232,6 +248,7 @@ public class ProcessBasicSuite extends AbstractGremlinSuite {
             CoalesceTest.class,
             ConstantTest.class,
             CountTest.class,
+            ElementMapTest.class,
             FlatMapTest.class,
             FoldTest.class,
             LoopsTest.class,
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/StructureBasicSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/StructureBasicSuite.java
index 593e89359e..ea5df1ab96 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/StructureBasicSuite.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/StructureBasicSuite.java
@@ -30,6 +30,7 @@
 import org.apache.tinkerpop.gremlin.structure.GraphTest;
 import org.apache.tinkerpop.gremlin.structure.PropertyTest;
 import org.apache.tinkerpop.gremlin.structure.SerializationTest;
+import org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest;
 import org.apache.tinkerpop.gremlin.structure.TransactionTest;
 import org.apache.tinkerpop.gremlin.structure.VariablesTest;
 import org.apache.tinkerpop.gremlin.structure.VertexPropertyTest;
@@ -64,7 +65,8 @@ public class StructureBasicSuite extends AbstractGremlinSuite {
     /**
      * This list of tests in the suite that will be executed.
      * Gremlin developers should add to this list
-     * as needed to enforce tests upon implementations.
+     * as needed to enforce tests upon implementations. This list is synchronized
+     * with TinkerPop 3.7.6's official StructureStandardSuite.
      */
     private static final Class[] ALL_TESTS = new Class[]{
             CommunityGeneratorTest.class,
@@ -94,6 +96,7 @@ public class StructureBasicSuite extends AbstractGremlinSuite {
             SerializationTest.class,
             StarGraphTest.class,
             TransactionTest.class,
+            TransactionMultiThreadedTest.class,
             VertexTest.class
     };
 
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java
index 9ef6d9affd..793b823c6b 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java
@@ -430,6 +430,7 @@ public void initModernSchema(IdStrategy idStrategy) {
         SchemaManager schema = this.graph.schema();
 
         schema.propertyKey("weight").asDouble().ifNotExist().create();
+        schema.propertyKey("a").asInt().ifNotExist().create();
         schema.propertyKey("name").ifNotExist().create();
         schema.propertyKey("lang").ifNotExist().create();
         schema.propertyKey("age").asInt().ifNotExist().create();
@@ -499,12 +500,12 @@ public void initModernSchema(IdStrategy idStrategy) {
         }
 
         schema.edgeLabel("knows").link("person", "person")
-              .properties("weight", "year")
-              .nullableKeys("weight", "year")
+              .properties("weight", "year", "a")
+              .nullableKeys("weight", "year", "a")
               .ifNotExist().create();
         schema.edgeLabel("created").link("person", "software")
-              .properties("weight")
-              .nullableKeys("weight")
+              .properties("weight", "a")
+              .nullableKeys("weight", "a")
               .ifNotExist().create();
         schema.edgeLabel("codeveloper").link("person", "person")
               .properties("year")
@@ -591,9 +592,15 @@ public void initClassicSchema(IdStrategy idStrategy) {
 
     @Watched
     public void initBasicSchema(IdStrategy idStrategy, String defaultVL) {
+        this.initBasicSchema(idStrategy, defaultVL, defaultVL);
+    }
+
+    @Watched
+    public void initBasicSchema(IdStrategy idStrategy, String defaultVL,
+                                String selfVL) {
         this.initBasicPropertyKey();
         this.initBasicVertexLabelV(idStrategy, defaultVL);
-        this.initBasicVertexLabelAndEdgeLabelExceptV(defaultVL);
+        this.initBasicVertexLabelAndEdgeLabelExceptV(defaultVL, selfVL);
     }
 
     @Watched
@@ -603,7 +610,9 @@ private void initBasicPropertyKey() {
         schema.propertyKey("__id").ifNotExist().create();
         schema.propertyKey("oid").asInt().ifNotExist().create();
         schema.propertyKey("communityIndex").asInt().ifNotExist().create();
-        schema.propertyKey("test").ifNotExist().create();
+        if (!this.graph.existsPropertyKey("test")) {
+            schema.propertyKey("test").ifNotExist().create();
+        }
         schema.propertyKey("testing").ifNotExist().create();
         schema.propertyKey("data").ifNotExist().create();
         schema.propertyKey("name").ifNotExist().create();
@@ -748,7 +757,8 @@ private void initBasicVertexLabelV(IdStrategy idStrategy, String defaultVL) {
     }
 
     @Watched
-    private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL) {
+    private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL,
+                                                         String selfVL) {
         SchemaManager schema = this.graph.schema();
 
         if (!"person".equals(defaultVL)) {
@@ -770,7 +780,7 @@ private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL) {
               .nullableKeys("test")
               .ifNotExist().create();
 
-        schema.edgeLabel("self").link(defaultVL, defaultVL)
+        schema.edgeLabel("self").link(selfVL, selfVL)
               .properties("__id", "test", "name", "some", "acl", "weight",
                           "here", "to-change", "dropped", "not-dropped", "new",
                           "to-drop", "short", "long")
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java
index c257e8bd1c..a8b07817a1 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java
@@ -47,11 +47,13 @@
 import org.apache.tinkerpop.gremlin.FeatureRequirements;
 import org.apache.tinkerpop.gremlin.LoadGraphWith;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest;
 import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.LazyBarrierStrategy;
 import org.apache.tinkerpop.gremlin.structure.Element;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.Graph.Features.VertexPropertyFeatures;
 import org.apache.tinkerpop.gremlin.structure.Transaction;
+import org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest;
 import org.junit.Assert;
 import org.junit.Assume;
 import org.slf4j.Logger;
@@ -85,10 +87,19 @@ public class TestGraphProvider extends AbstractGraphProvider {
     private static final String GREMLIN_GRAPH_KEY = "gremlin.graph";
     private static final String GREMLIN_GRAPH_VALUE =
             "org.apache.hugegraph.tinkerpop.TestGraphFactory";
+    private static final String BACKEND = "backend";
+    private static final String BACKEND_ROCKSDB = "rocksdb";
+    private static final String ROCKSDB_DATA_PATH = "rocksdb.data_path";
+    private static final String ROCKSDB_WAL_PATH = "rocksdb.wal_path";
+    private static final String ROCKSDB_DATA_DISKS = "rocksdb.data_disks";
+    private static final String TEST_PATH_SEPARATOR = "/";
+    private static final int MAX_ROCKSDB_PATH_SUFFIX_PREFIX_LENGTH = 80;
 
     private static final String AKEY_CLASS_PREFIX =
             "org.apache.tinkerpop.gremlin.structure." +
             "PropertyTest.PropertyFeatureSupportTest";
+    private static final String SUPPORTS_PREFIX = "supports";
+    private static final String FEATURE_VALUES_SUFFIX = "Values";
     private static final String IO_CLASS_PREFIX =
             "org.apache.tinkerpop.gremlin.structure.io.IoGraphTest";
     private static final String IO_TEST_PREFIX =
@@ -182,8 +193,17 @@ public Map getBaseConfiguration(
             confMap.put(key, config.getProperty(key));
         }
         String storePrefix = config.getString(CoreOptions.STORE.name());
-        confMap.put(CoreOptions.STORE.name(),
-                    storePrefix + "_" + this.suite + "_" + graphName);
+        String store = storePrefix + "_" + this.suite + "_" + graphName;
+        if (isTransactionMultiThreadedPropertyTest(testClass, testMethod)) {
+            store += "_txprop";
+        } else if (isMergeEdgeSelfTest(testClass, testMethod)) {
+            store += "_meself";
+        }
+        confMap.put(CoreOptions.STORE.name(), store);
+        if (isRocksDBBackend(config)) {
+            this.isolateRocksDBPaths(confMap, graphName, testClass,
+                                     testMethod);
+        }
         confMap.put(GREMLIN_GRAPH_KEY, GREMLIN_GRAPH_VALUE);
         confMap.put(TEST_CLASS, testClass);
         confMap.put(TEST_METHOD, testMethod);
@@ -193,6 +213,90 @@ public Map getBaseConfiguration(
         return confMap;
     }
 
+    private void isolateRocksDBPaths(Map confMap,
+                                     String graphName, Class testClass,
+                                     String testMethod) {
+        String testClassName = testClass.getName();
+        String rawSuffix = this.suite + "_" + graphName + "_" +
+                           testClassName + "_" + testMethod;
+        String prefix = sanitizePathPart(this.suite + "_" + graphName + "_" +
+                                         testClass.getSimpleName() + "_" +
+                                         testMethod);
+        if (prefix.length() > MAX_ROCKSDB_PATH_SUFFIX_PREFIX_LENGTH) {
+            prefix = prefix.substring(0,
+                                      MAX_ROCKSDB_PATH_SUFFIX_PREFIX_LENGTH);
+        }
+        String pathSuffix = prefix + "_" + shortHash(rawSuffix);
+        isolatePath(confMap, ROCKSDB_DATA_PATH, pathSuffix);
+        isolatePath(confMap, ROCKSDB_WAL_PATH, pathSuffix);
+
+        Object dataDisks = confMap.get(ROCKSDB_DATA_DISKS);
+        if (dataDisks != null) {
+            confMap.put(ROCKSDB_DATA_DISKS,
+                        isolateDataDisks(dataDisks, pathSuffix));
+        }
+    }
+
+    private static void isolatePath(Map confMap, String key,
+                                    String pathSuffix) {
+        Object path = confMap.get(key);
+        if (path == null) {
+            return;
+        }
+        confMap.put(key, appendPath(path.toString(), pathSuffix));
+    }
+
+    private static String isolateDataDisks(Object dataDisks,
+                                           String pathSuffix) {
+        String value = dataDisks.toString().trim();
+        if (value.isEmpty()) {
+            return value;
+        }
+
+        boolean wrapped = value.startsWith("[") && value.endsWith("]");
+        String body = wrapped ? value.substring(1, value.length() - 1) : value;
+        String[] entries = body.split(",");
+        StringBuilder builder = new StringBuilder();
+        for (String entry : entries) {
+            String item = entry.trim();
+            int index = item.indexOf(':');
+            if (index < 0) {
+                return value;
+            }
+            String table = item.substring(0, index).trim();
+            String path = item.substring(index + 1).trim();
+            if (table.isEmpty() || path.isEmpty()) {
+                return value;
+            }
+            if (builder.length() > 0) {
+                builder.append(',');
+            }
+            builder.append(table).append(':')
+                   .append(appendPath(path, pathSuffix));
+        }
+        return wrapped ? "[" + builder + "]" : builder.toString();
+    }
+
+    private static String appendPath(String path, String suffix) {
+        if (path.endsWith("/") || path.endsWith("\\")) {
+            return path + suffix;
+        }
+        return path + TEST_PATH_SEPARATOR + suffix;
+    }
+
+    private static String sanitizePathPart(String value) {
+        return value.replaceAll("[^A-Za-z0-9._-]", "_");
+    }
+
+    private static String shortHash(String value) {
+        return Integer.toHexString(value.hashCode());
+    }
+
+    private static boolean isRocksDBBackend(Configuration config) {
+        return config != null &&
+               BACKEND_ROCKSDB.equals(config.getString(BACKEND, ""));
+    }
+
     private static boolean customizedId(Class test, String testMethod) {
         Method method;
         try {
@@ -215,10 +319,41 @@ private static boolean customizedId(Class test, String testMethod) {
         return false;
     }
 
+    private static boolean isTransactionMultiThreadedPropertyTest(
+            Class testClass, String testMethod) {
+        return testClass == TransactionMultiThreadedTest.class &&
+               testMethod.equals("shouldChangeVertexProperty");
+    }
+
+    private static boolean isMergeEdgeSelfTest(Class testClass,
+                                               String testMethod) {
+        return testClass == MergeEdgeTest.Traversals.class &&
+               testMethod.equals("g_V_mergeEXlabel_self_weight_05X");
+    }
+
     private static String getAKeyType(Class clazz, String method) {
         if (clazz.getCanonicalName().startsWith(AKEY_CLASS_PREFIX)) {
-            return method.substring(method.indexOf('[') + 9,
-                                    method.indexOf('(') - 6);
+            String feature = method;
+            int featureStart = method.indexOf('[');
+            int featureEnd = method.indexOf(']');
+            if (featureStart >= 0 && featureEnd > featureStart) {
+                feature = method.substring(featureStart + 1, featureEnd);
+            }
+
+            if (!feature.startsWith(SUPPORTS_PREFIX)) {
+                return null;
+            }
+            feature = feature.substring(SUPPORTS_PREFIX.length());
+
+            int valueStart = feature.indexOf('(');
+            if (valueStart >= 0) {
+                feature = feature.substring(0, valueStart);
+            }
+            if (!feature.endsWith(FEATURE_VALUES_SUFFIX)) {
+                return null;
+            }
+            return feature.substring(0, feature.length() -
+                                        FEATURE_VALUES_SUFFIX.length());
         }
         return null;
     }
@@ -292,8 +427,15 @@ public Graph openTestGraph(final Configuration config) {
             testGraph.initPropertyKey("long", "Long");
         }
 
+        if (isTransactionMultiThreadedPropertyTest(testClass, testMethod)) {
+            testGraph.initPropertyKey("test", "Integer");
+        }
+
         // Basic schema is initiated by default once a graph is open
-        testGraph.initBasicSchema(idStrategy(config), TestGraph.DEFAULT_VL);
+        String selfVL = isMergeEdgeSelfTest(testClass, testMethod) ?
+                        "person" : TestGraph.DEFAULT_VL;
+        testGraph.initBasicSchema(idStrategy(config), TestGraph.DEFAULT_VL,
+                                  selfVL);
         if (testClass.getName().equals(
                 "org.apache.tinkerpop.gremlin.process.traversal.step.map.ReadTest$Traversals")) {
             testGraph.initEdgeLabelPersonKnowsPerson();
@@ -330,6 +472,10 @@ public void clear(Graph graph, Configuration config) throws Exception {
         String graphName = config.getString(CoreOptions.STORE.name());
         if (!testGraph.initedBackend()) {
             testGraph.close();
+            if (this.graphs.get(graphName) == testGraph) {
+                this.graphs.remove(graphName);
+            }
+            return;
         }
         if (testGraph.closed()) {
             if (this.graphs.get(graphName) == testGraph) {
@@ -349,6 +495,13 @@ public void clear(Graph graph, Configuration config) throws Exception {
         Class testClass = (Class) config.getProperty(TEST_CLASS);
         testGraph.clearAll(testClass.getCanonicalName());
 
+        if (isRocksDBBackend(config)) {
+            testGraph.close();
+            if (this.graphs.get(graphName) == testGraph) {
+                this.graphs.remove(graphName);
+            }
+        }
+
         LOG.debug("Clear graph '{}'", graphName);
     }
 
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
index c79db5056f..affe484c0a 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
@@ -57,6 +57,40 @@ public void testCanExtractHasContainerWithoutGraph() {
                 null, new HasContainer("~id", P.eq("1"))));
         Assert.assertFalse(TraversalUtil.canExtractHasContainer(
                 null, new HasContainer("name", P.eq("marko"))));
+        Assert.assertFalse(TraversalUtil.canExtractHasContainer(
+                null, new HasContainer(null, P.eq("marko"))));
+        Assert.assertFalse(TraversalUtil.canExtractHasContainer(
+                null, new HasContainer(T.label.getAccessor(), P.eq(null))));
+        Assert.assertFalse(TraversalUtil.canExtractHasContainer(
+                null, new HasContainer(T.label.getAccessor(),
+                                       P.within(null, "person"))));
+    }
+
+    @Test
+    public void testExtractHasContainerKeepsNullKeyLocal() {
+        Traversal.Admin traversal = __.V()
+                                           .has((String) null,
+                                                "test-null-key")
+                                           .asAdmin();
+        HugeGraphStep newStep = replaceGraphStep(traversal);
+
+        TraversalUtil.extractHasContainer(newStep, traversal);
+
+        Assert.assertTrue(newStep.getHasContainers().isEmpty());
+        Assert.assertTrue(hasStepExists(traversal));
+    }
+
+    @Test
+    public void testExtractHasContainerKeepsMixedNullLabelLocal() {
+        Traversal.Admin traversal = __.V()
+                                           .hasLabel(null, "person")
+                                           .asAdmin();
+        HugeGraphStep newStep = replaceGraphStep(traversal);
+
+        TraversalUtil.extractHasContainer(newStep, traversal);
+
+        Assert.assertTrue(newStep.getHasContainers().isEmpty());
+        Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor()));
     }
 
     @Test
@@ -314,6 +348,11 @@ public void testIsPositiveLabelContainer() {
         Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(
                 new HasContainer(T.label.getAccessor(),
                                  P.within(Collections.emptyList()))));
+        Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(
+                new HasContainer(T.label.getAccessor(), P.eq(null))));
+        Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(
+                new HasContainer(T.label.getAccessor(),
+                                 P.within(null, "person"))));
     }
 
     @Test
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
index 433e75a812..b42e1aec0e 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
@@ -17,6 +17,7 @@
 
 package org.apache.hugegraph.unit;
 
+import org.apache.hugegraph.api.cypher.CypherClientTest;
 import org.apache.hugegraph.core.RoleElectionStateMachineTest;
 import org.apache.hugegraph.meta.EtcdMetaDriverTest;
 import org.apache.hugegraph.meta.MetaManagerSchemaCacheClearEventTest;
@@ -32,6 +33,7 @@
 import org.apache.hugegraph.unit.cache.CachedSchemaTransactionTest;
 import org.apache.hugegraph.unit.cache.RamTableTest;
 import org.apache.hugegraph.unit.cassandra.CassandraTest;
+import org.apache.hugegraph.unit.config.GremlinConfigCompatibilityTest;
 import org.apache.hugegraph.unit.core.AnalyzerTest;
 import org.apache.hugegraph.unit.core.BackendMutationTest;
 import org.apache.hugegraph.unit.core.BackendStoreInfoTest;
@@ -40,6 +42,8 @@
 import org.apache.hugegraph.unit.core.DataTypeTest;
 import org.apache.hugegraph.unit.core.DirectionsTest;
 import org.apache.hugegraph.unit.core.ExceptionTest;
+import org.apache.hugegraph.unit.core.GroovyScriptEngineCompatibilityTest;
+import org.apache.hugegraph.unit.core.HugeFeaturesTest;
 import org.apache.hugegraph.unit.core.LocksTableTest;
 import org.apache.hugegraph.unit.core.PageStateTest;
 import org.apache.hugegraph.unit.core.QueryTest;
@@ -67,6 +71,7 @@
 import org.apache.hugegraph.unit.serializer.BinaryScatterSerializerTest;
 import org.apache.hugegraph.unit.serializer.BinarySerializerTest;
 import org.apache.hugegraph.unit.serializer.BytesBufferTest;
+import org.apache.hugegraph.unit.serializer.HugeGraphSONModuleTest;
 import org.apache.hugegraph.unit.serializer.SerializerFactoryTest;
 import org.apache.hugegraph.unit.serializer.StoreSerializerTest;
 import org.apache.hugegraph.unit.serializer.TableBackendEntryTest;
@@ -95,6 +100,7 @@
 
         /* api gremlin */
         GremlinQueryAPITest.class,
+        CypherClientTest.class,
 
         /* api space */
         GraphSpaceAPITest.class,
@@ -127,6 +133,8 @@
         AnalyzerTest.class,
         BackendMutationTest.class,
         ConditionTest.class,
+        GroovyScriptEngineCompatibilityTest.class,
+        HugeFeaturesTest.class,
         ConditionQueryFlattenTest.class,
         QueryTest.class,
         RangeTest.class,
@@ -152,9 +160,13 @@
         BinaryBackendEntryTest.class,
         BinarySerializerTest.class,
         BinaryScatterSerializerTest.class,
+        HugeGraphSONModuleTest.class,
         StoreSerializerTest.class,
         TextSerializerTest.class,
 
+        /* config */
+        GremlinConfigCompatibilityTest.class,
+
         /* cassandra */
         CassandraTest.class,
 
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java
new file mode 100644
index 0000000000..292a5cd922
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java
@@ -0,0 +1,582 @@
+/*
+ * 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.hugegraph.unit.config;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.hugegraph.backend.id.EdgeId;
+import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.MutablePath;
+import org.apache.tinkerpop.gremlin.server.Settings;
+import org.apache.tinkerpop.gremlin.util.MessageSerializer;
+import org.apache.tinkerpop.gremlin.util.message.ResponseMessage;
+import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode;
+import org.apache.tinkerpop.gremlin.util.ser.MessageTextSerializer;
+import org.junit.Test;
+import org.yaml.snakeyaml.Yaml;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+
+public class GremlinConfigCompatibilityTest extends BaseUnitTest {
+
+    private static final Pattern CLASS_NAME =
+            Pattern.compile("className:\\s*([^,}\\s]+)");
+    private static final String SERIALIZER_PACKAGE =
+            "org.apache.tinkerpop.gremlin.util.ser.";
+    private static final String GRAPHSON_UNTYPED_V1 =
+            SERIALIZER_PACKAGE + "GraphSONUntypedMessageSerializerV1";
+    private static final String IO_REGISTRY =
+            "org.apache.hugegraph.io.HugeGraphIoRegistry";
+    private static final String GREMLIN_SERVER_CONFIG = "gremlin-server.yaml";
+    private static final String REMOTE_OBJECTS_CONFIG = "remote-objects.yaml";
+    private static final List GREMLIN_SERVER_CONFIG_VARIANTS =
+            Arrays.asList(
+                    "static/conf/gremlin-server.yaml",
+                    "travis/conf-raft1/gremlin-server.yaml",
+                    "travis/conf-raft2/gremlin-server.yaml",
+                    "travis/conf-raft3/gremlin-server.yaml"
+            );
+    private static final List REMOTE_CONFIGS = Arrays.asList(
+            "gremlin-driver-settings.yaml",
+            "remote.yaml",
+            REMOTE_OBJECTS_CONFIG
+    );
+    private static final List TYPED_FALLBACK_SERIALIZERS =
+            Arrays.asList(
+                    SERIALIZER_PACKAGE + "GraphSONMessageSerializerV1",
+                    SERIALIZER_PACKAGE + "GraphSONMessageSerializerV2",
+                    SERIALIZER_PACKAGE + "GraphSONMessageSerializerV3"
+            );
+    private static final List TYPED_GRAPHSON_MIME_TYPES =
+            Arrays.asList(
+                    "application/vnd.gremlin-v1.0+json",
+                    "application/vnd.gremlin-v2.0+json",
+                    "application/vnd.gremlin-v3.0+json"
+            );
+    private static final List UNTYPED_GRAPHSON_MIME_TYPES =
+            Arrays.asList(
+                    "application/vnd.gremlin-v1.0+json;types=false",
+                    "application/vnd.gremlin-v2.0+json;types=false",
+                    "application/vnd.gremlin-v3.0+json;types=false"
+            );
+
+    @Test
+    public void testGremlinServerSerializersUseTinkerPopUtilPackage() throws IOException {
+        String content = readConfig(GREMLIN_SERVER_CONFIG);
+
+        assertUsesHugeGraphIoRegistry(GREMLIN_SERVER_CONFIG, content);
+        assertSerializerClassNamesUseUtilPackage(GREMLIN_SERVER_CONFIG,
+                                                 content);
+    }
+
+    @Test
+    public void testRemoteSerializersUseTinkerPopUtilPackage() throws IOException {
+        for (String file : REMOTE_CONFIGS) {
+            String content = readConfig(file);
+
+            assertUsesHugeGraphIoRegistry(file, content);
+            assertSerializerClassNamesUseUtilPackage(file, content);
+        }
+    }
+
+    @Test
+    public void testConfiguredSerializerClassesAreLoadable() throws Exception {
+        assertConfiguredSerializerClassesAreLoadable(
+                GREMLIN_SERVER_CONFIG, readConfig(GREMLIN_SERVER_CONFIG));
+        for (String file : REMOTE_CONFIGS) {
+            assertConfiguredSerializerClassesAreLoadable(file,
+                                                         readConfig(file));
+        }
+    }
+
+    @Test
+    public void testGremlinServerConfigVariantsSupportGraphSONMimeTypes()
+            throws Exception {
+        Path assembly = serverAssemblyPath();
+
+        for (String variant : GREMLIN_SERVER_CONFIG_VARIANTS) {
+            Settings settings = Settings.read(assembly.resolve(variant)
+                                                   .toString());
+
+            assertSupportsTypedAndUntypedGraphSONMimeTypes(variant,
+                                                           graphSONMimeTypes(settings));
+        }
+    }
+
+    private static void assertSupportsTypedAndUntypedGraphSONMimeTypes(
+            String fileName, Map graphSONMimeTypes) {
+        for (String mimeType : UNTYPED_GRAPHSON_MIME_TYPES) {
+            Assert.assertTrue(fileName + " should support untyped " +
+                              "GraphSON MIME " + mimeType,
+                              graphSONMimeTypes.containsKey(mimeType));
+        }
+        for (String mimeType : TYPED_GRAPHSON_MIME_TYPES) {
+            Assert.assertTrue(fileName + " should support typed GraphSON " +
+                              "MIME " + mimeType,
+                              graphSONMimeTypes.containsKey(mimeType));
+        }
+        Assert.assertEquals(fileName + " should keep application/json " +
+                            "mapped to the untyped V1 serializer",
+                            GRAPHSON_UNTYPED_V1,
+                            graphSONMimeTypes.get("application/json"));
+    }
+
+    @Test
+    public void testConfiguredGraphSONSerializersCanSerializeHugeGraphTypes()
+            throws Exception {
+        Settings settings = readGremlinServerSettings();
+        List typedSerializers = new ArrayList<>();
+        boolean foundUntyped = false;
+
+        for (Settings.SerializerSettings serializerSettings :
+             settings.serializers) {
+            if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE +
+                                                         "GraphSON")) {
+                continue;
+            }
+
+            MessageTextSerializer serializer =
+                    newTextSerializer(serializerSettings.className);
+            serializer.configure(config(serializerSettings.config),
+                                 Collections.emptyMap());
+            boolean typed = !serializerSettings.className.startsWith(
+                    SERIALIZER_PACKAGE + "GraphSONUntyped");
+            if (typed) {
+                typedSerializers.add(serializerSettings.className);
+            } else {
+                foundUntyped = true;
+            }
+            assertCanSerializeHugeGraphTypes(
+                    serializer,
+                    typed && usesStableGraphSONTypes(
+                            serializerSettings.className));
+        }
+
+        Assert.assertTrue("No untyped GraphSON serializer settings found in " +
+                          GREMLIN_SERVER_CONFIG, foundUntyped);
+        Assert.assertEquals("Configured typed GraphSON serializers should " +
+                            "match the fallback set",
+                            TYPED_FALLBACK_SERIALIZERS, typedSerializers);
+    }
+
+    @Test
+    public void testTypedFallbackSerializersCanRoundTripHugeGraphIds()
+            throws Exception {
+        Map config = graphSONV1Config(
+                readGremlinServerSettings());
+
+        for (String serializer : TYPED_FALLBACK_SERIALIZERS) {
+            MessageTextSerializer textSerializer =
+                    newTextSerializer(serializer);
+
+            textSerializer.configure(config(config), Collections.emptyMap());
+            assertCanRoundTripHugeGraphIds(serializer, textSerializer);
+        }
+    }
+
+    @Test
+    public void testConfiguredGraphBinarySerializersCanRoundTripStandardPredicate()
+            throws Exception {
+        Settings settings = readGremlinServerSettings();
+        boolean found = false;
+
+        for (Settings.SerializerSettings serializerSettings :
+             settings.serializers) {
+            if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE +
+                                                         "GraphBinary")) {
+                continue;
+            }
+
+            MessageSerializer serializer =
+                    newMessageSerializer(serializerSettings.className);
+            serializer.configure(config(serializerSettings.config),
+                                 Collections.emptyMap());
+            assertCanRoundTripStandardPredicate(serializerSettings.className,
+                                                serializer);
+            found = true;
+        }
+
+        Assert.assertTrue("No GraphBinary serializer settings found in " +
+                          GREMLIN_SERVER_CONFIG, found);
+    }
+
+    @Test
+    public void testRemoteObjectsSerializerCanSerializePathShape()
+            throws Exception {
+        RemoteSerializerSettings settings =
+                readRemoteSerializerSettings(REMOTE_OBJECTS_CONFIG);
+        MessageTextSerializer serializer =
+                newTextSerializer(settings.className);
+
+        serializer.configure(config(settings.config), Collections.emptyMap());
+
+        String json = serializeResponse(serializer, testPath());
+
+        Assert.assertContains("\"labels\"", json);
+        Assert.assertContains("\"objects\"", json);
+        Assert.assertContains("marko", json);
+        Assert.assertContains("lop", json);
+        Assert.assertContains("\"a\"", json);
+        Assert.assertContains("\"b\"", json);
+        Assert.assertContains("\"software\"", json);
+    }
+
+    private static Settings readGremlinServerSettings() throws Exception {
+        return Settings.read(configPath(GREMLIN_SERVER_CONFIG).toString());
+    }
+
+    private static String readConfig(String fileName) throws IOException {
+        return Files.readString(configPath(fileName), StandardCharsets.UTF_8);
+    }
+
+    private static Path configPath(String fileName) {
+        return findConfDir().resolve(fileName);
+    }
+
+    private static Path serverAssemblyPath() {
+        return findConfDir().getParent().getParent();
+    }
+
+    private static Path findConfDir() {
+        String configuredDir = System.getProperty("hugegraph.conf.dir");
+        Path configuredPath = resolveConfiguredDir(configuredDir);
+        if (configuredPath != null) {
+            return configuredPath;
+        }
+
+        String envDir = System.getenv("HUGEGRAPH_CONF_DIR");
+        Path envPath = resolveConfiguredDir(envDir);
+        if (envPath != null) {
+            return envPath;
+        }
+
+        Path userDir = Paths.get(System.getProperty("user.dir"));
+        List candidates = new ArrayList<>();
+
+        Path parent = userDir.getParent();
+        if (parent != null) {
+            candidates.add(parent.resolve("hugegraph-dist")
+                                 .resolve("src")
+                                 .resolve("assembly")
+                                 .resolve("static")
+                                 .resolve("conf"));
+        }
+        candidates.add(userDir.resolve("hugegraph-server")
+                              .resolve("hugegraph-dist")
+                              .resolve("src")
+                              .resolve("assembly")
+                              .resolve("static")
+                              .resolve("conf"));
+
+        for (Path candidate : candidates) {
+            if (Files.isDirectory(candidate)) {
+                return candidate;
+            }
+        }
+
+        Assert.fail(String.format("Can't find hugegraph-dist static conf from" +
+                                  " %s (hugegraph.conf.dir=%s," +
+                                  " HUGEGRAPH_CONF_DIR=%s, candidates=%s)",
+                                  userDir, configuredDir, envDir, candidates));
+        return userDir;
+    }
+
+    private static Path resolveConfiguredDir(String path) {
+        if (path == null || path.isEmpty()) {
+            return null;
+        }
+        Path configured = Paths.get(path);
+        if (Files.isDirectory(configured)) {
+            return configured;
+        }
+        return null;
+    }
+
+    private static void assertUsesHugeGraphIoRegistry(String fileName,
+                                                      String content) {
+        Assert.assertTrue(fileName + " should keep HugeGraphIoRegistry",
+                          content.contains(IO_REGISTRY));
+    }
+
+    private static void assertSerializerClassNamesUseUtilPackage(
+            String fileName, String content) {
+        Assert.assertFalse(content.contains(
+                "org.apache.tinkerpop.gremlin.driver.ser."));
+        Assert.assertFalse(content.contains(
+                "org.apache.tinkerpop.gremlin.server.ser."));
+
+        Matcher matcher = CLASS_NAME.matcher(content);
+        boolean found = false;
+        while (matcher.find()) {
+            found = true;
+            String className = matcher.group(1);
+            Assert.assertTrue(fileName + " has outdated serializer " +
+                              className,
+                              className.startsWith(SERIALIZER_PACKAGE));
+        }
+        Assert.assertTrue("No serializer className found in " + fileName,
+                          found);
+    }
+
+    private static void assertConfiguredSerializerClassesAreLoadable(
+            String fileName, String content) throws ClassNotFoundException {
+        Matcher matcher = CLASS_NAME.matcher(content);
+        boolean found = false;
+        while (matcher.find()) {
+            found = true;
+            Class.forName(matcher.group(1));
+        }
+        Assert.assertTrue("No serializer className found in " + fileName,
+                          found);
+    }
+
+    private static Map graphSONMimeTypes(Settings settings)
+            throws Exception {
+        Map mimeTypes = new HashMap<>();
+
+        for (Settings.SerializerSettings serializerSettings :
+             settings.serializers) {
+            if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE +
+                                                         "GraphSON")) {
+                continue;
+            }
+
+            MessageSerializer serializer =
+                    newMessageSerializer(serializerSettings.className);
+            for (String mimeType : serializer.mimeTypesSupported()) {
+                mimeTypes.putIfAbsent(mimeType, serializerSettings.className);
+            }
+        }
+
+        return mimeTypes;
+    }
+
+    private static MessageTextSerializer newTextSerializer(String className)
+            throws Exception {
+        MessageSerializer serializer = newMessageSerializer(className);
+
+        Assert.assertTrue(className + " should be a MessageTextSerializer",
+                          serializer instanceof MessageTextSerializer);
+        return (MessageTextSerializer) serializer;
+    }
+
+    private static MessageSerializer newMessageSerializer(String className)
+            throws Exception {
+        Object serializer = Class.forName(className)
+                                 .getDeclaredConstructor()
+                                 .newInstance();
+
+        Assert.assertTrue(className + " should be a MessageSerializer",
+                          serializer instanceof MessageSerializer);
+        return (MessageSerializer) serializer;
+    }
+
+    private static String serializeResponse(MessageTextSerializer serializer,
+                                            Object result)
+            throws Exception {
+        ResponseMessage response = ResponseMessage.build(UUID.randomUUID())
+                                                  .code(ResponseStatusCode.SUCCESS)
+                                                  .result(result)
+                                                  .create();
+
+        return serializer.serializeResponseAsString(response,
+                                                    ByteBufAllocator.DEFAULT);
+    }
+
+    private static ResponseMessage roundTripResponse(
+            MessageTextSerializer serializer, Object result)
+            throws Exception {
+        ResponseMessage response = ResponseMessage.build(UUID.randomUUID())
+                                                  .code(ResponseStatusCode.SUCCESS)
+                                                  .result(result)
+                                                  .create();
+        String json = serializer.serializeResponseAsString(
+                response, ByteBufAllocator.DEFAULT);
+        return serializer.deserializeResponse(json);
+    }
+
+    private static ResponseMessage roundTripBinaryResponse(
+            MessageSerializer serializer, Object result)
+            throws Exception {
+        ResponseMessage response = ResponseMessage.build(UUID.randomUUID())
+                                                  .code(ResponseStatusCode.SUCCESS)
+                                                  .result(result)
+                                                  .create();
+        ByteBuf buffer = serializer.serializeResponseAsBinary(
+                response, ByteBufAllocator.DEFAULT);
+        try {
+            return serializer.deserializeResponse(buffer);
+        } finally {
+            buffer.release();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static RemoteSerializerSettings readRemoteSerializerSettings(
+            String fileName) throws IOException {
+        try (InputStream input = Files.newInputStream(configPath(fileName))) {
+            Map root = new Yaml().load(input);
+            Map serializer =
+                    (Map) root.get("serializer");
+
+            Assert.assertNotNull("No serializer in " + fileName, serializer);
+            String className = (String) serializer.get("className");
+            Map config =
+                    (Map) serializer.get("config");
+
+            Assert.assertNotNull("No serializer className in " + fileName,
+                                 className);
+            Assert.assertNotNull("No serializer config in " + fileName,
+                                 config);
+            return new RemoteSerializerSettings(className, config);
+        }
+    }
+
+    private static Map graphSONV1Config(Settings settings) {
+        for (Settings.SerializerSettings serializer : settings.serializers) {
+            if (GRAPHSON_UNTYPED_V1.equals(serializer.className)) {
+                Assert.assertNotNull(serializer.config);
+                return serializer.config;
+            }
+        }
+
+        Assert.fail("No " + GRAPHSON_UNTYPED_V1 + " found in " +
+                    GREMLIN_SERVER_CONFIG);
+        return Collections.emptyMap();
+    }
+
+    private static Map config(Map config) {
+        if (config == null) {
+            return Collections.emptyMap();
+        }
+        return new HashMap<>(config);
+    }
+
+    private static org.apache.tinkerpop.gremlin.process.traversal.Path testPath() {
+        return MutablePath.make()
+                          .extend(IdGenerator.of("marko"), Set.of("a"))
+                          .extend(IdGenerator.of("lop"),
+                                  Set.of("b", "software"));
+    }
+
+    private static void assertCanSerializeHugeGraphTypes(
+            MessageTextSerializer serializer, boolean typed)
+            throws Exception {
+        Object id = IdGenerator.of("marko");
+        Object uuidId = IdGenerator.of(
+                UUID.fromString("3cfcafc8-7906-4ab7-a207-4ded056f58de"));
+        Object edgeId = EdgeId.parse("S1>2>3>4>L6");
+        String fileJson = serializeResponse(serializer, new File("test.text"));
+        String idJson = serializeResponse(serializer, id);
+        String uuidJson = serializeResponse(serializer, uuidId);
+        String edgeJson = serializeResponse(serializer, edgeId);
+
+        Assert.assertContains("\"file\"", fileJson);
+        Assert.assertContains("test.text", fileJson);
+        Assert.assertContains("marko", idJson);
+        Assert.assertContains("3cfcafc8-7906-4ab7-a207-4ded056f58de",
+                              uuidJson);
+        Assert.assertContains("S1>2>3>4>L6", edgeJson);
+
+        if (typed) {
+            assertContainsGraphSONType(fileJson, "hugegraph:File");
+            assertContainsGraphSONType(idJson, "hugegraph:StringId");
+            assertContainsGraphSONType(uuidJson, "hugegraph:UuidId");
+            assertContainsGraphSONType(edgeJson, "hugegraph:EdgeId");
+        }
+    }
+
+    private static boolean usesStableGraphSONTypes(String serializer) {
+        // GraphSON V1 uses legacy @class wrapping; assert stable
+        // hugegraph:* @type names for V2/V3 typed fallback serializers.
+        return !serializer.endsWith("GraphSONMessageSerializerV1");
+    }
+
+    private static void assertCanRoundTripHugeGraphIds(
+            String serializerName, MessageTextSerializer serializer)
+            throws Exception {
+        List ids = Arrays.asList(
+                IdGenerator.of("marko"),
+                IdGenerator.of(123L),
+                IdGenerator.of(UUID.fromString(
+                        "3cfcafc8-7906-4ab7-a207-4ded056f58de")),
+                EdgeId.parse("S1>2>3>4>L6")
+        );
+
+        for (Object expected : ids) {
+            ResponseMessage response = roundTripResponse(serializer, expected);
+            Object actual = response.getResult().getData();
+            String message = serializerName + " should round-trip " +
+                             expected.getClass().getSimpleName();
+            Assert.assertEquals(message, expected.getClass(),
+                                actual.getClass());
+            Assert.assertEquals(message, expected, actual);
+        }
+    }
+
+    private static void assertCanRoundTripStandardPredicate(
+            String serializerName, MessageSerializer serializer)
+            throws Exception {
+        P expected = P.eq("marko");
+        ResponseMessage response = roundTripBinaryResponse(serializer,
+                                                           expected);
+        Object actual = response.getResult().getData();
+        String message = serializerName +
+                         " should round-trip a standard predicate";
+        Assert.assertInstanceOf(P.class, actual);
+        Assert.assertEquals(message, expected, actual);
+    }
+
+    private static void assertContainsGraphSONType(String json,
+                                                   String graphSONType) {
+        Assert.assertContains("\"@type\"", json);
+        Assert.assertContains(graphSONType, json);
+    }
+
+    private static final class RemoteSerializerSettings {
+
+        private final String className;
+        private final Map config;
+
+        private RemoteSerializerSettings(String className,
+                                         Map config) {
+            this.className = className;
+            this.config = config;
+        }
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionTest.java
index ba4b09dcab..bd376d495d 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionTest.java
@@ -25,8 +25,10 @@
 import org.apache.hugegraph.backend.query.Condition.RelationType;
 import org.apache.hugegraph.backend.query.Condition.SyspropRelation;
 import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.traversal.optimize.ConditionP;
 import org.apache.hugegraph.type.define.HugeKeys;
 import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate;
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableList;
@@ -149,6 +151,69 @@ public void testConditionEq() {
         Assert.assertFalse(c4.test(new Date(0L)));
     }
 
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testRelationTypeImplementsTinkerPopBiPredicate() {
+        PBiPredicate contains =
+                (PBiPredicate) (Object) RelationType.CONTAINS;
+        Assert.assertEquals("contains", contains.getPredicateName());
+        Assert.assertTrue(contains.test(ImmutableList.of("marko", "josh"),
+                                        "marko"));
+        Assert.assertFalse(contains.test(ImmutableList.of("marko", "josh"),
+                                         "vadas"));
+
+        PBiPredicate containsKey =
+                (PBiPredicate) (Object)
+                RelationType.CONTAINS_KEY;
+        Assert.assertEquals("containsk", containsKey.getPredicateName());
+        Assert.assertTrue(containsKey.test(ImmutableMap.of("name", "marko"),
+                                           "name"));
+
+        PBiPredicate textContains =
+                (PBiPredicate) (Object)
+                RelationType.TEXT_CONTAINS;
+        Assert.assertEquals("textcontains",
+                            textContains.getPredicateName());
+        Assert.assertTrue(textContains.test("marko", "ark"));
+        Assert.assertFalse(textContains.test("marko", "vadas"));
+    }
+
+    @Test
+    public void testConditionPUsesRelationTypeBiPredicate() {
+        ConditionP contains = ConditionP.contains("marko");
+        Assert.assertEquals("contains",
+                            contains.getBiPredicate().getPredicateName());
+        Assert.assertTrue(contains.test(ImmutableList.of("marko", "josh")));
+        Assert.assertFalse(ConditionP.contains("vadas")
+                                     .test(ImmutableList.of("marko", "josh")));
+
+        ConditionP containsKey = ConditionP.containsK("name");
+        Assert.assertEquals("containsk",
+                            containsKey.getBiPredicate().getPredicateName());
+        Assert.assertTrue(containsKey.test(ImmutableMap.of("name", "marko")));
+        Assert.assertFalse(ConditionP.containsK("age")
+                                     .test(ImmutableMap.of("name", "marko")));
+
+        ConditionP containsValue = ConditionP.containsV("marko");
+        Assert.assertEquals("containsv",
+                            containsValue.getBiPredicate().getPredicateName());
+        Assert.assertTrue(containsValue.test(
+                ImmutableMap.of("name", "marko")));
+        Assert.assertFalse(ConditionP.containsV("vadas")
+                                     .test(ImmutableMap.of("name", "marko")));
+
+        ConditionP textContains = ConditionP.textContains("ark");
+        Assert.assertEquals("textcontains",
+                            textContains.getBiPredicate().getPredicateName());
+        Assert.assertTrue(textContains.test("marko"));
+        Assert.assertFalse(ConditionP.textContains("vadas").test("marko"));
+
+        ConditionP eq = ConditionP.eq(new String[]{"a", "b"});
+        Assert.assertEquals("==", eq.getBiPredicate().getPredicateName());
+        Assert.assertTrue(eq.test(new String[]{"a", "b"}));
+        Assert.assertFalse(eq.test(new String[]{"a", "c"}));
+    }
+
     @Test
     public void testConditionGt() {
         Condition c1 = Condition.gt(HugeKeys.ID, 123);
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GroovyScriptEngineCompatibilityTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GroovyScriptEngineCompatibilityTest.java
new file mode 100644
index 0000000000..3db519db8f
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GroovyScriptEngineCompatibilityTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.hugegraph.unit.core;
+
+import javax.script.Compilable;
+import javax.script.CompiledScript;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.junit.Test;
+
+public class GroovyScriptEngineCompatibilityTest extends BaseUnitTest {
+
+    @Test
+    public void testGroovyJsr223EngineCanCompileAndEvaluate()
+           throws ScriptException {
+        ScriptEngine engine = new ScriptEngineManager().getEngineByName(
+                "groovy");
+
+        Assert.assertNotNull(engine);
+        Assert.assertEquals("org.codehaus.groovy.jsr223." +
+                            "GroovyScriptEngineImpl",
+                            engine.getClass().getName());
+        Assert.assertTrue(engine instanceof Compilable);
+
+        CompiledScript script = ((Compilable) engine).compile(
+                "def add = { a, b -> a + b }; add(2, 3)");
+
+        Assert.assertEquals(5, script.eval());
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugeFeaturesTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugeFeaturesTest.java
new file mode 100644
index 0000000000..08f12e9fc8
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugeFeaturesTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.hugegraph.unit.core;
+
+import org.apache.hugegraph.structure.HugeFeatures;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class HugeFeaturesTest {
+
+    @Test
+    public void testUniformListValueFeatureContract() {
+        HugeFeatures features = new HugeFeatures(null, true);
+
+        Assert.assertTrue(features.graph().variables()
+                                  .supportsUniformListValues());
+        Assert.assertFalse(features.vertex().properties()
+                                   .supportsUniformListValues());
+        Assert.assertFalse(features.edge().properties()
+                                   .supportsUniformListValues());
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.java
new file mode 100644
index 0000000000..5b7b5fa4ab
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.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.hugegraph.unit.serializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.hugegraph.backend.id.EdgeId;
+import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.backend.id.IdGenerator.LongId;
+import org.apache.hugegraph.backend.id.IdGenerator.StringId;
+import org.apache.hugegraph.backend.id.IdGenerator.UuidId;
+import org.apache.hugegraph.io.HugeGraphIoRegistry;
+import org.apache.hugegraph.schema.PropertyKey;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.hugegraph.unit.FakeObjects;
+import org.apache.hugegraph.util.JsonUtil;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONMapper;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONReader;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONVersion;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONWriter;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.TypeInfo;
+import org.junit.Test;
+
+public class HugeGraphSONModuleTest extends BaseUnitTest {
+
+    @Test
+    public void testSerializeFileWithGraphSONTypeInfo() throws IOException {
+        String json = writeTyped(new File("test.text"));
+        Map typedFile = JsonUtil.fromJson(json, Map.class);
+        Object value = typedFile.get("@value");
+
+        Assert.assertEquals("hugegraph:File", typedFile.get("@type"));
+        Assert.assertInstanceOf(Map.class, value);
+        Assert.assertEquals("test.text", ((Map) value).get("file"));
+        Assert.assertContains("hugegraph:File", json);
+        Assert.assertContains("\"file\"", json);
+        Assert.assertContains("test.text", json);
+
+        File file = readTyped(json, File.class);
+        Assert.assertEquals("test.text", file.getName());
+    }
+
+    @Test
+    public void testRoundTripIdWithGraphSONTypeInfo() throws IOException {
+        StringId expectedStringId = (StringId) IdGenerator.of("marko");
+        LongId expectedLongId = (LongId) IdGenerator.of(123L);
+        UuidId expectedUuidId = (UuidId) IdGenerator.of(
+                UUID.fromString("3cfcafc8-7906-4ab7-a207-4ded056f58de"));
+        EdgeId expectedEdgeId = EdgeId.parse("S1>2>3>4>L6");
+
+        String stringId = writeTyped(expectedStringId);
+        String longId = writeTyped(expectedLongId);
+        String uuidId = writeTyped(expectedUuidId);
+        String edgeId = writeTyped(expectedEdgeId);
+
+        Assert.assertContains("hugegraph:StringId", stringId);
+        Assert.assertContains("marko", stringId);
+        Assert.assertContains("hugegraph:LongId", longId);
+        Assert.assertContains("123", longId);
+        Assert.assertContains("hugegraph:UuidId", uuidId);
+        Assert.assertContains("3cfcafc8-7906-4ab7-a207-4ded056f58de",
+                              uuidId);
+        Assert.assertContains("hugegraph:EdgeId", edgeId);
+        Assert.assertContains("S1>2>3>4>L6", edgeId);
+
+        Assert.assertEquals(expectedStringId,
+                            readTyped(stringId, StringId.class));
+        Assert.assertEquals(expectedLongId,
+                            readTyped(longId, LongId.class));
+        Assert.assertEquals(expectedUuidId,
+                            readTyped(uuidId, UuidId.class));
+        Assert.assertEquals(expectedEdgeId,
+                            readTyped(edgeId, EdgeId.class));
+    }
+
+    @Test
+    public void testSerializeSchemaWithUntypedGraphSONModule() throws IOException {
+        FakeObjects objects = new FakeObjects();
+        PropertyKey propertyKey = objects.newPropertyKey(IdGenerator.of(1L),
+                                                         "name");
+
+        String json = writeUntyped(propertyKey);
+
+        Assert.assertContains("\"name\"", json);
+    }
+
+    private static String writeTyped(Object object) throws IOException {
+        GraphSONMapper mapper = mapper(TypeInfo.PARTIAL_TYPES);
+        GraphSONWriter writer = GraphSONWriter.build().mapper(mapper).create();
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+        writer.writeObject(output, object);
+
+        return output.toString(StandardCharsets.UTF_8.name());
+    }
+
+    private static  T readTyped(String json, Class clazz)
+            throws IOException {
+        GraphSONMapper mapper = mapper(TypeInfo.PARTIAL_TYPES);
+        GraphSONReader reader = GraphSONReader.build().mapper(mapper).create();
+        ByteArrayInputStream input = new ByteArrayInputStream(
+                json.getBytes(StandardCharsets.UTF_8));
+
+        return reader.readObject(input, clazz);
+    }
+
+    private static String writeUntyped(Object object) throws IOException {
+        GraphSONMapper mapper = mapper(TypeInfo.NO_TYPES);
+        GraphSONWriter writer = GraphSONWriter.build().mapper(mapper).create();
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+        writer.writeObject(output, object);
+
+        return output.toString(StandardCharsets.UTF_8.name());
+    }
+
+    private static GraphSONMapper mapper(TypeInfo typeInfo) {
+        GraphSONMapper mapper = GraphSONMapper.build()
+                                              .version(GraphSONVersion.V3_0)
+                                              .typeInfo(typeInfo)
+                                              .addRegistry(HugeGraphIoRegistry.instance())
+                                              .create();
+
+        return mapper;
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter b/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter
index 2d162bd705..82fb620d00 100644
--- a/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter
+++ b/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter
@@ -17,40 +17,38 @@
 #
 
 ####################    structure suite    ####################
-
-## automatic ID for edges
+## Filter reason categories:
+## HG-LEGACY: Existing HugeGraph feature boundary before this upgrade
+## TP37-SEMANTICS: New or renamed TinkerPop 3.7 suite expectation
+## TEST-INFRA: Test harness/storage lifecycle issue, not a feature regression
+## PERF: Fast-suite-only runtime guard
+## FOLLOW-UP: Keep tracked for later behavior or infrastructure cleanup
+
+## HG-LEGACY: automatic ID for edges
 org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldNotEvaluateToEqualDifferentId: Not support automatic/custom id for edge
 org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldNotEvaluateToEqualDifferentId: Not support automatic/custom id for edge
 org.apache.tinkerpop.gremlin.structure.EdgeTest.BasicEdgeTest.shouldValidateIdEquality: Not support automatic/custom id for edge
 org.apache.tinkerpop.gremlin.structure.EdgeTest.BasicEdgeTest.shouldValidateEquality: Not support automatic/custom id for edge
 
-## property ID
+## HG-LEGACY: property ID
 org.apache.tinkerpop.gremlin.structure.util.detached.DetachedPropertyTest.shouldNotBeEqualPropertiesAsThereIsDifferentKey: Not support property id
 
-## id should be String type
+## HG-LEGACY: id should be String type
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldAddVertexWithUserSuppliedStringId: expect vertex.id is String type
 
-## same id for element
+## HG-LEGACY: same id for element
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldHaveExceptionConsistencyWhenAssigningSameIdOnVertex: Assigning the same ID to an Element is accepted(override), but tinkerpop expect throw an exception
 
-## uniform list
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertexOnAdd[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnEdgeOnAdd[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertex[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnEdge[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertexOnAdd[supportsUniformListValues([try1, try2])]: Uniform list can't be accessed by vertex.property() which is supposed right by tinkerpop
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertex[supportsUniformListValues([try1, try2])]: Uniform list can't be accessed by vertex.property() which is supposed right by tinkerpop
-
-# expect multi properties after setting single property multi times
+## HG-LEGACY: expect multi properties after setting single property multi times
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldOverwriteEarlierKeyValuesWithLaterKeyValuesOnAddVertexIfMultiProperty: Expect multi properties after setting single property multi times
 org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldRemoveMultiProperties: Expect multi properties after setting single property multi times
 org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldRemoveMultiPropertiesWhenVerticesAreRemoved: Expect multi properties after setting single property multi times
 org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyAddition.shouldAllowIdenticalValuedMultiProperties: Expect multi properties after setting single property multi times
 
-# not support nested property
+## HG-LEGACY: not support nested property
 org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldAllowIteratingAndRemovingVertexPropertyProperties: Not support nested property
 
-## failed tests with wrong edge number, reason: replicated edges treated as one
+## HG-LEGACY: failed tests with wrong edge number, reason: replicated edges treated as one
 org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.ProcessorTest.shouldProcessEdges: Replicated edges treated as one
 org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateSameGraph[test(NormalDistribution{stdDeviation=2.0, mean=0.0},NormalDistribution{stdDeviation=2.0, mean=0.0})]: Replicated edges treated as one
 org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateDifferentGraph[test(NormalDistribution{stdDeviation=2.0, mean=0.0},NormalDistribution{stdDeviation=2.0, mean=0.0})]: Replicated edges treated as one
@@ -65,27 +63,54 @@ org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.Diffe
 org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateSameGraph[test(PowerLawDistribution{gamma=2.3, multiplier=0.0},PowerLawDistribution{gamma=2.8, multiplier=0.0})]: Replicated edges treated as one
 org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateDifferentGraph[test(PowerLawDistribution{gamma=2.3, multiplier=0.0},PowerLawDistribution{gamma=2.8, multiplier=0.0})]: Replicated edges treated as one
 
-## user supplied numeric id
+## HG-LEGACY: user supplied numeric id
 org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeNumericLong: Not open the feature 'support user supplied id of numeric long' although numeric long id is allowed
 org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeNumericInt: Not open the feature 'support user supplied id of numeric long' although numeric long id is allowed
 
-## user supplied uuid id
+## HG-LEGACY/FOLLOW-UP: user supplied uuid id feature flag remains closed
 org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeUuid: Not open the feature 'support user supplied id of uuid' although uuid id is allowed
 
-## edge id format validate firstly and throw NotFoundException if invalid
+## HG-LEGACY/FOLLOW-UP: edge id format validation throws NotFoundException first
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldHaveExceptionConsistencyWhenFindEdgeByIdThatIsNonExistentViaIterator: Invalid format of edge id will introduce throwing NotFoundException before try to query in backend store
 
-## not bugs
+## HG-LEGACY/TEST-INFRA/FOLLOW-UP: existing non-bug behavior and long-running transaction case
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldRemoveVertices: Random UUID as edge label
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldRemoveEdges: Random UUID as edge label
 org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldExecuteWithCompetingThreads: Hang the tests because property 'test' have both String and long value
 
-## vertex properties doesn't have nested structures with HugeVertexSerializer
+## TEST-INFRA/FOLLOW-UP: gryo-v3 migration tests reopen source graph path before RocksDB releases lock
+org.apache.tinkerpop.gremlin.structure.io.IoGraphTest.shouldMigrateModernGraph[gryo-v3]: RocksDB lock not released between source and target graph in gryo-v3 migration test
+org.apache.tinkerpop.gremlin.structure.io.IoGraphTest.shouldMigrateClassicGraph[gryo-v3]: RocksDB lock not released between source and target graph in gryo-v3 migration test
+
+## TP37-SEMANTICS/FOLLOW-UP: TinkerPop 3.7 transaction lifecycle checks spawned thread transactions
+org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldExecuteCompetingThreadsOnMultipleDbInstances: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close
+org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldAllowReferenceOfEdgeIdOutsideOfOriginalThreadManual: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close
+org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldSupportTransactionIsolationCommitCheck: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close
+org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldAllowReferenceOfVertexIdOutsideOfOriginalThreadManual: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close
+
+## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: TransactionMultiThreadedTest assumes spawned transaction isolation and cleanup
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldCommit: Memory backend exposes uncommitted vertices across ThreadLocal transactions and spawned tx stays open
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldDeleteVertexOnCommit: Spawned read transaction is not auto-closed after thread exit
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldDeleteRelatedEdgesOnVertexDelete: Spawned verification transaction is not auto-closed after thread exit
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollbackAddedVertex: Memory backend exposes uncommitted vertices and spawned tx stays open
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldCommitEdge: Spawned read transactions are not auto-closed after thread exit
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollbackAddedEdge: Spawned read transactions are not auto-closed after thread exit
+
+## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: MergeEdgeTest hard-codes numeric vertex ids although HugeGraph does not advertise numeric id support
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X_exists: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_V_hasXperson_name_marko_X_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_injectXlabel_knows_out_marko_in_vadasX_mergeE: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeE_with_outV_inV_options: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+
+## HG-LEGACY/FOLLOW-UP: vertex properties don't have nested structures with HugeVertexSerializer
 org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializeVertex: Vertex properties doesn't have nested structures with HugeVertexSerializer
 org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializePath: Vertex properties doesn't have nested structures with HugeVertexSerializer
 org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializeTree: Vertex properties doesn't have nested structures with HugeVertexSerializer
 
-## update property but no commit, lead there are changes in indexTx, can't do index query
+## HG-LEGACY: update property but no commit leaves changes in indexTx
 org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldConstructDetachedEdge: Can't do index query when there are changes in transaction
 org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldConstructDetachedEdgeAsReference: Can't do index query when there are changes in transaction
 org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldConstructReferenceEdge: Can't do index query when there are changes in transaction
@@ -95,9 +120,12 @@ org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldCo
 # unsupported automatic edge id, therefore number of edge is wrong
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdge: Unsupported automatic edge id, therefore number of edge is wrong
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdgeByPath: Unsupported automatic edge id, therefore number of edge is wrong
+org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdgeViaMergeE: Unsupported automatic edge id/event tracking for mergeE
+org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddVertexWithPropertyThenPropertyAdded: Unsupported vertex property changed event tracking for addV().property()
 # shouldWriteToMultiplePartitions
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldWriteToMultiplePartitions: It's not allowed to query by index when there are uncommitted records
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldWriteVerticesToMultiplePartitions: It's not allowed to query by index when there are uncommitted records
+org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldPartitionWithAbstractLambdaChildTraversal: Unsupported partition test schema label
 
 # assert error, long time, reason: replicated edges treated as one
 org.apache.tinkerpop.gremlin.process.traversal.step.map.CountTest.Traversals.g_V_both_both_count: Replicated edges treated as one
@@ -118,7 +146,7 @@ org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals
 org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals.g_addVXanimalX_propertyXname_mateoX_propertyXname_gateoX_propertyXname_cateoX_propertyXage_5X: Expect multi properties after setting single property multi times
 org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals.g_V_addVXanimalX_propertyXname_valuesXnameXX_propertyXname_an_animalX_propertyXvaluesXnameX_labelX: Expect multi properties after setting single property multi times
 
-# pass, long time
+## PERF: pass but too slow for fast TinkerPop profile
 org.apache.tinkerpop.gremlin.process.traversal.step.map.CountTest.Traversals.g_V_repeatXoutX_timesX5X_asXaX_outXwrittenByX_asXbX_selectXa_bX_count: long time
 org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchTest.CountMatchTraversals.g_V_matchXa_followedBy_count_isXgtX10XX_b__a_0followedBy_count_isXgtX10XX_bX_count: long time
 org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchTest.CountMatchTraversals.g_V_hasLabelXsongsX_matchXa_name_b__a_performances_cX_selectXb_cX_count: long time
@@ -154,7 +182,7 @@ org.apache.tinkerpop.gremlin.process.traversal.step.ComplexTest.Traversals.class
 org.apache.tinkerpop.gremlin.process.traversal.step.ComplexTest.Traversals.playlistPaths: long time
 org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.TreeTest.Traversals.g_V_out_out_treeXaX_capXaX: long time
 
-# Unsupported query
+## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: unsupported has predicates and empty-id semantics
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXwithoutXemptyXX_count: Unsupported query 'NOT IN []'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXemptyX_count: Unsupported query 'hasId(EmptyList)'
 
@@ -163,13 +191,20 @@ org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_startingWithXmarXX: Unsupported predicate 'startingWith(mar)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27X_count: Unsupported relation 'NEQ'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXperson_name_containingXoX_andXltXmXXX: Unsupported predicate 'containing(o)'
+org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXrMarXX: Unsupported predicate 'regex(^mar)'
+org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXTinkerXX: Unsupported predicate 'regex(Tinker)'
+org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXTinkerUnicodeXX: Unsupported predicate 'regex(Tinker.*)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_startingWithXmarXX: Unsupported predicate 'notStartingWith(mar)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_containingXarkXX: Unsupported predicate 'notContaining(ark)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_endingWithXasXX: Unsupported predicate 'notEndingWith(as)'
+org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_notRegexXrMarXX: Unsupported predicate 'notRegex(^mar)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27_29X_count: Unsupported relation 'NEQ'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_gtXmX_andXcontainingXoXXX: Unsupported predicate 'containing(o)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXp_neqXvXX: Don't accept query based on properties [p] that are not indexed in any label, may not match not-equal condition
 
+org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest.shouldSetIdOnAddEWithNamePropertyKeySpecifiedAndNameSuppliedAsProperty: Unsupported ElementIdStrategy edge id/property index query
+org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest.shouldGenerateDefaultIdOnGraphAddVWithGeneratedCustomId: Unsupported ElementIdStrategy generated custom id schema
+
 # Unsupport edge label 'created': 'software' -> 'person', has existed an edgelabel (created: person -> software) in this case
 org.apache.tinkerpop.gremlin.process.traversal.step.map.AddEdgeTest.Traversals.g_V_hasXname_markoX_asXaX_outEXcreatedX_asXbX_inV_addEXselectXbX_labelX_toXaX: Unsupport edge from inV to outV
 
diff --git a/hugegraph-server/hugegraph-test/src/main/resources/methods.filter b/hugegraph-server/hugegraph-test/src/main/resources/methods.filter
index d4b7c3e787..193eaf05ab 100644
--- a/hugegraph-server/hugegraph-test/src/main/resources/methods.filter
+++ b/hugegraph-server/hugegraph-test/src/main/resources/methods.filter
@@ -16,40 +16,37 @@
 # limitations under the License.
 #
 ####################    structure suite    ####################
+## Filter reason categories:
+## HG-LEGACY: Existing HugeGraph feature boundary before this upgrade
+## TP37-SEMANTICS: New or renamed TinkerPop 3.7 suite expectation
+## TEST-INFRA: Test harness/storage lifecycle issue, not a feature regression
+## FOLLOW-UP: Keep tracked for later behavior or infrastructure cleanup
 
-## automatic ID for edges
+## HG-LEGACY: automatic ID for edges
 org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldNotEvaluateToEqualDifferentId: Not support automatic/custom id for edge
 org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldNotEvaluateToEqualDifferentId: Not support automatic/custom id for edge
 org.apache.tinkerpop.gremlin.structure.EdgeTest.BasicEdgeTest.shouldValidateIdEquality: Not support automatic/custom id for edge
 org.apache.tinkerpop.gremlin.structure.EdgeTest.BasicEdgeTest.shouldValidateEquality: Not support automatic/custom id for edge
 
-## property ID
+## HG-LEGACY: property ID
 org.apache.tinkerpop.gremlin.structure.util.detached.DetachedPropertyTest.shouldNotBeEqualPropertiesAsThereIsDifferentKey: Not support property id
 
-## id should be String type
+## HG-LEGACY: id should be String type
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldAddVertexWithUserSuppliedStringId: expect vertex.id is String type
 
-## same id for element
+## HG-LEGACY: same id for element
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldHaveExceptionConsistencyWhenAssigningSameIdOnVertex: Assigning the same ID to an Element is accepted(override), but tinkerpop expect throw an exception
 
-## uniform list
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertexOnAdd[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnEdgeOnAdd[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertex[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnEdge[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertexOnAdd[supportsUniformListValues([try1, try2])]: Uniform list can't be accessed by vertex.property() which is supposed right by tinkerpop
-org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertex[supportsUniformListValues([try1, try2])]: Uniform list can't be accessed by vertex.property() which is supposed right by tinkerpop
-
-# expect multi properties after setting single property multi times
+## HG-LEGACY: expect multi properties after setting single property multi times
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldOverwriteEarlierKeyValuesWithLaterKeyValuesOnAddVertexIfMultiProperty: Expect multi properties after setting single property multi times
 org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldRemoveMultiProperties: Expect multi properties after setting single property multi times
 org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldRemoveMultiPropertiesWhenVerticesAreRemoved: Expect multi properties after setting single property multi times
 org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyAddition.shouldAllowIdenticalValuedMultiProperties: Expect multi properties after setting single property multi times
 
-# not support nested property
+## HG-LEGACY: not support nested property
 org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldAllowIteratingAndRemovingVertexPropertyProperties: Not support nested property
 
-## failed tests with wrong edge number, reason: replicated edges treated as one
+## HG-LEGACY: failed tests with wrong edge number, reason: replicated edges treated as one
 org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.ProcessorTest.shouldProcessEdges: Replicated edges treated as one
 org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateSameGraph[test(NormalDistribution{stdDeviation=2.0, mean=0.0},NormalDistribution{stdDeviation=2.0, mean=0.0})]: Replicated edges treated as one
 org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateDifferentGraph[test(NormalDistribution{stdDeviation=2.0, mean=0.0},NormalDistribution{stdDeviation=2.0, mean=0.0})]: Replicated edges treated as one
@@ -64,27 +61,54 @@ org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.Diffe
 org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateSameGraph[test(PowerLawDistribution{gamma=2.3, multiplier=0.0},PowerLawDistribution{gamma=2.8, multiplier=0.0})]: Replicated edges treated as one
 org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateDifferentGraph[test(PowerLawDistribution{gamma=2.3, multiplier=0.0},PowerLawDistribution{gamma=2.8, multiplier=0.0})]: Replicated edges treated as one
 
-## user supplied numeric id
+## HG-LEGACY: user supplied numeric id
 org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeNumericLong: Not open the feature 'support user supplied id of numeric long' although numeric long id is allowed
 org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeNumericInt: Not open the feature 'support user supplied id of numeric long' although numeric long id is allowed
 
-## user supplied uuid id
+## HG-LEGACY/FOLLOW-UP: user supplied uuid id feature flag remains closed
 org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeUuid: Not open the feature 'support user supplied id of uuid' although uuid id is allowed
 
-## edge id format validate firstly and throw NotFoundException if invalid
+## HG-LEGACY/FOLLOW-UP: edge id format validation throws NotFoundException first
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldHaveExceptionConsistencyWhenFindEdgeByIdThatIsNonExistentViaIterator: Invalid format of edge id will introduce throwing NotFoundException before try to query in backend store
 
-## not bugs
+## HG-LEGACY/TEST-INFRA/FOLLOW-UP: existing non-bug behavior and long-running transaction case
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldRemoveVertices: Random UUID as edge label
 org.apache.tinkerpop.gremlin.structure.GraphTest.shouldRemoveEdges: Random UUID as edge label
 org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldExecuteWithCompetingThreads: Hang the tests because property 'test' have both String and long value
 
-## vertex properties doesn't have nested structures with HugeVertexSerializer
+## TEST-INFRA/FOLLOW-UP: gryo-v3 migration tests reopen source graph path before RocksDB releases lock
+org.apache.tinkerpop.gremlin.structure.io.IoGraphTest.shouldMigrateModernGraph[gryo-v3]: RocksDB lock not released between source and target graph in gryo-v3 migration test
+org.apache.tinkerpop.gremlin.structure.io.IoGraphTest.shouldMigrateClassicGraph[gryo-v3]: RocksDB lock not released between source and target graph in gryo-v3 migration test
+
+## TP37-SEMANTICS/FOLLOW-UP: TinkerPop 3.7 transaction lifecycle checks spawned thread transactions
+org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldExecuteCompetingThreadsOnMultipleDbInstances: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close
+org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldAllowReferenceOfEdgeIdOutsideOfOriginalThreadManual: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close
+org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldSupportTransactionIsolationCommitCheck: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close
+org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldAllowReferenceOfVertexIdOutsideOfOriginalThreadManual: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close
+
+## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: TransactionMultiThreadedTest assumes spawned transaction isolation and cleanup
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldCommit: Memory backend exposes uncommitted vertices across ThreadLocal transactions and spawned tx stays open
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldDeleteVertexOnCommit: Spawned read transaction is not auto-closed after thread exit
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldDeleteRelatedEdgesOnVertexDelete: Spawned verification transaction is not auto-closed after thread exit
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollbackAddedVertex: Memory backend exposes uncommitted vertices and spawned tx stays open
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldCommitEdge: Spawned read transactions are not auto-closed after thread exit
+org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollbackAddedEdge: Spawned read transactions are not auto-closed after thread exit
+
+## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: MergeEdgeTest hard-codes numeric vertex ids although HugeGraph does not advertise numeric id support
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X_exists: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_V_hasXperson_name_marko_X_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_injectXlabel_knows_out_marko_in_vadasX_mergeE: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeE_with_outV_inV_options: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+
+## HG-LEGACY/FOLLOW-UP: vertex properties don't have nested structures with HugeVertexSerializer
 org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializeVertex: Vertex properties doesn't have nested structures with HugeVertexSerializer
 org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializePath: Vertex properties doesn't have nested structures with HugeVertexSerializer
 org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializeTree: Vertex properties doesn't have nested structures with HugeVertexSerializer
 
-## update property but no commit, lead there are changes in indexTx, can't do index query
+## HG-LEGACY: update property but no commit leaves changes in indexTx
 org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldConstructDetachedEdge: Can't do index query when there are changes in transaction
 org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldConstructDetachedEdgeAsReference: Can't do index query when there are changes in transaction
 org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldConstructReferenceEdge: Can't do index query when there are changes in transaction
@@ -94,9 +118,12 @@ org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldCo
 # unsupported automatic edge id, therefore number of edge is wrong
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdge: Unsupported automatic edge id, therefore number of edge is wrong
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdgeByPath: Unsupported automatic edge id, therefore number of edge is wrong
+org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdgeViaMergeE: Unsupported automatic edge id/event tracking for mergeE
+org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddVertexWithPropertyThenPropertyAdded: Unsupported vertex property changed event tracking for addV().property()
 # shouldWriteToMultiplePartitions
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldWriteToMultiplePartitions: It's not allowed to query by index when there are uncommitted records
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldWriteVerticesToMultiplePartitions: It's not allowed to query by index when there are uncommitted records
+org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldPartitionWithAbstractLambdaChildTraversal: Unsupported partition test schema label
 
 # assert error, long time, reason: replicated edges treated as one
 org.apache.tinkerpop.gremlin.process.traversal.step.map.CountTest.Traversals.g_V_both_both_count: Replicated edges treated as one
@@ -117,7 +144,7 @@ org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals
 org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals.g_addVXanimalX_propertyXname_mateoX_propertyXname_gateoX_propertyXname_cateoX_propertyXage_5X: Expect multi properties after setting single property multi times
 org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals.g_V_addVXanimalX_propertyXname_valuesXnameXX_propertyXname_an_animalX_propertyXvaluesXnameX_labelX: Expect multi properties after setting single property multi times
 
-# unsupported predicate
+## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: unsupported has predicates and empty-id semantics
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXwithoutXemptyXX_count: Unsupported query 'NOT IN []'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXemptyX_count: Unsupported query 'hasId(EmptyList)'
 
@@ -126,13 +153,20 @@ org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_startingWithXmarXX: Unsupported predicate 'startingWith(mar)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27X_count: Unsupported relation 'NEQ'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXperson_name_containingXoX_andXltXmXXX: Unsupported predicate 'containing(o)'
+org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXrMarXX: Unsupported predicate 'regex(^mar)'
+org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXTinkerXX: Unsupported predicate 'regex(Tinker)'
+org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXTinkerUnicodeXX: Unsupported predicate 'regex(Tinker.*)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_startingWithXmarXX: Unsupported predicate 'notStartingWith(mar)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_containingXarkXX: Unsupported predicate 'notContaining(ark)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_endingWithXasXX: Unsupported predicate 'notEndingWith(as)'
+org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_notRegexXrMarXX: Unsupported predicate 'notRegex(^mar)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27_29X_count: Unsupported relation 'NEQ'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_gtXmX_andXcontainingXoXXX: Unsupported predicate 'containing(o)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXp_neqXvXX: Don't accept query based on properties [p] that are not indexed in any label, may not match not-equal condition
 
+org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest.shouldSetIdOnAddEWithNamePropertyKeySpecifiedAndNameSuppliedAsProperty: Unsupported ElementIdStrategy edge id/property index query
+org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest.shouldGenerateDefaultIdOnGraphAddVWithGeneratedCustomId: Unsupported ElementIdStrategy generated custom id schema
+
 # Unsupport edge label 'created': 'software' -> 'person', has existed an edgelabel (created: person -> software) in this case
 org.apache.tinkerpop.gremlin.process.traversal.step.map.AddEdgeTest.Traversals.g_V_hasXname_markoX_asXaX_outEXcreatedX_asXbX_inV_addEXselectXbX_labelX_toXaX: Unsupport edge from inV to outV
 
diff --git a/hugegraph-server/pom.xml b/hugegraph-server/pom.xml
index 226199cdc8..ac00be7fc9 100644
--- a/hugegraph-server/pom.xml
+++ b/hugegraph-server/pom.xml
@@ -42,7 +42,7 @@
         1.2.17
         2.17.1
         4.13.1
-        3.5.1
+        3.7.6
         2.7
         25.1-jre
         4.5.13
@@ -163,12 +163,12 @@
             
             
                 org.apache.tinkerpop
-                gremlin-groovy-test
-                3.2.11
+                gremlin-driver
+                ${tinkerpop.version}
             
             
                 org.apache.tinkerpop
-                gremlin-driver
+                gremlin-util
                 ${tinkerpop.version}
             
 
diff --git a/hugegraph-store/hg-store-core/pom.xml b/hugegraph-store/hg-store-core/pom.xml
index 0ecf723280..21278f002a 100644
--- a/hugegraph-store/hg-store-core/pom.xml
+++ b/hugegraph-store/hg-store-core/pom.xml
@@ -29,6 +29,10 @@
 
     hg-store-core
 
+    
+        3.7.6
+    
+
     
         
             org.apache.hugegraph
@@ -129,7 +133,7 @@
         
             org.apache.tinkerpop
             gremlin-core
-            3.5.1
+            ${tinkerpop.version}
             
                 
                     org.yaml
@@ -152,7 +156,7 @@
         
             org.apache.tinkerpop
             gremlin-groovy
-            3.5.1
+            ${tinkerpop.version}
             
                 
                     com.github.jeremyh
diff --git a/hugegraph-struct/pom.xml b/hugegraph-struct/pom.xml
index b88d0ae204..78404f939b 100644
--- a/hugegraph-struct/pom.xml
+++ b/hugegraph-struct/pom.xml
@@ -34,7 +34,7 @@
         11
         UTF-8
         25.1-jre
-        3.5.1
+        3.7.6
     
 
     
@@ -50,10 +50,17 @@
             3.0.0
         
 
+        
+            org.apache.tinkerpop
+            gremlin-core
+            ${tinkerpop.version}
+        
+
         
             org.apache.tinkerpop
             gremlin-test
             ${tinkerpop.version}
+            test
         
 
         
@@ -86,7 +93,7 @@
         
             org.apache.tinkerpop
             gremlin-shaded
-            3.5.1
+            ${tinkerpop.version}
         
         
             org.mindrot
diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Condition.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Condition.java
index 0d1b7ad05b..e2fe822c9b 100644
--- a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Condition.java
+++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Condition.java
@@ -30,10 +30,10 @@
 import org.apache.hugegraph.util.DateUtil;
 import org.apache.hugegraph.util.E;
 import org.apache.hugegraph.util.NumericUtil;
+import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate;
 
 import java.util.*;
 import java.util.function.BiFunction;
-import java.util.function.BiPredicate;
 import java.util.regex.Pattern;
 
 public abstract class Condition {
@@ -199,7 +199,7 @@ public enum ConditionType {
         NOT
     }
 
-    public enum RelationType implements BiPredicate {
+    public enum RelationType implements PBiPredicate {
 
         EQ("==", RelationType::equals),
 
@@ -522,6 +522,11 @@ public String string() {
             return this.operator;
         }
 
+        @Override
+        public String getPredicateName() {
+            return this.operator;
+        }
+
         private void checkBaseType(Object value, Class clazz) {
             if (!clazz.isInstance(value)) {
                 String valueClass = value == null ? "null" :
diff --git a/pom.xml b/pom.xml
index 045a24ad56..a159df1c7e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -88,6 +88,8 @@
     
         5.6.0
         1.7.0
+        4.0.25
+        1.28
         1.18.30
         hugegraph
         11
@@ -109,6 +111,18 @@
 
     
         
+            
+                org.apache.groovy
+                groovy-bom
+                ${groovy.version}
+                pom
+                import
+            
+            
+                org.yaml
+                snakeyaml
+                ${snakeyaml.version}
+            
             
                 org.projectlombok
                 lombok

From 848d334a28c732fe3c079cd1ec33c31c7537a318 Mon Sep 17 00:00:00 2001
From: contrueCT 
Date: Fri, 7 Aug 2026 22:37:02 +0800
Subject: [PATCH 2/4] feat(server): handle tp-3.7.6 compatibility gaps (#163)

- Connects TinkerPop 3.7.6's official Gherkin features to HugeGraph's provider lifecycle, with an exact 345-scenario guard and precise capability skips.
- Preserves transaction-visible vertex and edge counts, and keeps `TextP` filtering local when the backend cannot safely push it down.
- Encodes HugeGraph IDs as standard GraphBinary primitives and delegates `Path` and `Tree` GraphSON handling to TinkerPop 3.7.6.
- Hardens factory registration, provider cleanup, server/cluster test infrastructure, and upgrade-branch CI coverage.
---
 .github/workflows/pd-store-ci.yml             | 207 ++++++-
 .github/workflows/server-ci.yml               |  15 +-
 .../conf/rest-server.properties.template      |   2 +-
 .../org/apache/hugegraph/ct/base/EnvUtil.java |  13 +
 .../ct/config/GremlinServerConfig.java        |  69 +++
 .../hugegraph/ct/config/ServerConfig.java     |   5 +-
 .../apache/hugegraph/ct/env/AbstractEnv.java  |  33 +-
 .../org/apache/hugegraph/ct/env/BaseEnv.java  |   2 +
 .../ct/node/AbstractNodeWrapper.java          |  18 +-
 .../hugegraph/ct/node/ServerNodeWrapper.java  |  31 +
 .../apache/hugegraph/ct/base/EnvUtilTest.java |  37 ++
 .../ct/config/GremlinServerConfigTest.java    |  52 ++
 .../ct/node/AbstractNodeWrapperTest.java      | 156 +++++
 .../BaseMultiClusterTest.java                 |  19 +-
 hugegraph-pd/hg-pd-test/pom.xml               |   6 -
 .../hugegraph/pd/client/PDClientTest.java     |  12 +-
 .../backend/serializer/SerializerFactory.java |  10 +-
 .../backend/store/BackendProviderFactory.java |  10 +-
 .../backend/tx/GraphTransaction.java          |  39 +-
 .../hugegraph/io/HugeGraphSONModule.java      |   8 +-
 ...ugeGraphTypeSerializerRegistryBuilder.java |  72 +++
 .../traversal/optimize/TraversalUtil.java     | 223 ++++++-
 .../org/apache/hugegraph/util/JsonUtil.java   |   1 +
 .../assembly/static/bin/start-hugegraph.sh    |  25 +-
 .../assembly/static/conf/gremlin-server.yaml  |   1 +
 .../src/assembly/travis/ci-service-utils.sh   |  31 +
 .../travis/conf-raft1/gremlin-server.yaml     |   1 +
 .../travis/conf-raft2/gremlin-server.yaml     |   1 +
 .../travis/conf-raft3/gremlin-server.yaml     |   1 +
 .../src/assembly/travis/run-tinkerpop-test.sh |  69 ++-
 .../assembly/travis/test-ci-service-utils.sh  |  69 +++
 .../travis/test-start-hugegraph-signal.sh     | 107 ++++
 .../assembly/travis/test-start-hugegraph.sh   |   9 +-
 hugegraph-server/hugegraph-test/pom.xml       |   8 +
 .../apache/hugegraph/api/MetricsApiTest.java  |  10 +-
 .../apache/hugegraph/core/CoreTestSuite.java  |   1 +
 .../hugegraph/core/CountStrategyCoreTest.java | 238 ++++++++
 .../core/TinkerPop37StepsCoreTest.java        | 438 ++++++++++++++
 .../tinkerpop/HugeGraphFeatureTest.java       |  95 +++
 .../tinkerpop/HugeGraphProviderContext.java   |  47 ++
 .../HugeGraphProviderLifecycleTest.java       |  65 ++
 .../HugeGraphScenarioCountPlugin.java         |  63 ++
 .../HugeGraphTestInfrastructureTest.java      | 142 +++++
 .../hugegraph/tinkerpop/HugeGraphWorld.java   | 198 ++++++
 .../apache/hugegraph/tinkerpop/TestGraph.java |  56 +-
 .../tinkerpop/TestGraphProvider.java          |   9 +-
 .../optimize/TraversalUtilOptimizeTest.java   | 101 +++-
 .../apache/hugegraph/unit/UnitTestSuite.java  |   4 +
 .../GremlinConfigCompatibilityTest.java       | 572 +++++++++++++++++-
 .../unit/core/BackendProviderFactoryTest.java | 108 ++++
 .../serializer/SerializerFactoryTest.java     |  74 ++-
 .../hugegraph/unit/util/JsonUtilTest.java     |  29 +
 .../io.cucumber.core.backend.ObjectFactory    |   1 +
 .../src/main/resources/fast-methods.filter    |  31 +-
 .../src/main/resources/methods.filter         |  31 +-
 .../store/business/BusinessHandlerImpl.java   |   4 +-
 .../hugegraph/store/meta/GraphIdManager.java  | 221 ++++++-
 .../node/controller/FixGraphIdController.java |  14 +-
 .../controller/FixGraphIdControllerTest.java  |  63 ++
 hugegraph-store/hg-store-test/pom.xml         |   2 +
 .../store/core/BatchGraphIsolationTest.java   | 188 ++++++
 .../store/meta/GraphIDManagerTest.java        | 363 +++++++++--
 62 files changed, 4291 insertions(+), 239 deletions(-)
 create mode 100644 hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/GremlinServerConfig.java
 create mode 100644 hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/base/EnvUtilTest.java
 create mode 100644 hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/config/GremlinServerConfigTest.java
 create mode 100644 hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/node/AbstractNodeWrapperTest.java
 create mode 100644 hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java
 create mode 100644 hugegraph-server/hugegraph-dist/src/assembly/travis/test-ci-service-utils.sh
 create mode 100644 hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-signal.sh
 create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TinkerPop37StepsCoreTest.java
 create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphFeatureTest.java
 create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderContext.java
 create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderLifecycleTest.java
 create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphScenarioCountPlugin.java
 create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphTestInfrastructureTest.java
 create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphWorld.java
 create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendProviderFactoryTest.java
 create mode 100644 hugegraph-server/hugegraph-test/src/main/resources/META-INF/services/io.cucumber.core.backend.ObjectFactory
 create mode 100644 hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/FixGraphIdControllerTest.java
 create mode 100644 hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/BatchGraphIsolationTest.java

diff --git a/.github/workflows/pd-store-ci.yml b/.github/workflows/pd-store-ci.yml
index a12cd7a2a1..aa6ab8b1a5 100644
--- a/.github/workflows/pd-store-ci.yml
+++ b/.github/workflows/pd-store-ci.yml
@@ -295,7 +295,17 @@ jobs:
       TRAVIS_DIR: hugegraph-server/hugegraph-dist/src/assembly/travis
       REPORT_DIR: target/site/jacoco
       BACKEND: hstore
-      RELEASE_BRANCH: ${{ startsWith(github.ref_name, 'release-') || startsWith(github.ref_name, 'test-') || startsWith(github.base_ref, 'release-') }}
+      RUN_TINKERPOP_TESTS: >-
+        ${{
+          startsWith(github.ref_name, 'release-') ||
+          startsWith(github.ref_name, 'test-') ||
+          startsWith(github.head_ref, 'release-') ||
+          startsWith(github.head_ref, 'test-') ||
+          startsWith(github.base_ref, 'release-') ||
+          startsWith(github.base_ref, 'test-') ||
+          github.base_ref == 'task/tinkerpop-3.7-upgrade' ||
+          github.head_ref == 'task/tinkerpop-3.7-upgrade'
+        }}
 
     steps:
       - name: Install JDK 11
@@ -329,6 +339,39 @@ jobs:
 
       - name: Prepare env and service
         run: |
+          VERSION=$(grep -E '^VersionInBash=' \
+            hugegraph-commons/hugegraph-common/src/main/resources/version.properties | cut -d'=' -f2-)
+          [[ "$VERSION" =~ ^[0-9A-Za-z._-]+$ ]] || {
+            echo "Invalid VersionInBash: $VERSION"
+            exit 1
+          }
+          STORE_CONFIG=hugegraph-store/apache-hugegraph-store-$VERSION/conf/application-pd.yml
+          [[ -f "$STORE_CONFIG" ]] || {
+            echo "Store config not found: $STORE_CONFIG"
+            exit 1
+          }
+          [[ $(grep -Ec '^[[:space:]]*min_write_buffer_number_to_merge:' \
+                       "$STORE_CONFIG") -eq 1 ]] || {
+            echo "Unable to locate RocksDB config insertion point: $STORE_CONFIG"
+            exit 1
+          }
+          # Bound file-handle caching for this CI topology without changing
+          # the released Store default for deployments with different limits.
+          sed -i \
+            -e '/^[[:space:]]*max_open_files:/d' \
+            -e '/^[[:space:]]*min_write_buffer_number_to_merge:/a\  max_open_files: 4096' \
+            "$STORE_CONFIG"
+          grep -n 'max_open_files' "$STORE_CONFIG"
+
+          SOFT_OPEN_FILES=$(ulimit -Sn)
+          echo "[ci] open-file limit before HStore startup:" \
+               "soft=$SOFT_OPEN_FILES, hard=$(ulimit -Hn)"
+          if [[ "$SOFT_OPEN_FILES" != "unlimited" ]] &&
+             (( SOFT_OPEN_FILES < 65535 )); then
+            ulimit -Sn 65535
+          fi
+          echo "[ci] open-file limit for HStore:" \
+               "soft=$(ulimit -Sn), hard=$(ulimit -Hn)"
           $TRAVIS_DIR/install-backend.sh $BACKEND
 
       - name: Run unit test
@@ -336,6 +379,9 @@ jobs:
           $TRAVIS_DIR/run-unit-test.sh $BACKEND
 
       - name: Run core test
+        # Bound stuck HStore scans so the diagnostic steps below can run
+        # before the GitHub-hosted job reaches its global execution limit.
+        timeout-minutes: 45
         run: |
           $TRAVIS_DIR/run-core-test.sh $BACKEND
 
@@ -348,39 +394,168 @@ jobs:
         run: |
           $TRAVIS_DIR/run-api-test-for-raft.sh $BACKEND $REPORT_DIR
 
-      # HStore remains covered by unit/core/API here; TinkerPop compliance is
-      # exercised in Server CI on memory and rocksdb.
+      # Include HStore in the upgrade compatibility gate. The dedicated HStore
+      # service setup above makes its structure/process suites exercise the
+      # distributed backend rather than only the memory and RocksDB paths.
       - name: Run TinkerPop structure test
-        if: ${{ env.RELEASE_BRANCH == 'true' && env.BACKEND != 'hstore' }}
-        timeout-minutes: 60
+        if: ${{ env.RUN_TINKERPOP_TESTS == 'true' }}
+        # The HStore suite needs close to one hour before global cleanup, so
+        # keep enough headroom to finish cleanup and emit Surefire results.
+        timeout-minutes: 120
         run: |
           $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND structure
 
-      - name: Run TinkerPop process test
-        if: ${{ env.RELEASE_BRANCH == 'true' && env.BACKEND != 'hstore' }}
-        timeout-minutes: 60
+      - name: Run TinkerPop process standard test
+        if: ${{ env.RUN_TINKERPOP_TESTS == 'true' }}
+        timeout-minutes: 120
         run: |
-          $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND process
+          $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND process-standard
 
-      - name: Show HStore diagnostics on failure
-        if: failure()
+      - name: Run TinkerPop process feature test
+        if: ${{ env.RUN_TINKERPOP_TESTS == 'true' }}
+        timeout-minutes: 120
         run: |
+          $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND process-feature
+
+      - name: Collect HStore diagnostics on failure or timeout
+        if: ${{ failure() || cancelled() }}
+        timeout-minutes: 10
+        run: |
+          set +e
+          DIAGNOSTICS_DIR=hstore-diagnostics
+          mkdir -p "$DIAGNOSTICS_DIR/threads" \
+                   "$DIAGNOSTICS_DIR/surefire" \
+                   "$DIAGNOSTICS_DIR/services"
+          exec > >(tee -a "$DIAGNOSTICS_DIR/summary.log") 2>&1
+
           VERSION=$(grep -E '^VersionInBash=' \
             hugegraph-commons/hugegraph-common/src/main/resources/version.properties | cut -d'=' -f2-)
           [[ "$VERSION" =~ ^[0-9A-Za-z._-]+$ ]] || {
             echo "Invalid VersionInBash: $VERSION"
-            exit 1
+            exit 0
           }
           PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION
           STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION
+          SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION
+
+          echo "::group::HugeGraphStore file descriptors"
+          STORE_PID_FILE="$STORE_DIR/bin/pid"
+          if [[ -f "$STORE_PID_FILE" ]]; then
+            STORE_PID=$(tr -d '[:space:]' < "$STORE_PID_FILE")
+            if [[ "$STORE_PID" =~ ^[0-9]+$ ]] &&
+               [[ -r "/proc/$STORE_PID/limits" ]] &&
+               [[ -d "/proc/$STORE_PID/fd" ]]; then
+              cat "/proc/$STORE_PID/limits" |
+                tee "$DIAGNOSTICS_DIR/services/store-process-limits.txt" || true
+
+              FD_TARGETS="$DIAGNOSTICS_DIR/services/store-fd-targets.txt"
+              find "/proc/$STORE_PID/fd" -mindepth 1 -maxdepth 1 -type l \
+                -printf '%l\n' >"$FD_TARGETS" \
+                2>"$DIAGNOSTICS_DIR/services/store-fd-errors.txt" || true
+              ls -l "/proc/$STORE_PID/fd" \
+                >"$DIAGNOSTICS_DIR/services/store-fds.txt" 2>&1 || true
+
+              STORE_FD_COUNT=$(wc -l < "$FD_TARGETS" | tr -d '[:space:]')
+              echo "[ci] Store PID $STORE_PID open fd count: $STORE_FD_COUNT" |
+                tee "$DIAGNOSTICS_DIR/services/store-fd-count.txt" || true
+
+              awk '
+                /\.sst/ { types["rocksdb-sst"]++; next }
+                /^socket:/ { types["socket"]++; next }
+                /^pipe:/ { types["pipe"]++; next }
+                /^anon_inode:/ { types["anon-inode"]++; next }
+                /\/storage\// { types["rocksdb-other"]++; next }
+                { types["other"]++ }
+                END {
+                  for (type in types) {
+                    print types[type], type
+                  }
+                }
+              ' "$FD_TARGETS" | sort -nr |
+                tee "$DIAGNOSTICS_DIR/services/store-fd-types.txt" || true
+
+              sort "$FD_TARGETS" | uniq -c | sort -nr \
+                >"$DIAGNOSTICS_DIR/services/store-fd-target-counts.txt" || true
+              echo "[ci] Store top fd targets:"
+              head -n 50 \
+                "$DIAGNOSTICS_DIR/services/store-fd-target-counts.txt" || true
+            else
+              echo "[ci] Store PID is unavailable for fd diagnostics: $STORE_PID"
+            fi
+          else
+            echo "[ci] Store pid file not found: $STORE_PID_FILE"
+          fi
+          echo "::endgroup::"
+
+          echo "::group::Runner process snapshot"
+          ps -eo pid,ppid,stat,etime,%cpu,%mem,args --sort=pid |
+            tee "$DIAGNOSTICS_DIR/processes.txt" || true
+          (ss -ltnp || netstat -ltnp || true) 2>&1 |
+            tee "$DIAGNOSTICS_DIR/listening-ports.txt" || true
+          echo "::endgroup::"
+
+          echo "::group::Java thread dumps"
+          if command -v jcmd >/dev/null 2>&1; then
+            jcmd -l | tee "$DIAGNOSTICS_DIR/jcmd-list.txt" || true
+            while read -r pid _; do
+              [[ "$pid" =~ ^[0-9]+$ ]] || continue
+              kill -0 "$pid" >/dev/null 2>&1 || continue
+              thread_dump="$DIAGNOSTICS_DIR/threads/java-$pid.txt"
+              timeout 30s jcmd "$pid" Thread.print -l >"$thread_dump" 2>&1
+              if [[ $? -ne 0 ]] && command -v jstack >/dev/null 2>&1; then
+                timeout 30s jstack -l "$pid" >"$thread_dump" 2>&1 || true
+              fi
+              echo "[ci] captured Java thread dump: $thread_dump"
+            done < "$DIAGNOSTICS_DIR/jcmd-list.txt"
+          else
+            echo "[ci] jcmd is unavailable"
+          fi
+          echo "::endgroup::"
+
           bash $TRAVIS_DIR/ci-service-utils.sh dump "$PD_DIR" HugeGraphPD || true
           bash $TRAVIS_DIR/ci-service-utils.sh dump "$STORE_DIR" HugeGraphStore || true
-          find . -path '*/surefire-reports/*' -type f \
-            \( -name '*.txt' -o -name '*.xml' \) | sort | while read -r report; do
-            echo "--- tail -n 120 $report ---"
-            tail -n 120 "$report" || true
+          bash $TRAVIS_DIR/ci-service-utils.sh dump \
+            "$SERVER_DIR" HugeGraphServer || true
+
+          for service in \
+            "pd:$PD_DIR/logs" \
+            "store:$STORE_DIR/logs" \
+            "server:$SERVER_DIR/logs"; do
+            service_name="${service%%:*}"
+            log_dir="${service#*:}"
+            if [[ -d "$log_dir" ]]; then
+              mkdir -p "$DIAGNOSTICS_DIR/services/$service_name"
+              cp -a "$log_dir/." \
+                "$DIAGNOSTICS_DIR/services/$service_name/" || true
+            fi
           done
 
+          echo "::group::Surefire reports"
+          while read -r report; do
+            destination="$DIAGNOSTICS_DIR/surefire/${report#./}"
+            mkdir -p "$(dirname "$destination")"
+            cp "$report" "$destination" || true
+            echo "--- tail -n 200 $report ---"
+            tail -n 200 "$report" || true
+          done < <(
+            find . -path "./$DIAGNOSTICS_DIR" -prune -o \
+              -path '*/surefire-reports/*' -type f \
+              \( -name '*.txt' -o -name '*.xml' -o \
+                 -name '*.dump' -o -name '*.dumpstream' \) \
+              -print | sort
+          )
+          echo "::endgroup::"
+          exit 0
+
+      - name: Upload HStore diagnostics
+        if: ${{ failure() || cancelled() }}
+        uses: actions/upload-artifact@v4
+        with:
+          name: hstore-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}
+          path: hstore-diagnostics
+          if-no-files-found: warn
+          retention-days: 14
+
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v3.0.0
         with:
diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml
index 68177e397c..a139bd5e94 100644
--- a/.github/workflows/server-ci.yml
+++ b/.github/workflows/server-ci.yml
@@ -21,12 +21,14 @@ jobs:
       HEAD_BRANCH_NAME: ${{ github.head_ref }}
       BASE_BRANCH_NAME: ${{ github.base_ref }}
       TARGET_BRANCH_NAME: ${{ github.base_ref != '' && github.base_ref || github.ref_name }}
-      RELEASE_BRANCH: >-
+      RUN_TINKERPOP_TESTS: >-
         ${{
           startsWith(github.ref_name, 'release-') ||
           startsWith(github.ref_name, 'test-') ||
           startsWith(github.head_ref, 'release-') ||
-          startsWith(github.head_ref, 'test-')
+          startsWith(github.head_ref, 'test-') ||
+          github.base_ref == 'task/tinkerpop-3.7-upgrade' ||
+          github.head_ref == 'task/tinkerpop-3.7-upgrade'
         }}
       RAFT_MODE: ${{ startsWith(github.ref_name, 'raft-') || startsWith(github.head_ref, 'raft-') }}
 
@@ -95,6 +97,8 @@ jobs:
           mvn package -Dmaven.test.skip=true -pl hugegraph-server/hugegraph-dist -am -ntp
           VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
           SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION/
+          bash $TRAVIS_DIR/test-ci-service-utils.sh "$TRAVIS_DIR/ci-service-utils.sh"
+          bash $TRAVIS_DIR/test-start-hugegraph-signal.sh "$SERVER_DIR/bin/start-hugegraph.sh"
           $TRAVIS_DIR/test-start-hugegraph.sh $SERVER_DIR
 
       - name: Startup tests skipped (missing prerequisites)
@@ -128,15 +132,16 @@ jobs:
       # TinkerPop compliance is covered by memory and rocksdb in CI.
       # HBase still runs compile/unit/core/API because its full suite exceeds the CI budget.
       - name: Run TinkerPop structure test
-        if: ${{ env.RELEASE_BRANCH == 'true' && env.BACKEND != 'hbase' }}
+        if: ${{ env.RUN_TINKERPOP_TESTS == 'true' && env.BACKEND != 'hbase' }}
         timeout-minutes: 60
         run: |
           echo "[WARNING] Enter Tinkerpop Structure Test, current 'github.ref_name' is ${{ github.ref_name }}"
           $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND structure
 
       - name: Run TinkerPop process test
-        if: ${{ env.RELEASE_BRANCH == 'true' && env.BACKEND != 'hbase' }}
-        timeout-minutes: 60
+        if: ${{ env.RUN_TINKERPOP_TESTS == 'true' && env.BACKEND != 'hbase' }}
+        # TODO: split ProcessStandardTest and Gherkin into separate CI jobs.
+        timeout-minutes: 120
         run: |
           echo "[WARNING] Enter Tinkerpop Process Test, current 'github.ref_name' is ${{ github.ref_name }}"
           $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND process
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-dist/src/assembly/static/conf/rest-server.properties.template b/hugegraph-cluster-test/hugegraph-clustertest-dist/src/assembly/static/conf/rest-server.properties.template
index 01744ac2c0..302d20b0e8 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-dist/src/assembly/static/conf/rest-server.properties.template
+++ b/hugegraph-cluster-test/hugegraph-clustertest-dist/src/assembly/static/conf/rest-server.properties.template
@@ -19,7 +19,7 @@
 # could use '0.0.0.0' or specified (real)IP to expose external network access
 restserver.url=http://$REST_SERVER_ADDRESS$
 # gremlin server url, need to be consistent with host and port in gremlin-server.yaml
-#gremlinserver.url=http://$REST_SERVER_ADDRESS$
+gremlinserver.url=http://$GREMLIN_SERVER_ADDRESS$
 
 graphs=./conf/graphs
 
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/EnvUtil.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/EnvUtil.java
index 4d4bab3831..1c5f4c1357 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/EnvUtil.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/EnvUtil.java
@@ -18,7 +18,9 @@
 package org.apache.hugegraph.ct.base;
 
 import java.io.IOException;
+import java.net.InetSocketAddress;
 import java.net.ServerSocket;
+import java.net.Socket;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
@@ -30,6 +32,7 @@
 public class EnvUtil {
 
     private static final Logger LOG = HGTestLogger.UTIL_LOG;
+    private static final int PORT_CONNECT_TIMEOUT_MILLIS = 500;
     private static final Set ports = new HashSet<>();
 
     public static int getAvailablePort() {
@@ -48,6 +51,16 @@ public static int getAvailablePort() {
         }
     }
 
+    public static boolean isPortOpen(String host, int port) {
+        try (Socket socket = new Socket()) {
+            socket.connect(new InetSocketAddress(host, port),
+                           PORT_CONNECT_TIMEOUT_MILLIS);
+            return true;
+        } catch (IOException ignored) {
+            return false;
+        }
+    }
+
     public static void copyFileToDestination(Path source, Path destination) {
         try {
             ensureParentDirectoryExists(destination);
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/GremlinServerConfig.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/GremlinServerConfig.java
new file mode 100644
index 0000000000..eeba926cab
--- /dev/null
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/GremlinServerConfig.java
@@ -0,0 +1,69 @@
+/*
+ * 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.hugegraph.ct.config;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public final class GremlinServerConfig {
+
+    private static final Pattern HOST_SETTING =
+            Pattern.compile("^#?host\\s*:.*$");
+    private static final Pattern PORT_SETTING =
+            Pattern.compile("^#?port\\s*:.*$");
+
+    private GremlinServerConfig() {
+        throw new IllegalStateException("Utility class");
+    }
+
+    public static void update(Path configPath, String host, int port) {
+        if (port < 0 || port > 65535) {
+            throw new IllegalArgumentException("Invalid Gremlin port: " + port);
+        }
+
+        try {
+            List lines = Files.readAllLines(configPath,
+                                                    StandardCharsets.UTF_8);
+            boolean hostUpdated = false;
+            boolean portUpdated = false;
+            for (int i = 0; i < lines.size(); i++) {
+                String line = lines.get(i);
+                if (HOST_SETTING.matcher(line).matches()) {
+                    lines.set(i, "host: " + host);
+                    hostUpdated = true;
+                } else if (PORT_SETTING.matcher(line).matches()) {
+                    lines.set(i, "port: " + port);
+                    portUpdated = true;
+                }
+            }
+
+            if (!hostUpdated || !portUpdated) {
+                throw new IllegalStateException(
+                        "Missing host or port setting in " + configPath);
+            }
+            Files.write(configPath, lines, StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            throw new IllegalStateException(
+                    "Failed to update Gremlin server config " + configPath, e);
+        }
+    }
+}
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ServerConfig.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ServerConfig.java
index 569a11dddf..1cfb5f5897 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ServerConfig.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ServerConfig.java
@@ -32,13 +32,17 @@ public class ServerConfig extends AbstractConfig {
 
     private final int rpcPort;
     private final int restPort;
+    private final int gremlinPort;
 
     public ServerConfig() {
         readTemplate(Paths.get(CONFIG_FILE_PATH + SERVER_TEMPLATE_FILE));
         this.fileName = SERVER_PROPERTIES;
         this.rpcPort = getAvailablePort();
         this.restPort = getAvailablePort();
+        this.gremlinPort = getAvailablePort();
         properties.put("REST_SERVER_ADDRESS", LOCALHOST + ":" + this.restPort);
+        properties.put("GREMLIN_SERVER_ADDRESS",
+                       LOCALHOST + ":" + this.gremlinPort);
         properties.put("RPC_PORT", String.valueOf(this.rpcPort));
     }
 
@@ -51,4 +55,3 @@ public void setRole(String role) {
     }
 }
 
-
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java
index e85a138bfe..ccf0582a00 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java
@@ -75,9 +75,12 @@ protected void init(int pdCnt, int storeCnt, int serverCnt) {
         }
 
         for (int i = 0; i < serverCnt; i++) {
-            ServerNodeWrapper serverNodeWrapper = new ServerNodeWrapper(cluster_id, i);
-            serverNodeWrappers.add(serverNodeWrapper);
             ServerConfig serverConfig = clusterConfig.getServerConfig(i);
+            ServerNodeWrapper serverNodeWrapper =
+                    new ServerNodeWrapper(cluster_id, i,
+                                          serverConfig.getRestPort(),
+                                          serverConfig.getGremlinPort());
+            serverNodeWrappers.add(serverNodeWrapper);
             serverConfig.setServerID(serverNodeWrapper.getID());
             GraphConfig graphConfig = clusterConfig.getGraphConfig(i);
             if (i == 0) {
@@ -113,17 +116,17 @@ private static void waitUntilStarted(AbstractNodeWrapper nodeWrapper) {
         long deadline = System.nanoTime() +
                         TimeUnit.SECONDS.toNanos(NODE_START_TIMEOUT_SECONDS);
         while (System.nanoTime() < deadline) {
-            if (nodeWrapper.isStarted()) {
-                System.out.printf("[cluster-test] %s started%n",
-                                  nodeWrapper.getID());
-                return;
-            }
             if (!nodeWrapper.isAlive()) {
                 nodeWrapper.dumpLog();
                 throw new AssertionError(String.format(
                         "%s failed to start, process status: %s",
                         nodeWrapper.getID(), nodeWrapper.processStatus()));
             }
+            if (nodeWrapper.isStarted()) {
+                System.out.printf("[cluster-test] %s started%n",
+                                  nodeWrapper.getID());
+                return;
+            }
             sleepBeforeRetry();
         }
 
@@ -155,6 +158,22 @@ public void stopCluster() {
         }
     }
 
+    public void dumpClusterStatus() {
+        System.out.println("===== cluster node diagnostics =====");
+        dumpNodeStatus(pdNodeWrappers);
+        dumpNodeStatus(storeNodeWrappers);
+        dumpNodeStatus(serverNodeWrappers);
+    }
+
+    private static void dumpNodeStatus(
+            List nodeWrappers) {
+        for (AbstractNodeWrapper nodeWrapper : nodeWrappers) {
+            System.out.printf("[cluster-test] %s process status: %s%n",
+                              nodeWrapper.getID(), nodeWrapper.processStatus());
+            nodeWrapper.dumpLog();
+        }
+    }
+
     public ClusterConfig getConf() {
         return this.clusterConfig;
     }
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/BaseEnv.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/BaseEnv.java
index f6c4ba5fb6..422901959b 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/BaseEnv.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/BaseEnv.java
@@ -29,6 +29,8 @@ public interface BaseEnv {
     /* clear the cluster env and all config*/
     void stopCluster();
 
+    void dumpClusterStatus();
+
     ClusterConfig getConf();
 
     void init();
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java
index 97585756a1..81d3a73923 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java
@@ -20,8 +20,6 @@
 import static org.apache.hugegraph.ct.base.ClusterConstant.CT_PACKAGE_PATH;
 
 import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -147,12 +145,17 @@ public void updateConfigPath(String ConfigPath) {
 
     @Override
     public boolean isStarted() {
-        try (Scanner sc = new Scanner(new FileReader(getLogPath()))) {
+        if (!isAlive()) {
+            return false;
+        }
+
+        try (Scanner sc = new Scanner(Paths.get(getLogPath()),
+                                      StandardCharsets.UTF_8.name())) {
             while (sc.hasNextLine()) {
                 String line = sc.nextLine();
                 if (line.contains(startLine)) return true;
             }
-        } catch (FileNotFoundException ignored) {
+        } catch (IOException ignored) {
         }
         return false;
     }
@@ -161,6 +164,13 @@ public void stop() {
         if (this.instance == null) {
             return;
         }
+        if (!this.instance.isAlive()) {
+            System.out.printf("[cluster-test] %s stopped unexpectedly: %s%n",
+                              getID(), processStatus());
+            dumpLog();
+            deleteDir();
+            return;
+        }
         this.instance.destroy();
         try {
             if (!this.instance.waitFor(20, TimeUnit.SECONDS)) {
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java
index e16b96781e..5fb8584c6c 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java
@@ -26,6 +26,7 @@
 import static org.apache.hugegraph.ct.base.ClusterConstant.JAVA_CMD;
 import static org.apache.hugegraph.ct.base.ClusterConstant.LIB_DIR;
 import static org.apache.hugegraph.ct.base.ClusterConstant.LOG4J_FILE;
+import static org.apache.hugegraph.ct.base.ClusterConstant.LOCALHOST;
 import static org.apache.hugegraph.ct.base.ClusterConstant.PLUGINS_DIR;
 import static org.apache.hugegraph.ct.base.ClusterConstant.REMOTE_OBJECTS_SETTING_FILE;
 import static org.apache.hugegraph.ct.base.ClusterConstant.REMOTE_SETTING_FILE;
@@ -33,6 +34,7 @@
 import static org.apache.hugegraph.ct.base.ClusterConstant.SERVER_PACKAGE_PATH;
 import static org.apache.hugegraph.ct.base.ClusterConstant.SERVER_TEMPLATE_PATH;
 import static org.apache.hugegraph.ct.base.ClusterConstant.isJava11OrHigher;
+import static org.apache.hugegraph.ct.base.EnvUtil.isPortOpen;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -46,16 +48,37 @@
 import java.util.Collections;
 import java.util.List;
 
+import org.apache.hugegraph.ct.config.GremlinServerConfig;
+
 public class ServerNodeWrapper extends AbstractNodeWrapper {
 
     private static List hgJars = loadHgJarsOnce();
+    private final int restPort;
+    private final int gremlinPort;
+
     public ServerNodeWrapper(int clusterIndex, int index) {
+        this(clusterIndex, index, -1, -1);
+    }
+
+    public ServerNodeWrapper(int clusterIndex, int index, int restPort) {
+        this(clusterIndex, index, restPort, -1);
+    }
+
+    public ServerNodeWrapper(int clusterIndex, int index, int restPort,
+                             int gremlinPort) {
         super(clusterIndex, index);
+        this.restPort = restPort;
+        this.gremlinPort = gremlinPort;
         this.fileNames = new ArrayList<>(
                 List.of(LOG4J_FILE, GREMLIN_SERVER_FILE, GREMLIN_DRIVER_SETTING_FILE,
                         REMOTE_SETTING_FILE, REMOTE_OBJECTS_SETTING_FILE));
         this.workPath = SERVER_LIB_PATH;
         createNodeDir(Paths.get(SERVER_TEMPLATE_PATH), getNodePath() + CONF_DIR + File.separator);
+        if (this.gremlinPort >= 0) {
+            GremlinServerConfig.update(
+                    Paths.get(getNodePath(), CONF_DIR, GREMLIN_SERVER_FILE),
+                    LOCALHOST, this.gremlinPort);
+        }
         this.fileNames = new ArrayList<>(List.of(EMPTY_SAMPLE_GROOVY_FILE, EXAMPLE_GROOVY_FILE));
         this.startLine = "INFO: [HttpServer] Started.";
         createNodeDir(Paths.get(SERVER_PACKAGE_PATH), getNodePath());
@@ -138,6 +161,14 @@ public void start() {
         }
     }
 
+    @Override
+    public boolean isStarted() {
+        return super.isStarted() &&
+               (this.restPort < 0 || isPortOpen(LOCALHOST, this.restPort)) &&
+               (this.gremlinPort < 0 ||
+                isPortOpen(LOCALHOST, this.gremlinPort));
+    }
+
     @Override
     public String getID() {
         return "Server" + this.index;
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/base/EnvUtilTest.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/base/EnvUtilTest.java
new file mode 100644
index 0000000000..00fc93bf8d
--- /dev/null
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/base/EnvUtilTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.hugegraph.ct.base;
+
+import java.net.ServerSocket;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class EnvUtilTest {
+
+    @Test
+    public void testPortOpen() throws Exception {
+        int port;
+        try (ServerSocket socket = new ServerSocket(0)) {
+            port = socket.getLocalPort();
+            Assert.assertTrue(EnvUtil.isPortOpen("127.0.0.1", port));
+        }
+
+        Assert.assertFalse(EnvUtil.isPortOpen("127.0.0.1", port));
+    }
+}
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/config/GremlinServerConfigTest.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/config/GremlinServerConfigTest.java
new file mode 100644
index 0000000000..ae051b4d0a
--- /dev/null
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/config/GremlinServerConfigTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.hugegraph.ct.config;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class GremlinServerConfigTest {
+
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    @Test
+    public void testUpdateCommentedHostAndPort() throws Exception {
+        File config = temporaryFolder.newFile("gremlin-server.yaml");
+        String content = "#host: 127.0.0.1\n" +
+                         "#port: 8182\n" +
+                         "evaluationTimeout: 30000\n";
+        Files.write(config.toPath(), content.getBytes(StandardCharsets.UTF_8));
+
+        GremlinServerConfig.update(config.toPath(), "127.0.0.1", 12345);
+
+        List updated = Files.readAllLines(config.toPath(),
+                                                  StandardCharsets.UTF_8);
+        Assert.assertTrue(updated.toString(),
+                          updated.contains("host: 127.0.0.1"));
+        Assert.assertTrue(updated.toString(), updated.contains("port: 12345"));
+        Assert.assertFalse(updated.toString(), updated.contains("port: 8182"));
+    }
+}
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/node/AbstractNodeWrapperTest.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/node/AbstractNodeWrapperTest.java
new file mode 100644
index 0000000000..2d452f04d3
--- /dev/null
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/node/AbstractNodeWrapperTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.hugegraph.ct.node;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.PrintStream;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class AbstractNodeWrapperTest {
+
+    private static final String START_LINE = "node is ready";
+
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    @Test
+    public void testStartedRequiresLiveProcess() throws Exception {
+        TestNodeWrapper wrapper = new TestNodeWrapper(newNodeDirectory());
+        Files.write(Paths.get(wrapper.getLogPath()),
+                    START_LINE.getBytes(StandardCharsets.UTF_8));
+
+        Assert.assertFalse(wrapper.isStarted());
+
+        Process process = startSleeperProcess();
+        wrapper.attach(process);
+        try {
+            Assert.assertTrue(process.isAlive());
+            Assert.assertTrue(wrapper.isStarted());
+        } finally {
+            process.destroyForcibly();
+            process.waitFor(10, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testStopDumpsLogOfUnexpectedlyExitedProcess() throws Exception {
+        TestNodeWrapper wrapper = new TestNodeWrapper(newNodeDirectory());
+        String failure = "fatal startup failure";
+        Files.write(Paths.get(wrapper.getLogPath()),
+                    failure.getBytes(StandardCharsets.UTF_8));
+        Process process = startExitedProcess();
+        wrapper.attach(process);
+
+        PrintStream originalOut = System.out;
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        try {
+            System.setOut(new PrintStream(output, true,
+                                          StandardCharsets.UTF_8.name()));
+            wrapper.stop();
+        } finally {
+            System.setOut(originalOut);
+        }
+
+        String diagnostics = output.toString(StandardCharsets.UTF_8.name());
+        Assert.assertTrue(diagnostics, diagnostics.contains(failure));
+    }
+
+    private File newNodeDirectory() throws Exception {
+        File nodeDirectory = temporaryFolder.newFolder();
+        File logDirectory = new File(nodeDirectory, "logs");
+        Assert.assertTrue(logDirectory.mkdir());
+        return nodeDirectory;
+    }
+
+    private static Process startSleeperProcess() throws Exception {
+        String java = javaExecutable();
+        String classpath = testClassesPath();
+        return new ProcessBuilder(java, "-cp", classpath,
+                                  Sleeper.class.getName()).start();
+    }
+
+    private static Process startExitedProcess() throws Exception {
+        Process process = new ProcessBuilder(javaExecutable(), "-version").start();
+        Assert.assertTrue(process.waitFor(10, TimeUnit.SECONDS));
+        return process;
+    }
+
+    private static String javaExecutable() {
+        String executable = System.getProperty("os.name").startsWith("Windows") ?
+                            "java.exe" : "java";
+        return Paths.get(System.getProperty("java.home"), "bin", executable)
+                    .toString();
+    }
+
+    private static String testClassesPath() throws URISyntaxException {
+        return Paths.get(AbstractNodeWrapperTest.class.getProtectionDomain()
+                                                     .getCodeSource()
+                                                     .getLocation()
+                                                     .toURI())
+                    .toString();
+    }
+
+    public static class Sleeper {
+
+        public static void main(String[] args) throws Exception {
+            TimeUnit.SECONDS.sleep(30);
+        }
+    }
+
+    private static class TestNodeWrapper extends AbstractNodeWrapper {
+
+        private File nodeDirectory;
+
+        private TestNodeWrapper(File nodeDirectory) {
+            this.nodeDirectory = nodeDirectory;
+            this.startLine = START_LINE;
+        }
+
+        private void attach(Process process) {
+            this.instance = process;
+        }
+
+        @Override
+        public void start() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public String getID() {
+            return "TestNode";
+        }
+
+        @Override
+        public String getNodePath() {
+            if (this.nodeDirectory == null) {
+                return System.getProperty("java.io.tmpdir") + File.separator;
+            }
+            return this.nodeDirectory.getAbsolutePath() + File.separator;
+        }
+    }
+}
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/MultiClusterTest/BaseMultiClusterTest.java b/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/MultiClusterTest/BaseMultiClusterTest.java
index 9e90933026..fa5d4501eb 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/MultiClusterTest/BaseMultiClusterTest.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/MultiClusterTest/BaseMultiClusterTest.java
@@ -115,8 +115,21 @@ protected static String assertResponseStatus(int status,
     public static Response createAndAssert(RestClient client, String path,
                                            String body,
                                            int status) {
-        Response r = client.post(path, body);
-        assertResponseStatus(status, r);
-        return r;
+        try {
+            Response r = client.post(path, body);
+            assertResponseStatus(status, r);
+            return r;
+        } catch (RuntimeException | AssertionError e) {
+            System.out.printf("[cluster-test] POST %s%s failed: %s%n",
+                              client.target().getUri(), path, e.getMessage());
+            try {
+                env.dumpClusterStatus();
+            } catch (Throwable diagnosticError) {
+                if (diagnosticError != e) {
+                    e.addSuppressed(diagnosticError);
+                }
+            }
+            throw e;
+        }
     }
 }
diff --git a/hugegraph-pd/hg-pd-test/pom.xml b/hugegraph-pd/hg-pd-test/pom.xml
index 44d5d084ca..a2a7c67873 100644
--- a/hugegraph-pd/hg-pd-test/pom.xml
+++ b/hugegraph-pd/hg-pd-test/pom.xml
@@ -190,12 +190,6 @@
             2.0.0-RC.3
             compile
         
-        
-            org.apache.tinkerpop
-            gremlin-shaded
-            3.5.1
-            compile
-        
     
 
     
diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/client/PDClientTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/client/PDClientTest.java
index 3676122612..3e7bbda39a 100644
--- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/client/PDClientTest.java
+++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/client/PDClientTest.java
@@ -21,7 +21,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import org.apache.tinkerpop.shaded.minlog.Log;
+import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.Mockito;
 
@@ -323,14 +323,12 @@ public void testChangePeerList() {
     @Test
     public void testSplitData() {
         try {
-            Metapb.PDConfig config = pdClient.getPDConfig();
-            pdClient.setPDConfig(config.toBuilder()
-                                       .setMaxShardsPerStore(12)
-                                       .build());
-            System.out.println(pdClient.getPDConfig());
             pdClient.splitData();
+            Assert.fail("Expected splitData() to reject an unready cluster");
         } catch (PDException e) {
-            Log.error("testSplitData", e);
+            Assert.assertEquals(
+                    Pdpb.ErrorType.Cluster_State_Forbid_Splitting_VALUE,
+                    e.getErrorCode());
         }
     }
 
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SerializerFactory.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SerializerFactory.java
index db5fe0a8cd..213cf2b264 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SerializerFactory.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SerializerFactory.java
@@ -72,13 +72,11 @@ public static void register(String name, String classPath) {
                                        "AbstractSerializer: '%s'", classPath);
         }
 
-        // Check exists
-        if (serializers.containsKey(name)) {
+        // Register atomically: identical re-registration is a no-op
+        Class registered = serializers.putIfAbsent(name, (Class) clazz);
+        if (registered != null && !registered.equals(clazz)) {
             throw new BackendException("Exists serializer: %s(Class '%s')",
-                                       name, serializers.get(name).getName());
+                                       name, registered.getName());
         }
-
-        // Register class
-        serializers.put(name, (Class) clazz);
     }
 }
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendProviderFactory.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendProviderFactory.java
index d3751c11ba..2bbcca708e 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendProviderFactory.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendProviderFactory.java
@@ -113,12 +113,10 @@ public static void register(String name, String classPath) {
         BackendException.check(subclass, "Class '%s' is not a subclass of " +
                                          "class BackendStoreProvider", classPath);
 
-        // Check exists
-        BackendException.check(!providers.containsKey(name),
+        // Register atomically: identical re-registration is a no-op
+        Class registered = providers.putIfAbsent(name, (Class) clazz);
+        BackendException.check(registered == null || registered.equals(clazz),
                                "Exists BackendStoreProvider: %s (%s)",
-                               name, providers.get(name));
-
-        // Register class
-        providers.put(name, (Class) clazz);
+                               name, registered);
     }
 }
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java
index a0792ee99e..7bbbd458ed 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java
@@ -551,24 +551,26 @@ public Number queryNumber(Query query) {
         boolean hasUpdate = this.hasUpdate();
         Aggregate aggregate = query.aggregateNotNull();
 
-        // TODO: we can concat index-query results and tx uncommitted records.
         if (hasUpdate) {
-            E.checkArgument(!isConditionQuery,
-                            "It's not allowed to query by index when " +
-                            "there are uncommitted records.");
+            E.checkArgument(aggregate.func() == AggregateFunc.COUNT,
+                            "The %s operator with uncommitted records " +
+                            "is not supported",
+                            aggregate.func().string());
+            Query queryWithoutAggregate = query.copy();
+            queryWithoutAggregate.aggregate(null);
+            Iterator results = queryWithoutAggregate.resultType().isVertex() ?
+                                  this.queryVertices(queryWithoutAggregate) :
+                                  this.queryEdges(queryWithoutAggregate);
+            return countAndClose(results);
         }
 
         QueryList queries = this.optimizeQueries(query, q -> {
             boolean isIndexQuery = q instanceof IdQuery;
             assert isIndexQuery || isConditionQuery || q == query;
-            // Need to fall back if there are uncommitted records
-            boolean fallback = hasUpdate;
+            boolean fallback = false;
             Number result;
 
-            if (fallback) {
-                // Here just ignore it, and do fall back later
-                result = null;
-            } else if (!isIndexQuery || !isConditionQuery) {
+            if (!isIndexQuery || !isConditionQuery) {
                 // It's a sysprop-query, let parent tx do it
                 assert !fallback;
                 result = super.queryNumber(q);
@@ -608,6 +610,19 @@ public Number queryNumber(Query query) {
         return aggregate.reduce(results.iterator());
     }
 
+    private static long countAndClose(Iterator results) {
+        try {
+            long count = 0L;
+            while (results.hasNext()) {
+                results.next();
+                count++;
+            }
+            return count;
+        } finally {
+            CloseableIterator.closeIterator(results);
+        }
+    }
+
     @Watched(prefix = "graph")
     public HugeVertex addVertex(Object... keyValues) {
         return this.addVertex(this.constructVertex(true, keyValues));
@@ -834,7 +849,7 @@ public Iterator queryVertices() {
     public Iterator queryVertices(Query query) {
         if (this.hasUpdate()) {
             E.checkArgument(query.noLimitAndOffset(),
-                            "It's not allowed to query with offser/limit " +
+                            "It's not allowed to query with offset/limit " +
                             "when there are uncommitted records.");
             // TODO: also add check: no SCAN, no OLAP
             E.checkArgument(!query.paging(),
@@ -1000,7 +1015,7 @@ public Iterator queryEdges() {
     public Iterator queryEdges(Query query) {
         if (this.hasUpdate()) {
             E.checkArgument(query.noLimitAndOffset(),
-                            "It's not allowed to query with offser/limit " +
+                            "It's not allowed to query with offset/limit " +
                             "when there are uncommitted records.");
             // TODO: also add check: no SCAN, no OLAP
             E.checkArgument(!query.paging(),
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
index 7866e076a5..f584046463 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
@@ -57,7 +57,6 @@
 import org.apache.hugegraph.util.SafeDateUtil;
 import org.apache.tinkerpop.gremlin.process.traversal.Path;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
-import org.apache.tinkerpop.gremlin.structure.Element;
 import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONIo;
 import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONTokens;
 import org.apache.tinkerpop.gremlin.structure.io.graphson.TinkerPopJacksonModule;
@@ -225,7 +224,9 @@ public static void registerGraphSerializers(SimpleModule module) {
          */
         module.addSerializer(HugeVertex.class, new HugeVertexSerializer());
         module.addSerializer(HugeEdge.class, new HugeEdgeSerializer());
+    }
 
+    public static void registerTraversalSerializers(SimpleModule module) {
         module.addSerializer(Path.class, new PathSerializer());
         module.addSerializer(Tree.class, new TreeSerializer());
     }
@@ -886,9 +887,8 @@ public TreeSerializer() {
         public void serialize(Tree tree, JsonGenerator jsonGenerator,
                               SerializerProvider provider) throws IOException {
             jsonGenerator.writeStartArray();
-            @SuppressWarnings("unchecked")
-            Set> set = tree.entrySet();
-            for (Map.Entry entry : set) {
+            for (Object item : tree.entrySet()) {
+                Map.Entry entry = (Map.Entry) item;
                 jsonGenerator.writeStartObject();
                 jsonGenerator.writeObjectField(GraphSONTokens.KEY,
                                                entry.getKey());
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java
new file mode 100644
index 0000000000..f46e4ee399
--- /dev/null
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java
@@ -0,0 +1,72 @@
+/*
+ * 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.hugegraph.io;
+
+import java.io.IOException;
+
+import org.apache.hugegraph.backend.id.Id;
+import org.apache.tinkerpop.gremlin.structure.io.Buffer;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
+import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializer;
+import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry;
+import org.apache.tinkerpop.gremlin.structure.io.binary.types.SimpleTypeSerializer;
+import org.apache.tinkerpop.gremlin.structure.io.binary.types.TransformSerializer;
+
+public final class HugeGraphTypeSerializerRegistryBuilder
+        extends TypeSerializerRegistry.Builder {
+
+    private static final TypeSerializer ID_TRANSFORM_SERIALIZER =
+            new IdTransformSerializer();
+
+    public HugeGraphTypeSerializerRegistryBuilder() {
+        this.withFallbackResolver(type -> {
+            if (Id.class.isAssignableFrom(type)) {
+                return ID_TRANSFORM_SERIALIZER;
+            }
+            return null;
+        });
+    }
+
+    private static final class IdTransformSerializer
+            extends SimpleTypeSerializer
+            implements TransformSerializer {
+
+        private IdTransformSerializer() {
+            super(null);
+        }
+
+        @Override
+        protected Id readValue(Buffer buffer, GraphBinaryReader context)
+                throws IOException {
+            throw new IOException("HugeGraph Id is written as a wire primitive");
+        }
+
+        @Override
+        protected void writeValue(Id value, Buffer buffer,
+                                  GraphBinaryWriter context)
+                throws IOException {
+            throw new IOException("HugeGraph Id is written as a wire primitive");
+        }
+
+        @Override
+        public Object transform(Id value) {
+            return value.asObject();
+        }
+    }
+}
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
index a59be46a21..ea53f55e8a 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
@@ -60,6 +60,7 @@
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate;
 import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.TextP;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
@@ -640,27 +641,63 @@ public static void extractHasContainer(HugeVertexStep newStep,
     private static boolean extractHasContainers(HugeGraphStep newStep,
                                                 HasContainerHolder holder) {
         HugeGraph graph = TraversalUtil.tryGetGraph(newStep);
-        if (!canExtractHasContainers(graph, holder)) {
+        if (canExtractHasContainers(graph, holder)) {
+            for (HasContainer has : holder.getHasContainers()) {
+                if (!GraphStep.processHasContainerIds(newStep, has)) {
+                    newStep.addHasContainer(has);
+                }
+            }
+            return true;
+        }
+        if (!canPartiallyExtractWithLocalTextPropertyPredicates(graph,
+                                                                holder) ||
+            hasUnsupportedLabelContainer(holder)) {
             return false;
         }
+
+        List extracted = new ArrayList<>();
         for (HasContainer has : holder.getHasContainers()) {
+            if (!canExtractHasContainer(graph, has)) {
+                continue;
+            }
+            if (!isSysProp(has.getKey()) &&
+                !hasUsablePartialIndex(graph, newStep, holder, has)) {
+                continue;
+            }
             if (!GraphStep.processHasContainerIds(newStep, has)) {
                 newStep.addHasContainer(has);
             }
+            extracted.add(has);
         }
-        return true;
+        removeExtractedHasContainers(holder, extracted);
+        return holder.getHasContainers().isEmpty();
     }
 
     private static boolean extractHasContainers(HugeVertexStep newStep,
                                                 HasContainerHolder holder) {
         HugeGraph graph = TraversalUtil.tryGetGraph(newStep);
-        if (!canExtractHasContainers(graph, holder)) {
+        if (canExtractHasContainers(graph, holder)) {
+            for (HasContainer has : holder.getHasContainers()) {
+                newStep.addHasContainer(has);
+            }
+            return true;
+        }
+        if (!canPartiallyExtractWithLocalTextPropertyPredicates(graph,
+                                                                holder) ||
+            hasUnsupportedLabelContainer(holder)) {
             return false;
         }
+
+        List extracted = new ArrayList<>();
         for (HasContainer has : holder.getHasContainers()) {
+            if (!canExtractHasContainer(graph, has)) {
+                continue;
+            }
             newStep.addHasContainer(has);
+            extracted.add(has);
         }
-        return true;
+        removeExtractedHasContainers(holder, extracted);
+        return holder.getHasContainers().isEmpty();
     }
 
     private static boolean canExtractHasContainers(HugeGraph graph,
@@ -673,9 +710,174 @@ private static boolean canExtractHasContainers(HugeGraph graph,
         return true;
     }
 
+    private static boolean canPartiallyExtractWithLocalTextPropertyPredicates(
+            HugeGraph graph, HasContainerHolder holder) {
+        boolean seenLocalTextPropertyPredicate = false;
+        for (HasContainer has : holder.getHasContainers()) {
+            if (canExtractHasContainer(graph, has)) {
+                continue;
+            }
+            if (!isLocalTextPropertyPredicate(graph, has)) {
+                return false;
+            }
+            seenLocalTextPropertyPredicate = true;
+        }
+        return seenLocalTextPropertyPredicate;
+    }
+
+    private static boolean isLocalTextPropertyPredicate(HugeGraph graph,
+                                                        HasContainer has) {
+        if (graph == null || has.getKey() == null ||
+            has.getPredicate() == null || isSysProp(has.getKey()) ||
+            hasNullPredicate(has)) {
+            return false;
+        }
+
+        try {
+            PropertyKey pkey = graph.propertyKey(has.getKey());
+            return pkey != null && pkey.dataType().isText();
+        } catch (NotFoundException e) {
+            return false;
+        }
+    }
+
+    private static boolean hasUsablePartialIndex(HugeGraph graph,
+                                                 HugeGraphStep step,
+                                                 HasContainerHolder holder,
+                                                 HasContainer has) {
+        if (graph == null || hasNonIndexablePredicate(has)) {
+            return false;
+        }
+
+        PropertyKey pkey;
+        try {
+            pkey = graph.propertyKey(has.getKey());
+        } catch (NotFoundException e) {
+            return false;
+        }
+
+        Collection schemaLabels =
+                partialQuerySchemaLabels(graph, step, holder);
+        boolean seen = false;
+        for (SchemaLabel schemaLabel : schemaLabels) {
+            if (!schemaLabel.properties().contains(pkey.id())) {
+                continue;
+            }
+            seen = true;
+            if (!hasSingleFieldQueryIndex(graph, schemaLabel, pkey, has)) {
+                return false;
+            }
+        }
+        return seen;
+    }
+
+    private static boolean hasNonIndexablePredicate(HasContainer has) {
+        List> predicates = new ArrayList<>();
+        collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
+        for (P predicate : predicates) {
+            PBiPredicate bp = predicate.getBiPredicate();
+            if (bp == Compare.neq || bp == Contains.without) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static Collection partialQuerySchemaLabels(
+            HugeGraph graph, HugeGraphStep step,
+            HasContainerHolder holder) {
+        List labels = new ArrayList<>();
+        collectPositiveLabelValues(step, labels);
+        collectPositiveLabelValues(holder, labels);
+        if (labels.isEmpty()) {
+            List schemaLabels = new ArrayList<>();
+            if (step.returnsVertex()) {
+                schemaLabels.addAll(graph.vertexLabels());
+            } else {
+                schemaLabels.addAll(graph.edgeLabels());
+            }
+            return schemaLabels;
+        }
+
+        List schemaLabels = new ArrayList<>();
+        try {
+            for (Object label : labels) {
+                SchemaLabel schemaLabel;
+                if (label instanceof Id) {
+                    schemaLabel = step.returnsVertex() ?
+                                  graph.vertexLabel((Id) label) :
+                                  graph.edgeLabel((Id) label);
+                } else if (label instanceof String) {
+                    schemaLabel = step.returnsVertex() ?
+                                  graph.vertexLabel((String) label) :
+                                  graph.edgeLabel((String) label);
+                } else {
+                    return ImmutableList.of();
+                }
+                if (schemaLabel == null) {
+                    return ImmutableList.of();
+                }
+                schemaLabels.add(schemaLabel);
+            }
+        } catch (IllegalArgumentException e) {
+            return ImmutableList.of();
+        }
+        return schemaLabels;
+    }
+
+    private static void collectPositiveLabelValues(
+            HasContainerHolder holder, List labels) {
+        for (HasContainer has : holder.getHasContainers()) {
+            if (isPositiveLabelContainer(has)) {
+                addPositiveLabelValues(has, labels);
+            }
+        }
+    }
+
+    private static boolean hasSingleFieldQueryIndex(HugeGraph graph,
+                                                    SchemaLabel schemaLabel,
+                                                    PropertyKey pkey,
+                                                    HasContainer has) {
+        boolean requireRange = hasRangePredicate(has);
+        for (Id id : schemaLabel.indexLabels()) {
+            IndexLabel indexLabel = indexLabelOrNull(graph, id);
+            if (indexLabel == null ||
+                !indexLabel.status().ok() ||
+                !matchSingleFieldIndex(indexLabel, pkey)) {
+                continue;
+            }
+            if (requireRange ? indexLabel.indexType().isNumeric() :
+                !indexLabel.indexType().isSearch()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean hasRangePredicate(HasContainer has) {
+        List> predicates = new ArrayList<>();
+        collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
+        for (P predicate : predicates) {
+            PBiPredicate bp = predicate.getBiPredicate();
+            if (bp == Compare.gt || bp == Compare.gte ||
+                bp == Compare.lt || bp == Compare.lte) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static void removeExtractedHasContainers(HasContainerHolder holder,
+                                                     List extracted) {
+        for (HasContainer has : extracted) {
+            holder.removeHasContainer(has);
+        }
+    }
+
     static boolean canExtractHasContainer(HugeGraph graph,
                                           HasContainer has) {
-        if (has.getKey() == null || hasNullLabelValue(has)) {
+        if (has.getKey() == null || has.getPredicate() == null ||
+            hasNullLabelValue(has) || hasTextPredicate(has)) {
             return false;
         }
         if (isSysProp(has.getKey())) {
@@ -710,6 +912,17 @@ static boolean canExtractHasContainer(HugeGraph graph,
         return true;
     }
 
+    private static boolean hasTextPredicate(HasContainer has) {
+        List> predicates = new ArrayList<>();
+        collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
+        for (P predicate : predicates) {
+            if (TextP.class.isInstance(predicate)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     public static void extractOrder(Step newStep,
                                     Traversal.Admin traversal) {
         Step step = newStep;
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java
index fd00816f66..b2fce8da57 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java
@@ -52,6 +52,7 @@ public final class JsonUtil {
         HugeGraphSONModule.registerServiceSerializers(module);
         HugeGraphSONModule.registerGraphSpaceSerializers(module);
         HugeGraphSONModule.registerGraphSerializers(module);
+        HugeGraphSONModule.registerTraversalSerializers(module);
 
         MAPPER.registerModule(module);
 
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh
index 2c99238327..46044e932b 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh
@@ -99,6 +99,22 @@ if [[ $PRELOAD == "true" ]]; then
     sed -i -e '/registerBackends/d; /serverStarted/d' "${SCRIPTS}/${EXAMPLE_SCRIPT}"
 fi
 
+function forward_signal_and_wait() {
+    local signal="$1"
+    local exit_code="$2"
+
+    trap - SIGHUP SIGINT SIGQUIT SIGTERM
+    if kill -0 "$PID" 2>/dev/null; then
+        kill "-$signal" "$PID" 2>/dev/null || true
+        # The foreground wait is interrupted before the trap runs, so retry
+        # until the child has exited and been reaped.
+        while kill -0 "$PID" 2>/dev/null; do
+            wait "$PID" 2>/dev/null || true
+        done
+    fi
+    exit "$exit_code"
+}
+
 if [[ $DAEMON == "true" ]]; then
     echo "Starting HugeGraphServer in daemon mode..."
     "${BIN}"/hugegraph-server.sh "${CONF}/${GREMLIN_SERVER_CONF}" "${CONF}"/rest-server.properties \
@@ -133,7 +149,14 @@ else
     PID="$!"
     # Write pid to file
     echo "$PID" > "$PID_FILE"
-    trap 'kill $PID; wait $PID; exit $?' SIGHUP SIGINT SIGQUIT SIGTERM
+    trap 'forward_signal_and_wait HUP 129' SIGHUP
+    # The background JVM can inherit an ignored SIGINT disposition, so use
+    # SIGTERM to guarantee that Ctrl-C shuts it down while retaining exit 130.
+    trap 'forward_signal_and_wait TERM 130' SIGINT
+    # Forward TERM instead of QUIT: the JVM only dumps threads on SIGQUIT
+    # and keeps running, which would leave the wait loop below stuck.
+    trap 'forward_signal_and_wait TERM 131' SIGQUIT
+    trap 'forward_signal_and_wait TERM 143' SIGTERM
     wait $PID
     exit $?
 fi
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml
index e61e02a469..90075ccfb2 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml
@@ -85,6 +85,7 @@ serializers:
   - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1,
      config: {
        serializeResultToString: false,
+       builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/ci-service-utils.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/ci-service-utils.sh
index 663feda30d..934db1f171 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/ci-service-utils.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/ci-service-utils.sh
@@ -153,6 +153,37 @@ function wait_for_http_status() {
     return 1
 }
 
+function process_is_running() {
+    local pid="$1"
+    local state
+
+    if [[ ! "${pid}" =~ ^[0-9]+$ ]]; then
+        return 1
+    fi
+
+    if ! kill -0 "${pid}" 2>/dev/null; then
+        return 1
+    fi
+
+    state="$(ps -o stat= -p "${pid}" 2>/dev/null | tr -d '[:space:]')" ||
+        state=""
+    [[ "${state}" != Z* ]]
+}
+
+function wait_for_process_exit() {
+    local pid="$1"
+    local timeout_seconds="${2:-10}"
+    local deadline=$((SECONDS + timeout_seconds))
+
+    while process_is_running "${pid}"; do
+        if (( SECONDS >= deadline )); then
+            return 1
+        fi
+        sleep 1
+    done
+    return 0
+}
+
 if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
     command="$1"
     shift || true
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml
index 7972e96bd6..eccfe2182a 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml
@@ -77,6 +77,7 @@ serializers:
   - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1,
      config: {
        serializeResultToString: false,
+       builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml
index 019f6a5ccd..29f5000336 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml
@@ -77,6 +77,7 @@ serializers:
   - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1,
      config: {
        serializeResultToString: false,
+       builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml
index 96c37f94cd..e9b1b416d6 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml
@@ -77,6 +77,7 @@ serializers:
   - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1,
      config: {
        serializeResultToString: false,
+       builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder,
        ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
      }
   }
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-tinkerpop-test.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-tinkerpop-test.sh
index 87d60c8880..f37ba07049 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-tinkerpop-test.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-tinkerpop-test.sh
@@ -17,13 +17,74 @@
 #
 set -ev
 
+if [[ $# -ne 2 ]]; then
+    echo "Usage: $0 BACKEND {structure|process|process-standard|process-feature|tinkerpop}"
+    exit 2
+fi
+
 BACKEND=$1
 SUITE=$2
+REPORT_DIR=hugegraph-server/hugegraph-test/target/surefire-reports
 
-if [[ "$SUITE" == "structure" || "$SUITE" == "tinkerpop" ]]; then
+function run_structure_test() {
     mvn test -pl hugegraph-server/hugegraph-test -am -P tinkerpop-structure-test,$BACKEND
-fi
+}
 
-if [[ "$SUITE" == "process" || "$SUITE" == "tinkerpop" ]]; then
+function run_process_test() {
     mvn test -pl hugegraph-server/hugegraph-test -am -P tinkerpop-process-test,$BACKEND
-fi
+}
+
+function run_selected_process_test() {
+    local tests=$1
+    shift
+    if [[ $# -eq 0 ]]; then
+        echo "At least one expected Surefire report is required"
+        exit 2
+    fi
+    local expected_reports=("$@")
+    local expected_report
+    local report
+
+    for expected_report in "${expected_reports[@]}"; do
+        report="$REPORT_DIR/TEST-org.apache.hugegraph.tinkerpop.$expected_report.xml"
+        rm -f "$report"
+    done
+    mvn test -pl hugegraph-server/hugegraph-test -am \
+        -P tinkerpop-process-test,$BACKEND \
+        -Dtest="$tests" \
+        -Dsurefire.failIfNoSpecifiedTests=false
+
+    for expected_report in "${expected_reports[@]}"; do
+        report="$REPORT_DIR/TEST-org.apache.hugegraph.tinkerpop.$expected_report.xml"
+        if [[ ! -s "$report" ]] || ! grep -Eq 'tests="[1-9][0-9]*"' "$report"; then
+            echo "Expected a non-empty Surefire report: $report"
+            exit 1
+        fi
+    done
+}
+
+case "$SUITE" in
+    structure)
+        run_structure_test
+        ;;
+    process)
+        run_process_test
+        ;;
+    process-standard)
+        run_selected_process_test \
+            "ProcessStandardTest,HugeGraphProviderLifecycleTest" \
+            "ProcessStandardTest" \
+            "HugeGraphProviderLifecycleTest"
+        ;;
+    process-feature)
+        run_selected_process_test "HugeGraphFeatureTest" "HugeGraphFeatureTest"
+        ;;
+    tinkerpop)
+        run_structure_test
+        run_process_test
+        ;;
+    *)
+        echo "Unsupported TinkerPop suite: $SUITE"
+        exit 2
+        ;;
+esac
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-ci-service-utils.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-ci-service-utils.sh
new file mode 100644
index 0000000000..a776591a1e
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-ci-service-utils.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+#
+# 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.
+#
+
+set -uo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+UTILS="${1:-${SCRIPT_DIR}/ci-service-utils.sh}"
+ACTIVE_PID=""
+
+cleanup() {
+    if [[ -n "${ACTIVE_PID}" ]]; then
+        kill "${ACTIVE_PID}" 2>/dev/null || true
+        wait "${ACTIVE_PID}" 2>/dev/null || true
+    fi
+}
+trap cleanup EXIT
+
+source "${UTILS}"
+
+if ! declare -F process_is_running >/dev/null ||
+   ! declare -F wait_for_process_exit >/dev/null; then
+    echo "FAIL: process exit helpers are not available"
+    exit 1
+fi
+
+sleep 10 &
+ACTIVE_PID=$!
+if wait_for_process_exit "${ACTIVE_PID}" 1; then
+    echo "FAIL: a running process was reported as exited"
+    exit 1
+fi
+kill "${ACTIVE_PID}" 2>/dev/null || true
+wait "${ACTIVE_PID}" 2>/dev/null || true
+ACTIVE_PID=""
+
+sleep 1 &
+ACTIVE_PID=$!
+if ! wait_for_process_exit "${ACTIVE_PID}" 5; then
+    echo "FAIL: a terminated process was reported as running"
+    exit 1
+fi
+wait "${ACTIVE_PID}" 2>/dev/null || true
+ACTIVE_PID=""
+
+ps() {
+    echo "Z"
+}
+if process_is_running "$$"; then
+    echo "FAIL: a zombie process was reported as running"
+    exit 1
+fi
+unset -f ps
+
+echo "PASS: process exit helpers handle running, terminated, and zombie states"
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-signal.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-signal.sh
new file mode 100644
index 0000000000..cd3df0814c
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-signal.sh
@@ -0,0 +1,107 @@
+#!/bin/bash
+#
+# 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.
+#
+# Verifies that foreground start-hugegraph.sh exits on SIGINT when its
+# background server child ignores SIGINT, as a JVM may do in this launch mode.
+
+set -uo pipefail
+
+START_SCRIPT="${1:-}"
+if [[ -z "$START_SCRIPT" ]]; then
+    echo "Usage: $0 "
+    exit 2
+fi
+
+if [[ ! -f "$START_SCRIPT" ]]; then
+    echo "ERROR: start script not found: $START_SCRIPT"
+    exit 2
+fi
+
+if ! command -v timeout >/dev/null 2>&1; then
+    echo "SKIP: required tool 'timeout' not found"
+    exit 77
+fi
+
+TEST_ROOT=$(mktemp -d)
+PID_FILE="$TEST_ROOT/bin/pid"
+
+cleanup() {
+    if [[ -s "$PID_FILE" ]]; then
+        kill -TERM "$(cat "$PID_FILE")" 2>/dev/null || true
+    fi
+    rm -rf "$TEST_ROOT"
+}
+trap cleanup EXIT
+
+mkdir -p "$TEST_ROOT/bin" "$TEST_ROOT/conf" "$TEST_ROOT/logs" "$TEST_ROOT/scripts"
+cp "$START_SCRIPT" "$TEST_ROOT/bin/start-hugegraph.sh"
+
+cat > "$TEST_ROOT/bin/util.sh" <<'EOF'
+read_property() {
+    local file="$1"
+    local property="$2"
+    grep "^${property}=" "$file" | head -n 1 | cut -d '=' -f 2-
+}
+
+check_port() {
+    :
+}
+EOF
+
+cat > "$TEST_ROOT/bin/hugegraph-server.sh" <<'EOF'
+#!/bin/bash
+trap 'exit 0' TERM
+trap '' INT
+while true; do
+    sleep 1
+done
+EOF
+
+cat > "$TEST_ROOT/conf/rest-server.properties" <<'EOF'
+gremlinserver.url=http://127.0.0.1:8182
+restserver.url=http://127.0.0.1:8080
+EOF
+
+chmod +x "$TEST_ROOT/bin/start-hugegraph.sh" "$TEST_ROOT/bin/hugegraph-server.sh"
+
+export PID_FILE
+export START_SCRIPT="$TEST_ROOT/bin/start-hugegraph.sh"
+
+timeout --signal=TERM --kill-after=5s 10s bash -c '
+    target_pid=$$
+    (
+        while [[ ! -s "$PID_FILE" ]]; do
+            sleep 0.05
+        done
+        sleep 0.1
+        kill -INT "$target_pid"
+    ) &
+    exec "$START_SCRIPT" -d false
+'
+ACTUAL_EXIT=$?
+
+if [[ "$ACTUAL_EXIT" -ne 130 ]]; then
+    echo "FAIL: expected exit 130 after SIGINT, got $ACTUAL_EXIT"
+    exit 1
+fi
+
+if [[ -s "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
+    echo "FAIL: server child is still running after SIGINT"
+    exit 1
+fi
+
+echo "PASS: SIGINT terminates the foreground wrapper and its server child"
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh
index 353cab6518..68dbd84dd4 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh
@@ -30,6 +30,9 @@
 
 set -uo pipefail
 
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "${SCRIPT_DIR}/ci-service-utils.sh"
+
 HUGEGRAPH_ROOT="${1:-$(pwd)}"
 BIN="$HUGEGRAPH_ROOT/bin"
 START_SCRIPT="$BIN/start-hugegraph.sh"
@@ -394,9 +397,9 @@ else
     wait_script_exit "$SCRIPT_PID"
     ACTUAL_EXIT=$?
 
-    # If the trap fired correctly, the wrapper's `wait $PID` already reaped Java.
-    # If wait_script_exit timed out (killer fired), Java may still be running — also a failure.
-    if ! ps -p "$FG_PID" >/dev/null 2>&1; then
+    # Allow a bounded shutdown window and treat a zombie as already terminated.
+    # If wait_script_exit timed out, a live Java process remains a failure.
+    if wait_for_process_exit "$FG_PID" "$SETTLE_WAIT"; then
         pass "Java process terminated after SIGTERM sent to wrapper"
     else
         fail "Java process still running after SIGTERM — signal not forwarded"
diff --git a/hugegraph-server/hugegraph-test/pom.xml b/hugegraph-server/hugegraph-test/pom.xml
index bef5496b5a..f9f37d2169 100644
--- a/hugegraph-server/hugegraph-test/pom.xml
+++ b/hugegraph-server/hugegraph-test/pom.xml
@@ -86,6 +86,12 @@
             gremlin-test
             ${tinkerpop.version}
         
+        
+            com.google.inject
+            guice
+            4.2.3
+            provided
+        
         
             org.glassfish.jersey.test-framework.providers
             jersey-test-framework-provider-grizzly2
@@ -202,6 +208,8 @@
                             
                             
                                 **/ProcessStandardTest.java
+                                **/HugeGraphFeatureTest.java
+                                **/HugeGraphProviderLifecycleTest.java
                             
                         
                     
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/MetricsApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/MetricsApiTest.java
index 2a903af234..264fa3de1e 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/MetricsApiTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/MetricsApiTest.java
@@ -29,6 +29,8 @@ public class MetricsApiTest extends BaseApiTest {
 
     private static final String PATH = "/metrics";
     private static final String STATISTICS_PATH = PATH + "/statistics";
+    private static final String GREMLIN_CHANNEL_METRICS_PREFIX =
+            "org_apache_tinkerpop_gremlin_server_GremlinServer_channels_";
 
     @Test
     public void testBaseMetricsAll() {
@@ -46,7 +48,13 @@ public void testBaseMetricsAll() {
     @Test
     public void testBaseMetricsPromAll() {
         Response r = client().get(PATH);
-        assertResponseStatus(200, r);
+        String result = assertResponseStatus(200, r);
+        Assert.assertContains(GREMLIN_CHANNEL_METRICS_PREFIX + "paused",
+                              result);
+        Assert.assertContains(GREMLIN_CHANNEL_METRICS_PREFIX + "total",
+                              result);
+        Assert.assertContains(GREMLIN_CHANNEL_METRICS_PREFIX +
+                              "write_pauses", result);
     }
 
     @Test
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java
index 230b8d2d06..45d468bb58 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java
@@ -41,6 +41,7 @@
         VertexCoreTest.class,
         EdgeCoreTest.class,
         CountStrategyCoreTest.class,
+        TinkerPop37StepsCoreTest.class,
         ParentAndSubEdgeCoreTest.class,
         PropertyCoreTest.VertexPropertyCoreTest.class,
         PropertyCoreTest.EdgePropertyCoreTest.class,
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
index 7e8bbdfb2d..3d33d199c6 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
@@ -18,13 +18,20 @@
 package org.apache.hugegraph.core;
 
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
 
+import org.apache.hugegraph.backend.query.Aggregate;
+import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc;
+import org.apache.hugegraph.backend.query.Query;
+import org.apache.hugegraph.backend.tx.GraphTransaction;
 import org.apache.hugegraph.exception.NoIndexException;
 import org.apache.hugegraph.schema.SchemaManager;
 import org.apache.hugegraph.testutil.Assert;
 import org.apache.hugegraph.traversal.optimize.HugeCountStep;
 import org.apache.hugegraph.traversal.optimize.HugeGraphStep;
+import org.apache.hugegraph.type.HugeType;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.Step;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
@@ -35,6 +42,7 @@
 import org.apache.tinkerpop.gremlin.structure.Edge;
 import org.apache.tinkerpop.gremlin.structure.T;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator;
 import org.junit.Test;
 
 public class CountStrategyCoreTest extends BaseCoreTest {
@@ -112,6 +120,15 @@ private static boolean hasRemainingHasStep(GraphTraversal traversal,
         return false;
     }
 
+    private static void assertUncommittedRangeUnsupported(
+            GraphTraversal traversal) {
+        Assert.assertThrows(IllegalArgumentException.class, traversal::next,
+                            e -> {
+                                Assert.assertContains("offset/limit", e.getMessage());
+                                Assert.assertContains("uncommitted records", e.getMessage());
+                            });
+    }
+
     private void initTextRangeSchema(boolean withEdge) {
         SchemaManager schema = graph().schema();
         schema.propertyKey("vp4").asText().create();
@@ -254,6 +271,200 @@ public void testOptimizedGraphCountEqualityIgnoresExecutionState() {
         Assert.assertTrue(steps.contains(firstStep));
     }
 
+    @Test
+    public void testOptimizedGraphCountIncludesUncommittedRecords() {
+        this.initSchema();
+        graph().schema().indexLabel("personByName")
+               .onV("person").by("name").create();
+
+        graph().addVertex(T.label, "person", "name", "marko");
+
+        long count = graph().traversal().V()
+                            .hasLabel("person")
+                            .has("name", "marko")
+                            .count().next();
+
+        Assert.assertEquals(1L, count);
+    }
+
+    @Test
+    public void testVertexLimitCountRejectsUncommittedAddition() {
+        this.initSchema();
+        graph().addVertex(T.label, "person", "name", "marko");
+
+        assertUncommittedRangeUnsupported(
+                graph().traversal().V().limit(1L).count());
+    }
+
+    @Test
+    public void testVertexRangeCountRejectsUncommittedDeletion() {
+        this.initSchema();
+        graph().schema().indexLabel("personByName")
+               .onV("person").by("name").create();
+        this.initGraph();
+        Vertex marko = graph().traversal().V()
+                              .hasLabel("person")
+                              .has("name", "marko")
+                              .next();
+        marko.remove();
+
+        assertUncommittedRangeUnsupported(
+                graph().traversal().V().range(1L, 3L).count());
+    }
+
+    @Test
+    public void testQueryNumberKeepsOriginalAggregate() {
+        this.initSchema();
+        graph().addVertex(T.label, "person", "name", "marko");
+
+        Query query = new Query(HugeType.VERTEX);
+        Aggregate aggregate = new Aggregate(AggregateFunc.COUNT, null);
+        query.aggregate(aggregate);
+
+        Assert.assertEquals(1L, graph().queryNumber(query).longValue());
+        Assert.assertSame(aggregate, query.aggregate());
+    }
+
+    @Test
+    public void testUncommittedVertexCountClosesIteratorOnFailure() {
+        FailingCloseableIterator vertices =
+                new FailingCloseableIterator<>();
+        AtomicBoolean dirty = new AtomicBoolean(true);
+        GraphTransaction transaction =
+                this.newFailingCountTransaction(vertices, null, dirty);
+
+        try {
+            Query query = countQuery(HugeType.VERTEX);
+            Assert.assertThrows(IllegalStateException.class,
+                                () -> transaction.queryNumber(query));
+            Assert.assertTrue(vertices.closed());
+        } finally {
+            dirty.set(false);
+            transaction.close();
+        }
+    }
+
+    @Test
+    public void testUncommittedEdgeCountClosesIteratorOnFailure() {
+        FailingCloseableIterator edges =
+                new FailingCloseableIterator<>();
+        AtomicBoolean dirty = new AtomicBoolean(true);
+        GraphTransaction transaction =
+                this.newFailingCountTransaction(null, edges, dirty);
+
+        try {
+            Query query = countQuery(HugeType.EDGE);
+            Assert.assertThrows(IllegalStateException.class,
+                                () -> transaction.queryNumber(query));
+            Assert.assertTrue(edges.closed());
+        } finally {
+            dirty.set(false);
+            transaction.close();
+        }
+    }
+
+    @Test
+    public void testOptimizedEdgeCountIncludesUncommittedRecords() {
+        this.initSchema();
+        graph().schema().indexLabel("personByName")
+               .onV("person").by("name").create();
+        this.initGraph();
+
+        Vertex josh = graph().traversal().V()
+                             .hasLabel("person").has("name", "josh").next();
+        Vertex marko = graph().traversal().V()
+                              .hasLabel("person").has("name", "marko").next();
+        josh.addEdge("knows", marko);
+
+        long count = graph().traversal().E().hasLabel("knows").count().next();
+
+        Assert.assertEquals(2L, count);
+    }
+
+    private static Query countQuery(HugeType type) {
+        Query query = new Query(type);
+        query.aggregate(new Aggregate(AggregateFunc.COUNT, null));
+        return query;
+    }
+
+    private GraphTransaction newFailingCountTransaction(
+            Iterator vertices, Iterator edges,
+            AtomicBoolean dirty) {
+        return new GraphTransaction(params(), params().loadGraphStore()) {
+
+            @Override
+            public boolean hasUpdate() {
+                return dirty.get();
+            }
+
+            @Override
+            public Iterator queryVertices(Query query) {
+                return vertices;
+            }
+
+            @Override
+            public Iterator queryEdges(Query query) {
+                return edges;
+            }
+        };
+    }
+
+    private static final class FailingCloseableIterator
+            implements CloseableIterator {
+
+        private boolean closed;
+
+        @Override
+        public boolean hasNext() {
+            throw new IllegalStateException("Injected iterator failure");
+        }
+
+        @Override
+        public T next() {
+            throw new IllegalStateException("Injected iterator failure");
+        }
+
+        @Override
+        public void close() {
+            this.closed = true;
+        }
+
+        public boolean closed() {
+            return this.closed;
+        }
+    }
+
+    @Test
+    public void testEdgeRangeCountRejectsUncommittedAddition() {
+        this.initSchema();
+        graph().schema().indexLabel("personByName")
+               .onV("person").by("name").create();
+        this.initGraph();
+        Vertex josh = graph().traversal().V()
+                             .hasLabel("person")
+                             .has("name", "josh")
+                             .next();
+        Vertex marko = graph().traversal().V()
+                              .hasLabel("person")
+                              .has("name", "marko")
+                              .next();
+        josh.addEdge("knows", marko);
+
+        assertUncommittedRangeUnsupported(
+                graph().traversal().E().range(1L, 3L).count());
+    }
+
+    @Test
+    public void testEdgeLimitCountRejectsUncommittedDeletion() {
+        this.initSchema();
+        this.initGraph();
+        Edge edge = graph().traversal().E().hasLabel("knows").next();
+        edge.remove();
+
+        assertUncommittedRangeUnsupported(
+                graph().traversal().E().limit(1L).count());
+    }
+
     @Test
     public void testRepeatAfterTextRangeFilterWithEmptyResult() {
         this.initTextRangeSchema(true);
@@ -298,6 +509,33 @@ public void testTextRangeFilterKeepsMixedGraphHasStep() {
         Assert.assertEquals(direct, viaMatch);
     }
 
+    @Test
+    public void testTextRangeFilterExtractsIndexedGraphHasContainers() {
+        this.initTextRangeSchema(false);
+        graph().schema().indexLabel("vl1ByAge").onV("vl1")
+               .by("age").secondary().create();
+
+        graph().addVertex(T.label, "vl1", "vp4", "a", "age", 1);
+        graph().addVertex(T.label, "vl1", "vp4", "b", "age", 2);
+        commitTx();
+
+        GraphTraversal traversal = graph().traversal().V()
+                                                        .hasLabel("vl1")
+                                                        .has("vp4", P.lt(""))
+                                                        .has("age", 1)
+                                                        .count();
+        HugeGraphStep graphStep = applyAndGetGraphStep(traversal);
+
+        Assert.assertEquals(2, graphStep.getHasContainers().size());
+        Assert.assertTrue(graphStep.getHasContainers().stream().anyMatch(
+                has -> T.label.getAccessor().equals(has.getKey())));
+        Assert.assertTrue(graphStep.getHasContainers().stream().anyMatch(
+                has -> "age".equals(has.getKey())));
+        Assert.assertTrue(hasRemainingHasStep(traversal, "vp4"));
+        Assert.assertFalse(hasRemainingHasStep(traversal, "age"));
+        Assert.assertEquals(0L, traversal.next().longValue());
+    }
+
     @Test
     public void testTextRangeFilterKeepsMixedVertexHasStep() {
         this.initTextRangeSchema(true);
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TinkerPop37StepsCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TinkerPop37StepsCoreTest.java
new file mode 100644
index 0000000000..253440b8d3
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TinkerPop37StepsCoreTest.java
@@ -0,0 +1,438 @@
+/*
+ * 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.hugegraph.core;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.hugegraph.schema.SchemaManager;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.tinkerpop.gremlin.process.traversal.DT;
+import org.apache.tinkerpop.gremlin.process.traversal.Merge;
+import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.TextP;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.FailStep;
+import org.apache.tinkerpop.gremlin.structure.Direction;
+import org.apache.tinkerpop.gremlin.structure.Edge;
+import org.apache.tinkerpop.gremlin.structure.T;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.junit.Test;
+
+public class TinkerPop37StepsCoreTest extends BaseCoreTest {
+
+    @Test
+    public void testStringManipulationSteps() {
+        Assert.assertEquals("123", graph().traversal().inject(123)
+                                         .asString().next());
+        Assert.assertEquals(5, graph().traversal().inject("marko")
+                                      .length().next());
+        Assert.assertEquals("marko", graph().traversal().inject("MARKO")
+                                         .toLower().next());
+        Assert.assertEquals("MARKO", graph().traversal().inject("marko")
+                                         .toUpper().next());
+        Assert.assertEquals("marko", graph().traversal().inject("  marko  ")
+                                         .trim().next());
+        Assert.assertEquals("marko  ", graph().traversal().inject("  marko  ")
+                                           .lTrim().next());
+        Assert.assertEquals("  marko", graph().traversal().inject("  marko  ")
+                                           .rTrim().next());
+        Assert.assertEquals("huge-graph", graph().traversal().inject("huge_graph")
+                                              .replace("_", "-").next());
+        Assert.assertEquals("hugegraph", graph().traversal().inject("huge")
+                                             .concat("graph").next());
+        Assert.assertEquals("eguh", graph().traversal().inject("huge")
+                                        .reverse().next());
+        Assert.assertEquals(Arrays.asList("huge", "graph"),
+                            graph().traversal().inject("huge-graph")
+                                   .split("-").next());
+        Assert.assertEquals("graph", graph().traversal().inject("hugegraph")
+                                        .substring(4).next());
+        Assert.assertEquals("huge", graph().traversal().inject("hugegraph")
+                                       .substring(0, 4).next());
+
+        Map values = new HashMap<>();
+        values.put("name", "marko");
+        values.put("age", 29);
+        Assert.assertEquals("marko is 29 years old",
+                            graph().traversal().inject(values)
+                                   .format("%{name} is %{age} years old")
+                                   .next());
+    }
+
+    @Test
+    public void testListManipulationSteps() {
+        List values = Arrays.asList(1, 2);
+        List other = Arrays.asList(2, 3);
+
+        Assert.assertEquals(Arrays.asList(1, 2, 2, 3),
+                            graph().traversal().inject(values)
+                                   .combine(other).next());
+        Assert.assertEquals(setOf(1, 2, 3),
+                            asSet(graph().traversal().inject(values)
+                                         .merge(other).next()));
+        Assert.assertEquals(setOf(2),
+                            asSet(graph().traversal().inject(values)
+                                         .intersect(other).next()));
+        Assert.assertEquals(setOf(1),
+                            asSet(graph().traversal().inject(values)
+                                         .difference(other).next()));
+        Assert.assertEquals(setOf(1, 3),
+                            asSet(graph().traversal().inject(values)
+                                         .disjunct(other).next()));
+        Assert.assertEquals(Arrays.asList(Arrays.asList(1, 2),
+                                          Arrays.asList(1, 3),
+                                          Arrays.asList(2, 2),
+                                          Arrays.asList(2, 3)),
+                            graph().traversal().inject(values)
+                                   .product(other).next());
+        Assert.assertEquals(Arrays.asList(3, 2, 1),
+                            graph().traversal().inject(Arrays.asList(1, 2, 3))
+                                   .reverse().next());
+        Assert.assertEquals("huge-graph",
+                            graph().traversal()
+                                   .inject(Arrays.asList("huge", "graph"))
+                                   .conjoin("-").next());
+        Assert.assertEquals(Arrays.asList(1, 2, 3),
+                            graph().traversal()
+                                   .inject(Arrays.asList(1, 2, 3))
+                                   .all(P.gt(0)).next());
+        Assert.assertEquals(Arrays.asList(1, 2, 3),
+                            graph().traversal()
+                                   .inject(Arrays.asList(1, 2, 3))
+                                   .any(P.eq(2)).next());
+    }
+
+    @Test
+    public void testDateManipulationSteps() {
+        Date start = Date.from(Instant.parse("2023-08-02T00:00:00Z"));
+        Date expected = Date.from(Instant.parse("2023-08-09T00:00:00Z"));
+
+        Date actual = graph().traversal()
+                             .inject("2023-08-02T00:00:00Z")
+                             .asDate().dateAdd(DT.day, 7).next();
+        long seconds = graph().traversal()
+                              .inject("2023-08-02T00:00:00Z")
+                              .asDate().dateAdd(DT.day, 7)
+                              .dateDiff(start).next();
+
+        Assert.assertEquals(expected, actual);
+        Assert.assertEquals(604800L, seconds);
+    }
+
+    @Test
+    public void testMergeVertexWithHugeGraphIds() {
+        this.initMutationSchema();
+        Map search = map(T.label, "person",
+                                         "name", "marko");
+
+        Vertex created = graph().traversal().mergeV(search)
+                                .option(Merge.onCreate,
+                                        map("status", "created"))
+                                .next();
+        commitTx();
+        Vertex matched = graph().traversal().mergeV(search)
+                                .option(Merge.onMatch,
+                                        map("status", "matched"))
+                                .next();
+        commitTx();
+
+        Assert.assertEquals(created.id(), matched.id());
+        Assert.assertEquals("matched", matched.value("status"));
+        Assert.assertEquals(1L, graph().traversal().V()
+                                      .hasLabel("person")
+                                      .has("name", "marko")
+                                      .count().next());
+    }
+
+    @Test
+    public void testMergeEdgeWithHugeGraphIds() {
+        this.initMutationSchema();
+        Vertex marko = graph().addVertex(T.label, "person",
+                                         "name", "marko");
+        Vertex vadas = graph().addVertex(T.label, "person",
+                                         "name", "vadas");
+        commitTx();
+        Map search = map(T.label, "knows",
+                                         Direction.OUT, marko.id(),
+                                         Direction.IN, vadas.id());
+
+        Edge created = graph().traversal().mergeE(search)
+                              .option(Merge.onCreate,
+                                      map("status", "created",
+                                          "weight", 0.5D))
+                              .next();
+        commitTx();
+        Edge matched = graph().traversal().mergeE(search)
+                              .option(Merge.onMatch,
+                                      map("status", "matched"))
+                              .next();
+        commitTx();
+
+        Assert.assertEquals(created.id(), matched.id());
+        Assert.assertEquals("matched", matched.value("status"));
+        Assert.assertEquals(1L, graph().traversal().E()
+                                      .hasLabel("knows").count().next());
+    }
+
+    @Test
+    public void testMergeOnCreateValidation() {
+        this.initMutationSchema();
+        Map search = map(T.label, "person",
+                                         "name", "marko");
+        Map invalid = map(T.label, "person",
+                                          "name", "vadas");
+
+        Assert.assertThrows(IllegalArgumentException.class, () -> {
+            graph().traversal().mergeV(search)
+                   .option(Merge.onCreate, invalid);
+        });
+    }
+
+    @Test
+    public void testPropertyMapAndMidTraversalE() {
+        this.initMutationSchema();
+        Vertex marko = graph().addVertex(T.label, "person",
+                                         "name", "marko");
+        Vertex vadas = graph().addVertex(T.label, "person",
+                                         "name", "vadas");
+        Edge edge = marko.addEdge("knows", vadas);
+        commitTx();
+
+        graph().traversal().V(marko.id())
+               .property(map("status", "active"))
+               .iterate();
+        commitTx();
+
+        Assert.assertEquals("active", graph().traversal().V(marko.id())
+                                                .values("status").next());
+        Assert.assertEquals(edge.id(), graph().traversal().inject(1)
+                                         .E(edge.id()).next().id());
+    }
+
+    @Test
+    public void testUnproductiveByFiltersMissingGroupKey() {
+        this.initTextData();
+        graph().traversal().V()
+               .hasLabel("person")
+               .has("name", "marko")
+               .property("status", "active")
+               .iterate();
+        commitTx();
+
+        Map grouped = graph().traversal().V()
+                                             .group()
+                                             .by("status")
+                                             .by("name")
+                                             .next();
+
+        Assert.assertEquals(1, grouped.size());
+        Assert.assertEquals(Collections.singletonList("marko"),
+                            grouped.get("active"));
+        Assert.assertFalse(grouped.containsKey(null));
+    }
+
+    @Test
+    public void testUnproductiveByOmitsProjectKey() {
+        this.initTextData();
+
+        Map projected = graph().traversal().V()
+                                               .hasLabel("person")
+                                               .has("name", "vadas")
+                                               .project("name", "status")
+                                               .by("name")
+                                               .by("status")
+                                               .next();
+
+        Assert.assertEquals("vadas", projected.get("name"));
+        Assert.assertFalse(projected.containsKey("status"));
+    }
+
+    @Test
+    public void testMissingByValueCanUseExplicitFallback() {
+        this.initTextData();
+        graph().traversal().V()
+               .hasLabel("person")
+               .has("name", "marko")
+               .property("status", "active")
+               .iterate();
+        commitTx();
+
+        Map grouped = graph().traversal().V()
+                                             .group()
+                                             .by(__.coalesce(
+                                                  __.values("status"),
+                                                  __.constant("missing")))
+                                             .by("name")
+                                             .next();
+
+        Assert.assertEquals(Collections.singletonList("marko"),
+                            grouped.get("active"));
+        Assert.assertEquals(setOf("lop", "vadas"),
+                            asSet(grouped.get("missing")));
+    }
+
+    @Test
+    public void testFailStep() {
+        Assert.assertThrows(FailStep.FailException.class, () -> {
+            graph().traversal().inject(1).fail("expected failure").iterate();
+        });
+    }
+
+    @Test
+    public void testTextPContaining() {
+        this.initTextData();
+        Assert.assertEquals(Arrays.asList("marko"),
+                            this.names(TextP.containing("ark")));
+    }
+
+    @Test
+    public void testTextPStartingWith() {
+        this.initTextData();
+        Assert.assertEquals(Arrays.asList("marko"),
+                            this.names(TextP.startingWith("mar")));
+    }
+
+    @Test
+    public void testTextPEndingWith() {
+        this.initTextData();
+        Assert.assertEquals(Arrays.asList("vadas"),
+                            this.names(TextP.endingWith("das")));
+    }
+
+    @Test
+    public void testTextPRegex() {
+        this.initTextData();
+        Assert.assertEquals(Arrays.asList("marko"),
+                            this.names(TextP.regex("^mar")));
+    }
+
+    @Test
+    public void testTextPNegations() {
+        this.initTextData();
+        Assert.assertEquals(Arrays.asList("lop", "vadas"),
+                            this.names(TextP.notContaining("ar")));
+        Assert.assertEquals(Arrays.asList("lop", "vadas"),
+                            this.names(TextP.notStartingWith("mar")));
+        Assert.assertEquals(Arrays.asList("lop", "marko"),
+                            this.names(TextP.notEndingWith("das")));
+        Assert.assertEquals(Arrays.asList("lop", "vadas"),
+                            this.names(TextP.notRegex("^mar")));
+    }
+
+    @Test
+    public void testTextPWithLocalFilter() {
+        this.initTextData();
+
+        Assert.assertEquals(Arrays.asList("marko"),
+                            this.namesWithLocalFilter(
+                                 TextP.containing("ark")));
+        Assert.assertEquals(Arrays.asList("marko"),
+                            this.namesWithLocalFilter(
+                                 TextP.startingWith("mar")));
+        Assert.assertEquals(Arrays.asList("vadas"),
+                            this.namesWithLocalFilter(
+                                 TextP.endingWith("das")));
+        Assert.assertEquals(Arrays.asList("marko"),
+                            this.namesWithLocalFilter(TextP.regex("^mar")));
+        Assert.assertEquals(Arrays.asList("lop", "vadas"),
+                            this.namesWithLocalFilter(
+                                 TextP.notContaining("ar")));
+        Assert.assertEquals(Arrays.asList("lop", "vadas"),
+                            this.namesWithLocalFilter(
+                                 TextP.notStartingWith("mar")));
+        Assert.assertEquals(Arrays.asList("lop", "marko"),
+                            this.namesWithLocalFilter(
+                                 TextP.notEndingWith("das")));
+        Assert.assertEquals(Arrays.asList("lop", "vadas"),
+                            this.namesWithLocalFilter(
+                                 TextP.notRegex("^mar")));
+    }
+
+    private void initMutationSchema() {
+        SchemaManager schema = graph().schema();
+        schema.propertyKey("name").asText().create();
+        schema.propertyKey("status").asText().create();
+        schema.propertyKey("weight").asDouble().create();
+        schema.vertexLabel("person")
+              .properties("name", "status")
+              .primaryKeys("name")
+              .nullableKeys("status")
+              .create();
+        schema.edgeLabel("knows")
+              .link("person", "person")
+              .properties("status", "weight")
+              .nullableKeys("status", "weight")
+              .create();
+    }
+
+    private void initTextData() {
+        this.initMutationSchema();
+        graph().addVertex(T.label, "person", "name", "marko");
+        graph().addVertex(T.label, "person", "name", "vadas");
+        graph().addVertex(T.label, "person", "name", "lop");
+        commitTx();
+    }
+
+    private List names(TextP predicate) {
+        return graph().traversal().V()
+                      .hasLabel("person")
+                      .has("name", predicate)
+                      .values("name")
+                      .order()
+                      .toList();
+    }
+
+    private List namesWithLocalFilter(TextP predicate) {
+        return graph().traversal().V()
+                      .hasLabel("person")
+                      .filter(__.values("name").is(predicate))
+                      .values("name")
+                      .order()
+                      .toList();
+    }
+
+    private static Map map(Object... keyValues) {
+        Map result = new HashMap<>();
+        for (int i = 0; i < keyValues.length; i += 2) {
+            result.put(keyValues[i], keyValues[i + 1]);
+        }
+        return result;
+    }
+
+    private static Set setOf(Object... values) {
+        return new HashSet<>(Arrays.asList(values));
+    }
+
+    private static Set asSet(Object values) {
+        Assert.assertInstanceOf(Iterable.class, values);
+        List list = new ArrayList<>();
+        for (Object value : (Iterable) values) {
+            list.add(value);
+        }
+        return new HashSet<>(list);
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphFeatureTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphFeatureTest.java
new file mode 100644
index 0000000000..44de57dc3d
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphFeatureTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.hugegraph.tinkerpop;
+
+import org.apache.hugegraph.dist.RegisterUtil;
+import org.apache.tinkerpop.gremlin.features.AbstractGuiceFactory;
+import org.apache.tinkerpop.gremlin.features.World;
+import org.junit.runner.RunWith;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+
+import io.cucumber.guice.CucumberModules;
+import io.cucumber.junit.Cucumber;
+import io.cucumber.junit.CucumberOptions;
+
+@RunWith(Cucumber.class)
+@CucumberOptions(
+        tags = HugeGraphFeatureTest.TAGS,
+        name = HugeGraphFeatureTest.NAMES,
+        glue = {"org.apache.tinkerpop.gremlin.features"},
+        objectFactory = HugeGraphFeatureTest.HugeGraphGuiceFactory.class,
+        features = {
+                "classpath:/org/apache/tinkerpop/gremlin/test/features"
+        },
+        plugin = {
+                "progress",
+                "junit:target/cucumber-tp37.xml",
+                "org.apache.hugegraph.tinkerpop." +
+                "HugeGraphScenarioCountPlugin"
+        })
+public class HugeGraphFeatureTest {
+
+    public static final String NAMES =
+            "^g_(?!mergeEXlabel_knows_out_marko_in_vadas_weight_05X_" +
+            "exists$)(?!V_hasXperson_name_marko_X_mergeEXlabel_knowsX_" +
+            "optionXonCreate_created_YX_optionXonMatch_created_NX_" +
+            "exists_updated$).*";
+
+    public static final String TAGS =
+            "(@StepAsString or @StepConcat or @StepFormat or " +
+            "@StepLength or @StepSplit or @StepSubstring or " +
+            "@StepReplace or @StepReverse or @StepToLower or " +
+            "@StepToUpper or @StepTrim or @StepLTrim or @StepRTrim or " +
+            "@StepCombine or @StepMerge or @StepIntersect or " +
+            "@StepDifference or @StepDisjunct or @StepConjoin or " +
+            "@StepProduct or @StepAll or @StepAny or @StepAsDate or " +
+            "@StepDateAdd or @StepDateDiff or @StepMergeV or " +
+            "@StepMergeE or @StepFail) and " +
+            "not @RemoteOnly and not @GraphComputerOnly and " +
+            "not @AllowNullPropertyValues and not @MetaProperties and " +
+            "not @MultiProperties and " +
+            "not @UserSuppliedVertexIds and not @UserSuppliedEdgeIds and " +
+            "not @UserSuppliedVertexPropertyIds and " +
+            "not @InsertionOrderingRequired";
+
+    public static class HugeGraphGuiceFactory extends AbstractGuiceFactory {
+
+        public HugeGraphGuiceFactory() {
+            super(createInjector());
+        }
+
+        private static Injector createInjector() {
+            RegisterUtil.registerBackends();
+            return Guice.createInjector(Stage.PRODUCTION,
+                                        CucumberModules.createScenarioModule(),
+                                        new ServiceModule());
+        }
+    }
+
+    public static final class ServiceModule extends AbstractModule {
+
+        @Override
+        protected void configure() {
+            bind(World.class).to(HugeGraphWorld.class);
+        }
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderContext.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderContext.java
new file mode 100644
index 0000000000..794d7fc5e2
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderContext.java
@@ -0,0 +1,47 @@
+/*
+ * 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.hugegraph.tinkerpop;
+
+import java.io.IOException;
+
+final class HugeGraphProviderContext {
+
+    private ProcessTestGraphProvider provider;
+
+    synchronized ProcessTestGraphProvider provider() {
+        if (this.provider == null) {
+            try {
+                this.provider = new ProcessTestGraphProvider();
+            } catch (IOException e) {
+                throw new IllegalStateException(
+                        "Failed to create graph provider", e);
+            }
+        }
+        return this.provider;
+    }
+
+    synchronized void clear() {
+        if (this.provider == null) {
+            return;
+        }
+
+        ProcessTestGraphProvider provider = this.provider;
+        this.provider = null;
+        provider.clear();
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderLifecycleTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderLifecycleTest.java
new file mode 100644
index 0000000000..c5ed11e59f
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderLifecycleTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.hugegraph.tinkerpop;
+
+import java.util.Map;
+
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.configuration2.MapConfiguration;
+import org.apache.hugegraph.dist.RegisterUtil;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.testutil.Utils;
+import org.junit.Assume;
+import org.junit.Test;
+
+public class HugeGraphProviderLifecycleTest {
+
+    @Test
+    public void testProviderContextLifecycleWithMemoryBackend()
+            throws Exception {
+        Assume.assumeTrue("memory".equals(
+                Utils.getConf().getString("backend")));
+        RegisterUtil.registerBackends();
+        HugeGraphProviderContext context = new HugeGraphProviderContext();
+        ProcessTestGraphProvider provider = context.provider();
+        TestGraph graph = null;
+        try {
+            Assert.assertSame(provider, context.provider());
+
+            Map config = provider.getBaseConfiguration(
+                    "provider_context", this.getClass(),
+                    "testProviderContextLifecycleWithMemoryBackend", null);
+            Configuration configuration = new MapConfiguration(config);
+            graph = (TestGraph) provider.openTestGraph(configuration);
+
+            Assert.assertEquals("memory", graph.hugegraph().backend());
+            Assert.assertFalse(graph.closed());
+
+            provider.clear(graph, configuration);
+            Assert.assertFalse(graph.closed());
+
+            context.clear();
+            Assert.assertTrue(graph.closed());
+
+            context.clear();
+            Assert.assertNotSame(provider, context.provider());
+        } finally {
+            context.clear();
+        }
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphScenarioCountPlugin.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphScenarioCountPlugin.java
new file mode 100644
index 0000000000..1e210d073e
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphScenarioCountPlugin.java
@@ -0,0 +1,63 @@
+/*
+ * 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.hugegraph.tinkerpop;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.cucumber.plugin.ConcurrentEventListener;
+import io.cucumber.plugin.event.EventPublisher;
+import io.cucumber.plugin.event.TestCaseStarted;
+import io.cucumber.plugin.event.TestRunFinished;
+
+public final class HugeGraphScenarioCountPlugin
+        implements ConcurrentEventListener {
+
+    /*
+     * This is the exact number selected by TAGS and NAMES for TinkerPop 3.7.6.
+     * Update it together with an intentional filter or TinkerPop change.
+     */
+    private static final int EXPECTED_SCENARIOS = 345;
+
+    private final AtomicInteger scenarioCount = new AtomicInteger();
+
+    @Override
+    public void setEventPublisher(EventPublisher publisher) {
+        publisher.registerHandlerFor(TestCaseStarted.class,
+                                     event -> this.scenarioCount.incrementAndGet());
+        publisher.registerHandlerFor(TestRunFinished.class,
+                                     event -> this.finishRun());
+    }
+
+    private void finishRun() {
+        try {
+            assertScenariosExecuted(this.scenarioCount.get());
+        } finally {
+            HugeGraphWorld.clearProvider();
+        }
+    }
+
+    static void assertScenariosExecuted(int scenarioCount) {
+        if (scenarioCount != EXPECTED_SCENARIOS) {
+            throw new AssertionError(
+                    scenarioCount + " TinkerPop Gherkin scenarios were " +
+                    "executed, expected exactly " + EXPECTED_SCENARIOS +
+                    " (check the TAGS/NAMES filters and update the expected " +
+                    "count for intentional changes)");
+        }
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphTestInfrastructureTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphTestInfrastructureTest.java
new file mode 100644
index 0000000000..06407e3c5a
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphTestInfrastructureTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.hugegraph.tinkerpop;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.hugegraph.HugeGraph;
+import org.apache.hugegraph.schema.PropertyKey;
+import org.apache.hugegraph.schema.SchemaManager;
+import org.apache.hugegraph.schema.VertexLabel;
+import org.apache.hugegraph.testutil.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class HugeGraphTestInfrastructureTest {
+
+    @Test
+    public void testProviderContextLifecycle() {
+        HugeGraphProviderContext context = new HugeGraphProviderContext();
+        ProcessTestGraphProvider provider = context.provider();
+        try {
+            Assert.assertSame(provider, context.provider());
+
+            context.clear();
+            context.clear();
+
+            Assert.assertNotSame(provider, context.provider());
+        } finally {
+            context.clear();
+        }
+    }
+
+    @Test
+    public void testExactScenarioCount() {
+        HugeGraphScenarioCountPlugin.assertScenariosExecuted(345);
+
+        Assert.assertThrows(AssertionError.class, () -> {
+            HugeGraphScenarioCountPlugin.assertScenariosExecuted(344);
+        }, e -> {
+            Assert.assertContains("expected exactly 345", e.getMessage());
+        });
+        Assert.assertThrows(AssertionError.class, () -> {
+            HugeGraphScenarioCountPlugin.assertScenariosExecuted(346);
+        }, e -> {
+            Assert.assertContains("expected exactly 345", e.getMessage());
+        });
+    }
+
+    @Test
+    public void testHStoreCleanupTruncatesDataBeforeClearingSchema() {
+        HugeGraph graph = Mockito.mock(HugeGraph.class);
+        SchemaManager schema = Mockito.mock(SchemaManager.class);
+        PropertyKey propertyKey = Mockito.mock(PropertyKey.class);
+        Mockito.when(graph.schema()).thenReturn(schema);
+        Mockito.when(schema.getPropertyKeys())
+               .thenReturn(Collections.singletonList(propertyKey));
+        Mockito.when(graph.backend()).thenReturn("hstore");
+
+        CleanupTestGraph testGraph = new CleanupTestGraph(graph);
+        testGraph.clearAll("");
+
+        Assert.assertTrue(testGraph.backendTruncated);
+        Assert.assertTrue(testGraph.schemaCleared);
+        Assert.assertEquals(Arrays.asList("truncate", "schema"),
+                            testGraph.cleanupSteps);
+    }
+
+    @Test
+    public void testHStoreLoadCleanupDoesNotTruncateBackend() {
+        HugeGraph graph = Mockito.mock(HugeGraph.class);
+        Mockito.when(graph.backend()).thenReturn("hstore");
+
+        CleanupTestGraph testGraph = new CleanupTestGraph(graph);
+        testGraph.clearForLoad();
+
+        Assert.assertFalse(testGraph.backendTruncated);
+        Assert.assertTrue(testGraph.schemaCleared);
+        Assert.assertEquals(Collections.singletonList("schema"),
+                            testGraph.cleanupSteps);
+    }
+
+    @Test
+    public void testHStoreCleanupDoesNotSkipSchemaWithoutPropertyKeys() {
+        HugeGraph graph = Mockito.mock(HugeGraph.class);
+        SchemaManager schema = Mockito.mock(SchemaManager.class);
+        VertexLabel vertexLabel = Mockito.mock(VertexLabel.class);
+        Mockito.when(graph.schema()).thenReturn(schema);
+        Mockito.when(schema.getPropertyKeys())
+               .thenReturn(Collections.emptyList());
+        Mockito.when(schema.getVertexLabels())
+               .thenReturn(Collections.singletonList(vertexLabel));
+        Mockito.when(graph.backend()).thenReturn("hstore");
+
+        CleanupTestGraph testGraph = new CleanupTestGraph(graph);
+        testGraph.clearAll("");
+
+        Assert.assertTrue(testGraph.backendTruncated);
+        Assert.assertTrue(testGraph.schemaCleared);
+    }
+
+    private static class CleanupTestGraph extends TestGraph {
+
+        private boolean backendTruncated;
+        private boolean schemaCleared;
+        private final List cleanupSteps;
+
+        private CleanupTestGraph(HugeGraph graph) {
+            super(graph);
+            this.cleanupSteps = new ArrayList<>();
+        }
+
+        @Override
+        protected void truncateBackend() {
+            this.backendTruncated = true;
+            this.cleanupSteps.add("truncate");
+        }
+
+        @Override
+        protected void clearSchema() {
+            this.schemaCleared = true;
+            this.cleanupSteps.add("schema");
+        }
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphWorld.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphWorld.java
new file mode 100644
index 0000000000..9842b2e1a1
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphWorld.java
@@ -0,0 +1,198 @@
+/*
+ * 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.hugegraph.tinkerpop;
+
+import java.lang.annotation.Annotation;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.configuration2.MapConfiguration;
+import org.apache.hugegraph.HugeGraph;
+import org.apache.hugegraph.schema.SchemaManager;
+import org.apache.hugegraph.type.define.IdStrategy;
+import org.apache.tinkerpop.gremlin.LoadGraphWith;
+import org.apache.tinkerpop.gremlin.features.World;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.structure.Element;
+import org.apache.tinkerpop.gremlin.structure.Graph;
+
+import io.cucumber.java.Scenario;
+
+public class HugeGraphWorld implements World {
+
+    private static final HugeGraphProviderContext PROVIDER_CONTEXT =
+            new HugeGraphProviderContext();
+
+    private final ProcessTestGraphProvider provider;
+    private Scenario scenario;
+    private Graph graph;
+    private Configuration configuration;
+
+    public HugeGraphWorld() {
+        this.provider = PROVIDER_CONTEXT.provider();
+    }
+
+    static void clearProvider() {
+        PROVIDER_CONTEXT.clear();
+    }
+
+    @Override
+    public void beforeEachScenario(Scenario scenario) {
+        this.scenario = scenario;
+    }
+
+    @Override
+    public GraphTraversalSource getGraphTraversalSource(
+            LoadGraphWith.GraphData graphData) {
+        if (this.scenario == null) {
+            throw new IllegalStateException("Scenario has not been initialized");
+        }
+        if (this.graph != null) {
+            this.clearGraph();
+        }
+
+        Map config = this.provider.getBaseConfiguration(
+                graphName(graphData), HugeGraphFeatureTest.class,
+                this.scenario.getName(), graphData);
+        this.configuration = new MapConfiguration(config);
+        this.graph = this.provider.openTestGraph(this.configuration);
+        this.prepareGraph(graphData);
+        return this.provider.traversal(this.graph);
+    }
+
+    @Override
+    public void afterEachScenario() {
+        this.clearGraph();
+    }
+
+    @Override
+    public String convertIdToScript(Object id,
+                                    Class type) {
+        return this.provider.convertId(id, type);
+    }
+
+    private void clearGraph() {
+        if (this.graph == null) {
+            return;
+        }
+
+        try {
+            this.provider.clear(this.graph, this.configuration);
+        } catch (Exception e) {
+            throw new IllegalStateException("Failed to clear test graph", e);
+        } finally {
+            this.graph = null;
+            this.configuration = null;
+        }
+    }
+
+    private void prepareGraph(LoadGraphWith.GraphData graphData) {
+        TestGraph testGraph = (TestGraph) this.graph;
+        if (graphData == null) {
+            testGraph.clearAll("");
+            testGraph.initModernSchema(IdStrategy.AUTOMATIC);
+            this.prepareGherkinSchema(testGraph);
+            testGraph.tx().commit();
+            testGraph.autoPerson(true);
+            return;
+        }
+
+        this.provider.loadGraphData(testGraph, new GraphDataLoader(graphData),
+                                    HugeGraphFeatureTest.class,
+                                    this.scenario.getName());
+        this.prepareGherkinSchema(testGraph);
+        testGraph.tx().commit();
+    }
+
+    private void prepareGherkinSchema(TestGraph testGraph) {
+        HugeGraph graph = testGraph.hugegraph();
+        SchemaManager schema = graph.schema();
+        schema.propertyKey("created").ifNotExist().create();
+        schema.propertyKey("matched").ifNotExist().create();
+        schema.vertexLabel("a").useAutomaticId().ifNotExist().create();
+        schema.vertexLabel("b").useAutomaticId().ifNotExist().create();
+        schema.vertexLabel("prefix_person").useAutomaticId()
+              .ifNotExist().create();
+        this.prepareVertexLabel(graph, schema, "person");
+        this.prepareVertexLabel(graph, schema, "software");
+        this.prepareVertexLabel(graph, schema, TestGraph.DEFAULT_VL);
+        this.prepareEdgeLabel(graph, schema, "knows");
+        this.prepareEdgeLabel(graph, schema, "created");
+        if (graph.existsVertexLabel("person")) {
+            schema.edgeLabel("self").link("person", "person")
+                  .properties("weight", "created", "matched")
+                  .nullableKeys("weight", "created", "matched")
+                  .ifNotExist().create();
+            this.prepareEdgeLabel(graph, schema, "self");
+        }
+    }
+
+    private void prepareVertexLabel(HugeGraph graph, SchemaManager schema,
+                                    String label) {
+        if (!graph.existsVertexLabel(label)) {
+            return;
+        }
+        schema.vertexLabel(label).properties("created", "matched")
+              .nullableKeys("created", "matched").append();
+        schema.indexLabel(label + "ByCreated").onV(label).by("created")
+              .secondary().ifNotExist().create();
+        schema.indexLabel(label + "ByMatched").onV(label).by("matched")
+              .secondary().ifNotExist().create();
+    }
+
+    private void prepareEdgeLabel(HugeGraph graph, SchemaManager schema,
+                                  String label) {
+        if (!graph.existsEdgeLabel(label)) {
+            return;
+        }
+        schema.edgeLabel(label).properties("created", "matched")
+              .nullableKeys("created", "matched").append();
+        schema.indexLabel(label + "ByCreated").onE(label).by("created")
+              .secondary().ifNotExist().create();
+        schema.indexLabel(label + "ByMatched").onE(label).by("matched")
+              .secondary().ifNotExist().create();
+    }
+
+    private static String graphName(LoadGraphWith.GraphData graphData) {
+        if (graphData == null) {
+            return "gherkin_empty_standard";
+        }
+        return "gherkin_" + graphData.name().toLowerCase(Locale.ROOT) +
+               "_standard";
+    }
+
+    private static final class GraphDataLoader implements LoadGraphWith {
+
+        private final GraphData graphData;
+
+        private GraphDataLoader(GraphData graphData) {
+            this.graphData = graphData;
+        }
+
+        @Override
+        public GraphData value() {
+            return this.graphData;
+        }
+
+        @Override
+        public Class annotationType() {
+            return LoadGraphWith.class;
+        }
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java
index 793b823c6b..8e2a852458 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java
@@ -53,7 +53,10 @@ public class TestGraph implements Graph {
 
     public static final String DEFAULT_VL = "vertex";
 
-    public static final Set TRUNCATE_BACKENDS = ImmutableSet.of("rocksdb", "mysql");
+    public static final Set TRUNCATE_BACKENDS =
+            ImmutableSet.of("rocksdb", "mysql");
+
+    private static final String HSTORE_BACKEND = "hstore";
 
     private static volatile int id = 666;
 
@@ -95,24 +98,53 @@ protected void clearBackend() {
 
     @Watched
     protected void clearAll(String testClass) {
-        List pks = this.graph.schema().getPropertyKeys();
-        if (pks.isEmpty()) {
-            // No need to clear if there is no PKs(that's no schema and data)
+        if (!this.hasSchema() &&
+            !testClass.endsWith("VariableAsMapTest")) {
+            // No need to clear if there is no schema, data, or variables
             return;
         }
 
-        if (TRUNCATE_BACKENDS.contains(this.graph.backend())) {
+        String backend = this.graph.backend();
+        if (HSTORE_BACKEND.equals(backend)) {
+            // HStore keeps schema in PD, outside the truncated data store
+            this.truncateBackend();
+            this.clearSchemaAndVariables(testClass);
+        } else if (TRUNCATE_BACKENDS.contains(backend)) {
             // Delete all data by truncating tables
             this.truncateBackend();
         } else {
-            // Clear schema (also include data)
+            this.clearSchemaAndVariables(testClass);
+        }
+    }
+
+    @Watched
+    protected void clearForLoad() {
+        if (HSTORE_BACKEND.equals(this.graph.backend())) {
+            // An auxiliary graph can be loaded while its source remains open.
+            // Truncating it makes the source invisible to HStore scans.
+            // Only the bootstrap schema needs to be removed at this point.
             this.clearSchema();
+        } else {
+            this.clearAll("");
+        }
+    }
 
-            // Clear variables if needed (would not clear when clearing schema)
-            if (testClass.endsWith("VariableAsMapTest")) {
-                this.clearVariables();
-                this.tx().commit();
-            }
+    private boolean hasSchema() {
+        SchemaManager schema = this.graph.schema();
+        return !schema.getPropertyKeys().isEmpty() ||
+               !schema.getVertexLabels().isEmpty() ||
+               !schema.getEdgeLabels().isEmpty() ||
+               !schema.getIndexLabels().isEmpty();
+    }
+
+    private void clearSchemaAndVariables(String testClass) {
+        // Clear schema (also include data)
+        this.clearSchema();
+
+        // Clear variables if needed (would not clear when clearing schema)
+        if (testClass.endsWith("VariableAsMapTest")) {
+            this.clearVariables();
+            this.tx().commit();
         }
     }
 
@@ -788,6 +820,8 @@ private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL,
                             "here", "to-change", "dropped", "not-dropped",
                             "new", "to-drop", "short", "long")
               .ifNotExist().create();
+        schema.edgeLabel("self-but-different").link(selfVL, selfVL)
+              .ifNotExist().create();
         schema.edgeLabel("aTOa").link(defaultVL, defaultVL)
               .properties("gremlin.partitionGraphStrategy.partition")
               .nullableKeys("gremlin.partitionGraphStrategy.partition")
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java
index a8b07817a1..45c93c2774 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java
@@ -30,6 +30,7 @@
 
 import org.apache.commons.configuration2.Configuration;
 import org.apache.commons.configuration2.PropertiesConfiguration;
+import org.apache.commons.text.StringEscapeUtils;
 import org.apache.hugegraph.HugeGraph;
 import org.apache.hugegraph.config.CoreOptions;
 import org.apache.hugegraph.perf.PerfUtil.Watched;
@@ -542,7 +543,7 @@ public void loadGraphData(final Graph graph,
         TestGraph testGraph = (TestGraph) graph;
 
         // Clear basic schema initiated in openTestGraph
-        testGraph.clearAll("");
+        testGraph.clearForLoad();
 
         if (testGraph.loadedGraph() == null) {
             testGraph.loadedGraph(REGULAR_LOAD);
@@ -594,6 +595,10 @@ public GraphTraversalSource traversal(Graph graph) {
 
     @Override
     public String convertId(Object id, Class c) {
-        return id.toString();
+        if (id instanceof Number) {
+            return id.toString();
+        }
+        return String.format("\"%s\"", StringEscapeUtils.escapeJava(
+                id.toString()));
     }
 }
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
index affe484c0a..8083a53973 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
@@ -24,15 +24,21 @@
 import org.apache.hugegraph.backend.id.Id;
 import org.apache.hugegraph.backend.id.IdGenerator;
 import org.apache.hugegraph.exception.NotFoundException;
+import org.apache.hugegraph.schema.IndexLabel;
 import org.apache.hugegraph.schema.PropertyKey;
+import org.apache.hugegraph.schema.VertexLabel;
 import org.apache.hugegraph.testutil.Assert;
 import org.apache.hugegraph.type.define.DataType;
+import org.apache.hugegraph.type.define.IndexType;
+import org.apache.hugegraph.type.define.SchemaStatus;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.TextP;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
 import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
 import org.apache.tinkerpop.gremlin.process.traversal.step.filter.AndStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep;
@@ -66,6 +72,12 @@ null, new HasContainer(T.label.getAccessor(),
                                        P.within(null, "person"))));
     }
 
+    @Test
+    public void testCanExtractHasContainerWithNullPredicate() {
+        Assert.assertFalse(TraversalUtil.canExtractHasContainer(
+                null, new HasContainer("name", null)));
+    }
+
     @Test
     public void testExtractHasContainerKeepsNullKeyLocal() {
         Traversal.Admin traversal = __.V()
@@ -156,6 +168,68 @@ public void testExtractHasContainerKeepsTextRangeGraphHasStep() {
         Assert.assertTrue(hasStepExists(traversal));
     }
 
+    @Test
+    public void testExtractHasContainerKeepsUnindexedGraphPropertyLocal() {
+        HugeGraph graph = Mockito.mock(HugeGraph.class);
+        PropertyKey age = propertyKey(1L, "age", DataType.INT);
+        PropertyKey name = propertyKey(2L, "name", DataType.TEXT);
+        Mockito.when(graph.propertyKey("age")).thenReturn(age);
+        Mockito.when(graph.propertyKey("name")).thenReturn(name);
+
+        Traversal.Admin traversal = traversal(
+                __.V().has("person", "name", TextP.containing("ar")),
+                graph);
+        HasStep hasStep = (HasStep) traversal.getEndStep();
+        hasStep.addHasContainer(new HasContainer("age", P.eq(29)));
+        HugeGraphStep newStep = replaceGraphStep(traversal);
+
+        TraversalUtil.extractHasContainer(newStep, traversal);
+
+        Assert.assertTrue(hasContainer(newStep, T.label.getAccessor()));
+        Assert.assertFalse(hasContainer(newStep, "age"));
+        Assert.assertFalse(hasContainer(newStep, "name"));
+        Assert.assertFalse(hasStepExists(traversal, T.label.getAccessor()));
+        Assert.assertTrue(hasStepExists(traversal, "age"));
+        Assert.assertTrue(hasStepExists(traversal, "name"));
+    }
+
+    @Test
+    public void testExtractHasContainerKeepsRebuildingIndexPropertyLocal() {
+        HugeGraph graph = Mockito.mock(HugeGraph.class);
+        PropertyKey age = propertyKey(1L, "age", DataType.INT);
+        PropertyKey name = propertyKey(2L, "name", DataType.TEXT);
+        VertexLabel person = new VertexLabel(graph, IdGenerator.of(3L),
+                                             "person");
+        person.properties(age.id(), name.id());
+        IndexLabel ageIndex = new IndexLabel(graph, IdGenerator.of(4L),
+                                             "personByAge");
+        ageIndex.indexField(age.id());
+        ageIndex.indexType(IndexType.SECONDARY);
+        ageIndex.status(SchemaStatus.REBUILDING);
+        person.addIndexLabel(ageIndex.id());
+
+        Mockito.when(graph.propertyKey("age")).thenReturn(age);
+        Mockito.when(graph.propertyKey("name")).thenReturn(name);
+        Mockito.when(graph.vertexLabel("person")).thenReturn(person);
+        Mockito.when(graph.indexLabel(ageIndex.id())).thenReturn(ageIndex);
+
+        Traversal.Admin traversal = traversal(
+                __.V().has("person", "name", TextP.containing("ar")),
+                graph);
+        HasStep hasStep = (HasStep) traversal.getEndStep();
+        hasStep.addHasContainer(new HasContainer("age", P.eq(29)));
+        HugeGraphStep newStep = replaceGraphStep(traversal);
+
+        TraversalUtil.extractHasContainer(newStep, traversal);
+
+        Assert.assertTrue(hasContainer(newStep, T.label.getAccessor()));
+        Assert.assertFalse(hasContainer(newStep, "age"));
+        Assert.assertFalse(hasContainer(newStep, "name"));
+        Assert.assertFalse(hasStepExists(traversal, T.label.getAccessor()));
+        Assert.assertTrue(hasStepExists(traversal, "age"));
+        Assert.assertTrue(hasStepExists(traversal, "name"));
+    }
+
     @Test
     public void testExtractHasContainerKeepsTextRangeWithoutGraph() {
         Traversal.Admin traversal = __.V()
@@ -314,6 +388,31 @@ public void testExtractHasContainerKeepsTextRangeVertexHasStep() {
         Assert.assertTrue(hasStepExists(traversal));
     }
 
+    @Test
+    public void testExtractHasContainerPartiallyExtractsVertexHasStep() {
+        HugeGraph graph = Mockito.mock(HugeGraph.class);
+        PropertyKey age = propertyKey(1L, "age", DataType.INT);
+        PropertyKey name = propertyKey(2L, "name", DataType.TEXT);
+        Mockito.when(graph.propertyKey("age")).thenReturn(age);
+        Mockito.when(graph.propertyKey("name")).thenReturn(name);
+
+        Traversal.Admin traversal = traversal(
+                __.V().out().has("person", "name", TextP.containing("ar")),
+                graph);
+        HasStep hasStep = (HasStep) traversal.getEndStep();
+        hasStep.addHasContainer(new HasContainer("age", P.eq(29)));
+        HugeVertexStep newStep = replaceVertexStep(traversal);
+
+        TraversalUtil.extractHasContainer(newStep, traversal);
+
+        Assert.assertTrue(hasContainer(newStep, T.label.getAccessor()));
+        Assert.assertTrue(hasContainer(newStep, "age"));
+        Assert.assertFalse(hasContainer(newStep, "name"));
+        Assert.assertFalse(hasStepExists(traversal, T.label.getAccessor()));
+        Assert.assertFalse(hasStepExists(traversal, "age"));
+        Assert.assertTrue(hasStepExists(traversal, "name"));
+    }
+
     @Test
     public void testExtractHasContainerRemovesSafeVertexHasStep() {
         HugeGraph graph = Mockito.mock(HugeGraph.class);
@@ -458,7 +557,7 @@ private static void replaceStep(Step origin, Step newStep,
         TraversalHelper.replaceStep((Step) origin, (Step) newStep, traversal);
     }
 
-    private static boolean hasContainer(HugeGraphStep step, String key) {
+    private static boolean hasContainer(HasContainerHolder step, String key) {
         for (HasContainer has : step.getHasContainers()) {
             if (key.equals(has.getKey())) {
                 return true;
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
index b42e1aec0e..a5c1b89e4b 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
@@ -21,6 +21,7 @@
 import org.apache.hugegraph.core.RoleElectionStateMachineTest;
 import org.apache.hugegraph.meta.EtcdMetaDriverTest;
 import org.apache.hugegraph.meta.MetaManagerSchemaCacheClearEventTest;
+import org.apache.hugegraph.tinkerpop.HugeGraphTestInfrastructureTest;
 import org.apache.hugegraph.traversal.optimize.TraversalUtilOptimizeTest;
 import org.apache.hugegraph.unit.api.filter.LoadDetectFilterTest;
 import org.apache.hugegraph.unit.api.filter.PathFilterTest;
@@ -36,6 +37,7 @@
 import org.apache.hugegraph.unit.config.GremlinConfigCompatibilityTest;
 import org.apache.hugegraph.unit.core.AnalyzerTest;
 import org.apache.hugegraph.unit.core.BackendMutationTest;
+import org.apache.hugegraph.unit.core.BackendProviderFactoryTest;
 import org.apache.hugegraph.unit.core.BackendStoreInfoTest;
 import org.apache.hugegraph.unit.core.ConditionQueryFlattenTest;
 import org.apache.hugegraph.unit.core.ConditionTest;
@@ -132,6 +134,7 @@
         RowLockTest.class,
         AnalyzerTest.class,
         BackendMutationTest.class,
+        BackendProviderFactoryTest.class,
         ConditionTest.class,
         GroovyScriptEngineCompatibilityTest.class,
         HugeFeaturesTest.class,
@@ -151,6 +154,7 @@
         RoleElectionStateMachineTest.class,
         HugeGraphAuthProxyTest.class,
         SchemaElementTest.class,
+        HugeGraphTestInfrastructureTest.class,
 
         /* serializer */
         BytesBufferTest.class,
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java
index 292a5cd922..8e243b4624 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java
@@ -34,19 +34,36 @@
 import java.util.UUID;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 
 import org.apache.hugegraph.backend.id.EdgeId;
+import org.apache.hugegraph.backend.id.Id;
 import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.structure.HugeEdge;
+import org.apache.hugegraph.structure.HugeFeatures;
+import org.apache.hugegraph.structure.HugeVertex;
 import org.apache.hugegraph.testutil.Assert;
 import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.hugegraph.unit.FakeObjects;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.MutablePath;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
 import org.apache.tinkerpop.gremlin.server.Settings;
+import org.apache.tinkerpop.gremlin.structure.Edge;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge;
+import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedProperty;
+import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex;
+import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertexProperty;
+import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdge;
+import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex;
 import org.apache.tinkerpop.gremlin.util.MessageSerializer;
 import org.apache.tinkerpop.gremlin.util.message.ResponseMessage;
 import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode;
+import org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1;
 import org.apache.tinkerpop.gremlin.util.ser.MessageTextSerializer;
 import org.junit.Test;
+import org.mockito.Mockito;
 import org.yaml.snakeyaml.Yaml;
 
 import io.netty.buffer.ByteBuf;
@@ -56,10 +73,29 @@ public class GremlinConfigCompatibilityTest extends BaseUnitTest {
 
     private static final Pattern CLASS_NAME =
             Pattern.compile("className:\\s*([^,}\\s]+)");
+    private static final Pattern XML_COMMENT =
+            Pattern.compile("", Pattern.DOTALL);
+    private static final Pattern TINKERPOP_DEPENDENCY = Pattern.compile(
+            "\\s*org\\.apache\\.tinkerpop" +
+            "(.*?)", Pattern.DOTALL);
+    private static final Pattern DEPENDENCY_VERSION =
+            Pattern.compile("(.*?)", Pattern.DOTALL);
+    private static final Pattern TINKERPOP_VERSION_PROPERTY = Pattern.compile(
+            "(.*?)");
+    private static final Pattern GUICE_DEPENDENCY = Pattern.compile(
+            "\\s*com\\.google\\.inject" +
+            "(.*?)", Pattern.DOTALL);
+    private static final String SUPPORTED_TINKERPOP_VERSION = "3.7.6";
+    /* Must match the guice.version property of the TinkerPop parent pom
+     * (gremlin-test declares guice as a provided dependency, so HugeGraph
+     * must supply the version TinkerPop itself was built against). */
+    private static final String SUPPORTED_GUICE_VERSION = "4.2.3";
     private static final String SERIALIZER_PACKAGE =
             "org.apache.tinkerpop.gremlin.util.ser.";
     private static final String GRAPHSON_UNTYPED_V1 =
             SERIALIZER_PACKAGE + "GraphSONUntypedMessageSerializerV1";
+    private static final String GRAPHBINARY_BUILDER =
+            "org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder";
     private static final String IO_REGISTRY =
             "org.apache.hugegraph.io.HugeGraphIoRegistry";
     private static final String GREMLIN_SERVER_CONFIG = "gremlin-server.yaml";
@@ -104,6 +140,23 @@ public void testGremlinServerSerializersUseTinkerPopUtilPackage() throws IOExcep
                                                  content);
     }
 
+    @Test
+    public void testTinkerPopPomVersionsUseSupportedVersion()
+            throws IOException {
+        List mismatches = new ArrayList<>();
+        try (Stream files = Files.walk(repositoryRoot())) {
+            files.filter(path -> "pom.xml".equals(
+                         path.getFileName().toString()))
+                 .forEach(path -> collectTinkerPopVersionMismatches(
+                         path, mismatches));
+        }
+
+        Assert.assertTrue("TinkerPop dependencies must use " +
+                          SUPPORTED_TINKERPOP_VERSION + " and guice must " +
+                          "use " + SUPPORTED_GUICE_VERSION + ": " + mismatches,
+                          mismatches.isEmpty());
+    }
+
     @Test
     public void testRemoteSerializersUseTinkerPopUtilPackage() throws IOException {
         for (String file : REMOTE_CONFIGS) {
@@ -138,6 +191,31 @@ public void testGremlinServerConfigVariantsSupportGraphSONMimeTypes()
         }
     }
 
+    @Test
+    public void testGremlinServerConfigVariantsUseHugeGraphBinaryBuilder()
+            throws Exception {
+        Path assembly = serverAssemblyPath();
+
+        for (String variant : GREMLIN_SERVER_CONFIG_VARIANTS) {
+            Settings settings = Settings.read(assembly.resolve(variant)
+                                                   .toString());
+            boolean found = false;
+            for (Settings.SerializerSettings serializer :
+                 settings.serializers) {
+                if (!serializer.className.startsWith(SERIALIZER_PACKAGE +
+                                                     "GraphBinary")) {
+                    continue;
+                }
+                Assert.assertNotNull(variant, serializer.config);
+                Assert.assertEquals(variant, GRAPHBINARY_BUILDER,
+                                    serializer.config.get("builder"));
+                found = true;
+            }
+            Assert.assertTrue("No GraphBinary serializer in " + variant,
+                              found);
+        }
+    }
+
     private static void assertSupportsTypedAndUntypedGraphSONMimeTypes(
             String fileName, Map graphSONMimeTypes) {
         for (String mimeType : UNTYPED_GRAPHSON_MIME_TYPES) {
@@ -235,6 +313,196 @@ public void testConfiguredGraphBinarySerializersCanRoundTripStandardPredicate()
                           GREMLIN_SERVER_CONFIG, found);
     }
 
+    @Test
+    public void testConfiguredGraphBinarySerializersCanRoundTripElementProperties()
+            throws Exception {
+        Settings settings = readGremlinServerSettings();
+        boolean found = false;
+
+        for (Settings.SerializerSettings serializerSettings :
+             settings.serializers) {
+            if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE +
+                                                         "GraphBinary")) {
+                continue;
+            }
+
+            MessageSerializer serializer =
+                    newMessageSerializer(serializerSettings.className);
+            serializer.configure(config(serializerSettings.config),
+                                 Collections.emptyMap());
+            assertCanRoundTripElementProperties(serializerSettings.className,
+                                                serializer);
+            found = true;
+        }
+
+        Assert.assertTrue("No GraphBinary serializer settings found in " +
+                          GREMLIN_SERVER_CONFIG, found);
+    }
+
+    @Test
+    public void testConfiguredGraphBinarySerializersCanRoundTripHugeGraphElements()
+            throws Exception {
+        Settings settings = readGremlinServerSettings();
+        boolean found = false;
+
+        for (Settings.SerializerSettings serializerSettings :
+             settings.serializers) {
+            if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE +
+                                                         "GraphBinary")) {
+                continue;
+            }
+
+            MessageSerializer serializer =
+                    newMessageSerializer(serializerSettings.className);
+            serializer.configure(config(serializerSettings.config),
+                                 Collections.emptyMap());
+            assertCanRoundTripHugeGraphElements(serializerSettings.className,
+                                                serializer);
+            found = true;
+        }
+
+        Assert.assertTrue("No GraphBinary serializer settings found in " +
+                          GREMLIN_SERVER_CONFIG, found);
+    }
+
+    @Test
+    public void testConfiguredGraphBinarySerializersUsePrimitiveHugeGraphIds()
+            throws Exception {
+        Settings settings = readGremlinServerSettings();
+        boolean found = false;
+
+        for (Settings.SerializerSettings serializerSettings :
+             settings.serializers) {
+            if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE +
+                                                         "GraphBinary")) {
+                continue;
+            }
+
+            MessageSerializer serializer =
+                    newMessageSerializer(serializerSettings.className);
+            serializer.configure(config(serializerSettings.config),
+                                 Collections.emptyMap());
+            assertUsesPrimitiveHugeGraphIds(serializerSettings.className,
+                                            serializer);
+            found = true;
+        }
+
+        Assert.assertTrue("No GraphBinary serializer settings found in " +
+                          GREMLIN_SERVER_CONFIG, found);
+    }
+
+    @Test
+    public void testConfiguredGraphBinarySerializersCanRoundTripReferenceElements()
+            throws Exception {
+        Settings settings = readGremlinServerSettings();
+        boolean found = false;
+
+        for (Settings.SerializerSettings serializerSettings :
+             settings.serializers) {
+            if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE +
+                                                         "GraphBinary")) {
+                continue;
+            }
+
+            MessageSerializer serializer =
+                    newMessageSerializer(serializerSettings.className);
+            serializer.configure(config(serializerSettings.config),
+                                 Collections.emptyMap());
+            assertCanRoundTripReferenceElements(serializerSettings.className,
+                                                serializer);
+            found = true;
+        }
+
+        Assert.assertTrue("No GraphBinary serializer settings found in " +
+                          GREMLIN_SERVER_CONFIG, found);
+    }
+
+    @Test
+    public void testConfiguredGraphSONSerializersIncludeElementProperties()
+            throws Exception {
+        Settings settings = readGremlinServerSettings();
+        boolean found = false;
+
+        for (Settings.SerializerSettings serializerSettings :
+             settings.serializers) {
+            if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE +
+                                                         "GraphSON")) {
+                continue;
+            }
+
+            MessageTextSerializer serializer =
+                    newTextSerializer(serializerSettings.className);
+            serializer.configure(config(serializerSettings.config),
+                                 Collections.emptyMap());
+            assertIncludesElementProperties(serializerSettings.className,
+                                            serializer);
+            found = true;
+        }
+
+        Assert.assertTrue("No GraphSON serializer settings found in " +
+                          GREMLIN_SERVER_CONFIG, found);
+    }
+
+    @Test
+    public void testConfiguredGraphSONSerializersIncludeTreeElementProperties()
+            throws Exception {
+        Settings settings = readGremlinServerSettings();
+        boolean found = false;
+
+        for (Settings.SerializerSettings serializerSettings :
+             settings.serializers) {
+            if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE +
+                                                         "GraphSON")) {
+                continue;
+            }
+
+            MessageTextSerializer serializer =
+                    newTextSerializer(serializerSettings.className);
+            serializer.configure(config(serializerSettings.config),
+                                 Collections.emptyMap());
+            String tree = serializeResponse(serializer, elementTree());
+            Assert.assertTrue(serializerSettings.className,
+                              tree.contains("properties"));
+            Assert.assertTrue(serializerSettings.className,
+                              tree.contains("marko"));
+            Assert.assertTrue(serializerSettings.className,
+                              tree.contains("weight"));
+            found = true;
+        }
+
+        Assert.assertTrue("No GraphSON serializer settings found in " +
+                          GREMLIN_SERVER_CONFIG, found);
+    }
+
+    @Test
+    public void testDefaultGraphSONSerializerIncludesHugeGraphElementProperties()
+            throws Exception {
+        Settings settings = readGremlinServerSettings();
+        Map config = graphSONV1Config(settings);
+        MessageTextSerializer serializer =
+                newTextSerializer(GRAPHSON_UNTYPED_V1);
+        serializer.configure(config(config), Collections.emptyMap());
+
+        HugeEdge edge = hugeGraphEdgeWithProperties();
+        HugeVertex vertex = (HugeVertex) edge.outVertex();
+        String vertexJson = serializeResponse(serializer, vertex);
+        String edgeJson = serializeResponse(serializer, edge);
+        String pathJson = serializeResponse(
+                serializer, elementPath(vertex, edge));
+
+        Assert.assertContains("properties", vertexJson);
+        Assert.assertContains("name", vertexJson);
+        Assert.assertContains("tom", vertexJson);
+        Assert.assertContains("age", vertexJson);
+        Assert.assertContains("18", vertexJson);
+        Assert.assertContains("properties", edgeJson);
+        Assert.assertContains("weight", edgeJson);
+        Assert.assertContains("0.75", edgeJson);
+        Assert.assertContains("properties", pathJson);
+        Assert.assertContains("tom", pathJson);
+        Assert.assertContains("0.75", pathJson);
+    }
+
     @Test
     public void testRemoteObjectsSerializerCanSerializePathShape()
             throws Exception {
@@ -272,6 +540,75 @@ private static Path serverAssemblyPath() {
         return findConfDir().getParent().getParent();
     }
 
+    private static Path repositoryRoot() {
+        Path current = Paths.get(System.getProperty("user.dir"))
+                            .toAbsolutePath();
+        while (current != null) {
+            if (Files.isDirectory(current.resolve("hugegraph-server")) &&
+                Files.isDirectory(current.resolve("hugegraph-pd"))) {
+                return current;
+            }
+            current = current.getParent();
+        }
+
+        Assert.fail("Can't find HugeGraph repository root from " +
+                    System.getProperty("user.dir"));
+        return Paths.get(System.getProperty("user.dir"));
+    }
+
+    private static void collectTinkerPopVersionMismatches(
+            Path pom, List mismatches) {
+        try {
+            String content = Files.readString(pom, StandardCharsets.UTF_8);
+            content = XML_COMMENT.matcher(content).replaceAll("");
+
+            Matcher property = TINKERPOP_VERSION_PROPERTY.matcher(content);
+            while (property.find()) {
+                collectVersionMismatch(pom, property.group(1), mismatches);
+            }
+
+            Matcher dependency = TINKERPOP_DEPENDENCY.matcher(content);
+            while (dependency.find()) {
+                Matcher version = DEPENDENCY_VERSION.matcher(
+                        dependency.group(1));
+                if (version.find()) {
+                    collectVersionMismatch(pom, version.group(1), mismatches);
+                }
+            }
+
+            Matcher guice = GUICE_DEPENDENCY.matcher(content);
+            while (guice.find()) {
+                Matcher version = DEPENDENCY_VERSION.matcher(
+                        guice.group(1));
+                if (version.find()) {
+                    collectGuiceVersionMismatch(pom, version.group(1),
+                                                mismatches);
+                }
+            }
+        } catch (IOException e) {
+            throw new IllegalStateException("Failed to read " + pom, e);
+        }
+    }
+
+    private static void collectGuiceVersionMismatch(Path pom, String version,
+                                                    List mismatches) {
+        String actual = version.trim();
+        if (SUPPORTED_GUICE_VERSION.equals(actual)) {
+            return;
+        }
+        mismatches.add(repositoryRoot().relativize(pom) + "=guice:" + actual);
+    }
+
+    private static void collectVersionMismatch(Path pom, String version,
+                                               List mismatches) {
+        String actual = version.trim();
+        if (SUPPORTED_TINKERPOP_VERSION.equals(actual) ||
+            "${tinkerpop.version}".equals(actual)) {
+            return;
+        }
+        mismatches.add(repositoryRoot().relativize(pom) + "=" + actual);
+    }
+
     private static Path findConfDir() {
         String configuredDir = System.getProperty("hugegraph.conf.dir");
         Path configuredPath = resolveConfiguredDir(configuredDir);
@@ -433,14 +770,21 @@ private static ResponseMessage roundTripResponse(
     private static ResponseMessage roundTripBinaryResponse(
             MessageSerializer serializer, Object result)
             throws Exception {
+        return roundTripBinaryResponse(serializer, serializer, result);
+    }
+
+    private static ResponseMessage roundTripBinaryResponse(
+            MessageSerializer serverSerializer,
+            MessageSerializer clientSerializer, Object result)
+            throws Exception {
         ResponseMessage response = ResponseMessage.build(UUID.randomUUID())
                                                   .code(ResponseStatusCode.SUCCESS)
                                                   .result(result)
                                                   .create();
-        ByteBuf buffer = serializer.serializeResponseAsBinary(
+        ByteBuf buffer = serverSerializer.serializeResponseAsBinary(
                 response, ByteBufAllocator.DEFAULT);
         try {
-            return serializer.deserializeResponse(buffer);
+            return clientSerializer.deserializeResponse(buffer);
         } finally {
             buffer.release();
         }
@@ -562,6 +906,230 @@ private static void assertCanRoundTripStandardPredicate(
         Assert.assertEquals(message, expected, actual);
     }
 
+    private static void assertCanRoundTripElementProperties(
+            String serializerName, MessageSerializer serializer)
+            throws Exception {
+        ResponseMessage vertexResponse = roundTripBinaryResponse(
+                serializer, vertexWithProperties());
+        Object vertexResult = vertexResponse.getResult().getData();
+        Assert.assertInstanceOf(Vertex.class, vertexResult);
+        Vertex vertex = (Vertex) vertexResult;
+        Assert.assertEquals(serializerName, 1, vertex.id());
+        Assert.assertEquals(serializerName, "person", vertex.label());
+        Assert.assertEquals(serializerName, "marko", vertex.value("name"));
+        Assert.assertEquals(serializerName, 29,
+                            ((Number) vertex.value("age")).intValue());
+
+        ResponseMessage edgeResponse = roundTripBinaryResponse(
+                serializer, edgeWithProperties());
+        Object edgeResult = edgeResponse.getResult().getData();
+        Assert.assertInstanceOf(Edge.class, edgeResult);
+        Edge edge = (Edge) edgeResult;
+        Assert.assertEquals(serializerName, 7, edge.id());
+        Assert.assertEquals(serializerName, "knows", edge.label());
+        Assert.assertEquals(serializerName, 0.5D,
+                            (Double) edge.value("weight"), 0.0D);
+
+        org.apache.tinkerpop.gremlin.process.traversal.Path sourcePath =
+                elementPath(vertexWithProperties(), edgeWithProperties());
+        Object pathResult = roundTripBinaryResponse(serializer, sourcePath)
+                            .getResult().getData();
+        Assert.assertInstanceOf(
+                org.apache.tinkerpop.gremlin.process.traversal.Path.class,
+                pathResult);
+        org.apache.tinkerpop.gremlin.process.traversal.Path path =
+                (org.apache.tinkerpop.gremlin.process.traversal.Path)
+                        pathResult;
+        Vertex pathVertex = path.get(0);
+        Edge pathEdge = path.get(1);
+        Assert.assertEquals(serializerName, "marko",
+                            pathVertex.value("name"));
+        Assert.assertEquals(serializerName, 0.5D,
+                            (Double) pathEdge.value("weight"), 0.0D);
+    }
+
+    private static void assertCanRoundTripHugeGraphElements(
+            String serializerName, MessageSerializer serializer)
+            throws Exception {
+        HugeEdge expectedEdge = hugeGraphEdgeWithProperties();
+        HugeVertex expectedVertex = (HugeVertex) expectedEdge.outVertex();
+        MessageSerializer standardClient =
+                new GraphBinaryMessageSerializerV1();
+
+        Object vertexResult = roundTripBinaryResponse(serializer,
+                                                      standardClient,
+                                                      expectedVertex)
+                              .getResult().getData();
+        Assert.assertInstanceOf(Vertex.class, vertexResult);
+        Vertex vertex = (Vertex) vertexResult;
+        Assert.assertEquals(serializerName, expectedVertex.id().asLong(),
+                            ((Number) vertex.id()).longValue());
+        Assert.assertEquals(serializerName, "person", vertex.label());
+        Assert.assertEquals(serializerName, "tom", vertex.value("name"));
+        Assert.assertEquals(serializerName, 18,
+                            ((Number) vertex.value("age")).intValue());
+
+        Object edgeResult = roundTripBinaryResponse(serializer, standardClient,
+                                                    expectedEdge)
+                            .getResult().getData();
+        Assert.assertInstanceOf(Edge.class, edgeResult);
+        Edge edge = (Edge) edgeResult;
+        Assert.assertEquals(serializerName, expectedEdge.id().asString(),
+                            edge.id().toString());
+        Assert.assertEquals(serializerName, "knows", edge.label());
+        Assert.assertEquals(serializerName, 0.75D,
+                            (Double) edge.value("weight"), 0.0D);
+
+        Object pathResult = roundTripBinaryResponse(
+                serializer, standardClient,
+                elementPath(expectedVertex, expectedEdge))
+                .getResult().getData();
+        Assert.assertInstanceOf(
+                org.apache.tinkerpop.gremlin.process.traversal.Path.class,
+                pathResult);
+        org.apache.tinkerpop.gremlin.process.traversal.Path path =
+                (org.apache.tinkerpop.gremlin.process.traversal.Path)
+                        pathResult;
+        Vertex pathVertex = path.get(0);
+        Edge pathEdge = path.get(1);
+        Assert.assertEquals(serializerName, "tom",
+                            pathVertex.value("name"));
+        Assert.assertEquals(serializerName, 0.75D,
+                            (Double) pathEdge.value("weight"), 0.0D);
+    }
+
+    private static void assertUsesPrimitiveHugeGraphIds(
+            String serializerName, MessageSerializer serializer)
+            throws Exception {
+        List ids = Arrays.asList(
+                IdGenerator.of("marko"),
+                IdGenerator.of(123L),
+                IdGenerator.of(UUID.fromString(
+                        "3cfcafc8-7906-4ab7-a207-4ded056f58de")),
+                EdgeId.parse("S1>2>3>4>L6")
+        );
+        MessageSerializer standardClient =
+                new GraphBinaryMessageSerializerV1();
+
+        for (Id id : ids) {
+            Object actual = roundTripBinaryResponse(serializer, standardClient,
+                                                    id)
+                            .getResult().getData();
+            Assert.assertEquals(serializerName, id.asObject(), actual);
+        }
+    }
+
+    private static void assertCanRoundTripReferenceElements(
+            String serializerName, MessageSerializer serializer)
+            throws Exception {
+        ReferenceVertex marko = new ReferenceVertex(1, "person");
+        ReferenceVertex vadas = new ReferenceVertex(2, "person");
+        ReferenceEdge knows = new ReferenceEdge(7, "knows", marko, vadas);
+
+        Object vertexResult = roundTripBinaryResponse(serializer, marko)
+                              .getResult().getData();
+        Assert.assertInstanceOf(Vertex.class, vertexResult);
+        Assert.assertFalse(serializerName,
+                           ((Vertex) vertexResult).properties().hasNext());
+
+        Object edgeResult = roundTripBinaryResponse(serializer, knows)
+                            .getResult().getData();
+        Assert.assertInstanceOf(Edge.class, edgeResult);
+        Assert.assertFalse(serializerName,
+                           ((Edge) edgeResult).properties().hasNext());
+
+        org.apache.tinkerpop.gremlin.process.traversal.Path sourcePath =
+                elementPath(marko, knows);
+        Object pathResult = roundTripBinaryResponse(serializer, sourcePath)
+                            .getResult().getData();
+        Assert.assertInstanceOf(
+                org.apache.tinkerpop.gremlin.process.traversal.Path.class,
+                pathResult);
+        org.apache.tinkerpop.gremlin.process.traversal.Path path =
+                (org.apache.tinkerpop.gremlin.process.traversal.Path)
+                        pathResult;
+        Assert.assertFalse(serializerName,
+                           ((Vertex) path.get(0)).properties().hasNext());
+        Assert.assertFalse(serializerName,
+                           ((Edge) path.get(1)).properties().hasNext());
+    }
+
+    private static void assertIncludesElementProperties(
+            String serializerName, MessageTextSerializer serializer)
+            throws Exception {
+        String vertex = serializeResponse(serializer, vertexWithProperties());
+        Assert.assertTrue(serializerName, vertex.contains("properties"));
+        Assert.assertTrue(serializerName, vertex.contains("name"));
+        Assert.assertTrue(serializerName, vertex.contains("marko"));
+        Assert.assertTrue(serializerName, vertex.contains("age"));
+        Assert.assertTrue(serializerName, vertex.contains("29"));
+
+        String edge = serializeResponse(serializer, edgeWithProperties());
+        Assert.assertTrue(serializerName, edge.contains("properties"));
+        Assert.assertTrue(serializerName, edge.contains("weight"));
+        Assert.assertTrue(serializerName, edge.contains("0.5"));
+
+        String path = serializeResponse(
+                serializer,
+                elementPath(vertexWithProperties(), edgeWithProperties()));
+        Assert.assertTrue(serializerName, path.contains("properties"));
+        Assert.assertTrue(serializerName, path.contains("marko"));
+        Assert.assertTrue(serializerName, path.contains("weight"));
+    }
+
+    private static DetachedVertex vertexWithProperties() {
+        return DetachedVertex.build()
+                             .setId(1)
+                             .setLabel("person")
+                             .addProperty(new DetachedVertexProperty<>(
+                                     11, "name", "marko",
+                                     Collections.emptyMap()))
+                             .addProperty(new DetachedVertexProperty<>(
+                                     12, "age", 29,
+                                     Collections.emptyMap()))
+                             .create();
+    }
+
+    private static org.apache.tinkerpop.gremlin.process.traversal.Path
+            elementPath(Vertex vertex, Edge edge) {
+        return MutablePath.make()
+                          .extend(vertex, Set.of("v"))
+                          .extend(edge, Set.of("e"));
+    }
+
+    private static Tree elementTree() {
+        Tree tree = new Tree<>();
+        Tree children = new Tree<>();
+        children.put(edgeWithProperties(), new Tree<>());
+        tree.put(vertexWithProperties(), children);
+        return tree;
+    }
+
+    private static HugeEdge hugeGraphEdgeWithProperties() {
+        FakeObjects objects = new FakeObjects();
+        Mockito.doReturn(new HugeFeatures(objects.graph(), false))
+               .when(objects.graph()).features();
+        return objects.newEdge(123, 456);
+    }
+
+    private static DetachedEdge edgeWithProperties() {
+        DetachedVertex marko = DetachedVertex.build()
+                                              .setId(1)
+                                              .setLabel("person")
+                                              .create();
+        DetachedVertex vadas = DetachedVertex.build()
+                                              .setId(2)
+                                              .setLabel("person")
+                                              .create();
+        return DetachedEdge.build()
+                           .setId(7)
+                           .setLabel("knows")
+                           .setOutV(marko)
+                           .setInV(vadas)
+                           .addProperty(new DetachedProperty<>("weight", 0.5D))
+                           .create();
+    }
+
     private static void assertContainsGraphSONType(String json,
                                                    String graphSONType) {
         Assert.assertContains("\"@type\"", json);
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendProviderFactoryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendProviderFactoryTest.java
new file mode 100644
index 0000000000..767febd620
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendProviderFactoryTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.hugegraph.unit.core;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.hugegraph.backend.BackendException;
+import org.apache.hugegraph.backend.store.BackendProviderFactory;
+import org.apache.hugegraph.backend.store.memory.InMemoryDBStoreProvider;
+import org.apache.hugegraph.backend.store.raft.RaftBackendStoreProvider;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.junit.Test;
+
+public class BackendProviderFactoryTest extends BaseUnitTest {
+
+    @Test
+    public void testRegister() {
+        String name = "fake-provider";
+        String provider = InMemoryDBStoreProvider.class.getName();
+
+        BackendProviderFactory.register(name, provider);
+        BackendProviderFactory.register(name, provider);
+
+        Assert.assertThrows(BackendException.class, () -> {
+            BackendProviderFactory.register(
+                    name, RaftBackendStoreProvider.class.getName());
+        }, e -> {
+            Assert.assertContains("Exists BackendStoreProvider:",
+                                  e.getMessage());
+        });
+    }
+
+    @Test
+    public void testRegisterConcurrently() throws Exception {
+        String name = "concurrent-provider-" + System.nanoTime();
+        CountDownLatch ready = new CountDownLatch(2);
+        CountDownLatch start = new CountDownLatch(1);
+        ExecutorService executor = Executors.newFixedThreadPool(2);
+        try {
+            List>> registrations = List.of(
+                    executor.submit(() -> {
+                        ready.countDown();
+                        start.await();
+                        BackendProviderFactory.register(
+                                name,
+                                InMemoryDBStoreProvider.class.getName());
+                        return InMemoryDBStoreProvider.class;
+                    }),
+                    executor.submit(() -> {
+                        ready.countDown();
+                        start.await();
+                        BackendProviderFactory.register(
+                                name,
+                                RaftBackendStoreProvider.class.getName());
+                        return RaftBackendStoreProvider.class;
+                    }));
+
+            Assert.assertTrue(ready.await(5L, TimeUnit.SECONDS));
+            start.countDown();
+
+            int successes = 0;
+            int conflicts = 0;
+            Class registered = null;
+            for (Future> registration : registrations) {
+                try {
+                    registered = registration.get(5L, TimeUnit.SECONDS);
+                    successes++;
+                } catch (ExecutionException e) {
+                    Assert.assertInstanceOf(BackendException.class,
+                                            e.getCause());
+                    Assert.assertContains("Exists BackendStoreProvider:",
+                                          e.getCause().getMessage());
+                    conflicts++;
+                }
+            }
+
+            Assert.assertEquals(1, successes);
+            Assert.assertEquals(1, conflicts);
+            Assert.assertNotNull(registered);
+            BackendProviderFactory.register(name, registered.getName());
+        } finally {
+            start.countDown();
+            executor.shutdownNow();
+        }
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/SerializerFactoryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/SerializerFactoryTest.java
index 67afa69842..15294439db 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/SerializerFactoryTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/SerializerFactoryTest.java
@@ -17,6 +17,14 @@
 
 package org.apache.hugegraph.unit.serializer;
 
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
 import org.apache.hugegraph.backend.BackendException;
 import org.apache.hugegraph.backend.serializer.AbstractSerializer;
 import org.apache.hugegraph.backend.serializer.BinaryScatterSerializer;
@@ -58,9 +66,14 @@ public void testRegister() {
         Assert.assertEquals(FakeSerializer.class,
                             SerializerFactory.serializer(config, "fake").getClass());
 
+        // Identical registration is idempotent
+        SerializerFactory.register("fake", FakeSerializer.class.getName());
+        Assert.assertEquals(FakeSerializer.class,
+                            SerializerFactory.serializer(config, "fake").getClass());
+
         Assert.assertThrows(BackendException.class, () -> {
-            // exist
-            SerializerFactory.register("fake", FakeSerializer.class.getName());
+            // conflict
+            SerializerFactory.register("fake", TextSerializer.class.getName());
         }, e -> {
             Assert.assertContains("Exists serializer:", e.getMessage());
         });
@@ -81,6 +94,63 @@ public void testRegister() {
         });
     }
 
+    @Test
+    public void testRegisterConcurrently() throws Exception {
+        String name = "concurrent-serializer-" + System.nanoTime();
+        CountDownLatch ready = new CountDownLatch(2);
+        CountDownLatch start = new CountDownLatch(1);
+        ExecutorService executor = Executors.newFixedThreadPool(2);
+        try {
+            List>> registrations = List.of(
+                    executor.submit(() -> {
+                        ready.countDown();
+                        start.await();
+                        SerializerFactory.register(
+                                name, FakeSerializer.class.getName());
+                        return FakeSerializer.class;
+                    }),
+                    executor.submit(() -> {
+                        ready.countDown();
+                        start.await();
+                        SerializerFactory.register(
+                                name, TextSerializer.class.getName());
+                        return TextSerializer.class;
+                    }));
+
+            Assert.assertTrue(ready.await(5L, TimeUnit.SECONDS));
+            start.countDown();
+
+            int successes = 0;
+            int conflicts = 0;
+            Class registered = null;
+            for (Future> registration : registrations) {
+                try {
+                    registered = registration.get(5L, TimeUnit.SECONDS);
+                    successes++;
+                } catch (ExecutionException e) {
+                    Assert.assertInstanceOf(BackendException.class,
+                                            e.getCause());
+                    Assert.assertContains("Exists serializer:",
+                                          e.getCause().getMessage());
+                    conflicts++;
+                }
+            }
+
+            Assert.assertEquals(1, successes);
+            Assert.assertEquals(1, conflicts);
+            Assert.assertNotNull(registered);
+
+            SerializerFactory.register(name, registered.getName());
+            HugeConfig config = FakeObjects.newConfig();
+            Assert.assertEquals(
+                    registered,
+                    SerializerFactory.serializer(config, name).getClass());
+        } finally {
+            start.countDown();
+            executor.shutdownNow();
+        }
+    }
+
     public static class FakeSerializer extends BinarySerializer {
 
         public FakeSerializer(HugeConfig config) {
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java
index c76d536ff0..722f747ed8 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java
@@ -22,6 +22,7 @@
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.UUID;
 
 import org.apache.commons.lang3.tuple.Pair;
@@ -51,6 +52,9 @@
 import org.apache.hugegraph.unit.FakeObjects;
 import org.apache.hugegraph.util.JsonUtil;
 import org.apache.hugegraph.util.collection.CollectionFactory;
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.MutablePath;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
 import org.apache.tinkerpop.shaded.jackson.core.type.TypeReference;
 import org.eclipse.collections.api.map.primitive.MutableIntObjectMap;
 import org.junit.Test;
@@ -308,6 +312,31 @@ public void testSerializeEdge() {
                             "\"weight\":0.8}}", json);
     }
 
+    @Test
+    public void testSerializePath() {
+        Path path = MutablePath.make()
+                               .extend("marko", Set.of("a"))
+                               .extend(29, Set.of("b"));
+
+        String json = JsonUtil.toJson(path);
+
+        Assert.assertEquals("{\"labels\":[[\"a\"],[\"b\"]]," +
+                            "\"objects\":[\"marko\",29]}", json);
+    }
+
+    @Test
+    public void testSerializeTree() {
+        Tree child = new Tree<>();
+        child.put("lop", new Tree<>());
+        Tree tree = new Tree<>();
+        tree.put("marko", child);
+
+        String json = JsonUtil.toJson(tree);
+
+        Assert.assertEquals("[{\"key\":\"marko\",\"value\":[{" +
+                            "\"key\":\"lop\",\"value\":[]}]}]", json);
+    }
+
     @Test
     public void testDeserializeList() {
         String json = "[\"1\", \"2\", \"3\"]";
diff --git a/hugegraph-server/hugegraph-test/src/main/resources/META-INF/services/io.cucumber.core.backend.ObjectFactory b/hugegraph-server/hugegraph-test/src/main/resources/META-INF/services/io.cucumber.core.backend.ObjectFactory
new file mode 100644
index 0000000000..8361d8526d
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/resources/META-INF/services/io.cucumber.core.backend.ObjectFactory
@@ -0,0 +1 @@
+org.apache.hugegraph.tinkerpop.HugeGraphFeatureTest$HugeGraphGuiceFactory
diff --git a/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter b/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter
index 82fb620d00..02970dc2ee 100644
--- a/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter
+++ b/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter
@@ -96,14 +96,16 @@ org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollba
 org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldCommitEdge: Spawned read transactions are not auto-closed after thread exit
 org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollbackAddedEdge: Spawned read transactions are not auto-closed after thread exit
 
-## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: MergeEdgeTest hard-codes numeric vertex ids although HugeGraph does not advertise numeric id support
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X_exists: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_V_hasXperson_name_marko_X_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_injectXlabel_knows_out_marko_in_vadasX_mergeE: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeE_with_outV_inV_options: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+## HG-MODEL/FOLLOW-UP: scenarios supply custom vertex ids while the standard test graph uses AUTOMATIC id strategy
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person'
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person'
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person'
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_injectXlabel_knows_out_marko_in_vadasX_mergeE: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person'
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeE_with_outV_inV_options: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person'
+
+## HG-MODEL/FOLLOW-UP: HugeGraph multi-edge labels require a non-null sort key on every parallel edge
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X_exists: Scenario requires two same-label parallel edges while one edge has no sort-key value
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_V_hasXperson_name_marko_X_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Scenario requires two same-label parallel edges while one edge has no sort-key value
 
 ## HG-LEGACY/FOLLOW-UP: vertex properties don't have nested structures with HugeVertexSerializer
 org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializeVertex: Vertex properties doesn't have nested structures with HugeVertexSerializer
@@ -120,7 +122,6 @@ org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldCo
 # unsupported automatic edge id, therefore number of edge is wrong
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdge: Unsupported automatic edge id, therefore number of edge is wrong
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdgeByPath: Unsupported automatic edge id, therefore number of edge is wrong
-org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdgeViaMergeE: Unsupported automatic edge id/event tracking for mergeE
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddVertexWithPropertyThenPropertyAdded: Unsupported vertex property changed event tracking for addV().property()
 # shouldWriteToMultiplePartitions
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldWriteToMultiplePartitions: It's not allowed to query by index when there are uncommitted records
@@ -186,20 +187,8 @@ org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.TreeTest.Traversa
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXwithoutXemptyXX_count: Unsupported query 'NOT IN []'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXemptyX_count: Unsupported query 'hasId(EmptyList)'
 
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_containingXarkXX: Unsupported predicate 'containing(ark)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_endingWithXasXX: Unsupported predicate 'endingWith(as)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_startingWithXmarXX: Unsupported predicate 'startingWith(mar)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27X_count: Unsupported relation 'NEQ'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXperson_name_containingXoX_andXltXmXXX: Unsupported predicate 'containing(o)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXrMarXX: Unsupported predicate 'regex(^mar)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXTinkerXX: Unsupported predicate 'regex(Tinker)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXTinkerUnicodeXX: Unsupported predicate 'regex(Tinker.*)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_startingWithXmarXX: Unsupported predicate 'notStartingWith(mar)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_containingXarkXX: Unsupported predicate 'notContaining(ark)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_endingWithXasXX: Unsupported predicate 'notEndingWith(as)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_notRegexXrMarXX: Unsupported predicate 'notRegex(^mar)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27_29X_count: Unsupported relation 'NEQ'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_gtXmX_andXcontainingXoXXX: Unsupported predicate 'containing(o)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXp_neqXvXX: Don't accept query based on properties [p] that are not indexed in any label, may not match not-equal condition
 
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest.shouldSetIdOnAddEWithNamePropertyKeySpecifiedAndNameSuppliedAsProperty: Unsupported ElementIdStrategy edge id/property index query
diff --git a/hugegraph-server/hugegraph-test/src/main/resources/methods.filter b/hugegraph-server/hugegraph-test/src/main/resources/methods.filter
index 193eaf05ab..33c4425e86 100644
--- a/hugegraph-server/hugegraph-test/src/main/resources/methods.filter
+++ b/hugegraph-server/hugegraph-test/src/main/resources/methods.filter
@@ -94,14 +94,16 @@ org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollba
 org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldCommitEdge: Spawned read transactions are not auto-closed after thread exit
 org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollbackAddedEdge: Spawned read transactions are not auto-closed after thread exit
 
-## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: MergeEdgeTest hard-codes numeric vertex ids although HugeGraph does not advertise numeric id support
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X_exists: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_V_hasXperson_name_marko_X_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_injectXlabel_knows_out_marko_in_vadasX_mergeE: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
-org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeE_with_outV_inV_options: Test requires raw numeric vertex ids while HugeGraph exposes custom Id values
+## HG-MODEL/FOLLOW-UP: scenarios supply custom vertex ids while the standard test graph uses AUTOMATIC id strategy
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person'
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person'
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person'
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_injectXlabel_knows_out_marko_in_vadasX_mergeE: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person'
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeE_with_outV_inV_options: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person'
+
+## HG-MODEL/FOLLOW-UP: HugeGraph multi-edge labels require a non-null sort key on every parallel edge
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X_exists: Scenario requires two same-label parallel edges while one edge has no sort-key value
+org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_V_hasXperson_name_marko_X_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Scenario requires two same-label parallel edges while one edge has no sort-key value
 
 ## HG-LEGACY/FOLLOW-UP: vertex properties don't have nested structures with HugeVertexSerializer
 org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializeVertex: Vertex properties doesn't have nested structures with HugeVertexSerializer
@@ -118,7 +120,6 @@ org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldCo
 # unsupported automatic edge id, therefore number of edge is wrong
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdge: Unsupported automatic edge id, therefore number of edge is wrong
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdgeByPath: Unsupported automatic edge id, therefore number of edge is wrong
-org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdgeViaMergeE: Unsupported automatic edge id/event tracking for mergeE
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddVertexWithPropertyThenPropertyAdded: Unsupported vertex property changed event tracking for addV().property()
 # shouldWriteToMultiplePartitions
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldWriteToMultiplePartitions: It's not allowed to query by index when there are uncommitted records
@@ -148,20 +149,8 @@ org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXwithoutXemptyXX_count: Unsupported query 'NOT IN []'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXemptyX_count: Unsupported query 'hasId(EmptyList)'
 
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_containingXarkXX: Unsupported predicate 'containing(ark)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_endingWithXasXX: Unsupported predicate 'endingWith(as)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_startingWithXmarXX: Unsupported predicate 'startingWith(mar)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27X_count: Unsupported relation 'NEQ'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXperson_name_containingXoX_andXltXmXXX: Unsupported predicate 'containing(o)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXrMarXX: Unsupported predicate 'regex(^mar)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXTinkerXX: Unsupported predicate 'regex(Tinker)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_regexXTinkerUnicodeXX: Unsupported predicate 'regex(Tinker.*)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_startingWithXmarXX: Unsupported predicate 'notStartingWith(mar)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_containingXarkXX: Unsupported predicate 'notContaining(ark)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_endingWithXasXX: Unsupported predicate 'notEndingWith(as)'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_notRegexXrMarXX: Unsupported predicate 'notRegex(^mar)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27_29X_count: Unsupported relation 'NEQ'
-org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_gtXmX_andXcontainingXoXXX: Unsupported predicate 'containing(o)'
 org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXp_neqXvXX: Don't accept query based on properties [p] that are not indexed in any label, may not match not-equal condition
 
 org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest.shouldSetIdOnAddEWithNamePropertyKeySpecifiedAndNameSuppliedAsProperty: Unsupported ElementIdStrategy edge id/property index query
diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java
index 9287bfe267..266d90ad9f 100644
--- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java
+++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java
@@ -1577,7 +1577,7 @@ private TxBuilderImpl(String graph, int partId, RocksDBSession dbSession) {
         public TxBuilder put(int code, String table, byte[] key, byte[] value) throws
                                                                                HgStoreException {
             try {
-                byte[] targetKey = keyCreator.getKey(this.partId, graph, code, key);
+                byte[] targetKey = keyCreator.getKeyOrCreate(this.partId, graph, code, key);
                 this.op.put(table, targetKey, value);
             } catch (DBStoreException e) {
                 throw new HgStoreException(HgStoreException.EC_RKDB_DOPUT_FAIL, e.toString());
@@ -1642,7 +1642,7 @@ public TxBuilder merge(int code, String table, byte[] key, byte[] value) throws
                                                                                  HgStoreException {
 
             try {
-                byte[] targetKey = keyCreator.getKey(this.partId, graph, code, key);
+                byte[] targetKey = keyCreator.getKeyOrCreate(this.partId, graph, code, key);
                 op.merge(table, targetKey, value);
             } catch (DBStoreException e) {
                 throw new HgStoreException(HgStoreException.EC_RKDB_DOMERGE_FAIL, e.toString());
diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java
index 3a9e2c18e7..831619cfb4 100644
--- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java
+++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java
@@ -17,19 +17,28 @@
 
 package org.apache.hugegraph.store.meta;
 
+import static org.apache.hugegraph.store.constant.HugeServerTables.VERTEX_TABLE;
+
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
-import java.util.List;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 
+import org.apache.hugegraph.rocksdb.access.RocksDBSession;
+import org.apache.hugegraph.rocksdb.access.SessionOperator;
 import org.apache.hugegraph.store.meta.base.DBSessionBuilder;
 import org.apache.hugegraph.store.meta.base.PartitionMetaStore;
 import org.apache.hugegraph.store.term.Bits;
+import org.apache.hugegraph.store.util.Asserts;
 import org.apache.hugegraph.store.util.HgStoreException;
 
 import com.google.protobuf.Int64Value;
+import com.google.protobuf.InvalidProtocolBufferException;
 
 import lombok.extern.slf4j.Slf4j;
 
@@ -41,7 +50,7 @@
 public class GraphIdManager extends PartitionMetaStore {
 
     protected static final String GRAPH_ID_PREFIX = "@GRAPH_ID@";
-    // FIXME: we need to ensure the right num & proper logic for it (IMPORTANT)
+    // Missing-graph sentinel; allocatable graph IDs are [0, maxGraphID)
     protected static int maxGraphID = 65535 - 1;
     static Object graphIdLock = new Object();
     static Object cidLock = new Object();
@@ -66,13 +75,18 @@ public long getGraphId(String graphName) {
                     byte[] key = MetadataKeyHelper.getGraphIDKey(graphName);
                     Int64Value id = get(Int64Value.parser(), key);
                     if (id == null) {
-                        id = Int64Value.of(maxGraphID);
+                        l = (long) maxGraphID;
+                    } else {
+                        l = checkGraphId(graphName, id.getValue(),
+                                         this.partitionId);
                     }
-                    l = id.getValue();
                     graphIdCache.put(graphName, l);
                 }
             }
         }
+        if (l != maxGraphID) {
+            checkGraphId(graphName, l, this.partitionId);
+        }
         return l;
     }
 
@@ -85,26 +99,148 @@ public long getGraphIdOrCreate(String graphName) {
                     byte[] key = MetadataKeyHelper.getGraphIDKey(graphName);
                     Int64Value id = get(Int64Value.parser(), key);
                     if (id == null) {
-                        id = Int64Value.of(getCId(GRAPH_ID_PREFIX, maxGraphID - 1));
+                        id = Int64Value.of(getCId(GRAPH_ID_PREFIX, maxGraphID));
                         if (id.getValue() == -1) {
                             throw new HgStoreException(HgStoreException.EC_FAIL,
                                                        "The number of graphs exceeds the maximum " +
-                                                       "65535");
+                                                       maxGraphID);
                         }
                         log.info("partition: {}, Graph ID {} is allocated for graph {}, stack: {}",
                                  this.partitionId, id.getValue(), graphName,
                                  Arrays.toString(Thread.currentThread().getStackTrace()));
                         put(key, id);
                         flush();
+                    } else {
+                        checkGraphId(graphName, id.getValue(),
+                                     this.partitionId);
                     }
                     l = id.getValue();
                     graphIdCache.put(graphName, l);
                 }
             }
         }
+        checkGraphId(graphName, l, this.partitionId);
         return l;
     }
 
+    public static long checkGraphId(String graphName, long graphId,
+                                    int partitionId) {
+        if (graphId < 0L || graphId >= maxGraphID) {
+            throw new HgStoreException(
+                    HgStoreException.EC_FAIL,
+                    "Invalid graph ID %s for graph '%s' in partition %s, " +
+                    "expected a value in [0, %s)",
+                    graphId, graphName, partitionId, maxGraphID);
+        }
+        return graphId;
+    }
+
+    public static void checkGraphIds(Map graphIds,
+                                     int partitionId) {
+        Map graphNames = new HashMap<>();
+        graphIds.forEach((graphName, graphId) -> {
+            if (graphId == null) {
+                throw new HgStoreException(HgStoreException.EC_FAIL,
+                                           "Invalid null graph ID for graph '%s' " +
+                                           "in partition %s",
+                                           graphName, partitionId);
+            }
+            checkGraphId(graphName, graphId, partitionId);
+            String previous = graphNames.putIfAbsent(graphId, graphName);
+            if (previous != null && !previous.equals(graphName)) {
+                throw new HgStoreException(
+                        HgStoreException.EC_FAIL,
+                        "Graph ID %s is assigned to multiple graphs '%s' and '%s' " +
+                        "in partition %s",
+                        graphId, previous, graphName, partitionId);
+            }
+        });
+    }
+
+    public void updateGraphIds(Map graphIds) {
+        Map updates = new HashMap<>(graphIds);
+        checkGraphIds(updates, this.partitionId);
+        synchronized (graphIdLock) {
+            Map finalGraphIds = this.graphIds();
+            Set graphIdsToRelease = updates.keySet().stream()
+                                                 .map(finalGraphIds::get)
+                                                 .filter(graphId -> graphId != null)
+                                                 .collect(Collectors.toSet());
+            finalGraphIds.putAll(updates);
+            this.checkUniqueGraphIds(finalGraphIds);
+            graphIdsToRelease.removeAll(finalGraphIds.values());
+            this.writeGraphIds(updates, graphIdsToRelease);
+            this.graphIdCache.putAll(updates);
+        }
+    }
+
+    private Map graphIds() {
+        byte[] prefix = MetadataKeyHelper.getGraphIDKey("");
+        Map graphIds = new HashMap<>();
+        for (RocksDBSession.BackendColumn column : scan(prefix)) {
+            String graphName = new String(column.name, prefix.length,
+                                          column.name.length - prefix.length,
+                                          StandardCharsets.UTF_8);
+            try {
+                long graphId = Int64Value.parseFrom(column.value).getValue();
+                graphIds.put(graphName, graphId);
+            } catch (InvalidProtocolBufferException e) {
+                throw new HgStoreException(HgStoreException.EC_FAIL, e);
+            }
+        }
+        return graphIds;
+    }
+
+    private void checkUniqueGraphIds(Map graphIds) {
+        Map graphNames = new HashMap<>();
+        graphIds.forEach((graphName, graphId) -> {
+            // Ignore legacy invalid IDs here so this repair path can replace them.
+            if (graphId < 0L || graphId >= maxGraphID) {
+                return;
+            }
+            String previous = graphNames.putIfAbsent(graphId, graphName);
+            if (previous != null && !previous.equals(graphName)) {
+                throw new HgStoreException(
+                        HgStoreException.EC_FAIL,
+                        "Graph ID %s is assigned to multiple graphs '%s' and '%s' " +
+                        "in partition %s",
+                        graphId, previous, graphName, this.partitionId);
+            }
+        });
+    }
+
+    private void writeGraphIds(Map graphIds,
+                               Set graphIdsToRelease) {
+        try (RocksDBSession dbSession = getRocksDBSession()) {
+            SessionOperator operator = dbSession.sessionOp();
+            try {
+                operator.prepare();
+                // Release only previous slots no longer referenced after this repair.
+                for (Long graphId : graphIdsToRelease) {
+                    byte[] slotKey = genCIDSlotKey(GRAPH_ID_PREFIX, graphId);
+                    operator.delete(getCFName(), slotKey);
+                }
+                // Publish mappings only after all IDs are reserved in the same batch.
+                for (Map.Entry entry : graphIds.entrySet()) {
+                    Int64Value value = Int64Value.of(entry.getValue());
+                    byte[] slotKey = genCIDSlotKey(GRAPH_ID_PREFIX,
+                                                  entry.getValue());
+                    operator.put(getCFName(), slotKey, value.toByteArray());
+                }
+                for (Map.Entry entry : graphIds.entrySet()) {
+                    Int64Value value = Int64Value.of(entry.getValue());
+                    byte[] graphIdKey =
+                            MetadataKeyHelper.getGraphIDKey(entry.getKey());
+                    operator.put(getCFName(), graphIdKey, value.toByteArray());
+                }
+                operator.commit();
+            } catch (RuntimeException e) {
+                operator.rollback();
+                throw e;
+            }
+        }
+    }
+
     /**
      * Release a graph id
      */
@@ -128,8 +264,14 @@ public long releaseGraphId(String graphName) {
     private boolean checkCount(long l) {
         var start = new byte[2];
         Bits.putShort(start, 0, (short) l);
-        try (var itr = sessionBuilder.getSession(partitionId).sessionOp().scan("g+v", start)) {
-            return itr == null || !itr.hasNext();
+        try (var session = sessionBuilder.getSession(partitionId)) {
+            if (!session.tableIsExist(VERTEX_TABLE)) {
+                // Scanning a missing table creates it and requires a write lock
+                return true;
+            }
+            try (var iterator = session.sessionOp().scan(VERTEX_TABLE, start)) {
+                return iterator == null || !iterator.hasNext();
+            }
         }
     }
 
@@ -142,41 +284,58 @@ private boolean checkCount(long l) {
      * @return id
      */
     protected long getCId(String key, long max) {
+        Asserts.isTrue(max > 0L, "The maximum cyclic ID must be positive");
         byte[] cidNextKey = MetadataKeyHelper.getCidKey(key);
         synchronized (cidLock) {
             Int64Value value = get(Int64Value.parser(), cidNextKey);
-            long current = value != null ? value.getValue() : 0L;
-            long last = current == 0 ? max - 1 : current - 1;
-            // Find an unused cid
-            List ids =
-                    scan(Int64Value.parser(), genCIDSlotKey(key, current), genCIDSlotKey(key, max));
-            var idSet = ids.stream().map(Int64Value::getValue).collect(Collectors.toSet());
-
-            while (idSet.contains(current) || !checkCount(current)) {
-                current++;
+            long start = Math.floorMod(value != null ? value.getValue() : 0L, max);
+            Set assignedGraphIds = this.assignedGraphIds(key);
+            long current = this.findAvailableCId(key, start, max,
+                                                 assignedGraphIds);
+            if (current == -1L && start > 0L) {
+                current = this.findAvailableCId(key, 0L, start,
+                                                assignedGraphIds);
             }
-
-            if (current == max - 1) {
-                current = 0;
-                ids = scan(Int64Value.parser(), genCIDSlotKey(key, current),
-                           genCIDSlotKey(key, last));
-                idSet = ids.stream().map(Int64Value::getValue).collect(Collectors.toSet());
-                while (idSet.contains(current) || !checkCount(current)) {
-                    current++;
-                }
+            if (current == -1L) {
+                return -1L;
             }
 
-            if (current == last) {
-                return -1;
-            }
             // Save current id, mark as used
             put(genCIDSlotKey(key, current), Int64Value.of(current));
-            // Save the id for the next traversal
-            put(cidNextKey, Int64Value.of(current + 1));
+            // Keep the next traversal position inside [0, max)
+            long next = current + 1L;
+            put(cidNextKey, Int64Value.of(next == max ? 0L : next));
             return current;
         }
     }
 
+    private long findAvailableCId(String key, long start, long end,
+                                  Set assignedGraphIds) {
+        Set idSet = scan(Int64Value.parser(), genCIDSlotKey(key, start),
+                               genCIDSlotKey(key, end))
+                               .stream()
+                               .map(Int64Value::getValue)
+                               .collect(Collectors.toSet());
+        idSet.addAll(assignedGraphIds);
+        for (long current = start; current < end; current++) {
+            if (!idSet.contains(current) && checkCount(current)) {
+                return current;
+            }
+        }
+        return -1L;
+    }
+
+    private Set assignedGraphIds(String key) {
+        if (!GRAPH_ID_PREFIX.equals(key)) {
+            return Collections.emptySet();
+        }
+        // A persisted mapping stays authoritative even if its slot is missing.
+        return scan(Int64Value.parser(), MetadataKeyHelper.getGraphIDKey(""))
+                .stream()
+                .map(Int64Value::getValue)
+                .collect(Collectors.toSet());
+    }
+
     /**
      * Return key with used Cid
      */
diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/FixGraphIdController.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/FixGraphIdController.java
index 43a97e814c..e721d66469 100644
--- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/FixGraphIdController.java
+++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/FixGraphIdController.java
@@ -106,16 +106,14 @@ public String getNextId(@PathVariable(value = "partition_id") int pid) throws IO
     @PostMapping(value = "/update_graph_id/{partition_id}", produces = "application/json")
     public String updateGraphId(@PathVariable(value = "partition_id") int pid,
                                 @RequestBody Map idMap) throws IOException {
+        GraphIdManager.checkGraphIds(idMap, pid);
+
         var handler = (BusinessHandlerImpl) nodeService.getStoreEngine().getBusinessHandler();
         try (var manager = new GraphIdManager(handler, pid)) {
-            idMap.forEach((graphName, graphId) -> {
-                log.info("update graph id of {} to {}, partition, {}", graphName, graphId, pid);
-                var graphIdKey = MetadataKeyHelper.getGraphIDKey(graphName);
-                var slotKey = manager.genCIDSlotKey(GRAPH_ID_PREFIX, graphId);
-                var value = Int64Value.of(graphId);
-                manager.put(graphIdKey, value);
-                manager.put(slotKey, value);
-            });
+            idMap.forEach((graphName, graphId) ->
+                                  log.info("update graph id of {} to {}, partition, {}",
+                                           graphName, graphId, pid));
+            manager.updateGraphIds(idMap);
             manager.flush();
         }
         handler.getKeyCreator().clearCache(pid);
diff --git a/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/FixGraphIdControllerTest.java b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/FixGraphIdControllerTest.java
new file mode 100644
index 0000000000..510ab6e9c5
--- /dev/null
+++ b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/FixGraphIdControllerTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.hugegraph.store.node.controller;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.hugegraph.store.util.HgStoreException;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class FixGraphIdControllerTest {
+
+    @Test
+    public void testUpdateGraphIdRejectsIdsOutsideAllocatableDomain() {
+        FixGraphIdController controller = new FixGraphIdController();
+        long[] invalidGraphIds = {-1L, 65534L, 65536L};
+
+        for (long graphId : invalidGraphIds) {
+            String graphName = "invalid-" + graphId;
+            Map graphIds =
+                    Collections.singletonMap(graphName, graphId);
+
+            HgStoreException exception = Assert.assertThrows(
+                    HgStoreException.class,
+                    () -> controller.updateGraphId(0, graphIds));
+            Assert.assertTrue(exception.getMessage().contains("Invalid graph ID"));
+            Assert.assertTrue(exception.getMessage().contains(String.valueOf(graphId)));
+            Assert.assertTrue(exception.getMessage().contains(graphName));
+        }
+    }
+
+    @Test
+    public void testUpdateGraphIdRejectsDuplicateIds() {
+        FixGraphIdController controller = new FixGraphIdController();
+        Map graphIds = new LinkedHashMap<>();
+        graphIds.put("graph-a", 5L);
+        graphIds.put("graph-b", 5L);
+
+        HgStoreException exception = Assert.assertThrows(
+                HgStoreException.class,
+                () -> controller.updateGraphId(0, graphIds));
+        Assert.assertTrue(exception.getMessage().contains("graph-a"));
+        Assert.assertTrue(exception.getMessage().contains("graph-b"));
+        Assert.assertTrue(exception.getMessage().contains("5"));
+    }
+}
diff --git a/hugegraph-store/hg-store-test/pom.xml b/hugegraph-store/hg-store-test/pom.xml
index 36308f449d..5afcd8551f 100644
--- a/hugegraph-store/hg-store-test/pom.xml
+++ b/hugegraph-store/hg-store-test/pom.xml
@@ -236,6 +236,8 @@
                             
                             
                                 **/CoreSuiteTest.java
+                                **/BatchGraphIsolationTest.java
+                                **/GraphIDManagerTest.java
                             
                         
                     
diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/BatchGraphIsolationTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/BatchGraphIsolationTest.java
new file mode 100644
index 0000000000..6d1769ec69
--- /dev/null
+++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/BatchGraphIsolationTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.hugegraph.store.core;
+
+import static org.apache.hugegraph.store.constant.HugeServerTables.TABLES_MAP;
+import static org.apache.hugegraph.store.constant.HugeServerTables.VERTEX_TABLE;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hugegraph.store.UnitTestBase;
+import org.apache.hugegraph.store.business.BusinessHandler;
+import org.apache.hugegraph.store.business.BusinessHandlerImpl;
+import org.apache.hugegraph.store.grpc.common.Key;
+import org.apache.hugegraph.store.grpc.common.OpType;
+import org.apache.hugegraph.store.grpc.session.BatchEntry;
+import org.apache.hugegraph.store.meta.PartitionManager;
+import org.apache.hugegraph.store.options.HgStoreEngineOptions;
+import org.apache.hugegraph.store.options.RaftRocksdbOptions;
+import org.apache.hugegraph.store.pd.FakePdServiceProvider;
+import org.apache.hugegraph.store.pd.PdProvider;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.alipay.sofa.jraft.util.StorageOptionsFactory;
+import com.google.protobuf.ByteString;
+
+public class BatchGraphIsolationTest {
+
+    private static final int PARTITION_ID = 0;
+    private static final int EMPTY_PARTITION_ID = 1;
+    private static final int KEY_CODE = 0;
+    private static final byte[] SHARED_KEY =
+            "shared-key".getBytes(StandardCharsets.UTF_8);
+
+    private static Path databasePath;
+    private static BusinessHandler handler;
+
+    @BeforeClass
+    public static void setup() throws IOException {
+        databasePath = Files.createTempDirectory("hugegraph-batch-graph-isolation-");
+
+        Map rocksdbConfig = new HashMap<>();
+        rocksdbConfig.put("rocksdb.write_buffer_size", "1048576");
+        StorageOptionsFactory.releaseAllOptions();
+        RaftRocksdbOptions.initRocksdbGlobalConfig(rocksdbConfig);
+        BusinessHandlerImpl.initRocksdb(rocksdbConfig, null);
+
+        HgStoreEngineOptions options = new HgStoreEngineOptions();
+        options.setDataPath(databasePath.toString());
+        options.setRaftPath(databasePath.toString());
+
+        HgStoreEngineOptions.FakePdOptions fakePdOptions =
+                new HgStoreEngineOptions.FakePdOptions();
+        fakePdOptions.setPartitionCount(1);
+        fakePdOptions.setPeersList("127.0.0.1");
+        fakePdOptions.setStoreList("127.0.0.1");
+        options.setFakePdOptions(fakePdOptions);
+
+        PdProvider pdProvider = new FakePdServiceProvider(fakePdOptions);
+        PartitionManager partitionManager = new PartitionManager(pdProvider, options) {
+
+            @Override
+            public String getDbDataPath(int partitionId, String dbName) {
+                return databasePath.resolve("data").toString();
+            }
+
+            @Override
+            public boolean hasPartition(String graphName, int partitionId) {
+                return partitionId == PARTITION_ID;
+            }
+
+            @Override
+            public List getLeaderPartitionIds(String graph) {
+                return Collections.singletonList(PARTITION_ID);
+            }
+        };
+        handler = new BusinessHandlerImpl(partitionManager);
+        handler.createTable("setup", PARTITION_ID, VERTEX_TABLE);
+    }
+
+    @AfterClass
+    public static void teardown() {
+        if (handler != null) {
+            handler.closeAll();
+        }
+        if (databasePath != null) {
+            UnitTestBase.deleteDir(databasePath.toFile());
+        }
+    }
+
+    @Test
+    public void testGraphIdAllocationDoesNotCreateVertexTable() {
+        String graph = "graph-id-allocation";
+
+        Assert.assertFalse(handler.existsTable(graph, EMPTY_PARTITION_ID, VERTEX_TABLE));
+
+        ((BusinessHandlerImpl) handler).getKeyCreator()
+                                     .getGraphIdOrCreate(EMPTY_PARTITION_ID, graph);
+
+        Assert.assertFalse(handler.existsTable(graph, EMPTY_PARTITION_ID, VERTEX_TABLE));
+    }
+
+    @Test
+    public void testBatchPutKeepsGraphsIsolated() {
+        String graph1 = "batch-put-graph-1";
+        String graph2 = "batch-put-graph-2";
+        byte[] value1 = "graph-1-value".getBytes(StandardCharsets.UTF_8);
+        byte[] value2 = "graph-2-value".getBytes(StandardCharsets.UTF_8);
+
+        writeBatch(graph1, OpType.OP_TYPE_PUT, value1);
+        writeBatch(graph2, OpType.OP_TYPE_PUT, value2);
+
+        Assert.assertArrayEquals(value1, read(graph1));
+        Assert.assertArrayEquals(value2, read(graph2));
+
+        handler.truncate(graph2, PARTITION_ID);
+        Assert.assertArrayEquals(value1, read(graph1));
+    }
+
+    @Test
+    public void testBatchMergeKeepsGraphsIsolated() {
+        String graph1 = "batch-merge-graph-1";
+        String graph2 = "batch-merge-graph-2";
+
+        writeBatch(graph1, OpType.OP_TYPE_MERGE, longToBytes(10L));
+        writeBatch(graph2, OpType.OP_TYPE_MERGE, longToBytes(20L));
+
+        Assert.assertEquals(10L, bytesToLong(read(graph1)));
+        Assert.assertEquals(20L, bytesToLong(read(graph2)));
+    }
+
+    private static void writeBatch(String graph, OpType type, byte[] value) {
+        Key key = Key.newBuilder()
+                     .setCode(KEY_CODE)
+                     .setKey(ByteString.copyFrom(SHARED_KEY))
+                     .build();
+        BatchEntry entry = BatchEntry.newBuilder()
+                                     .setOpType(type)
+                                     .setTable(TABLES_MAP.get(VERTEX_TABLE))
+                                     .setStartKey(key)
+                                     .setValue(ByteString.copyFrom(value))
+                                     .build();
+        handler.doBatch(graph, PARTITION_ID, Collections.singletonList(entry));
+    }
+
+    private static byte[] read(String graph) {
+        return handler.doGet(graph, KEY_CODE, VERTEX_TABLE, SHARED_KEY);
+    }
+
+    private static byte[] longToBytes(long value) {
+        return ByteBuffer.allocate(Long.BYTES)
+                         .order(ByteOrder.LITTLE_ENDIAN)
+                         .putLong(value)
+                         .array();
+    }
+
+    private static long bytesToLong(byte[] value) {
+        return ByteBuffer.wrap(value)
+                         .order(ByteOrder.LITTLE_ENDIAN)
+                         .getLong();
+    }
+}
diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/meta/GraphIDManagerTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/meta/GraphIDManagerTest.java
index 60bb542a9b..e5ba0d69c1 100644
--- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/meta/GraphIDManagerTest.java
+++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/meta/GraphIDManagerTest.java
@@ -17,69 +17,340 @@
 
 package org.apache.hugegraph.store.meta;
 
+import static org.apache.hugegraph.store.constant.HugeServerTables.VERTEX_TABLE;
+
 import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 
-import org.apache.hugegraph.pd.common.PDException;
 import org.apache.hugegraph.rocksdb.access.RocksDBSession;
+import org.apache.hugegraph.rocksdb.access.SessionOperator;
 import org.apache.hugegraph.store.UnitTestBase;
+import org.apache.hugegraph.store.business.BusinessHandlerImpl;
 import org.apache.hugegraph.store.meta.base.DBSessionBuilder;
+import org.apache.hugegraph.store.term.Bits;
+import org.apache.hugegraph.store.util.HgStoreException;
+import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
+import com.google.protobuf.Int64Value;
+
 public class GraphIDManagerTest extends UnitTestBase {
+
+    private static final int PARTITION_ID = 0;
+    private static final int GRAPH_ID_LIMIT = 8;
+
+    private String dbName;
+    private File dbPath;
+    private RocksDBSession session;
+    private DBSessionBuilder sessionBuilder;
+    private int previousMaxGraphId;
+
     @Before
     public void init() {
-        String dbPath = "/tmp/junit";
-        UnitTestBase.deleteDir(new File(dbPath));
-        super.initDB(dbPath);
-    }
-
-    @Test
-    public void test() throws PDException {
-        GraphIdManager.maxGraphID = 64;
-        int max = GraphIdManager.maxGraphID;
-        try (RocksDBSession session = getDBSession("test")) {
-            GraphIdManager gid = new GraphIdManager(new DBSessionBuilder() {
-                @Override
-                public RocksDBSession getSession(int partId) {
-                    return session.clone();
-                }
-            }, 0);
-            for (int i = 0; i < max; i++) {
-                Assert.assertEquals(i, gid.getCId("Test", max));
-            }
+        this.previousMaxGraphId = GraphIdManager.maxGraphID;
+        GraphIdManager.maxGraphID = GRAPH_ID_LIMIT;
+        this.dbName = "graph-id-manager-" + System.nanoTime();
+        this.dbPath = new File("target/graph-id-manager-test", this.dbName);
+        Map config = new HashMap<>();
+        config.put("rocksdb.write_buffer_size", "1048576");
+        config.put("rocksdb.bloom_filter_bits_per_key", "10");
+        BusinessHandlerImpl.initRocksdb(config, null);
+        this.session = factory.createGraphDB(this.dbPath.getAbsolutePath(),
+                                             this.dbName);
+        this.sessionBuilder = partId -> this.session.clone();
+    }
 
-            Assert.assertEquals(-1, gid.getCId("Test", max));
+    @After
+    public void clear() {
+        GraphIdManager.maxGraphID = this.previousMaxGraphId;
+        if (this.session != null) {
+            this.session.close();
+        }
+        if (this.dbName != null) {
+            factory.releaseGraphDB(this.dbName);
+        }
+        if (this.dbPath != null) {
+            UnitTestBase.deleteDir(this.dbPath);
+        }
+    }
 
-            gid.delCId("Test", 3);
-            Assert.assertEquals(3, gid.getCId("Test", max));
-            Assert.assertEquals(-1, gid.getCId("Test", max));
+    @Test
+    public void testAllocateBoundaryIdBeforeWrap() {
+        GraphIdManager manager = this.newManager();
 
-            long start = System.currentTimeMillis();
-            for (int i = 0; i < GraphIdManager.maxGraphID; i++) {
-                long id = gid.getGraphId("g" + i);
-                Assert.assertEquals(i, id);
-            }
-            System.out.println("time is " + (System.currentTimeMillis() - start));
-            {
-                gid.releaseGraphId("g" + 10);
-                long id = gid.getGraphId("g" + 10);
-                Assert.assertEquals(10, id);
-            }
-            start = System.currentTimeMillis();
-            for (int i = 0; i < GraphIdManager.maxGraphID; i++) {
-                long id = gid.releaseGraphId("g" + i);
-                Assert.assertEquals(i, id);
+        Assert.assertEquals(0L, manager.getCId("boundary", 4));
+        Assert.assertEquals(1L, manager.getCId("boundary", 4));
+        Assert.assertEquals(2L, manager.getCId("boundary", 4));
+        Assert.assertEquals(3L, manager.getCId("boundary", 4));
+    }
+
+    @Test
+    public void testReturnMinusOneWhenAllIdsAreUsed() {
+        GraphIdManager manager = this.newManager();
+
+        for (int i = 0; i < 4; i++) {
+            Assert.assertEquals(i, manager.getCId("full", 4));
+        }
+        Assert.assertEquals(-1L, manager.getCId("full", 4));
+    }
+
+    @Test
+    public void testReuseReleasedId() {
+        GraphIdManager manager = this.newManager();
+
+        for (int i = 0; i < 4; i++) {
+            Assert.assertEquals(i, manager.getCId("release", 4));
+        }
+        manager.delCId("release", 1L);
+
+        Assert.assertEquals(1L, manager.getCId("release", 4));
+        Assert.assertEquals(-1L, manager.getCId("release", 4));
+    }
+
+    @Test
+    public void testWrapToBeginningAfterBoundary() {
+        GraphIdManager manager = this.newManager();
+
+        Assert.assertEquals(0L, manager.getCId("wrap", 4));
+        Assert.assertEquals(1L, manager.getCId("wrap", 4));
+        manager.delCId("wrap", 0L);
+        Assert.assertEquals(2L, manager.getCId("wrap", 4));
+        Assert.assertEquals(3L, manager.getCId("wrap", 4));
+
+        Assert.assertEquals(0L, manager.getCId("wrap", 4));
+    }
+
+    @Test
+    public void testPersistGraphIdAcrossManagerRestart() {
+        GraphIdManager firstManager = this.newManager();
+
+        Assert.assertEquals(0L, firstManager.getGraphIdOrCreate("first"));
+
+        this.reopenDatabase();
+        GraphIdManager restartedManager = this.newManager();
+        Assert.assertEquals(0L, restartedManager.getGraphId("first"));
+        Assert.assertEquals(1L, restartedManager.getGraphIdOrCreate("second"));
+    }
+
+    @Test
+    public void testSkipGraphIdMappingWithoutSlot() {
+        this.persistGraphId("existing", 0L);
+
+        Assert.assertEquals(1L,
+                            this.newManager().getGraphIdOrCreate("new"));
+    }
+
+    @Test
+    public void testUpdateGraphIdsRejectsPersistedCollisionBeforeWriting() {
+        this.persistGraphId("existing", 2L);
+        Map graphIds = new LinkedHashMap<>();
+        graphIds.put("graph-a", 1L);
+        graphIds.put("graph-b", 2L);
+
+        HgStoreException exception = Assert.assertThrows(
+                HgStoreException.class,
+                () -> this.newManager().updateGraphIds(graphIds));
+        Assert.assertTrue(exception.getMessage().contains("existing"));
+        Assert.assertTrue(exception.getMessage().contains("graph-b"));
+        Assert.assertTrue(exception.getMessage().contains("2"));
+        Assert.assertEquals(GRAPH_ID_LIMIT,
+                            this.newManager().getGraphId("graph-a"));
+        Assert.assertEquals(GRAPH_ID_LIMIT,
+                            this.newManager().getGraphId("graph-b"));
+    }
+
+    @Test
+    public void testUpdateGraphIdsPersistsMappingAndSlot() {
+        GraphIdManager manager = this.newManager();
+
+        manager.updateGraphIds(Collections.singletonMap("graph-a", 3L));
+
+        Assert.assertEquals(3L, this.newManager().getGraphId("graph-a"));
+        Int64Value slot = manager.get(
+                Int64Value.parser(),
+                manager.genCIDSlotKey(GraphIdManager.GRAPH_ID_PREFIX, 3L));
+        Assert.assertNotNull(slot);
+        Assert.assertEquals(3L, slot.getValue());
+    }
+
+    @Test
+    public void testUpdateGraphIdsAllowsIdempotentAssignment() {
+        GraphIdManager manager = this.newManager();
+        Map graphIds =
+                Collections.singletonMap("graph-a", 3L);
+
+        manager.updateGraphIds(graphIds);
+        manager.updateGraphIds(graphIds);
+
+        Assert.assertEquals(3L, this.newManager().getGraphId("graph-a"));
+    }
+
+    @Test
+    public void testReusePreviousGraphIdAfterRemapAndRelease() {
+        GraphIdManager manager = this.newManager();
+
+        manager.updateGraphIds(Collections.singletonMap("graph-a", 3L));
+        manager.updateGraphIds(Collections.singletonMap("graph-a", 4L));
+        Assert.assertEquals(4L, manager.releaseGraphId("graph-a"));
+        manager.put(MetadataKeyHelper.getCidKey(GraphIdManager.GRAPH_ID_PREFIX),
+                    Int64Value.of(3L));
+
+        Assert.assertEquals(3L,
+                            this.newManager().getGraphIdOrCreate("graph-b"));
+    }
+
+    @Test
+    public void testKeepPreviousGraphIdSlotWhenStillAssigned() {
+        GraphIdManager manager = this.newManager();
+        manager.updateGraphIds(Collections.singletonMap("graph-a", 3L));
+        this.persistGraphId("graph-b", 3L);
+
+        manager.updateGraphIds(Collections.singletonMap("graph-a", 4L));
+
+        Int64Value slot = manager.get(
+                Int64Value.parser(),
+                manager.genCIDSlotKey(GraphIdManager.GRAPH_ID_PREFIX, 3L));
+        Assert.assertNotNull(slot);
+        Assert.assertEquals(3L, slot.getValue());
+        Assert.assertEquals(3L, this.newManager().getGraphId("graph-b"));
+    }
+
+    @Test
+    public void testRejectPersistedGraphIdsOutsideAllocatableDomain() {
+        long[] invalidGraphIds = {-1L, GRAPH_ID_LIMIT, 65536L};
+
+        for (long graphId : invalidGraphIds) {
+            String readGraph = "invalid-read-" + graphId;
+            this.persistGraphId(readGraph, graphId);
+            this.assertInvalidPersistedGraphId(
+                    readGraph, graphId,
+                    () -> this.newManager().getGraphId(readGraph));
+
+            String writeGraph = "invalid-write-" + graphId;
+            this.persistGraphId(writeGraph, graphId);
+            this.assertInvalidPersistedGraphId(
+                    writeGraph, graphId,
+                    () -> this.newManager().getGraphIdOrCreate(writeGraph));
+        }
+    }
+
+    @Test
+    public void testSkipVertexDataWithoutGraphIdSlot() {
+        GraphIdManager manager = this.newManager();
+        this.persistVertexWithGraphId(0);
+
+        Assert.assertEquals(1L, manager.getCId("stale-forward", 4));
+    }
+
+    @Test
+    public void testSkipVertexDataWithoutGraphIdSlotAfterWrap() {
+        String key = "stale-wrap";
+        GraphIdManager manager = this.newManager();
+        this.persistVertexWithGraphId(0);
+        manager.put(MetadataKeyHelper.getCidKey(key), Int64Value.of(2L));
+        manager.put(manager.genCIDSlotKey(key, 2L), Int64Value.of(2L));
+        manager.put(manager.genCIDSlotKey(key, 3L), Int64Value.of(3L));
+
+        Assert.assertEquals(1L, manager.getCId(key, 4));
+    }
+
+    @Test
+    public void testGraphIdDomainExcludesMissingSentinel() {
+        GraphIdManager manager = this.newManager();
+
+        for (int i = 0; i < GRAPH_ID_LIMIT; i++) {
+            Assert.assertEquals(i, manager.getGraphIdOrCreate("graph-" + i));
+        }
+        Assert.assertEquals(GRAPH_ID_LIMIT, manager.getGraphId("missing"));
+        Assert.assertThrows(HgStoreException.class,
+                            () -> manager.getGraphIdOrCreate("overflow"));
+    }
+
+    @Test
+    public void testConcurrentCreateSameGraphReturnsSameId() throws Exception {
+        int threadCount = 8;
+        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+        CountDownLatch start = new CountDownLatch(1);
+        List> futures = new ArrayList<>(threadCount);
+
+        try {
+            for (int i = 0; i < threadCount; i++) {
+                futures.add(executor.submit(() -> {
+                    GraphIdManager manager = this.newManager();
+                    start.await();
+                    return manager.getGraphIdOrCreate("same-graph");
+                }));
             }
-            System.out.println("time is " + (System.currentTimeMillis() - start));
-            start = System.currentTimeMillis();
-            for (int i = 0; i < GraphIdManager.maxGraphID; i++) {
-                long id = gid.getCId(GraphIdManager.GRAPH_ID_PREFIX, GraphIdManager.maxGraphID);
-                //  long id = gid.getGraphId("g" + i);
-                Assert.assertTrue(id >= 0);
+            start.countDown();
+
+            Set graphIds = new HashSet<>();
+            for (Future future : futures) {
+                graphIds.add(future.get(10L, TimeUnit.SECONDS));
             }
-            System.out.println("time is " + (System.currentTimeMillis() - start));
+            Assert.assertEquals(1, graphIds.size());
+            Assert.assertTrue(graphIds.contains(0L));
+        } finally {
+            executor.shutdownNow();
         }
     }
+
+    private GraphIdManager newManager() {
+        return new GraphIdManager(this.sessionBuilder, PARTITION_ID);
+    }
+
+    private void persistGraphId(String graphName, long graphId) {
+        GraphIdManager manager = this.newManager();
+        manager.put(MetadataKeyHelper.getGraphIDKey(graphName),
+                    Int64Value.of(graphId));
+        manager.flush();
+    }
+
+    private void persistVertexWithGraphId(int graphId) {
+        this.session.createTables(VERTEX_TABLE);
+        byte[] key = new byte[3];
+        Bits.putShort(key, 0, graphId);
+        key[2] = 1;
+
+        SessionOperator operator = this.session.sessionOp();
+        try {
+            operator.prepare();
+            operator.put(VERTEX_TABLE, key, new byte[]{1});
+            operator.commit();
+        } catch (RuntimeException e) {
+            operator.rollback();
+            throw e;
+        }
+    }
+
+    private void assertInvalidPersistedGraphId(String graphName, long graphId,
+                                               Runnable action) {
+        HgStoreException exception =
+                Assert.assertThrows(HgStoreException.class, action::run);
+        Assert.assertTrue(exception.getMessage().contains("Invalid graph ID"));
+        Assert.assertTrue(exception.getMessage().contains(String.valueOf(graphId)));
+        Assert.assertTrue(exception.getMessage().contains(graphName));
+    }
+
+    private void reopenDatabase() {
+        this.session.close();
+        this.session = null;
+        factory.releaseGraphDB(this.dbName);
+        this.session = factory.createGraphDB(this.dbPath.getAbsolutePath(),
+                                             this.dbName);
+        this.sessionBuilder = partId -> this.session.clone();
+    }
 }

From 7a6e6ed8da0e87f554d346876dc9001d7242895a Mon Sep 17 00:00:00 2001
From: contrueCT 
Date: Wed, 12 Aug 2026 00:50:09 +0800
Subject: [PATCH 3/4] refactor: upgrade HugeGraph baseline to Java 17 (#183)

- Centralize the Java 17 build contract
- Adapt the runtime:
- Migrate delivery:
  - switch blocking workflows, CodeQL, dependency checks, and four product images to Java 17preflight checks without adding default Server or HStore Compose startup smoke tests.
---
 .github/workflows/check-dependencies.yml      |   6 +-
 .github/workflows/cluster-test-ci.yml         |   4 +-
 .github/workflows/codeql-analysis.yml         |   2 +-
 .github/workflows/commons-ci.yml              |   8 +-
 .github/workflows/docker-build-ci.yml         |  19 +-
 .github/workflows/pd-store-ci.yml             |  70 ++++++-
 .github/workflows/riscv64-ci.yml              |  45 +++--
 .github/workflows/server-ci.yml               |  42 ++++-
 .../memories/code_style_and_conventions.md    |   2 +-
 .../ecosystem_and_related_projects.md         |   4 +-
 .../implementation_patterns_and_guidelines.md |   6 +-
 .serena/memories/key_file_locations.md        |   2 +-
 .serena/memories/project_overview.md          |   6 +-
 .serena/memories/task_completion_checklist.md |   2 +-
 AGENTS.md                                     |   2 +-
 BUILDING.md                                   |   5 +-
 README.md                                     |   6 +-
 .../hugegraph-clustertest-minicluster/pom.xml |   2 -
 .../hugegraph/ct/base/ClusterConstant.java    |   5 +-
 .../hugegraph/ct/node/PDNodeWrapper.java      |   6 +-
 .../hugegraph/ct/node/ServerNodeWrapper.java  |  13 +-
 .../hugegraph/ct/node/StoreNodeWrapper.java   |   6 +-
 .../hugegraph-clustertest-test/pom.xml        |   3 -
 hugegraph-cluster-test/pom.xml                |   4 -
 hugegraph-commons/AGENTS.md                   |  23 ++-
 hugegraph-commons/hugegraph-common/build.sh   |   7 +-
 hugegraph-commons/hugegraph-common/pom.xml    |   1 -
 .../apache/hugegraph/testutil/AssertTest.java |   2 -
 hugegraph-commons/hugegraph-rpc/pom.xml       |   1 -
 hugegraph-commons/pom.xml                     |  10 +-
 hugegraph-pd/AGENTS.md                        |   6 +-
 hugegraph-pd/Dockerfile                       |   4 +-
 hugegraph-pd/README.md                        |   4 +-
 hugegraph-pd/docs/architecture.md             |   2 +-
 hugegraph-pd/docs/development.md              |  18 +-
 .../assembly/static/bin/start-hugegraph-pd.sh |  11 +-
 hugegraph-pd/hg-pd-test/pom.xml               |  14 +-
 hugegraph-pd/pom.xml                          |   5 -
 hugegraph-server/AGENTS.md                    |   2 +-
 hugegraph-server/Dockerfile                   |   7 +-
 hugegraph-server/Dockerfile-hstore            |   4 +-
 .../org/apache/hugegraph/util/Reflection.java |  53 ++++--
 .../assembly/static/bin/hugegraph-server.sh   |  20 +-
 .../src/assembly/static/bin/init-store.sh     |  10 +-
 .../assembly/static/bin/jvm-module.options    |  20 ++
 .../src/assembly/static/bin/start-monitor.sh  |   2 +-
 .../travis/run-native-runtime-smoke-test.sh   |   3 +-
 .../travis/run-rocksdb-jni-smoke-test.sh      |  14 +-
 .../travis/test-java-security-properties.sh   | 146 ++++++++++++++-
 .../travis/test-java17-upgrade-contracts.sh   | 174 ++++++++++++++++++
 .../travis/test-riscv64-java-runtime.sh       | 155 ++++++++++++++++
 .../conf/jvm-test-module.options              |  29 +++
 hugegraph-server/hugegraph-test/pom.xml       |  21 ++-
 .../unit/auth/HugeGraphAuthProxyTest.java     |  39 ++++
 hugegraph-store/AGENTS.md                     |   2 +-
 hugegraph-store/Dockerfile                    |   4 +-
 hugegraph-store/README.md                     |   6 +-
 hugegraph-store/docs/deployment-guide.md      |   2 +-
 hugegraph-store/docs/development-guide.md     |   8 +-
 .../static/bin/start-hugegraph-store.sh       |  11 +-
 hugegraph-store/hg-store-test/pom.xml         |  16 +-
 hugegraph-store/pom.xml                       |   7 -
 hugegraph-struct/AGENTS.md                    |   2 +-
 hugegraph-struct/pom.xml                      |   2 -
 pom.xml                                       |  79 +++++---
 65 files changed, 976 insertions(+), 240 deletions(-)
 create mode 100644 hugegraph-server/hugegraph-dist/src/assembly/static/bin/jvm-module.options
 create mode 100755 hugegraph-server/hugegraph-dist/src/assembly/travis/test-java17-upgrade-contracts.sh
 create mode 100755 hugegraph-server/hugegraph-dist/src/assembly/travis/test-riscv64-java-runtime.sh
 create mode 100644 hugegraph-server/hugegraph-test/conf/jvm-test-module.options

diff --git a/.github/workflows/check-dependencies.yml b/.github/workflows/check-dependencies.yml
index fa804e260c..f5ae1e00ce 100644
--- a/.github/workflows/check-dependencies.yml
+++ b/.github/workflows/check-dependencies.yml
@@ -19,11 +19,11 @@ jobs:
     steps:
       - name: Checkout source
         uses: actions/checkout@v4
-      - name: Set up JDK 11
+      - name: Set up JDK 17
         uses: actions/setup-java@v3
         with:
-          java-version: '11'
-          distribution: 'adopt'
+          java-version: '17'
+          distribution: 'temurin'
 
       - name: use staged maven repo settings
         if: ${{ env.USE_STAGE == 'true' }}
diff --git a/.github/workflows/cluster-test-ci.yml b/.github/workflows/cluster-test-ci.yml
index 8e9f4d870f..5030205abf 100644
--- a/.github/workflows/cluster-test-ci.yml
+++ b/.github/workflows/cluster-test-ci.yml
@@ -15,10 +15,10 @@ jobs:
       USE_STAGE: 'false' # Whether to include the stage repository.
 
     steps:
-      - name: Install JDK 11
+      - name: Install JDK 17
         uses: actions/setup-java@v3
         with:
-          java-version: '11'
+          java-version: '17'
           distribution: 'zulu'
 
       - name: Cache Maven packages
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index d66dc8cee9..214218bf3d 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -33,7 +33,7 @@ jobs:
         uses: actions/setup-java@v3
         with:
           distribution: 'zulu'
-          java-version: '11'
+          java-version: '17'
 
       - name: use staged maven repo settings
         if: ${{ env.USE_STAGE == 'true' }}
diff --git a/.github/workflows/commons-ci.yml b/.github/workflows/commons-ci.yml
index 5ba4c70f47..74daec7a35 100644
--- a/.github/workflows/commons-ci.yml
+++ b/.github/workflows/commons-ci.yml
@@ -18,7 +18,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        JAVA_VERSION: ['11']
+        JAVA_VERSION: ['17']
 
     steps:
       - name: Install JDK ${{ matrix.JAVA_VERSION }}
@@ -51,11 +51,13 @@ jobs:
 
       - name: Run common test
         run: |
-          mvn test -pl hugegraph-commons/hugegraph-common -Dtest=UnitTestSuite -DskipCommonsTests=false
+          mvn test -pl hugegraph-commons/hugegraph-common -Dtest=UnitTestSuite \
+            -DskipCommonsTests=false -Dsurefire.failIfNoSpecifiedTests=true
 
       - name: Run rpc test
         run: |
-          mvn test -pl hugegraph-commons/hugegraph-rpc -Dtest=UnitTestSuite -DskipCommonsTests=false
+          mvn test -pl hugegraph-commons/hugegraph-rpc -Dtest=UnitTestSuite \
+            -DskipCommonsTests=false -Dsurefire.failIfNoSpecifiedTests=true
 
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v3.0.0
diff --git a/.github/workflows/docker-build-ci.yml b/.github/workflows/docker-build-ci.yml
index ada012be80..1492a40a57 100644
--- a/.github/workflows/docker-build-ci.yml
+++ b/.github/workflows/docker-build-ci.yml
@@ -26,8 +26,16 @@ on:
     paths:
       - '**/Dockerfile*'
       - '.dockerignore'
+      - '.github/workflows/docker-build-ci.yml'
       - 'hugegraph-server/hugegraph-dist/docker/**'
-      - 'hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh'
+      - 'hugegraph-server/hugegraph-dist/src/assembly/static/bin/**'
+      - 'hugegraph-server/hugegraph-dist/src/assembly/static/conf/**'
+      - 'pom.xml'
+      - 'hugegraph-server/**'
+      - 'hugegraph-pd/**'
+      - 'hugegraph-store/**'
+      - 'hugegraph-commons/**'
+      - 'hugegraph-struct/**'
 
 jobs:
   docker-build:
@@ -54,6 +62,15 @@ jobs:
           echo "Healthcheck: $HC"
           [[ "$HC" != "null" ]] || { echo "ERROR: HEALTHCHECK missing in ${{ matrix.dockerfile }}"; exit 1; }
 
+      - name: Verify Java 17 runtime in ${{ matrix.dockerfile }}
+        run: |
+          JAVA_VERSION=$(docker run --rm --entrypoint java "$IMAGE_ID" -version 2>&1)
+          echo "$JAVA_VERSION"
+          grep -Eq 'version "17\.' <<< "$JAVA_VERSION" || {
+            echo "ERROR: expected a Java 17 runtime in ${{ matrix.dockerfile }}"
+            exit 1
+          }
+
       - name: Test server entrypoint property mapping
         if: matrix.dockerfile == 'hugegraph-server/Dockerfile'
         run: bash hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh
diff --git a/.github/workflows/pd-store-ci.yml b/.github/workflows/pd-store-ci.yml
index 55ba16b783..3a8d514baa 100644
--- a/.github/workflows/pd-store-ci.yml
+++ b/.github/workflows/pd-store-ci.yml
@@ -15,10 +15,10 @@ jobs:
     env:
       USE_STAGE: 'false'
     steps:
-      - name: Install JDK 11
+      - name: Install JDK 17
         uses: actions/setup-java@v3
         with:
-          java-version: '11'
+          java-version: '17'
           distribution: 'zulu'
 
       - name: Cache Maven packages
@@ -68,10 +68,10 @@ jobs:
       REPORT_DIR: target/site/jacoco
 
     steps:
-      - name: Install JDK 11
+      - name: Install JDK 17
         uses: actions/setup-java@v3
         with:
-          java-version: '11'
+          java-version: '17'
           distribution: 'zulu'
 
       - name: Cache Maven packages
@@ -100,6 +100,31 @@ jobs:
         run: |
           mvn test -pl hugegraph-pd/hg-pd-test -am -P pd-core-test
 
+      - name: Verify Surefire single-class routing
+        run: |
+          SUREFIRE_DIR=hugegraph-pd/hg-pd-test/target/surefire-reports
+          REPORT="$SUREFIRE_DIR/TEST-org.apache.hugegraph.pd.core.PDConfigTest.xml"
+          rm -f "$REPORT"
+          mvn test -pl hugegraph-pd/hg-pd-test -am \
+            -P pd-core-test -Dtest=PDConfigTest -ntp
+          test -s "$REPORT" || {
+            echo "::error::PD single-class test produced no report"
+            exit 1
+          }
+          NEGATIVE_LOG=$(mktemp)
+          trap 'rm -f "$NEGATIVE_LOG"' EXIT
+          if mvn test -pl hugegraph-pd/hg-pd-test -am \
+            -P pd-core-test -Dtest=NoSuchJava17ContractTest -ntp \
+            >"$NEGATIVE_LOG" 2>&1; then
+            cat "$NEGATIVE_LOG"
+            echo "::error::Surefire accepted a missing PD test class"
+            exit 1
+          fi
+          cat "$NEGATIVE_LOG"
+          grep -Fq 'No tests matching pattern "NoSuchJava17ContractTest"' \
+            "$NEGATIVE_LOG"
+          grep -Fq '(pd-core-test) @ hg-pd-test' "$NEGATIVE_LOG"
+
       # The above tests do not require starting a PD instance.
 
       - name: Package
@@ -177,10 +202,10 @@ jobs:
       REPORT_DIR: target/site/jacoco
 
     steps:
-      - name: Install JDK 11
+      - name: Install JDK 17
         uses: actions/setup-java@v3
         with:
-          java-version: '11'
+          java-version: '17'
           distribution: 'zulu'
 
       - name: Cache Maven packages
@@ -206,6 +231,31 @@ jobs:
         run: |
           mvn clean package -U -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -ntp --fail-at-end
 
+      - name: Verify Surefire single-class routing
+        run: |
+          SUREFIRE_DIR=hugegraph-store/hg-store-test/target/surefire-reports
+          REPORT="$SUREFIRE_DIR/TEST-org.apache.hugegraph.store.raftcore.ZeroByteStringHelperTest.xml"
+          rm -f "$REPORT"
+          mvn test -pl hugegraph-store/hg-store-test -am \
+            -P store-raftcore-test -Dtest=ZeroByteStringHelperTest -ntp
+          test -s "$REPORT" || {
+            echo "::error::Store single-class test produced no report"
+            exit 1
+          }
+          NEGATIVE_LOG=$(mktemp)
+          trap 'rm -f "$NEGATIVE_LOG"' EXIT
+          if mvn test -pl hugegraph-store/hg-store-test -am \
+            -P store-raftcore-test -Dtest=NoSuchJava17ContractTest -ntp \
+            >"$NEGATIVE_LOG" 2>&1; then
+            cat "$NEGATIVE_LOG"
+            echo "::error::Surefire accepted a missing Store test class"
+            exit 1
+          fi
+          cat "$NEGATIVE_LOG"
+          grep -Fq 'No tests matching pattern "NoSuchJava17ContractTest"' \
+            "$NEGATIVE_LOG"
+          grep -Fq '(store-raftcore-test) @ hg-store-test' "$NEGATIVE_LOG"
+
       - name: Check startup test prerequisites (Store)
         id: store-preflight
         run: |
@@ -308,14 +358,16 @@ jobs:
           startsWith(github.base_ref, 'release-') ||
           startsWith(github.base_ref, 'test-') ||
           github.base_ref == 'task/tinkerpop-3.7-upgrade' ||
-          github.head_ref == 'task/tinkerpop-3.7-upgrade'
+          github.head_ref == 'task/tinkerpop-3.7-upgrade' ||
+          github.base_ref == 'task/gsoc-phase2-java17' ||
+          github.head_ref == 'task/gsoc-phase2-java17'
         }}
 
     steps:
-      - name: Install JDK 11
+      - name: Install JDK 17
         uses: actions/setup-java@v3
         with:
-          java-version: '11'
+          java-version: '17'
           distribution: 'zulu'
 
       - name: Cache Maven packages
diff --git a/.github/workflows/riscv64-ci.yml b/.github/workflows/riscv64-ci.yml
index 9f8ca12057..b4b02666ae 100644
--- a/.github/workflows/riscv64-ci.yml
+++ b/.github/workflows/riscv64-ci.yml
@@ -28,18 +28,23 @@ env:
     tonistiigi/binfmt@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0
   RISCV64_BASE_IMAGE: >-
     ubuntu@sha256:4edded5722eb644868b7b976033d241d2ab3fff0a170924df69b200a59a2b994
-  DRAGONWELL_RISCV64_ARCHIVE: >-
-    Alibaba_Dragonwell_Extended_11.0.31.28.11_riscv64_linux.tar.gz
-  DRAGONWELL_RISCV64_SHA256: >-
-    7df2d308f0dca7a779d2854e6da19214f99cd77aa6d4982c3bf981a77266a79b
-  DRAGONWELL_RISCV64_URL: >-
-    https://github.com/dragonwell-project/dragonwell11/releases/download/dragonwell-extended-11.0.31.28_jdk-11.0.31-ga/Alibaba_Dragonwell_Extended_11.0.31.28.11_riscv64_linux.tar.gz
+  TEMURIN_RISCV64_ARCHIVE: >-
+    OpenJDK17U-jdk_riscv64_linux_hotspot_17.0.20_8.tar.gz
+  TEMURIN_RISCV64_SHA256: >-
+    bc36e8044c88df9f4ec2967c5277d1c8fae572378c12c0bac44dcae15d3af2f7
+  TEMURIN_RISCV64_BASE_URL: >-
+    https://github.com/adoptium/temurin17-binaries/releases/download
+  TEMURIN_RISCV64_RELEASE: jdk-17.0.20%2B8
+  EXPECTED_JAVA_MAJOR: '17'
+  EXPECTED_RISCV64_JAVA_VERSION: '17.0.20'
+  EXPECTED_RISCV64_JAVA_VENDOR: Eclipse Adoptium
   RISCV64_CONTAINER: >-
     hugegraph-riscv64-ci-${{ github.run_id }}-${{ github.run_attempt }}
 
 jobs:
   build-server-riscv64:
     runs-on: ubuntu-24.04
+    continue-on-error: true
     timeout-minutes: 90
 
     steps:
@@ -55,24 +60,28 @@ jobs:
             --name "$RISCV64_CONTAINER" "$RISCV64_BASE_IMAGE" sleep infinity
           test "$(docker exec "$RISCV64_CONTAINER" uname -m)" = riscv64
 
-      - name: Prepare source and Java 11
+      - name: Prepare source and Java 17
         run: |
-          ARCHIVE_PATH="$RUNNER_TEMP/$DRAGONWELL_RISCV64_ARCHIVE"
+          ARCHIVE_PATH="$RUNNER_TEMP/$TEMURIN_RISCV64_ARCHIVE"
           curl --fail --location --retry 3 --show-error \
-            "$DRAGONWELL_RISCV64_URL" --output "$ARCHIVE_PATH"
-          echo "$DRAGONWELL_RISCV64_SHA256  $ARCHIVE_PATH" | sha256sum -c -
+            "$TEMURIN_RISCV64_BASE_URL/$TEMURIN_RISCV64_RELEASE/$TEMURIN_RISCV64_ARCHIVE" \
+            --output "$ARCHIVE_PATH"
+          echo "$TEMURIN_RISCV64_SHA256  $ARCHIVE_PATH" | sha256sum -c -
 
-          docker exec "$RISCV64_CONTAINER" mkdir -p /workspace /opt/dragonwell
+          docker exec "$RISCV64_CONTAINER" mkdir -p /workspace /opt/temurin
           git ls-files -z | tar --null --files-from=- -cf - | \
             docker exec -i "$RISCV64_CONTAINER" tar -xf - -C /workspace
           docker cp "$ARCHIVE_PATH" \
-            "$RISCV64_CONTAINER:/tmp/$DRAGONWELL_RISCV64_ARCHIVE"
+            "$RISCV64_CONTAINER:/tmp/$TEMURIN_RISCV64_ARCHIVE"
 
       - name: Build and smoke test on RISC-V
         run: |
           docker exec \
-            --env DRAGONWELL_RISCV64_ARCHIVE="$DRAGONWELL_RISCV64_ARCHIVE" \
-            --env DRAGONWELL_RISCV64_SHA256="$DRAGONWELL_RISCV64_SHA256" \
+            --env TEMURIN_RISCV64_ARCHIVE="$TEMURIN_RISCV64_ARCHIVE" \
+            --env TEMURIN_RISCV64_SHA256="$TEMURIN_RISCV64_SHA256" \
+            --env EXPECTED_JAVA_MAJOR="$EXPECTED_JAVA_MAJOR" \
+            --env EXPECTED_RISCV64_JAVA_VERSION="$EXPECTED_RISCV64_JAVA_VERSION" \
+            --env EXPECTED_RISCV64_JAVA_VENDOR="$EXPECTED_RISCV64_JAVA_VENDOR" \
             "$RISCV64_CONTAINER" bash -euo pipefail -c '
               test "$(uname -m)" = riscv64
               apt-get -q update
@@ -81,11 +90,11 @@ jobs:
                 lsof maven procps \
                 protobuf-compiler protobuf-compiler-grpc-java-plugin
 
-              ARCHIVE_PATH="/tmp/$DRAGONWELL_RISCV64_ARCHIVE"
-              echo "$DRAGONWELL_RISCV64_SHA256  $ARCHIVE_PATH" | sha256sum -c -
-              tar -xzf "$ARCHIVE_PATH" --strip-components=1 -C /opt/dragonwell
+              ARCHIVE_PATH="/tmp/$TEMURIN_RISCV64_ARCHIVE"
+              echo "$TEMURIN_RISCV64_SHA256  $ARCHIVE_PATH" | sha256sum -c -
+              tar -xzf "$ARCHIVE_PATH" --strip-components=1 -C /opt/temurin
               rm "$ARCHIVE_PATH"
-              export JAVA_HOME=/opt/dragonwell
+              export JAVA_HOME=/opt/temurin
               export PATH="$JAVA_HOME/bin:$PATH"
 
               java -XshowSettings:vm -version
diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml
index faf564f9cd..5b7e2de752 100644
--- a/.github/workflows/server-ci.yml
+++ b/.github/workflows/server-ci.yml
@@ -22,6 +22,9 @@ jobs:
       - name: Run wait-storage.sh peer failover tests
         run: hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh
 
+      - name: Run RISC-V Java runtime contract tests
+        run: hugegraph-server/hugegraph-dist/src/assembly/travis/test-riscv64-java-runtime.sh
+
   build-server:
     # TODO: we need test & replace it to ubuntu-24.04 or ubuntu-latest
     runs-on: ubuntu-22.04
@@ -41,7 +44,9 @@ jobs:
           startsWith(github.head_ref, 'release-') ||
           startsWith(github.head_ref, 'test-') ||
           github.base_ref == 'task/tinkerpop-3.7-upgrade' ||
-          github.head_ref == 'task/tinkerpop-3.7-upgrade'
+          github.head_ref == 'task/tinkerpop-3.7-upgrade' ||
+          github.base_ref == 'task/gsoc-phase2-java17' ||
+          github.head_ref == 'task/gsoc-phase2-java17'
         }}
       RAFT_MODE: ${{ startsWith(github.ref_name, 'raft-') || startsWith(github.head_ref, 'raft-') }}
 
@@ -49,7 +54,7 @@ jobs:
       fail-fast: false
       matrix:
         BACKEND: [ memory, rocksdb, hbase ]
-        JAVA_VERSION: [ '11' ]
+        JAVA_VERSION: [ '17' ]
 
     steps:
       - name: Checkout
@@ -160,7 +165,34 @@ jobs:
           mvn package -Dmaven.test.skip=true -pl hugegraph-server/hugegraph-dist -am -ntp
           VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
           SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION/
-          $TRAVIS_DIR/test-java-security-properties.sh $SERVER_DIR
+          $TRAVIS_DIR/test-java-security-properties.sh \
+            "$SERVER_DIR" "$GITHUB_WORKSPACE"
+
+      - name: Verify Surefire single-class routing
+        if: ${{ env.BACKEND == 'rocksdb' }}
+        run: |
+          SUREFIRE_DIR=hugegraph-server/hugegraph-test/target/surefire-reports
+          REPORT="$SUREFIRE_DIR/TEST-org.apache.hugegraph.unit.auth.HugeGraphAuthProxyTest.xml"
+          rm -f "$REPORT"
+          mvn test -pl hugegraph-server/hugegraph-test -am \
+            -P unit-test -Dtest=HugeGraphAuthProxyTest -ntp
+          test -s "$REPORT" || {
+            echo "::error::Server single-class test produced no report"
+            exit 1
+          }
+          NEGATIVE_LOG=$(mktemp)
+          trap 'rm -f "$NEGATIVE_LOG"' EXIT
+          if mvn test -pl hugegraph-server/hugegraph-test -am \
+            -P unit-test -Dtest=NoSuchJava17ContractTest -ntp \
+            >"$NEGATIVE_LOG" 2>&1; then
+            cat "$NEGATIVE_LOG"
+            echo "::error::Surefire accepted a missing Server test class"
+            exit 1
+          fi
+          cat "$NEGATIVE_LOG"
+          grep -Fq 'No tests matching pattern "NoSuchJava17ContractTest"' \
+            "$NEGATIVE_LOG"
+          grep -Fq '(unit-test) @ hugegraph-test' "$NEGATIVE_LOG"
 
       - name: Check startup test prerequisites
         id: server-preflight
@@ -259,7 +291,7 @@ jobs:
       TRAVIS_DIR: hugegraph-server/hugegraph-dist/src/assembly/travis
       REPORT_DIR: target/site/jacoco
       BACKEND: rocksdb
-      JAVA_VERSION: '11'
+      JAVA_VERSION: '17'
       SERVER_JAVA_OPTIONS: ${{ matrix.server_java_options }}
 
     steps:
@@ -325,6 +357,8 @@ jobs:
           fi
 
   build-server-riscv64:
+    # Keep the emulated Java 17 RISC-V native-runtime signal visible while the
+    # platform lane stabilizes, but do not use it as a gate yet.
     uses: ./.github/workflows/riscv64-ci.yml
     permissions:
       contents: read
diff --git a/.serena/memories/code_style_and_conventions.md b/.serena/memories/code_style_and_conventions.md
index 159920cd3b..b89474635a 100644
--- a/.serena/memories/code_style_and_conventions.md
+++ b/.serena/memories/code_style_and_conventions.md
@@ -24,5 +24,5 @@
 - Validate: `mvn apache-rat:check -ntp` + `mvn editorconfig:check`
 
 ## Build
-- Java 11 target, `-Xlint:unchecked`, Lombok 1.18.30 (provided/optional)
+- Java 17 release target, `-Xlint:unchecked`, Lombok 1.18.30 (provided/optional)
 - Swagger: `io.swagger.core.v3:swagger-jaxrs2-jakarta` for REST API docs
diff --git a/.serena/memories/ecosystem_and_related_projects.md b/.serena/memories/ecosystem_and_related_projects.md
index 6bb1344d66..c88b6734b5 100644
--- a/.serena/memories/ecosystem_and_related_projects.md
+++ b/.serena/memories/ecosystem_and_related_projects.md
@@ -17,7 +17,7 @@ Sources → hugegraph-loader → hugegraph-server → Hubble / Computer / AI
 
 ## Integrations
 - Big Data: Flink, Spark, HDFS
-- Queries: Gremlin (TinkerPop 3.5.1), OpenCypher, REST API + Swagger UI
+- Queries: Gremlin (TinkerPop 3.7.6), OpenCypher, REST API + Swagger UI
 - Storage: RocksDB (default), HStore (distributed)
 
-## Version: Server 1.7.0, TinkerPop 3.5.1, Java 11+
+## Version: Server 1.7.0, TinkerPop 3.7.6, Java 17+
diff --git a/.serena/memories/implementation_patterns_and_guidelines.md b/.serena/memories/implementation_patterns_and_guidelines.md
index d04e33ce56..39201ffa69 100644
--- a/.serena/memories/implementation_patterns_and_guidelines.md
+++ b/.serena/memories/implementation_patterns_and_guidelines.md
@@ -23,7 +23,7 @@
 - After `.proto` changes: `mvn clean compile` → `target/generated-sources/protobuf/`
 
 ## Query Languages
-- **Gremlin**: Native TinkerPop 3.5.1
+- **Gremlin**: Native TinkerPop 3.7.6
 - **OpenCypher**: `hugegraph-api/opencypher/`
 - TinkerPop exceptions are passed through in Gremlin responses
 
@@ -35,7 +35,7 @@
 - **Profiles**: `unit-test`, `core-test`, `api-test`, `tinkerpop-structure-test`, `tinkerpop-process-test`
 - **Backends in CI**: memory, rocksdb, hbase (matrix)
 - **Single test class**: `mvn test -pl hugegraph-server/hugegraph-test -am -P core-test,memory -Dtest=ClassName`
-- TinkerPop tests: only on `release-*`/`test-*` branches
+- TinkerPop tests: `release-*`/`test-*` and upgrade branches selected in `server-ci.yml`
 - Raft tests: only on `test*`/`raft*` branches
 
 ## Docker
@@ -44,6 +44,6 @@
 - Container logs: stdout-based
 
 ## CI Pipelines
-- `server-ci.yml`: compile + unit/core/API tests (memory/rocksdb/hbase × Java 11)
+- `server-ci.yml`: compile + unit/core/API tests (memory/rocksdb/hbase × Java 17)
 - `rerun-ci.yml`: auto-rerun flaky failures (max 2 reruns, 180s delay)
 - `auto-pr-review.yml`: auto-comment on new PRs
diff --git a/.serena/memories/key_file_locations.md b/.serena/memories/key_file_locations.md
index 3f2a60dee0..0981a9eadd 100644
--- a/.serena/memories/key_file_locations.md
+++ b/.serena/memories/key_file_locations.md
@@ -29,7 +29,7 @@
 - Dist: `hugegraph-store/hg-store-dist/src/assembly/static/`
 
 ## CI Workflows (.github/workflows/)
-- `server-ci.yml` — Server tests (matrix: memory/rocksdb/hbase × Java 11)
+- `server-ci.yml` — Server tests (matrix: memory/rocksdb/hbase × Java 17)
 - `pd-store-ci.yml` — PD, Store & HStore tests
 - `commons-ci.yml` — Commons tests
 - `cluster-test-ci.yml` — Cluster integration
diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md
index 6375a3104e..da53c143fd 100644
--- a/.serena/memories/project_overview.md
+++ b/.serena/memories/project_overview.md
@@ -14,9 +14,9 @@ Apache HugeGraph is a fast-speed, highly-scalable graph database supporting 10+
 - Integration with Flink/Spark/HDFS
 
 ## Technology Stack
-- **Language**: Java 11+ (required)
-- **Build**: Maven 3.5+
-- **Graph Framework**: Apache TinkerPop 3.5.1
+- **Language**: Java 17+ (required)
+- **Build**: Maven 3.6.3+
+- **Graph Framework**: Apache TinkerPop 3.7.6
 - **RPC**: gRPC + Protocol Buffers
 - **API Docs**: Swagger (io.swagger.core.v3)
 - **Storage**: RocksDB (default/embedded), HStore (distributed/production), HBase (deprecated; planned for removal in 2.0)
diff --git a/.serena/memories/task_completion_checklist.md b/.serena/memories/task_completion_checklist.md
index bcee316d5a..853b01222d 100644
--- a/.serena/memories/task_completion_checklist.md
+++ b/.serena/memories/task_completion_checklist.md
@@ -20,7 +20,7 @@ mvn clean compile -Dmaven.javadoc.skip=true  # Compile warnings
 4. Run `./install-dist/scripts/dependency/regenerate_known_dependencies.sh`
 
 ## 4. CI Awareness
-- `server-ci.yml`: memory/rocksdb/hbase × Java 11
+- `server-ci.yml`: memory/rocksdb/hbase × Java 17
 - `rerun-ci.yml`: auto-retries flaky failures
 - `licence-checker.yml`: header validation
 - Raft tests: only `test*`/`raft*` branches
diff --git a/AGENTS.md b/AGENTS.md
index 2d6e81b15b..512222b6e7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,7 +6,7 @@ README.md covers human-facing deployment/ecosystem context; only consult it on d
 ## Stack & Modules
 
 Apache HugeGraph — Apache TinkerPop 3 compliant graph database.
-Java 11+, Maven 3.5+. Version managed via `${revision}` (currently `1.8.0`).
+Java 17+, Maven 3.6.3+. Version managed via `${revision}` (currently `1.7.0`).
 
 ```
 Client (Gremlin / Cypher / REST)
diff --git a/BUILDING.md b/BUILDING.md
index d4c807c748..b34c4bf796 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -3,8 +3,8 @@ Building HugeGraph
 
 Required:
 
-* Java 11
-* Maven 3.5+
+* Java 17+
+* Maven 3.6.3+
 
 To build without executing tests: `mvn clean package -Dmaven.test.skip=true`
 
@@ -38,4 +38,3 @@ To build without executing tests:
 To find the Java binary in your environment, run the appropriate command for your operating system:
 * Linux/macOS: `which java`
 * Windows: `for %i in (java.exe) do @echo. %~$PATH:i`
-
diff --git a/README.md b/README.md
index b223d4d53a..a5dc1dfe78 100644
--- a/README.md
+++ b/README.md
@@ -176,8 +176,8 @@ curl -X POST http://localhost:8080/gremlin \
 
 ### Prerequisites
 
-- **Java 11+** (required)
-- **Maven 3.5+** (for building from source)
+- **Java 17+** (required)
+- **Maven 3.6.3+** (for building from source)
 
 ### Option 1: Docker (Fastest)
 
@@ -327,7 +327,7 @@ For detailed architecture and development guidance, see [AGENTS.md](AGENTS.md).
    - Review the [Architecture Diagram](#architecture) above
 
 2. **Set Up Your Environment**
-   - Install Java 11+ and Maven 3.5+
+   - Install Java 17+ and Maven 3.6.3+
    - Follow [BUILDING.md](BUILDING.md) for build instructions
    - Configure your IDE to use `.editorconfig` for code style and `style/checkstyle.xml` for Checkstyle rules
 
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/pom.xml b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/pom.xml
index b59648304f..e6b364651a 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/pom.xml
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/pom.xml
@@ -30,8 +30,6 @@
 
 
     
-        11
-        11
         UTF-8
     
 
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/ClusterConstant.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/ClusterConstant.java
index 730bbc53ed..e1bad5bf36 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/ClusterConstant.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/ClusterConstant.java
@@ -49,6 +49,7 @@ public class ClusterConstant {
     public static final String GRAPH_TEMPLATE_FILE = "hugegraph.properties.template";
     public static final String GREMLIN_DRIVER_SETTING_FILE = "gremlin-driver-settings.yaml";
     public static final String GREMLIN_SERVER_FILE = "gremlin-server.yaml";
+    public static final String JVM_MODULE_OPTIONS_FILE = "jvm-module.options";
     public static final String REMOTE_SETTING_FILE = "remote.yaml";
     public static final String REMOTE_OBJECTS_SETTING_FILE = "remote-objects.yaml";
     public static final String EMPTY_SAMPLE_GROOVY_FILE = "scripts/empty-sample.groovy";
@@ -106,7 +107,7 @@ public static String getFileInDir(String path, String fileName) {
         return "";
     }
 
-    public static boolean isJava11OrHigher() {
+    public static boolean isJava17OrHigher() {
         String version = System.getProperty("java.version");
         if (version.startsWith("1.")) {
             version = version.substring(2, 3);
@@ -117,7 +118,7 @@ public static boolean isJava11OrHigher() {
             }
         }
         int versionNumber = Integer.parseInt(version);
-        return versionNumber >= 11;
+        return versionNumber >= 17;
     }
 
     public static String getProjectDir() {
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java
index a89c614c4c..e7a9e0f24f 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java
@@ -24,7 +24,7 @@
 import static org.apache.hugegraph.ct.base.ClusterConstant.PD_LIB_PATH;
 import static org.apache.hugegraph.ct.base.ClusterConstant.PD_TEMPLATE_PATH;
 import static org.apache.hugegraph.ct.base.ClusterConstant.getFileInDir;
-import static org.apache.hugegraph.ct.base.ClusterConstant.isJava11OrHigher;
+import static org.apache.hugegraph.ct.base.ClusterConstant.isJava17OrHigher;
 
 import java.io.File;
 import java.io.IOException;
@@ -62,8 +62,8 @@ public void start() {
             File stdoutFile = new File(getLogPath());
             List startCmd = new ArrayList<>();
             startCmd.add(JAVA_CMD);
-            if (!isJava11OrHigher()) {
-                LOG.error("Please make sure that the JDK is installed and the version >= 11");
+            if (!isJava17OrHigher()) {
+                LOG.error("Please make sure that the JDK is installed and the version >= 17");
                 return;
             }
 
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java
index 5fb8584c6c..332615b8e2 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java
@@ -17,6 +17,7 @@
 
 package org.apache.hugegraph.ct.node;
 
+import static org.apache.hugegraph.ct.base.ClusterConstant.BIN_DIR;
 import static org.apache.hugegraph.ct.base.ClusterConstant.CONF_DIR;
 import static org.apache.hugegraph.ct.base.ClusterConstant.EMPTY_SAMPLE_GROOVY_FILE;
 import static org.apache.hugegraph.ct.base.ClusterConstant.EXAMPLE_GROOVY_FILE;
@@ -24,6 +25,7 @@
 import static org.apache.hugegraph.ct.base.ClusterConstant.GREMLIN_DRIVER_SETTING_FILE;
 import static org.apache.hugegraph.ct.base.ClusterConstant.GREMLIN_SERVER_FILE;
 import static org.apache.hugegraph.ct.base.ClusterConstant.JAVA_CMD;
+import static org.apache.hugegraph.ct.base.ClusterConstant.JVM_MODULE_OPTIONS_FILE;
 import static org.apache.hugegraph.ct.base.ClusterConstant.LIB_DIR;
 import static org.apache.hugegraph.ct.base.ClusterConstant.LOG4J_FILE;
 import static org.apache.hugegraph.ct.base.ClusterConstant.LOCALHOST;
@@ -33,7 +35,7 @@
 import static org.apache.hugegraph.ct.base.ClusterConstant.SERVER_LIB_PATH;
 import static org.apache.hugegraph.ct.base.ClusterConstant.SERVER_PACKAGE_PATH;
 import static org.apache.hugegraph.ct.base.ClusterConstant.SERVER_TEMPLATE_PATH;
-import static org.apache.hugegraph.ct.base.ClusterConstant.isJava11OrHigher;
+import static org.apache.hugegraph.ct.base.ClusterConstant.isJava17OrHigher;
 import static org.apache.hugegraph.ct.base.EnvUtil.isPortOpen;
 
 import java.io.BufferedReader;
@@ -134,8 +136,8 @@ public void start() {
             File stdoutFile = new File(getLogPath());
             List startCmd = new ArrayList<>();
             startCmd.add(JAVA_CMD);
-            if (!isJava11OrHigher()) {
-                LOG.error("Please make sure that the JDK is installed and the version >= 11");
+            if (!isJava17OrHigher()) {
+                LOG.error("Please make sure that the JDK is installed and the version >= 17");
                 return;
             }
 
@@ -147,9 +149,8 @@ public void start() {
 
             startCmd.addAll(Arrays.asList(
                     "-Dname=HugeGraphServer" + this.index,
-                    "--add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED",
-                    "--add-modules=jdk.unsupported",
-                    "--add-exports=java.base/sun.nio.ch=ALL-UNNAMED",
+                    "@" + Paths.get(SERVER_PACKAGE_PATH, BIN_DIR,
+                                    JVM_MODULE_OPTIONS_FILE),
                     "-cp", storeClassPath,
                     "org.apache.hugegraph.dist.HugeGraphServer",
                     "./conf/gremlin-server.yaml",
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java
index 1cb0f67eae..d76ceaa0b8 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java
+++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java
@@ -24,7 +24,7 @@
 import static org.apache.hugegraph.ct.base.ClusterConstant.STORE_LIB_PATH;
 import static org.apache.hugegraph.ct.base.ClusterConstant.STORE_TEMPLATE_PATH;
 import static org.apache.hugegraph.ct.base.ClusterConstant.getFileInDir;
-import static org.apache.hugegraph.ct.base.ClusterConstant.isJava11OrHigher;
+import static org.apache.hugegraph.ct.base.ClusterConstant.isJava17OrHigher;
 
 import java.io.File;
 import java.io.IOException;
@@ -59,8 +59,8 @@ public void start() {
             File stdoutFile = new File(getLogPath());
             List startCmd = new ArrayList<>();
             startCmd.add(JAVA_CMD);
-            if (!isJava11OrHigher()) {
-                LOG.error("Please make sure that the JDK is installed and the version >= 11");
+            if (!isJava17OrHigher()) {
+                LOG.error("Please make sure that the JDK is installed and the version >= 17");
                 return;
             }
 
diff --git a/hugegraph-cluster-test/hugegraph-clustertest-test/pom.xml b/hugegraph-cluster-test/hugegraph-clustertest-test/pom.xml
index 735ea66b43..5c5acbfa57 100644
--- a/hugegraph-cluster-test/hugegraph-clustertest-test/pom.xml
+++ b/hugegraph-cluster-test/hugegraph-clustertest-test/pom.xml
@@ -29,8 +29,6 @@
     hugegraph-clustertest-test
 
     
-        11
-        11
         UTF-8
     
     
@@ -81,7 +79,6 @@
             
                 org.apache.maven.plugins
                 maven-surefire-plugin
-                2.20
                 
                     
                         simple-cluster-test
diff --git a/hugegraph-cluster-test/pom.xml b/hugegraph-cluster-test/pom.xml
index cd54ac0ffe..3e5b877e0e 100644
--- a/hugegraph-cluster-test/pom.xml
+++ b/hugegraph-cluster-test/pom.xml
@@ -39,8 +39,6 @@
     
 
     
-        11
-        11
         UTF-8
         apache-${release.name}-ct-${project.version}
     
@@ -96,7 +94,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 simple-cluster-test
@@ -120,7 +117,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 multi-cluster-test
diff --git a/hugegraph-commons/AGENTS.md b/hugegraph-commons/AGENTS.md
index c21ab4a5dc..94afcb6359 100644
--- a/hugegraph-commons/AGENTS.md
+++ b/hugegraph-commons/AGENTS.md
@@ -7,8 +7,8 @@ This file provides guidance to an AI coding tool when working with code in this
 hugegraph-commons is a shared utility module for Apache HugeGraph and its peripheral components. It provides core infrastructure components (locks, config, events, iterators, REST client, RPC framework) to simplify development across the HugeGraph ecosystem.
 
 **Technology Stack**:
-- Java 8+ (compiler source/target: 1.8)
-- Apache Maven 3.5+
+- Java 17+ (compiler release: 17)
+- Apache Maven 3.6.3+
 - Apache Commons Configuration2 for config management
 - OkHttp 4.10.0 for REST client (hugegraph-common)
 - Sofa-RPC 5.7.6 for RPC framework (hugegraph-rpc)
@@ -71,10 +71,10 @@ This is a Maven multi-module project with 2 main modules:
 
 ### Prerequisites
 ```bash
-# Verify Java version (8+ required)
+# Verify Java version (17+ required)
 java -version
 
-# Verify Maven version (3.5+ required)
+# Verify Maven version (3.6.3+ required)
 mvn -version
 ```
 
@@ -108,10 +108,12 @@ mvn test -pl hugegraph-common -DskipCommonsTests=false
 mvn test -pl hugegraph-rpc -am -DskipCommonsTests=false
 
 # Run single test class
-mvn test -pl hugegraph-common -Dtest=HugeConfigTest -DskipCommonsTests=false
+mvn test -pl hugegraph-common -Dtest=HugeConfigTest -DskipCommonsTests=false \
+  -Dsurefire.failIfNoSpecifiedTests=true
 
 # Run test suite (includes all unit tests)
-mvn test -pl hugegraph-common -Dtest=UnitTestSuite -DskipCommonsTests=false
+mvn test -pl hugegraph-common -Dtest=UnitTestSuite -DskipCommonsTests=false \
+  -Dsurefire.failIfNoSpecifiedTests=true
 ```
 
 ### Code Quality
@@ -231,13 +233,16 @@ When adding third-party dependencies:
 
 ```bash
 # Single test class
-mvn test -pl hugegraph-common -Dtest=HugeConfigTest -DskipCommonsTests=false
+mvn test -pl hugegraph-common -Dtest=HugeConfigTest -DskipCommonsTests=false \
+  -Dsurefire.failIfNoSpecifiedTests=true
 
 # Single test method
-mvn test -pl hugegraph-common -Dtest=HugeConfigTest#testGetOption -DskipCommonsTests=false
+mvn test -pl hugegraph-common -Dtest=HugeConfigTest#testGetOption \
+  -DskipCommonsTests=false -Dsurefire.failIfNoSpecifiedTests=true
 
 # Pattern matching
-mvn test -pl hugegraph-common -Dtest=*ConfigTest -DskipCommonsTests=false
+mvn test -pl hugegraph-common -Dtest=*ConfigTest -DskipCommonsTests=false \
+  -Dsurefire.failIfNoSpecifiedTests=true
 ```
 
 ### Debugging Tips
diff --git a/hugegraph-commons/hugegraph-common/build.sh b/hugegraph-commons/hugegraph-common/build.sh
index b2cb6211b9..7f3e849c17 100644
--- a/hugegraph-commons/hugegraph-common/build.sh
+++ b/hugegraph-commons/hugegraph-common/build.sh
@@ -16,8 +16,5 @@
 # limitations under the License.
 #
 
-export MAVEN_HOME=/home/scmtools/buildkit/maven/apache-maven-3.3.9/
-export JAVA_HOME=/home/scmtools/buildkit/java/jdk1.8.0_25/
-export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH
-
-mvn clean test -Dtest=UnitTestSuite
+mvn clean test -Dtest=UnitTestSuite \
+    -Dsurefire.failIfNoSpecifiedTests=true
diff --git a/hugegraph-commons/hugegraph-common/pom.xml b/hugegraph-commons/hugegraph-common/pom.xml
index 14f7cc217c..d703a2407d 100644
--- a/hugegraph-commons/hugegraph-common/pom.xml
+++ b/hugegraph-commons/hugegraph-common/pom.xml
@@ -257,7 +257,6 @@
             
                 org.jacoco
                 jacoco-maven-plugin
-                0.8.2
                 
                     
                         pre-unit-test
diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/testutil/AssertTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/testutil/AssertTest.java
index 53f60247e9..21f935aa0a 100644
--- a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/testutil/AssertTest.java
+++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/testutil/AssertTest.java
@@ -422,8 +422,6 @@ public void testAssertContains() {
 
         Assert.assertThrows(NullPointerException.class, () -> {
             Assert.assertContains(null, "null");
-        }, e -> {
-            Assert.assertNull(e.getMessage());
         });
     }
 
diff --git a/hugegraph-commons/hugegraph-rpc/pom.xml b/hugegraph-commons/hugegraph-rpc/pom.xml
index 23f0a32077..822c983cd7 100644
--- a/hugegraph-commons/hugegraph-rpc/pom.xml
+++ b/hugegraph-commons/hugegraph-rpc/pom.xml
@@ -124,7 +124,6 @@
             
                 org.jacoco
                 jacoco-maven-plugin
-                0.8.2
                 
                     
                         pre-unit-test
diff --git a/hugegraph-commons/pom.xml b/hugegraph-commons/pom.xml
index b45a4d7eec..5486cd9471 100644
--- a/hugegraph-commons/pom.xml
+++ b/hugegraph-commons/pom.xml
@@ -94,8 +94,6 @@
         1.7.0
         UTF-8
         ${project.basedir}/..
-        1.8
-        1.8
         2.18.0
         1.10
         2.10.1
@@ -132,8 +130,6 @@
             
                 maven-compiler-plugin
                 
-                    ${compiler.source}
-                    ${compiler.target}
                     
                         500
                     
@@ -193,7 +189,6 @@
             
                 org.jacoco
                 jacoco-maven-plugin
-                0.8.7
                 
                     
                         pre-unit-test
@@ -288,9 +283,12 @@
             
                 org.apache.maven.plugins
                 maven-surefire-plugin
-                2.20
                 
                     ${skipCommonsTests}
+                    
+                        @{argLine}
+                        @${project.parent.basedir}/../hugegraph-server/hugegraph-test/conf/jvm-test-module.options
+                    
                 
             
         
diff --git a/hugegraph-pd/AGENTS.md b/hugegraph-pd/AGENTS.md
index aaaa861f39..f31c055310 100644
--- a/hugegraph-pd/AGENTS.md
+++ b/hugegraph-pd/AGENTS.md
@@ -11,8 +11,8 @@ HugeGraph PD (Placement Driver) is a meta server for distributed HugeGraph deplo
 - Metadata coordination using Raft consensus
 
 **Technology Stack**:
-- Java 11+ (required)
-- Apache Maven 3.5+
+- Java 17+ (required)
+- Apache Maven 3.6.3+
 - gRPC + Protocol Buffers for RPC communication
 - JRaft (Ant Design's Raft implementation) for consensus
 - RocksDB for metadata persistence
@@ -320,7 +320,7 @@ docker build -f hugegraph-pd/Dockerfile -t hugegraph-pd:latest .
 
 The Dockerfile uses multi-stage build:
 1. Stage 1: Build with Maven
-2. Stage 2: Runtime with OpenJDK 11
+2. Stage 2: Runtime with Eclipse Temurin 17 JRE
 
 ### Running in Docker
 
diff --git a/hugegraph-pd/Dockerfile b/hugegraph-pd/Dockerfile
index 68e6e2555b..74387069ac 100644
--- a/hugegraph-pd/Dockerfile
+++ b/hugegraph-pd/Dockerfile
@@ -18,7 +18,7 @@
 
 # Dockerfile for HugeGraph PD
 # 1st stage: build source code
-FROM --platform=$BUILDPLATFORM maven:3.9.0-eclipse-temurin-11 AS build
+FROM --platform=$BUILDPLATFORM maven:3.9.16-eclipse-temurin-17 AS build
 
 WORKDIR /pkg
 
@@ -32,7 +32,7 @@ RUN --mount=type=cache,target=/root/.m2 \
 
 # 2nd stage: runtime env
 # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13
-FROM eclipse-temurin:11-jre-jammy
+FROM eclipse-temurin:17-jre-jammy
 
 COPY --from=build /pkg/hugegraph-pd/apache-hugegraph-pd-*/ /hugegraph-pd/
 LABEL maintainer="HugeGraph Docker Maintainers "
diff --git a/hugegraph-pd/README.md b/hugegraph-pd/README.md
index 794dba9b98..74359183ea 100644
--- a/hugegraph-pd/README.md
+++ b/hugegraph-pd/README.md
@@ -36,8 +36,8 @@ For detailed architecture and design, see [Architecture Documentation](docs/arch
 
 ### Prerequisites
 
-- **Java**: 11 or higher
-- **Maven**: 3.5 or higher
+- **Java**: 17 or higher
+- **Maven**: 3.6.3 or higher
 - **Disk Space**: At least 1GB for PD data directory
 
 ### Build
diff --git a/hugegraph-pd/docs/architecture.md b/hugegraph-pd/docs/architecture.md
index 080189be95..e5d2dad7dd 100644
--- a/hugegraph-pd/docs/architecture.md
+++ b/hugegraph-pd/docs/architecture.md
@@ -54,7 +54,7 @@ HugeGraph PD (Placement Driver) is the control plane for HugeGraph distributed d
 - **Storage**: RocksDB for persistent metadata
 - **Communication**: gRPC with Protocol Buffers
 - **Framework**: Spring Boot for REST APIs and dependency injection
-- **Language**: Java 11+
+- **Language**: Java 17+
 
 ## Module Architecture
 
diff --git a/hugegraph-pd/docs/development.md b/hugegraph-pd/docs/development.md
index 3f01b902ea..e15e6d1dea 100644
--- a/hugegraph-pd/docs/development.md
+++ b/hugegraph-pd/docs/development.md
@@ -20,8 +20,8 @@ Ensure you have the following tools installed:
 
 | Tool | Minimum Version | Recommended | Purpose |
 |------|----------------|-------------|---------|
-| **JDK** | 11 | 11 or 17 | Java runtime and compilation |
-| **Maven** | 3.5.0 | 3.8+ | Build tool and dependency management |
+| **JDK** | 17 | 17 LTS | Java runtime and compilation |
+| **Maven** | 3.6.3 | 3.8+ | Build tool and dependency management |
 | **Git** | 2.0+ | Latest | Version control |
 | **IDE** | N/A | IntelliJ IDEA | Development environment |
 
@@ -30,11 +30,11 @@ Ensure you have the following tools installed:
 ```bash
 # Check Java version
 java -version
-# Expected: openjdk version "11.0.x" or later
+# Expected: openjdk version "17.0.x" or later
 
 # Check Maven version
 mvn -version
-# Expected: Apache Maven 3.5.0 or later
+# Expected: Apache Maven 3.6.3 or later
 
 # Check Git version
 git --version
@@ -77,8 +77,8 @@ Required for Lombok support:
 #### Configure JDK
 
 1. **File → Project Structure → Project**
-2. **Project SDK**: Select JDK 11 or 17
-3. **Project language level**: 11
+2. **Project SDK**: Select JDK 17
+3. **Project language level**: 17
 4. **Apply** and **OK**
 
 ## Building from Source
@@ -196,10 +196,10 @@ mvn test -pl hugegraph-pd/hg-pd-test -am -P pd-rest-test
 
 ```bash
 # Run specific test class
-mvn -pl hugegraph-pd/hg-pd-test test -Dtest=PartitionServiceTest -DfailIfNoTests=false
+mvn -pl hugegraph-pd/hg-pd-test test -Dtest=PartitionServiceTest
 
 # Run specific test method
-mvn -pl hugegraph-pd/hg-pd-test test -Dtest=PartitionServiceTest#testSplitPartition -DfailIfNoTests=false
+mvn -pl hugegraph-pd/hg-pd-test test -Dtest=PartitionServiceTest#testSplitPartition
 ```
 
 #### Test from IDE
@@ -382,7 +382,7 @@ if (store == null) {
    - **Main class**: `org.apache.hugegraph.pd.HgPdApplication` (in `hg-pd-service`)
    - **Program arguments**: `--spring.config.location=file:./conf/application.yml`
    - **Working directory**: `hugegraph-pd/hg-pd-dist/target/hugegraph-pd-/`
-   - **JRE**: 11 or 17
+   - **JRE**: 17
 
 2. Set breakpoints in code
 
diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh
index 1329df2271..636f7ba0fe 100755
--- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh
+++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh
@@ -69,7 +69,7 @@ ensure_path_writable "$PLUGINS"
 # The maximum and minimum heap memory that service can use
 MAX_MEM=$((32 * 1024))
 MIN_MEM=$((1 * 512))
-EXPECT_JDK_VERSION=11
+EXPECT_JDK_VERSION=17
 
 # Change to $BIN's parent
 cd "${TOP}" || exit
@@ -82,8 +82,11 @@ else
 fi
 
 # check jdk version
-JAVA_VERSION=$($JAVA -version 2>&1 | awk 'NR==1{gsub(/"/,""); print $3}'  | awk -F'_' '{print $1}')
-if [[ $? -ne 0 || $JAVA_VERSION < $EXPECT_JDK_VERSION ]]; then
+JAVA_VERSION=$($JAVA -version 2>&1 |
+               awk -F'"' '/^(java|openjdk) version "/ {print $2; exit}' |
+               sed 's/^1\.//' | cut -d'.' -f1)
+JAVA_VERSION="${JAVA_VERSION%%[!0-9]*}"
+if [[ -z $JAVA_VERSION || $JAVA_VERSION -lt $EXPECT_JDK_VERSION ]]; then
     echo "Please make sure that the JDK is installed and the version >= $EXPECT_JDK_VERSION"  >> ${OUTPUT}
     exit 1
 fi
@@ -111,7 +114,7 @@ case "$GC_OPTION" in
                       -XX:InitiatingHeapOccupancyPercent=50 -XX:G1RSetUpdatingPauseTimePercent=5"
         ;;
     zgc|ZGC)
-        echo "Using ZGC as the default garbage collector (Only support Java 11+)"
+        echo "Using ZGC as the default garbage collector (Only support Java 17+)"
         JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseZGC -XX:+UnlockExperimentalVMOptions \
                                       -XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \
                                       -XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \
diff --git a/hugegraph-pd/hg-pd-test/pom.xml b/hugegraph-pd/hg-pd-test/pom.xml
index a2a7c67873..681353d69f 100644
--- a/hugegraph-pd/hg-pd-test/pom.xml
+++ b/hugegraph-pd/hg-pd-test/pom.xml
@@ -46,7 +46,6 @@
                     
                         org.jacoco
                         jacoco-maven-plugin
-                        0.8.4
                         
                             
                                 **/grpc/**.*
@@ -197,11 +196,18 @@
             
                 org.apache.maven.plugins
                 maven-surefire-plugin
-                2.20
+                
+                    
+                        org.apache.maven.surefire
+                        surefire-junit4
+                        ${maven.surefire.plugin.version}
+                    
+                
                 
                     
                         pd-client-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -214,6 +220,7 @@
                     
                         pd-core-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -226,6 +233,7 @@
                     
                         pd-common-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -238,6 +246,7 @@
                     
                         pd-rest-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -252,7 +261,6 @@
             
                 org.jacoco
                 jacoco-maven-plugin
-                0.8.4
                 
                     
                         pre-test
diff --git a/hugegraph-pd/pom.xml b/hugegraph-pd/pom.xml
index ceb8af33b2..c0a2613b5b 100644
--- a/hugegraph-pd/pom.xml
+++ b/hugegraph-pd/pom.xml
@@ -74,7 +74,6 @@
             
                 org.jacoco
                 jacoco-maven-plugin
-                0.8.4
                 
                     
                         **/grpc/**.*
@@ -150,7 +149,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 pd-common-test
@@ -174,7 +172,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 pd-core-test
@@ -198,7 +195,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 pd-client-test
@@ -222,7 +218,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 pd-rest-test
diff --git a/hugegraph-server/AGENTS.md b/hugegraph-server/AGENTS.md
index 0b6da40820..a2d51daabe 100644
--- a/hugegraph-server/AGENTS.md
+++ b/hugegraph-server/AGENTS.md
@@ -10,7 +10,7 @@ HugeGraph Server is the graph engine layer of Apache HugeGraph, consisting of:
 - **Backend Interface**: Abstraction layer for pluggable storage backends
 - **Storage Backend Implementations**: RocksDB (default), HStore (distributed), HBase (deprecated; planned for removal in 2.0), and Memory (test-only)
 
-Technology: Java 11+, Maven 3.5+, Apache TinkerPop 3.5.1, Jersey 3.0 (REST), gRPC (distributed communication)
+Technology: Java 17+, Maven 3.6.3+, Apache TinkerPop 3.7.6, Jersey 3.0 (REST), gRPC (distributed communication)
 
 ## Build Commands
 
diff --git a/hugegraph-server/Dockerfile b/hugegraph-server/Dockerfile
index 5caadd23cb..09d782593d 100644
--- a/hugegraph-server/Dockerfile
+++ b/hugegraph-server/Dockerfile
@@ -18,7 +18,7 @@
 
 # Dockerfile for HugeGraph Server
 # 1st stage: build source code
-FROM --platform=$BUILDPLATFORM maven:3.9.0-eclipse-temurin-11 AS build
+FROM --platform=$BUILDPLATFORM maven:3.9.16-eclipse-temurin-17 AS build
 
 WORKDIR /pkg
 
@@ -32,14 +32,13 @@ RUN --mount=type=cache,target=/root/.m2 \
 
 # 2nd stage: runtime env
 # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13
-FROM eclipse-temurin:11-jre-jammy
+FROM eclipse-temurin:17-jre-jammy
 
 COPY --from=build /pkg/hugegraph-server/apache-hugegraph-server-*/ /hugegraph-server/
 LABEL maintainer="HugeGraph Docker Maintainers "
 
 # TODO: use g1gc or zgc as default
-# Note: --add-exports is required for Java 11+ to access jdk.internal.reflect for auth proxy
-ENV JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:MaxRAMPercentage=50 -XshowSettings:vm --add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED" \
+ENV JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:MaxRAMPercentage=50 -XshowSettings:vm" \
     HUGEGRAPH_HOME="hugegraph-server" \
     STDOUT_MODE="true"
 
diff --git a/hugegraph-server/Dockerfile-hstore b/hugegraph-server/Dockerfile-hstore
index 7cd64e8f3b..9163772000 100644
--- a/hugegraph-server/Dockerfile-hstore
+++ b/hugegraph-server/Dockerfile-hstore
@@ -18,7 +18,7 @@
 
 # Dockerfile for HugeGraph Server (hstore backend)
 # 1st stage: build source code
-FROM --platform=$BUILDPLATFORM maven:3.9.0-eclipse-temurin-11 AS build
+FROM --platform=$BUILDPLATFORM maven:3.9.16-eclipse-temurin-17 AS build
 
 WORKDIR /pkg
 
@@ -32,7 +32,7 @@ RUN --mount=type=cache,target=/root/.m2 \
 
 # 2nd stage: runtime env
 # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13
-FROM eclipse-temurin:11-jre-jammy
+FROM eclipse-temurin:17-jre-jammy
 
 COPY --from=build /pkg/hugegraph-server/apache-hugegraph-server-*/ /hugegraph-server/
 # remove hugegraph.properties and rename hstore.properties.template for default hstore backend
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Reflection.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Reflection.java
index 6e5fd07527..14e7b77120 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Reflection.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Reflection.java
@@ -20,6 +20,8 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
 
 import org.apache.hugegraph.HugeException;
 import org.apache.hugegraph.exception.NotSupportException;
@@ -42,14 +44,14 @@ public class Reflection {
 
             registerFieldsToFilterMethodTemp =
                     reflectionClazzTemp.getMethod("registerFieldsToFilter",
-                                                  Class.class, String[].class);
+                                                  Class.class, Set.class);
 
             registerMethodsToFilterMethodTemp =
                     reflectionClazzTemp.getMethod("registerMethodsToFilter",
-                                                  Class.class, String[].class);
+                                                  Class.class, Set.class);
         } catch (ClassNotFoundException e) {
             LOG.error("Can't find jdk.internal.reflect.Reflection class, " +
-                      "please ensure you are using Java 11", e);
+                      "please ensure you are using Java 17", e);
         } catch (NoSuchMethodException e) {
             LOG.error("Can't find reflection filter methods", e);
         }
@@ -62,34 +64,59 @@ public class Reflection {
     public static void registerFieldsToFilter(Class containingClass, String... fieldNames) {
         if (REGISTER_FILEDS_TO_FILTER_METHOD == null) {
             throw new NotSupportException("Reflection.registerFieldsToFilter() - " +
-                                          "requires Java 11 or higher");
+                                          "requires Java 17 or higher");
         }
 
         try {
             REGISTER_FILEDS_TO_FILTER_METHOD.setAccessible(true);
-            REGISTER_FILEDS_TO_FILTER_METHOD.invoke(REFLECTION_CLAZZ, containingClass, fieldNames);
-        } catch (IllegalAccessException | InvocationTargetException e) {
-            throw new HugeException("Failed to register class '%s' fields to filter: %s",
-                                    containingClass, Arrays.toString(fieldNames));
+            REGISTER_FILEDS_TO_FILTER_METHOD.invoke(REFLECTION_CLAZZ, containingClass,
+                                                    toFilterSet(fieldNames));
+        } catch (IllegalAccessException e) {
+            throw new HugeException(
+                    "Failed to register class '%s' fields to filter: %s",
+                    e, containingClass, Arrays.toString(fieldNames));
+        } catch (InvocationTargetException e) {
+            throwInvocationTargetException(e, containingClass, fieldNames, "fields");
         }
     }
 
     public static void registerMethodsToFilter(Class containingClass, String... methodNames) {
         if (REGISTER_METHODS_TO_FILTER_METHOD == null) {
             throw new NotSupportException("Reflection.registerMethodsToFilter() - " +
-                                          "requires Java 11 or higher");
+                                          "requires Java 17 or higher");
         }
 
         try {
             REGISTER_METHODS_TO_FILTER_METHOD.setAccessible(true);
             REGISTER_METHODS_TO_FILTER_METHOD.invoke(REFLECTION_CLAZZ, containingClass,
-                                                     methodNames);
-        } catch (IllegalAccessException | InvocationTargetException e) {
-            throw new HugeException("Failed to register class '%s' methods to filter: %s",
-                                    containingClass, Arrays.toString(methodNames));
+                                                     toFilterSet(methodNames));
+        } catch (IllegalAccessException e) {
+            throw new HugeException(
+                    "Failed to register class '%s' methods to filter: %s",
+                    e, containingClass, Arrays.toString(methodNames));
+        } catch (InvocationTargetException e) {
+            throwInvocationTargetException(e, containingClass, methodNames, "methods");
         }
     }
 
+    private static Set toFilterSet(String... members) {
+        return new LinkedHashSet<>(Arrays.asList(members));
+    }
+
+    private static void throwInvocationTargetException(InvocationTargetException exception,
+                                                       Class containingClass,
+                                                       String[] members,
+                                                       String type) {
+        Throwable cause = exception.getCause();
+        if (cause instanceof IllegalArgumentException) {
+            throw (IllegalArgumentException) cause;
+        }
+        Throwable failure = cause != null ? cause : exception;
+        throw new HugeException(
+                "Failed to register class '%s' %s to filter: %s",
+                failure, containingClass, type, Arrays.toString(members));
+    }
+
     public static Class loadClass(String clazz) {
         try {
             return Class.forName(clazz);
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
index caffedc482..8ec31db4f6 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
@@ -63,11 +63,12 @@ ensure_path_writable "$PLUGINS"
 # The maximum and minimum heap memory that service can use
 MAX_MEM=$((32 * 1024))
 MIN_MEM=$((1 * 512))
-MIN_JAVA_VERSION=11
+MIN_JAVA_VERSION=17
 # JDK 24 removed the Security Manager (JEP 486): "-Djava.security.manager=allow"
 # is a fatal VM initialization error there and System.setSecurityManager() always
 # throws, so HugeSecurityManager cannot be installed on newer runtimes.
 MAX_SECURITY_JAVA_VERSION=23
+JVM_MODULE_OPTIONS="${BIN}/jvm-module.options"
 
 # Add the slf4j-log4j12 binding
 CP=$(find -L $LIB -name 'log4j-slf4j-impl*.jar' | sort | tr '\n' ':')
@@ -114,6 +115,11 @@ if [[ -z $JAVA_VERSION || $JAVA_VERSION -lt $MIN_JAVA_VERSION ]]; then
     exit 1
 fi
 
+if [[ ! -r ${JVM_MODULE_OPTIONS} ]]; then
+    echo "Missing or unreadable JVM module options file: ${JVM_MODULE_OPTIONS}" >> "${OUTPUT}"
+    exit 1
+fi
+
 # Set Java options
 if [ "$JAVA_OPTIONS" = "" ]; then
     XMX=$(calc_xmx $MIN_MEM $MAX_MEM)
@@ -128,12 +134,6 @@ if [ "$JAVA_OPTIONS" = "" ]; then
     #              -Xloggc:./logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
 fi
 
-if [[ $JAVA_VERSION -gt 9 ]]; then
-    JAVA_OPTIONS="${JAVA_OPTIONS} --add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED \
-                                  --add-modules=jdk.unsupported \
-                                  --add-exports=java.base/sun.nio.ch=ALL-UNNAMED "
-fi
-
 # Using G1GC as the default garbage collector (Recommended for large memory machines)
 # mention: zgc is only available on ARM-Mac with java > 13
 case "$GC_OPTION" in
@@ -144,7 +144,7 @@ case "$GC_OPTION" in
                                       -XX:G1RSetUpdatingPauseTimePercent=5"
         ;;
     zgc|ZGC)
-        echo "Using ZGC as the default garbage collector (Only support Java 11+)"
+        echo "Using ZGC as the default garbage collector (Only support Java 17+)"
         JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseZGC -XX:+UnlockExperimentalVMOptions \
                                       -XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \
                                       -XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \
@@ -258,12 +258,12 @@ fi
 
 # Turn on security check
 if [[ "${STDOUT_MODE:-false}" == "true" ]]; then
-    exec ${JAVA} -Dname="HugeGraphServer" ${JVM_OPTIONS} ${JAVA_OPTIONS} \
+    exec ${JAVA} @"${JVM_MODULE_OPTIONS}" -Dname="HugeGraphServer" ${JVM_OPTIONS} ${JAVA_OPTIONS} \
         ${SECURITY_MANAGER_OPTION} -cp ${CLASSPATH}: \
         org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap \
         ${OPEN_SECURITY_CHECK} ${GREMLIN_SERVER_CONF} ${REST_SERVER_CONF}
 else
-    exec ${JAVA} -Dname="HugeGraphServer" ${JVM_OPTIONS} ${JAVA_OPTIONS} \
+    exec ${JAVA} @"${JVM_MODULE_OPTIONS}" -Dname="HugeGraphServer" ${JVM_OPTIONS} ${JAVA_OPTIONS} \
         ${SECURITY_MANAGER_OPTION} -cp ${CLASSPATH}: \
         org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap \
         ${OPEN_SECURITY_CHECK} ${GREMLIN_SERVER_CONF} ${REST_SERVER_CONF} \
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh
index 74ec0bb731..d934192fad 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh
@@ -30,6 +30,7 @@ TOP="$(cd "${BIN}"/../ && pwd)"
 CONF="$TOP/conf"
 LIB="$TOP/lib"
 PLUGINS="$TOP/plugins"
+JVM_MODULE_OPTIONS="${BIN}/jvm-module.options"
 
 . "${BIN}"/util.sh
 
@@ -38,15 +39,16 @@ ensure_path_writable "${PLUGINS}"
 
 if [[ -n "$JAVA_HOME" ]]; then
     JAVA="$JAVA_HOME"/bin/java
-    EXT="$JAVA_HOME/jre/lib/ext:$LIB:$PLUGINS"
 else
     JAVA=java
-    EXT="$LIB:$PLUGINS"
 fi
 
 cd "${TOP}" || exit
 
-DEFAULT_JAVA_OPTIONS="--add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED"
+if [[ ! -r ${JVM_MODULE_OPTIONS} ]]; then
+    echo "Missing or unreadable JVM module options file: ${JVM_MODULE_OPTIONS}" >&2
+    exit 1
+fi
 
 echo "Initializing HugeGraph Store..."
 
@@ -54,7 +56,7 @@ echo "Initializing HugeGraph Store..."
 CP=$(find -L "${LIB}" -name 'hugegraph*.jar' | sort | tr '\n' ':')
 CP="$CP":$(find -L "${LIB}" -name '*.jar' \! -name 'hugegraph*' | sort | tr '\n' ':')
 CP="$CP":$(find -L "${PLUGINS}" -name '*.jar' | sort | tr '\n' ':')
-$JAVA -cp $CP ${DEFAULT_JAVA_OPTIONS} \
+"${JAVA}" @"${JVM_MODULE_OPTIONS}" -cp "$CP" \
 org.apache.hugegraph.cmd.InitStore "${CONF}"/rest-server.properties
 INIT_STORE_STATUS=$?
 if [[ ${INIT_STORE_STATUS} -ne 0 ]]; then
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/jvm-module.options b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/jvm-module.options
new file mode 100644
index 0000000000..eeb6114c5c
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/jvm-module.options
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+--add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED
+--add-modules=jdk.unsupported
+--add-exports=java.base/sun.nio.ch=ALL-UNNAMED
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh
index 570d07b545..ffe1a2243d 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh
@@ -31,7 +31,7 @@ TOP="$(cd $BIN/../ && pwd)"
 . $BIN/util.sh
 
 if [ "$JAVA_HOME" == "" ]; then
-    echo "Must set JAVA_HOME environment variable and installed jdk >= 1.8"
+    echo "Must set JAVA_HOME environment variable and install JDK >= 17"
     exit 1
 fi
 
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-native-runtime-smoke-test.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-native-runtime-smoke-test.sh
index 7cf09e30ee..e8570eef01 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-native-runtime-smoke-test.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-native-runtime-smoke-test.sh
@@ -26,7 +26,6 @@ TRAVIS_DIR=$(cd "$(dirname "$0")" && pwd)
 SERVER_DIR=$(cd "$1" && pwd)
 SERVER_URL=${SERVER_URL:-http://127.0.0.1:8080}
 EXPECTED_ARCH=${EXPECTED_ARCH:-riscv64}
-EXPECTED_JAVA_MAJOR=${EXPECTED_JAVA_MAJOR:-11}
 SERVER_START_ATTEMPTED=false
 SERVER_STARTUP_TIMEOUT=${SERVER_STARTUP_TIMEOUT:-300}
 SERVER_START_COMMAND_TIMEOUT=$((SERVER_STARTUP_TIMEOUT + 30))
@@ -34,7 +33,7 @@ STORE_DUMP_TIMEOUT=${STORE_DUMP_TIMEOUT:-120}
 WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/hugegraph-native-runtime-smoke.XXXXXX")
 RUN_ID="$(date +%s)_$$"
 
-export EXPECTED_ARCH EXPECTED_JAVA_MAJOR
+export EXPECTED_ARCH
 
 cleanup() {
     local status=$?
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-rocksdb-jni-smoke-test.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-rocksdb-jni-smoke-test.sh
index 5603431d5d..81d88796d0 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-rocksdb-jni-smoke-test.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-rocksdb-jni-smoke-test.sh
@@ -25,7 +25,9 @@ fi
 TRAVIS_DIR=$(cd "$(dirname "$0")" && pwd)
 SERVER_DIR=$(cd "$1" && pwd)
 EXPECTED_ARCH=${EXPECTED_ARCH:-}
-EXPECTED_JAVA_MAJOR=${EXPECTED_JAVA_MAJOR:-11}
+EXPECTED_JAVA_MAJOR=${EXPECTED_JAVA_MAJOR:-17}
+EXPECTED_RISCV64_JAVA_VERSION=${EXPECTED_RISCV64_JAVA_VERSION:-}
+EXPECTED_RISCV64_JAVA_VENDOR=${EXPECTED_RISCV64_JAVA_VENDOR:-}
 ACTUAL_ARCH=$(uname -m)
 
 if [[ -n "${JAVA_HOME:-}" ]]; then
@@ -88,8 +90,8 @@ fi
 configure_riscv64_libatomic
 
 if [[ "$ACTUAL_ARCH" == "riscv64" ]]; then
-    EXPECTED_RISCV64_JAVA_VERSION=${EXPECTED_RISCV64_JAVA_VERSION:-11.0.31.28}
-    if [[ "$JAVA_VERSION" != "$EXPECTED_RISCV64_JAVA_VERSION" ]]; then
+    if [[ -n "$EXPECTED_RISCV64_JAVA_VERSION" && \
+          "$JAVA_VERSION" != "$EXPECTED_RISCV64_JAVA_VERSION" ]]; then
         echo "Expected RISC-V Java $EXPECTED_RISCV64_JAVA_VERSION, got $JAVA_VERSION" >&2
         exit 1
     fi
@@ -97,8 +99,10 @@ if [[ "$ACTUAL_ARCH" == "riscv64" ]]; then
         echo "Expected RISC-V Server VM, got $JAVA_VM_NAME" >&2
         exit 1
     fi
-    if [[ "$JAVA_VM_VENDOR" != "Alibaba" ]]; then
-        echo "Expected RISC-V Java vendor Alibaba, got $JAVA_VM_VENDOR" >&2
+    if [[ -n "$EXPECTED_RISCV64_JAVA_VENDOR" && \
+          "$JAVA_VM_VENDOR" != "$EXPECTED_RISCV64_JAVA_VENDOR" ]]; then
+        echo "Expected RISC-V Java vendor $EXPECTED_RISCV64_JAVA_VENDOR," \
+             "got $JAVA_VM_VENDOR" >&2
         exit 1
     fi
     if [[ "$JAVA_VM_INFO" != *"mixed mode"* ]]; then
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java-security-properties.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java-security-properties.sh
index 796d69c83b..f009d91ece 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java-security-properties.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java-security-properties.sh
@@ -17,11 +17,13 @@
 
 set -euo pipefail
 
-SERVER_ROOT_INPUT="${1:?Usage: $0 PATH_TO_SERVER_DIST}"
+SERVER_ROOT_INPUT="${1:?Usage: $0 PATH_TO_SERVER_DIST [SOURCE_ROOT]}"
+SOURCE_ROOT_INPUT="${2:-}"
 SERVER_ROOT=$(cd "$SERVER_ROOT_INPUT" && pwd)
 SERVER_SCRIPT="${SERVER_ROOT}/bin/hugegraph-server.sh"
 CONF="${SERVER_ROOT}/conf"
 SECURITY_PROPERTIES="${CONF}/java-security.properties"
+JVM_MODULE_OPTIONS="${SERVER_ROOT}/bin/jvm-module.options"
 
 fail() {
     echo "FAIL: $1" >&2
@@ -43,12 +45,136 @@ assert_no_argument() {
     fi
 }
 
+assert_source_consumer() {
+    local source_file="$1"
+    local expected="$2"
+    [[ -f "$source_file" ]] || fail "source consumer is missing: $source_file"
+    grep -Fq -- "$expected" "$source_file" ||
+        fail "JVM module options consumer is not wired: $source_file"
+}
+
+assert_surefire_arg_lines() {
+    local pom="$1"
+    local expected="$2"
+    local total
+    local wired
+    local jacoco_wired
+    read -r total wired jacoco_wired < <(
+        awk -v expected="$expected" '
+            /maven-surefire-plugin<\/artifactId>/ {
+                in_surefire = 1
+            }
+            in_surefire && /]*)?>/ {
+                in_arg_line = 1
+                arg_line = ""
+            }
+            in_arg_line {
+                arg_line = arg_line $0
+            }
+            in_arg_line && /<\/argLine>/ {
+                total++
+                if (index(arg_line, expected) != 0) {
+                    wired++
+                }
+                if (index(arg_line, "@{argLine}") != 0) {
+                    jacoco_wired++
+                }
+                in_arg_line = 0
+            }
+            in_surefire && /<\/plugin>/ {
+                in_surefire = 0
+            }
+            END {
+                print total + 0, wired + 0, jacoco_wired + 0
+            }
+        ' "$pom"
+    )
+    if [[ "$total" -eq 0 || "$wired" -ne "$total" ]]; then
+        fail "all Surefire argLine values must use jvm-module.options: $pom"
+    fi
+    if [[ "$jacoco_wired" -ne "$total" ]]; then
+        fail "all Surefire argLine values must preserve @{argLine}: $pom"
+    fi
+}
+
+assert_no_inline_module_options() {
+    local pattern
+    local source_file
+    pattern="--add-(exports|opens)([[:space:]]+|=)[\"']?java\\.base/|"
+    pattern="${pattern}--add-modules([[:space:]]+|=)[\"']?jdk\.unsupported"
+    for source_file in "$@"; do
+        [[ -f "$source_file" ]] || fail "source consumer is missing: $source_file"
+    done
+    if grep -En -- "$pattern" "$@"; then
+        fail "JVM module options must only be declared in jvm-module.options"
+    fi
+}
+
 if [[ ! -x "$SERVER_SCRIPT" ]]; then
     fail "server script is not executable: $SERVER_SCRIPT"
 fi
 if [[ ! -f "$SECURITY_PROPERTIES" ]]; then
     fail "security properties file is missing: $SECURITY_PROPERTIES"
 fi
+if [[ ! -f "$JVM_MODULE_OPTIONS" ]]; then
+    fail "JVM module options file is missing: $JVM_MODULE_OPTIONS"
+fi
+
+assert_argument "--add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED" \
+                "$JVM_MODULE_OPTIONS"
+assert_argument "--add-modules=jdk.unsupported" "$JVM_MODULE_OPTIONS"
+assert_argument "--add-exports=java.base/sun.nio.ch=ALL-UNNAMED" \
+                "$JVM_MODULE_OPTIONS"
+
+if [[ -n "$SOURCE_ROOT_INPUT" ]]; then
+    if [[ ! -d "$SOURCE_ROOT_INPUT" ]]; then
+        fail "source root is not a directory: $SOURCE_ROOT_INPUT"
+    fi
+    SOURCE_ROOT=$(cd "$SOURCE_ROOT_INPUT" && pwd)
+    SERVER_DIST_SOURCE="${SOURCE_ROOT}/hugegraph-server/hugegraph-dist"
+    CLUSTER_SOURCE="${SOURCE_ROOT}/hugegraph-cluster-test/"\
+"hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct"
+    SERVER_LAUNCHER_SOURCE="${SERVER_DIST_SOURCE}/src/assembly/static/bin/"\
+"hugegraph-server.sh"
+    INIT_STORE_SOURCE="${SERVER_DIST_SOURCE}/src/assembly/static/bin/init-store.sh"
+    SUREFIRE_POM="${SOURCE_ROOT}/hugegraph-server/hugegraph-test/pom.xml"
+    TEST_JVM_MODULE_OPTIONS="${SOURCE_ROOT}/hugegraph-server/hugegraph-test/"\
+"conf/jvm-test-module.options"
+    COMMONS_POM="${SOURCE_ROOT}/hugegraph-commons/pom.xml"
+    CLUSTER_WRAPPER="${CLUSTER_SOURCE}/node/ServerNodeWrapper.java"
+    SERVER_DOCKERFILE="${SOURCE_ROOT}/hugegraph-server/Dockerfile"
+    HSTORE_DOCKERFILE="${SOURCE_ROOT}/hugegraph-server/Dockerfile-hstore"
+    SERVER_WORKFLOW="${SOURCE_ROOT}/.github/workflows/server-ci.yml"
+    DOCKER_WORKFLOW="${SOURCE_ROOT}/.github/workflows/docker-build-ci.yml"
+    UPGRADE_CONTRACT_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/"\
+"test-java17-upgrade-contracts.sh"
+
+    [[ -x "$UPGRADE_CONTRACT_SCRIPT" ]] || \
+        fail "Java 17 upgrade contract script is missing: $UPGRADE_CONTRACT_SCRIPT"
+    "$UPGRADE_CONTRACT_SCRIPT" "$SERVER_ROOT" "$SOURCE_ROOT"
+
+    assert_source_consumer "$SERVER_LAUNCHER_SOURCE" '@"${JVM_MODULE_OPTIONS}"'
+    assert_source_consumer "$INIT_STORE_SOURCE" '@"${JVM_MODULE_OPTIONS}"'
+    assert_surefire_arg_lines "$SUREFIRE_POM" \
+        '@${project.basedir}/../hugegraph-dist/src/assembly/static/bin/jvm-module.options'
+    [[ -f "$TEST_JVM_MODULE_OPTIONS" ]] || \
+        fail "JVM test module options file is missing: $TEST_JVM_MODULE_OPTIONS"
+    assert_argument \
+        "--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED" \
+        "$TEST_JVM_MODULE_OPTIONS"
+    assert_argument "--add-opens=java.base/java.lang=ALL-UNNAMED" \
+                    "$TEST_JVM_MODULE_OPTIONS"
+    assert_surefire_arg_lines "$SUREFIRE_POM" \
+        '@${project.basedir}/conf/jvm-test-module.options'
+    assert_surefire_arg_lines "$COMMONS_POM" \
+        '@${project.parent.basedir}/../hugegraph-server/hugegraph-test/conf/jvm-test-module.options'
+    assert_source_consumer "$CLUSTER_WRAPPER" \
+        '"@" + Paths.get(SERVER_PACKAGE_PATH, BIN_DIR,'
+    assert_no_inline_module_options \
+        "$SERVER_LAUNCHER_SOURCE" "$INIT_STORE_SOURCE" "$SUREFIRE_POM" \
+        "$COMMONS_POM" "$CLUSTER_WRAPPER" "$SERVER_DOCKERFILE" \
+        "$HSTORE_DOCKERFILE" "$SERVER_WORKFLOW" "$DOCKER_WORKFLOW"
+fi
 
 if [[ -n "${JAVA_HOME:-}" ]]; then
     JAVA_BIN="${JAVA_HOME}/bin/java"
@@ -409,7 +535,7 @@ if [[ " $* " == *" -version "* ]]; then
     if [[ -n "${MOCK_JAVA_PREAMBLE:-}" ]]; then
         echo "${MOCK_JAVA_PREAMBLE}" >&2
     fi
-    echo "openjdk version \"${MOCK_JAVA_VERSION:-11}.0.0\"" >&2
+    echo "openjdk version \"${MOCK_JAVA_VERSION:-17}.0.0\"" >&2
     exit 0
 fi
 printf '%s\n' "$@" > "$CAPTURE_FILE"
@@ -425,6 +551,7 @@ CAPTURE_FILE="$ENABLED_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \
 
 assert_argument \
     "-Djava.security.properties=${SECURITY_PROPERTIES}" "$ENABLED_CAPTURE"
+assert_argument "@${JVM_MODULE_OPTIONS}" "$ENABLED_CAPTURE"
 assert_no_argument '^-Djava\.security\.manager=' "$ENABLED_CAPTURE"
 assert_argument \
     "org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap" "$ENABLED_CAPTURE"
@@ -503,10 +630,10 @@ assert_argument "-Djava.security.manager=allow" "$AGENT_JDK21_CAPTURE"
 # ... and trip the JDK 24+ security guard when the agent version is high.
 HIGH_AGENT_PREAMBLE=$'Picked up JAVA_TOOL_OPTIONS: -javaagent:apm-agent.jar\nAPM agent version "24.0.1" is starting'
 
-HIGH_AGENT_CAPTURE="${TEMP_DIR}/agent-preamble-jdk11.args"
-HIGH_AGENT_ERROR="${TEMP_DIR}/agent-preamble-jdk11.err"
+HIGH_AGENT_CAPTURE="${TEMP_DIR}/agent-preamble-jdk17.args"
+HIGH_AGENT_ERROR="${TEMP_DIR}/agent-preamble-jdk17.err"
 CAPTURE_FILE="$HIGH_AGENT_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \
-    MOCK_JAVA_VERSION=11 MOCK_JAVA_PREAMBLE="$HIGH_AGENT_PREAMBLE" \
+    MOCK_JAVA_VERSION=17 MOCK_JAVA_PREAMBLE="$HIGH_AGENT_PREAMBLE" \
     STDOUT_MODE=true "$SERVER_SCRIPT" \
     "${CONF}/gremlin-server.yaml" "${CONF}/rest-server.properties" true \
     >/dev/null 2>"$HIGH_AGENT_ERROR"
@@ -518,6 +645,15 @@ assert_argument \
     "org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap" "$HIGH_AGENT_CAPTURE"
 assert_no_argument '^-Djava\.security\.manager=' "$HIGH_AGENT_CAPTURE"
 
+JDK11_ERROR="${TEMP_DIR}/jdk11.err"
+if JAVA_HOME="$MOCK_JAVA_HOME" MOCK_JAVA_VERSION=11 STDOUT_MODE=true \
+   "$SERVER_SCRIPT" "${CONF}/gremlin-server.yaml" \
+   "${CONF}/rest-server.properties" false >/dev/null 2>"$JDK11_ERROR"; then
+    fail "launcher accepted a Java 11 runtime"
+fi
+grep -Fq "version >= 17, current is 11" "${SERVER_ROOT}/logs/hugegraph-server.log" ||
+    fail "launcher did not report the Java 17 minimum"
+
 JDK24_DISABLED_CAPTURE="${TEMP_DIR}/jdk24-disabled.args"
 CAPTURE_FILE="$JDK24_DISABLED_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \
     MOCK_JAVA_VERSION=24 STDOUT_MODE=true "$SERVER_SCRIPT" \
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java17-upgrade-contracts.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java17-upgrade-contracts.sh
new file mode 100755
index 0000000000..2727c7d460
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java17-upgrade-contracts.sh
@@ -0,0 +1,174 @@
+#!/bin/bash
+#
+# 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.
+
+set -euo pipefail
+
+SERVER_ROOT_INPUT="${1:?Usage: $0 PATH_TO_SERVER_DIST PATH_TO_SOURCE_ROOT}"
+SOURCE_ROOT_INPUT="${2:?Usage: $0 PATH_TO_SERVER_DIST PATH_TO_SOURCE_ROOT}"
+SERVER_ROOT=$(cd "$SERVER_ROOT_INPUT" && pwd)
+SOURCE_ROOT=$(cd "$SOURCE_ROOT_INPUT" && pwd)
+
+fail() {
+    echo "FAIL: $1" >&2
+    exit 1
+}
+
+assert_argument() {
+    local argument="$1"
+    local capture="$2"
+    grep -Fxq -- "$argument" "$capture" || \
+        fail "missing JVM argument: $argument"
+}
+
+assert_default_test_is_tolerant() {
+    local pom="$1"
+
+    python3 - "$pom" <<'PY'
+import sys
+import xml.etree.ElementTree as ET
+
+pom = sys.argv[1]
+namespace = {"m": "http://maven.apache.org/POM/4.0.0"}
+root = ET.parse(pom).getroot()
+value = root.find(
+    "m:properties/m:surefire.failIfNoSpecifiedTests", namespace
+)
+if value is None or (value.text or "").strip() != "false":
+    raise SystemExit(
+        "{}: default-test must tolerate -Dtest misses in reactor modules".format(pom)
+    )
+PY
+}
+
+assert_surefire_execution_scope() {
+    local pom="$1"
+    shift
+
+    python3 - "$pom" "$@" <<'PY'
+import sys
+import xml.etree.ElementTree as ET
+
+pom = sys.argv[1]
+expected_ids = sys.argv[2:]
+namespace = {"m": "http://maven.apache.org/POM/4.0.0"}
+root = ET.parse(pom).getroot()
+
+properties = root.find("m:properties", namespace)
+if properties is not None:
+    module_value = properties.find("m:surefire.failIfNoSpecifiedTests", namespace)
+    if module_value is not None and (module_value.text or "").strip() == "true":
+        raise SystemExit(
+            "{}: leaf-wide failIfNoSpecifiedTests=true breaks default-test".format(pom)
+        )
+
+surefire = None
+for plugin in root.findall("m:build/m:plugins/m:plugin", namespace):
+    artifact_id = plugin.find("m:artifactId", namespace)
+    if artifact_id is not None and artifact_id.text == "maven-surefire-plugin":
+        surefire = plugin
+        break
+
+if surefire is None:
+    raise SystemExit("{}: maven-surefire-plugin is missing".format(pom))
+
+strict_executions = set()
+for execution in surefire.findall("m:executions/m:execution", namespace):
+    execution_id = execution.find("m:id", namespace)
+    strict = execution.find("m:configuration/m:failIfNoSpecifiedTests", namespace)
+    if execution_id is None or strict is None:
+        continue
+    if (strict.text or "").strip() == "true":
+        strict_executions.add(execution_id.text)
+
+missing = sorted(set(expected_ids) - strict_executions)
+if missing:
+    raise SystemExit(
+        "{}: named Surefire executions are not strict: {}".format(
+            pom, ", ".join(missing)
+        )
+    )
+PY
+}
+
+assert_default_test_is_tolerant "${SOURCE_ROOT}/pom.xml"
+assert_surefire_execution_scope \
+    "${SOURCE_ROOT}/hugegraph-server/hugegraph-test/pom.xml" \
+    core-test unit-test api-test tinkerpop-structure-test tinkerpop-process-test
+assert_surefire_execution_scope \
+    "${SOURCE_ROOT}/hugegraph-pd/hg-pd-test/pom.xml" \
+    pd-client-test pd-core-test pd-common-test pd-rest-test
+assert_surefire_execution_scope \
+    "${SOURCE_ROOT}/hugegraph-store/hg-store-test/pom.xml" \
+    store-client-test store-core-test store-common-test store-rocksdb-test \
+    store-server-test store-raftcore-test
+
+MODULE_OPTIONS="${SERVER_ROOT}/bin/jvm-module.options"
+SERVER_SCRIPT="${SERVER_ROOT}/bin/hugegraph-server.sh"
+INIT_STORE_SCRIPT="${SERVER_ROOT}/bin/init-store.sh"
+UTIL_SCRIPT="${SERVER_ROOT}/bin/util.sh"
+CONF_SOURCE="${SERVER_ROOT}/conf"
+
+for source_file in "$MODULE_OPTIONS" "$SERVER_SCRIPT" \
+                   "$INIT_STORE_SCRIPT" "$UTIL_SCRIPT"; do
+    [[ -f "$source_file" ]] || fail "runtime asset is missing: $source_file"
+done
+[[ -d "$CONF_SOURCE" ]] || fail "server conf is missing: $CONF_SOURCE"
+
+TEMP_DIR=$(mktemp -d)
+trap 'rm -rf "$TEMP_DIR"' EXIT
+
+DIST_ROOT="${TEMP_DIR}/server"
+MOCK_JAVA_HOME="${TEMP_DIR}/mock-java-home"
+mkdir -p "${DIST_ROOT}"/{bin,conf,ext,lib,logs,plugins} \
+         "${MOCK_JAVA_HOME}/bin"
+cp "$MODULE_OPTIONS" "$SERVER_SCRIPT" "$INIT_STORE_SCRIPT" "$UTIL_SCRIPT" \
+   "${DIST_ROOT}/bin/"
+cp -R "${CONF_SOURCE}/." "${DIST_ROOT}/conf/"
+
+# Model a full pre-Phase-2 conf/ directory: it has no module argfile. Both
+# launchers must get the immutable runtime copy from bin/ instead.
+if [[ -e "${DIST_ROOT}/conf/jvm-module.options" ]]; then
+    fail "legacy conf unexpectedly contains jvm-module.options"
+fi
+
+cat > "${MOCK_JAVA_HOME}/bin/java" <<'MOCK'
+#!/bin/bash
+for argument in "$@"; do
+    if [[ "$argument" == "-version" ]]; then
+        echo 'openjdk version "17.0.0"' >&2
+        exit 0
+    fi
+done
+printf '%s\n' "$@" > "${CAPTURE_FILE:?}"
+MOCK
+chmod +x "${MOCK_JAVA_HOME}/bin/java" "${DIST_ROOT}/bin/hugegraph-server.sh" \
+         "${DIST_ROOT}/bin/init-store.sh"
+
+SERVER_CAPTURE="${TEMP_DIR}/server.args"
+CAPTURE_FILE="$SERVER_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \
+    JAVA_OPTIONS="-Xms512m -Xmx512m" STDOUT_MODE=true \
+    "${DIST_ROOT}/bin/hugegraph-server.sh" \
+    "${DIST_ROOT}/conf/gremlin-server.yaml" \
+    "${DIST_ROOT}/conf/rest-server.properties" false >/dev/null
+assert_argument "@${DIST_ROOT}/bin/jvm-module.options" "$SERVER_CAPTURE"
+
+INIT_STORE_CAPTURE="${TEMP_DIR}/init-store.args"
+CAPTURE_FILE="$INIT_STORE_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \
+    "${DIST_ROOT}/bin/init-store.sh" >/dev/null
+assert_argument "@${DIST_ROOT}/bin/jvm-module.options" "$INIT_STORE_CAPTURE"
+
+echo "PASS: Java 17 upgrade contracts"
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-riscv64-java-runtime.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-riscv64-java-runtime.sh
new file mode 100755
index 0000000000..aa3131543b
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-riscv64-java-runtime.sh
@@ -0,0 +1,155 @@
+#!/bin/bash
+#
+# 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.
+#
+set -euo pipefail
+
+TRAVIS_DIR=$(cd "$(dirname "$0")" && pwd)
+SMOKE_SCRIPT="$TRAVIS_DIR/run-rocksdb-jni-smoke-test.sh"
+NATIVE_SMOKE_SCRIPT="$TRAVIS_DIR/run-native-runtime-smoke-test.sh"
+WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/hugegraph-riscv64-java-test.XXXXXX")
+MOCK_BIN="$WORK_DIR/bin"
+MOCK_JAVA_HOME="$WORK_DIR/java-home"
+SERVER_DIR="$WORK_DIR/server"
+
+cleanup() {
+    rm -rf "$WORK_DIR"
+}
+trap cleanup EXIT
+
+mkdir -p "$MOCK_BIN" "$MOCK_JAVA_HOME/bin" "$SERVER_DIR/bin" "$SERVER_DIR/lib"
+
+cat > "$MOCK_BIN/uname" <<'EOF'
+#!/bin/bash
+echo riscv64
+EOF
+
+cat > "$MOCK_JAVA_HOME/bin/java" <<'EOF'
+#!/bin/bash
+set -euo pipefail
+
+JAVA_VERSION=${MOCK_JAVA_VERSION:-17.0.20}
+JAVA_VENDOR=${MOCK_JAVA_VENDOR:-Eclipse Adoptium}
+
+case "${1:-}" in
+    -version)
+        echo "openjdk version \"$JAVA_VERSION\"" >&2
+        ;;
+    -XshowSettings:properties)
+        echo "    java.vm.name = OpenJDK 64-Bit Server VM" >&2
+        echo "    java.vm.vendor = $JAVA_VENDOR" >&2
+        echo "    java.vm.version = $JAVA_VERSION+8" >&2
+        echo "    java.vm.info = mixed mode, sharing" >&2
+        echo "openjdk version \"$JAVA_VERSION\"" >&2
+        ;;
+    -cp)
+        echo "rocksdb-jni-smoke-ok"
+        ;;
+    *)
+        echo "Unexpected Java arguments: $*" >&2
+        exit 1
+        ;;
+esac
+EOF
+
+cat > "$SERVER_DIR/bin/util.sh" <<'EOF'
+#!/bin/bash
+configure_riscv64_libatomic() {
+    LD_PRELOAD=libatomic.so.1
+}
+EOF
+
+cat > "$SERVER_DIR/bin/init-store.sh" <<'EOF'
+#!/bin/bash
+exit 42
+EOF
+
+chmod +x "$MOCK_BIN/uname" "$MOCK_JAVA_HOME/bin/java" \
+         "$SERVER_DIR/bin/init-store.sh"
+
+run_smoke() {
+    env -u LD_PRELOAD \
+        PATH="$MOCK_BIN:$PATH" \
+        JAVA_HOME="$MOCK_JAVA_HOME" \
+        "$@" "$SMOKE_SCRIPT" "$SERVER_DIR"
+}
+
+run_native_smoke() {
+    env -u LD_PRELOAD \
+        PATH="$MOCK_BIN:$PATH" \
+        JAVA_HOME="$MOCK_JAVA_HOME" \
+        "$@" "$NATIVE_SMOKE_SCRIPT" "$SERVER_DIR"
+}
+
+if ! DEFAULT_OUTPUT=$(run_smoke 2>&1); then
+    echo "$DEFAULT_OUTPUT" >&2
+    echo "RISC-V smoke rejected the Java 17 baseline" >&2
+    exit 1
+fi
+grep -q '^rocksdb-jni-smoke-ok$' <<< "$DEFAULT_OUTPUT"
+
+set +e
+NATIVE_OUTPUT=$(run_native_smoke 2>&1)
+NATIVE_STATUS=$?
+set -e
+if [[ $NATIVE_STATUS -ne 42 ]]; then
+    echo "$NATIVE_OUTPUT" >&2
+    echo "Native smoke did not reach the controlled post-JNI boundary" >&2
+    exit 1
+fi
+grep -q '^rocksdb-jni-smoke-ok$' <<< "$NATIVE_OUTPUT"
+
+for JAVA_MAJOR_MISMATCH in 11.0.31 21.0.8; do
+    if MAJOR_OUTPUT=$(run_smoke \
+                      "MOCK_JAVA_VERSION=$JAVA_MAJOR_MISMATCH" 2>&1); then
+        echo "$MAJOR_OUTPUT" >&2
+        echo "RISC-V smoke accepted Java $JAVA_MAJOR_MISMATCH" >&2
+        exit 1
+    fi
+    grep -Fq "Expected Java 17, got $JAVA_MAJOR_MISMATCH" <<< "$MAJOR_OUTPUT"
+done
+
+EXPECTED_ARGS=(
+    EXPECTED_JAVA_MAJOR=17
+    EXPECTED_RISCV64_JAVA_VERSION=17.0.20
+    "EXPECTED_RISCV64_JAVA_VENDOR=Eclipse Adoptium"
+)
+if ! EXPECTED_OUTPUT=$(run_smoke "${EXPECTED_ARGS[@]}" 2>&1); then
+    echo "$EXPECTED_OUTPUT" >&2
+    echo "RISC-V smoke rejected the configured Temurin 17 runtime" >&2
+    exit 1
+fi
+grep -q '^rocksdb-jni-smoke-ok$' <<< "$EXPECTED_OUTPUT"
+
+if VERSION_OUTPUT=$(run_smoke "${EXPECTED_ARGS[@]}" \
+                    MOCK_JAVA_VERSION=17.0.21 2>&1); then
+    echo "$VERSION_OUTPUT" >&2
+    echo "RISC-V smoke accepted an unexpected Java version" >&2
+    exit 1
+fi
+grep -Fq 'Expected RISC-V Java 17.0.20, got 17.0.21' <<< "$VERSION_OUTPUT"
+
+if VENDOR_OUTPUT=$(run_smoke "${EXPECTED_ARGS[@]}" \
+                   "MOCK_JAVA_VENDOR=Unknown Vendor" 2>&1); then
+    echo "$VENDOR_OUTPUT" >&2
+    echo "RISC-V smoke accepted an unexpected Java vendor" >&2
+    exit 1
+fi
+grep -Fq \
+    'Expected RISC-V Java vendor Eclipse Adoptium, got Unknown Vendor' \
+    <<< "$VENDOR_OUTPUT"
+
+echo "PASS: RISC-V Java runtime contract"
diff --git a/hugegraph-server/hugegraph-test/conf/jvm-test-module.options b/hugegraph-server/hugegraph-test/conf/jvm-test-module.options
new file mode 100644
index 0000000000..ae2d02f3be
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/conf/jvm-test-module.options
@@ -0,0 +1,29 @@
+# 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.
+
+# Test-only access required by TinkerPop 3.7.6 and its Kryo dependencies on
+# Java 17. Keep these permissions out of the Server runtime argument file.
+--add-exports=java.base/sun.security.x509=ALL-UNNAMED
+--add-exports=java.base/sun.security.action=ALL-UNNAMED
+--add-opens=java.base/java.io=ALL-UNNAMED
+--add-opens=java.base/java.nio=ALL-UNNAMED
+--add-opens=java.base/sun.nio.cs=ALL-UNNAMED
+--add-opens=java.base/java.lang=ALL-UNNAMED
+--add-opens=java.base/java.lang.invoke=ALL-UNNAMED
+--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
+--add-opens=java.base/java.util=ALL-UNNAMED
+--add-opens=java.base/java.util.concurrent=ALL-UNNAMED
+--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED
+--add-opens=java.base/java.net=ALL-UNNAMED
diff --git a/hugegraph-server/hugegraph-test/pom.xml b/hugegraph-server/hugegraph-test/pom.xml
index 443fdd7162..4bd8ff69ff 100644
--- a/hugegraph-server/hugegraph-test/pom.xml
+++ b/hugegraph-server/hugegraph-test/pom.xml
@@ -114,11 +114,18 @@
             
                 org.apache.maven.plugins
                 maven-surefire-plugin
-                2.20
+                
+                    
+                        @{argLine}
+                        @${project.basedir}/../hugegraph-dist/src/assembly/static/bin/jvm-module.options
+                        @${project.basedir}/conf/jvm-test-module.options
+                    
+                
                 
                     
                         core-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -131,6 +138,7 @@
                     
                         unit-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -143,6 +151,7 @@
                     
                         api-test
                         
+                            true
                             
@@ -161,6 +170,7 @@
                     
                         tinkerpop-structure-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -173,8 +183,14 @@
                     
                         tinkerpop-process-test
                         
+                            true
                             
-                            -Dbuild.dir=${project.build.directory}
+                            
+                                @{argLine}
+                                @${project.basedir}/../hugegraph-dist/src/assembly/static/bin/jvm-module.options
+                                @${project.basedir}/conf/jvm-test-module.options
+                                -Dbuild.dir=${project.build.directory}
+                            
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -212,7 +228,6 @@
             
                 org.jacoco
                 jacoco-maven-plugin
-                0.8.8
                 
                     
                         org/apache/hugegraph/traversal/algorithm/*.class
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java
index 1b209c9139..c080668594 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java
@@ -21,6 +21,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import org.apache.hugegraph.HugeException;
 import org.apache.hugegraph.HugeGraph;
 import org.apache.hugegraph.auth.AuthManager;
 import org.apache.hugegraph.auth.HugeAuthenticator;
@@ -35,6 +36,7 @@
 import org.apache.hugegraph.task.TaskScheduler;
 import org.apache.hugegraph.testutil.Assert;
 import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.hugegraph.util.Reflection;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.core.Filter;
@@ -50,6 +52,43 @@
 
 public class HugeGraphAuthProxyTest extends BaseUnitTest {
 
+    @Test
+    public void testJdk17ReflectionFilters() {
+        Reflection.registerFieldsToFilter(ReflectionFilterTarget.class, "field");
+        Reflection.registerMethodsToFilter(ReflectionFilterTarget.class, "method");
+
+        Assert.assertThrows(NoSuchFieldException.class,
+                            () -> ReflectionFilterTarget.class.getDeclaredField("field"));
+        Assert.assertThrows(NoSuchMethodException.class,
+                            () -> ReflectionFilterTarget.class.getDeclaredMethod("method"));
+        Assert.assertThrows(IllegalArgumentException.class,
+                            () -> Reflection.registerFieldsToFilter(
+                                    ReflectionFilterTarget.class, "field"));
+    }
+
+    @Test
+    public void testJdk17ReflectionFilterFailureCause() {
+        Throwable exception = Assert.assertThrows(
+                HugeException.class,
+                () -> Reflection.registerFieldsToFilter(
+                        ReflectionFailureTarget.class, (String) null));
+
+        Assert.assertInstanceOf(NullPointerException.class, exception.getCause());
+    }
+
+    private static class ReflectionFailureTarget {
+    }
+
+    private static class ReflectionFilterTarget {
+
+        @SuppressWarnings("unused")
+        private String field;
+
+        @SuppressWarnings("unused")
+        private void method() {
+        }
+    }
+
     private static HugeGraphAuthProxy.Context setContext(
             HugeGraphAuthProxy.Context context) {
         try {
diff --git a/hugegraph-store/AGENTS.md b/hugegraph-store/AGENTS.md
index 8b5ef46bab..c5d47c0295 100644
--- a/hugegraph-store/AGENTS.md
+++ b/hugegraph-store/AGENTS.md
@@ -7,7 +7,7 @@ This file provides guidance to an AI coding tool when working with code in this
 HugeGraph Store is a distributed storage backend for Apache HugeGraph, using RocksDB as the underlying storage engine with Raft consensus protocol for distributed coordination. It is designed for production-scale deployments requiring high availability and horizontal scalability.
 
 **Technology Stack**:
-- Java 11+
+- Java 17+
 - RocksDB: Embedded key-value storage engine
 - Raft (JRaft): Distributed consensus protocol
 - gRPC: Inter-node communication
diff --git a/hugegraph-store/Dockerfile b/hugegraph-store/Dockerfile
index f1b9db5fd4..f79f2c49e6 100644
--- a/hugegraph-store/Dockerfile
+++ b/hugegraph-store/Dockerfile
@@ -18,7 +18,7 @@
 
 # Dockerfile for HugeGraph Store
 # 1st stage: build source code
-FROM --platform=$BUILDPLATFORM maven:3.9.0-eclipse-temurin-11 AS build
+FROM --platform=$BUILDPLATFORM maven:3.9.16-eclipse-temurin-17 AS build
 
 WORKDIR /pkg
 
@@ -32,7 +32,7 @@ RUN --mount=type=cache,target=/root/.m2 \
 
 # 2nd stage: runtime env
 # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13
-FROM eclipse-temurin:11-jre-jammy
+FROM eclipse-temurin:17-jre-jammy
 
 COPY --from=build /pkg/hugegraph-store/apache-hugegraph-store-*/ /hugegraph-store/
 LABEL maintainer="HugeGraph Docker Maintainers "
diff --git a/hugegraph-store/README.md b/hugegraph-store/README.md
index 4e6bc8aca2..881feccc7e 100644
--- a/hugegraph-store/README.md
+++ b/hugegraph-store/README.md
@@ -23,7 +23,7 @@ HugeGraph Store is a distributed storage backend for HugeGraph that provides hig
 - **Storage Engine**: RocksDB 7.7.3 (optimized for graph workloads)
 - **Consensus Protocol**: Apache JRaft (Ant Financial's Raft implementation)
 - **RPC Framework**: gRPC + Protocol Buffers
-- **Deployment**: Java 11+, Docker/Kubernetes support
+- **Deployment**: Java 17+, Docker/Kubernetes support
 
 ### When to Use HugeGraph Store
 
@@ -87,8 +87,8 @@ For detailed architecture, Raft consensus mechanisms, and partition management,
 
 ### Prerequisites
 
-- **Java**: 11 or higher
-- **Maven**: 3.5 or higher
+- **Java**: 17 or higher
+- **Maven**: 3.6.3 or higher
 - **HugeGraph PD Cluster**: Store requires a running PD cluster for metadata coordination (see [PD README](../hugegraph-pd/README.md))
 - **Disk Space**: At least 10GB per Store node for data and Raft logs
 - **Network**: Low-latency network (<5ms) between Store nodes for Raft consensus
diff --git a/hugegraph-store/docs/deployment-guide.md b/hugegraph-store/docs/deployment-guide.md
index de07904d64..7cc9ecbccd 100644
--- a/hugegraph-store/docs/deployment-guide.md
+++ b/hugegraph-store/docs/deployment-guide.md
@@ -384,7 +384,7 @@ graph.name=hugegraph
 **On all nodes**:
 
 ```bash
-# Check Java version (11+ required)
+# Check Java version (17+ required)
 java -version
 
 # Check Maven (for building from source)
diff --git a/hugegraph-store/docs/development-guide.md b/hugegraph-store/docs/development-guide.md
index 44136776e7..a02f686312 100644
--- a/hugegraph-store/docs/development-guide.md
+++ b/hugegraph-store/docs/development-guide.md
@@ -18,8 +18,8 @@ Comprehensive guide for developing, testing, and contributing to HugeGraph Store
 ### Prerequisites
 
 **Required**:
-- Java: 11 or higher (OpenJDK or Oracle JDK)
-- Maven: 3.5 or higher
+- Java: 17 or higher (OpenJDK or Oracle JDK)
+- Maven: 3.6.3 or higher
 - Git: Latest version
 - IDE: IntelliJ IDEA (recommended) or Eclipse
 
@@ -303,12 +303,12 @@ mvn test -P store-core-test
 
 **Specific test class**:
 ```bash
-mvn test -Dtest=HgStoreEngineTest
+mvn test -pl hg-store-test -am -Dtest=HgStoreEngineTest
 ```
 
 **Specific test method**:
 ```bash
-mvn test -Dtest=HgStoreEngineTest#testPartitionCreation
+mvn test -pl hg-store-test -am -Dtest=HgStoreEngineTest#testPartitionCreation
 ```
 
 **From IntelliJ**:
diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh
index 88165e47b6..2609112493 100755
--- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh
+++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh
@@ -122,7 +122,7 @@ ensure_path_writable "$PLUGINS"
 # The maximum and minimum heap memory that service can use (for production env set it 36GB)
 MAX_MEM=$((2 * 1024))
 MIN_MEM=$((1 * 512))
-EXPECT_JDK_VERSION=11
+EXPECT_JDK_VERSION=17
 
 # Change to $BIN's parent
 cd ${TOP} || exit
@@ -135,8 +135,11 @@ else
 fi
 
 # check jdk version
-JAVA_VERSION=$($JAVA -version 2>&1 | awk 'NR==1{gsub(/"/,""); print $3}'  | awk -F'_' '{print $1}')
-if [[ $? -ne 0 || $JAVA_VERSION < $EXPECT_JDK_VERSION ]]; then
+JAVA_VERSION=$($JAVA -version 2>&1 |
+               awk -F'"' '/^(java|openjdk) version "/ {print $2; exit}' |
+               sed 's/^1\.//' | cut -d'.' -f1)
+JAVA_VERSION="${JAVA_VERSION%%[!0-9]*}"
+if [[ -z $JAVA_VERSION || $JAVA_VERSION -lt $EXPECT_JDK_VERSION ]]; then
     echo "Please make sure that the JDK is installed and the version >= $EXPECT_JDK_VERSION"  >> ${OUTPUT}
     exit 1
 fi
@@ -164,7 +167,7 @@ case "$GC_OPTION" in
                       -XX:InitiatingHeapOccupancyPercent=50 -XX:G1RSetUpdatingPauseTimePercent=5"
         ;;
     zgc|ZGC)
-        echo "Using ZGC as the default garbage collector (Only support Java 11+)"
+        echo "Using ZGC as the default garbage collector (Only support Java 17+)"
         JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseZGC -XX:+UnlockExperimentalVMOptions \
                                       -XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \
                                       -XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \
diff --git a/hugegraph-store/hg-store-test/pom.xml b/hugegraph-store/hg-store-test/pom.xml
index 5afcd8551f..7f2b2c1da9 100644
--- a/hugegraph-store/hg-store-test/pom.xml
+++ b/hugegraph-store/hg-store-test/pom.xml
@@ -44,7 +44,6 @@
                     
                         org.jacoco
                         jacoco-maven-plugin
-                        0.8.4
 
                         
                             
@@ -213,11 +212,18 @@
             
                 org.apache.maven.plugins
                 maven-surefire-plugin
-                2.20
+                
+                    
+                        org.apache.maven.surefire
+                        surefire-junit4
+                        ${maven.surefire.plugin.version}
+                    
+                
                 
                     
                         store-client-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -230,6 +236,7 @@
                     
                         store-core-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -244,6 +251,7 @@
                     
                         store-common-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -256,6 +264,7 @@
                     
                         store-rocksdb-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -268,6 +277,7 @@
                     
                         store-server-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -283,6 +293,7 @@
                     
                         store-raftcore-test
                         
+                            true
                             ${basedir}/src/main/java/
                             
                             ${basedir}/target/classes/
@@ -297,7 +308,6 @@
             
                 org.jacoco
                 jacoco-maven-plugin
-                0.8.4
                 
                     
                         pre-test
diff --git a/hugegraph-store/pom.xml b/hugegraph-store/pom.xml
index 9ff1e933e5..d8f730a4e0 100644
--- a/hugegraph-store/pom.xml
+++ b/hugegraph-store/pom.xml
@@ -98,7 +98,6 @@
             
                 org.jacoco
                 jacoco-maven-plugin
-                0.8.4
                 
                     
                         
@@ -168,7 +167,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 store-client-test
@@ -192,7 +190,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 store-core-test
@@ -216,7 +213,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 store-common-test
@@ -240,7 +236,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 store-rocksdb-test
@@ -264,7 +259,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 store-server-test
@@ -288,7 +282,6 @@
                     
                         org.apache.maven.plugins
                         maven-surefire-plugin
-                        2.20
                         
                             
                                 store-raftcore-test
diff --git a/hugegraph-struct/AGENTS.md b/hugegraph-struct/AGENTS.md
index dea4f06288..4a7bbb789f 100644
--- a/hugegraph-struct/AGENTS.md
+++ b/hugegraph-struct/AGENTS.md
@@ -168,7 +168,7 @@ Query classes provide backend-agnostic query building:
 
 - **hg-pd-client** (${project.version}): PD client for metadata coordination
 - **hugegraph-common** (${project.version}): Shared utilities
-- **Apache TinkerPop 3.5.1**: Graph computing framework
+- **Apache TinkerPop 3.7.6**: Graph computing framework
 - **Guava 25.1-jre**: Google utilities
 - **Eclipse Collections 10.4.0**: High-performance collections
 - **fastutil 8.1.0**: Fast primitive collections
diff --git a/hugegraph-struct/pom.xml b/hugegraph-struct/pom.xml
index 78404f939b..58b4320c3d 100644
--- a/hugegraph-struct/pom.xml
+++ b/hugegraph-struct/pom.xml
@@ -30,8 +30,6 @@
     
 
     
-        11
-        11
         UTF-8
         25.1-jre
         3.7.6
diff --git a/pom.xml b/pom.xml
index 02d45999e2..40be0a4316 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,8 +92,16 @@
         1.28
         1.18.30
         hugegraph
-        11
-        11
+        17
+        3.15.0
+        3.5.6
+        
+        false
+        0.8.15
+        3.6.2
+        
         UTF-8
         bash
         1.5.0
@@ -239,35 +247,13 @@
                 
                     org.apache.maven.plugins
                     maven-enforcer-plugin
-                    
-                        
-                            enforce-version
-                            
-                                enforce
-                            
-                            
-                            false
-                            
-                                
-                                    
-                                    
-                                    
-                                        [11,)
-                                    
-                                    
-                                        [3.5.0,)
-                                    
-                                
-                            
-                        
-                    
+                    ${maven.enforcer.plugin.version}
                 
                 
                     maven-compiler-plugin
-                    3.1
+                    ${maven.compiler.plugin.version}
                     
-                        ${maven.compiler.source}
-                        ${maven.compiler.target}
+                        ${maven.compiler.release}
                         
                             500
                         
@@ -276,6 +262,21 @@
                         
                     
                 
+                
+                    org.apache.maven.plugins
+                    maven-surefire-plugin
+                    ${maven.surefire.plugin.version}
+                    
+                        
+                            ${surefire.failIfNoSpecifiedTests}
+                        
+                    
+                
+                
+                    org.jacoco
+                    jacoco-maven-plugin
+                    ${jacoco.maven.plugin.version}
+                
                 
                     maven-remote-resources-plugin
                     3.3.0
@@ -288,6 +289,30 @@
         
 
         
+            
+                org.apache.maven.plugins
+                maven-enforcer-plugin
+                
+                    
+                        enforce-version
+                        
+                            enforce
+                        
+                        
+                            
+                                
+                                
+                                
+                                    [17,)
+                                
+                                
+                                    [3.6.3,)
+                                
+                            
+                        
+                    
+                
+            
             
                 org.apache.rat
                 apache-rat-plugin

From 6d9066b1135c987daac9a02cbda978f186a87fa2 Mon Sep 17 00:00:00 2001
From: contrueCT 
Date: Thu, 13 Aug 2026 00:29:40 +0800
Subject: [PATCH 4/4] chore: set all modules runtime to Java 17 only (#194)

---
 .../ecosystem_and_related_projects.md         |  2 +-
 .serena/memories/project_overview.md          |  2 +-
 AGENTS.md                                     |  3 +-
 BUILDING.md                                   |  6 ++-
 README.md                                     |  8 +++-
 hugegraph-commons/AGENTS.md                   |  4 +-
 hugegraph-pd/AGENTS.md                        |  2 +-
 hugegraph-pd/README.md                        |  2 +-
 hugegraph-pd/docs/architecture.md             |  2 +-
 hugegraph-pd/docs/development.md              |  8 ++--
 .../assembly/static/bin/start-hugegraph-pd.sh |  2 +-
 hugegraph-server/AGENTS.md                    |  3 +-
 .../assembly/static/bin/hugegraph-server.sh   |  2 +-
 .../travis/test-java17-upgrade-contracts.sh   | 46 +++++++++++++++++++
 hugegraph-store/AGENTS.md                     |  2 +-
 hugegraph-store/README.md                     |  4 +-
 hugegraph-store/docs/deployment-guide.md      |  2 +-
 hugegraph-store/docs/development-guide.md     |  2 +-
 .../static/bin/start-hugegraph-store.sh       |  2 +-
 pom.xml                                       |  5 +-
 20 files changed, 84 insertions(+), 25 deletions(-)

diff --git a/.serena/memories/ecosystem_and_related_projects.md b/.serena/memories/ecosystem_and_related_projects.md
index c88b6734b5..cc7635df52 100644
--- a/.serena/memories/ecosystem_and_related_projects.md
+++ b/.serena/memories/ecosystem_and_related_projects.md
@@ -20,4 +20,4 @@ Sources → hugegraph-loader → hugegraph-server → Hubble / Computer / AI
 - Queries: Gremlin (TinkerPop 3.7.6), OpenCypher, REST API + Swagger UI
 - Storage: RocksDB (default), HStore (distributed)
 
-## Version: Server 1.7.0, TinkerPop 3.7.6, Java 17+
+## Version: Server 1.7.0, TinkerPop 3.7.6, Java 17
diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md
index da53c143fd..2a709f2da2 100644
--- a/.serena/memories/project_overview.md
+++ b/.serena/memories/project_overview.md
@@ -14,7 +14,7 @@ Apache HugeGraph is a fast-speed, highly-scalable graph database supporting 10+
 - Integration with Flink/Spark/HDFS
 
 ## Technology Stack
-- **Language**: Java 17+ (required)
+- **Language**: Java 17 (required; currently supported release)
 - **Build**: Maven 3.6.3+
 - **Graph Framework**: Apache TinkerPop 3.7.6
 - **RPC**: gRPC + Protocol Buffers
diff --git a/AGENTS.md b/AGENTS.md
index 512222b6e7..da73b6818e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,7 +6,8 @@ README.md covers human-facing deployment/ecosystem context; only consult it on d
 ## Stack & Modules
 
 Apache HugeGraph — Apache TinkerPop 3 compliant graph database.
-Java 17+, Maven 3.6.3+. Version managed via `${revision}` (currently `1.7.0`).
+Java 17 (currently supported release), Maven 3.6.3+. Version managed via
+`${revision}` (currently `1.7.0`).
 
 ```
 Client (Gremlin / Cypher / REST)
diff --git a/BUILDING.md b/BUILDING.md
index b34c4bf796..ced2086d3a 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -3,9 +3,13 @@ Building HugeGraph
 
 Required:
 
-* Java 17+
+* Java 17 (currently the only supported Java release)
 * Maven 3.6.3+
 
+The launch scripts enforce Java 17 as the minimum runtime. This check does not
+qualify later Java releases; build HugeGraph with Java 17 unless another
+release is explicitly listed as supported.
+
 To build without executing tests: `mvn clean package -Dmaven.test.skip=true`
 
 ## Building in IDEA
diff --git a/README.md b/README.md
index a5dc1dfe78..76bdf49701 100644
--- a/README.md
+++ b/README.md
@@ -176,9 +176,13 @@ curl -X POST http://localhost:8080/gremlin \
 
 ### Prerequisites
 
-- **Java 17+** (required)
+- **Java 17** (required and currently the only supported Java release)
 - **Maven 3.6.3+** (for building from source)
 
+The launch scripts reject Java versions older than 17. That minimum-version
+check does not qualify later Java releases; use Java 17 unless another release
+is explicitly listed as supported.
+
 ### Option 1: Docker (Fastest)
 
 Docker is the quickest way to get started for **testing or development**:
@@ -327,7 +331,7 @@ For detailed architecture and development guidance, see [AGENTS.md](AGENTS.md).
    - Review the [Architecture Diagram](#architecture) above
 
 2. **Set Up Your Environment**
-   - Install Java 17+ and Maven 3.6.3+
+   - Install Java 17 and Maven 3.6.3+
    - Follow [BUILDING.md](BUILDING.md) for build instructions
    - Configure your IDE to use `.editorconfig` for code style and `style/checkstyle.xml` for Checkstyle rules
 
diff --git a/hugegraph-commons/AGENTS.md b/hugegraph-commons/AGENTS.md
index 94afcb6359..a218e85b0c 100644
--- a/hugegraph-commons/AGENTS.md
+++ b/hugegraph-commons/AGENTS.md
@@ -7,7 +7,7 @@ This file provides guidance to an AI coding tool when working with code in this
 hugegraph-commons is a shared utility module for Apache HugeGraph and its peripheral components. It provides core infrastructure components (locks, config, events, iterators, REST client, RPC framework) to simplify development across the HugeGraph ecosystem.
 
 **Technology Stack**:
-- Java 17+ (compiler release: 17)
+- Java 17 (currently supported release; compiler release: 17)
 - Apache Maven 3.6.3+
 - Apache Commons Configuration2 for config management
 - OkHttp 4.10.0 for REST client (hugegraph-common)
@@ -71,7 +71,7 @@ This is a Maven multi-module project with 2 main modules:
 
 ### Prerequisites
 ```bash
-# Verify Java version (17+ required)
+# Verify Java version (Java 17 required)
 java -version
 
 # Verify Maven version (3.6.3+ required)
diff --git a/hugegraph-pd/AGENTS.md b/hugegraph-pd/AGENTS.md
index f31c055310..acdd0ec15f 100644
--- a/hugegraph-pd/AGENTS.md
+++ b/hugegraph-pd/AGENTS.md
@@ -11,7 +11,7 @@ HugeGraph PD (Placement Driver) is a meta server for distributed HugeGraph deplo
 - Metadata coordination using Raft consensus
 
 **Technology Stack**:
-- Java 17+ (required)
+- Java 17 (currently supported release; required)
 - Apache Maven 3.6.3+
 - gRPC + Protocol Buffers for RPC communication
 - JRaft (Ant Design's Raft implementation) for consensus
diff --git a/hugegraph-pd/README.md b/hugegraph-pd/README.md
index 74359183ea..4bc5fbef90 100644
--- a/hugegraph-pd/README.md
+++ b/hugegraph-pd/README.md
@@ -36,7 +36,7 @@ For detailed architecture and design, see [Architecture Documentation](docs/arch
 
 ### Prerequisites
 
-- **Java**: 17 or higher
+- **Java**: 17 (currently the only supported release)
 - **Maven**: 3.6.3 or higher
 - **Disk Space**: At least 1GB for PD data directory
 
diff --git a/hugegraph-pd/docs/architecture.md b/hugegraph-pd/docs/architecture.md
index e5d2dad7dd..6fd56dd212 100644
--- a/hugegraph-pd/docs/architecture.md
+++ b/hugegraph-pd/docs/architecture.md
@@ -54,7 +54,7 @@ HugeGraph PD (Placement Driver) is the control plane for HugeGraph distributed d
 - **Storage**: RocksDB for persistent metadata
 - **Communication**: gRPC with Protocol Buffers
 - **Framework**: Spring Boot for REST APIs and dependency injection
-- **Language**: Java 17+
+- **Language**: Java 17 (currently supported release)
 
 ## Module Architecture
 
diff --git a/hugegraph-pd/docs/development.md b/hugegraph-pd/docs/development.md
index e15e6d1dea..30f710c95b 100644
--- a/hugegraph-pd/docs/development.md
+++ b/hugegraph-pd/docs/development.md
@@ -18,10 +18,10 @@ This document provides comprehensive guidance for developing, testing, and contr
 
 Ensure you have the following tools installed:
 
-| Tool | Minimum Version | Recommended | Purpose |
+| Tool | Version Requirement | Recommended | Purpose |
 |------|----------------|-------------|---------|
-| **JDK** | 17 | 17 LTS | Java runtime and compilation |
-| **Maven** | 3.6.3 | 3.8+ | Build tool and dependency management |
+| **JDK** | 17 (only supported release) | 17 LTS | Java runtime and compilation |
+| **Maven** | 3.6.3+ | 3.8+ | Build tool and dependency management |
 | **Git** | 2.0+ | Latest | Version control |
 | **IDE** | N/A | IntelliJ IDEA | Development environment |
 
@@ -30,7 +30,7 @@ Ensure you have the following tools installed:
 ```bash
 # Check Java version
 java -version
-# Expected: openjdk version "17.0.x" or later
+# Expected: openjdk version "17.0.x"
 
 # Check Maven version
 mvn -version
diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh
index 636f7ba0fe..82a92dc9f9 100755
--- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh
+++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh
@@ -114,7 +114,7 @@ case "$GC_OPTION" in
                       -XX:InitiatingHeapOccupancyPercent=50 -XX:G1RSetUpdatingPauseTimePercent=5"
         ;;
     zgc|ZGC)
-        echo "Using ZGC as the default garbage collector (Only support Java 17+)"
+        echo "Using ZGC as the default garbage collector (requires Java 17 or later)"
         JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseZGC -XX:+UnlockExperimentalVMOptions \
                                       -XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \
                                       -XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \
diff --git a/hugegraph-server/AGENTS.md b/hugegraph-server/AGENTS.md
index a2d51daabe..bf1767a126 100644
--- a/hugegraph-server/AGENTS.md
+++ b/hugegraph-server/AGENTS.md
@@ -10,7 +10,8 @@ HugeGraph Server is the graph engine layer of Apache HugeGraph, consisting of:
 - **Backend Interface**: Abstraction layer for pluggable storage backends
 - **Storage Backend Implementations**: RocksDB (default), HStore (distributed), HBase (deprecated; planned for removal in 2.0), and Memory (test-only)
 
-Technology: Java 17+, Maven 3.6.3+, Apache TinkerPop 3.7.6, Jersey 3.0 (REST), gRPC (distributed communication)
+Technology: Java 17 (currently supported release), Maven 3.6.3+,
+Apache TinkerPop 3.7.6, Jersey 3.0 (REST), gRPC (distributed communication)
 
 ## Build Commands
 
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
index 8ec31db4f6..2c93aa085c 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
@@ -144,7 +144,7 @@ case "$GC_OPTION" in
                                       -XX:G1RSetUpdatingPauseTimePercent=5"
         ;;
     zgc|ZGC)
-        echo "Using ZGC as the default garbage collector (Only support Java 17+)"
+        echo "Using ZGC as the default garbage collector (requires Java 17 or later)"
         JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseZGC -XX:+UnlockExperimentalVMOptions \
                                       -XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \
                                       -XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java17-upgrade-contracts.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java17-upgrade-contracts.sh
index 2727c7d460..b371003713 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java17-upgrade-contracts.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java17-upgrade-contracts.sh
@@ -54,6 +54,51 @@ if value is None or (value.text or "").strip() != "false":
 PY
 }
 
+assert_supported_java_contract() {
+    local pom="$1"
+
+    python3 - "$pom" <<'PY'
+import sys
+import xml.etree.ElementTree as ET
+
+pom = sys.argv[1]
+namespace = {"m": "http://maven.apache.org/POM/4.0.0"}
+root = ET.parse(pom).getroot()
+properties = root.find("m:properties", namespace)
+if properties is None:
+    raise SystemExit("{}: Maven properties are missing".format(pom))
+
+release = properties.find("m:maven.compiler.release", namespace)
+if release is None or (release.text or "").strip() != "17":
+    raise SystemExit("{}: compiler release must remain 17".format(pom))
+
+supported_range = properties.find("m:java.supported.version.range", namespace)
+if supported_range is None or (supported_range.text or "").strip() != "[17,18)":
+    raise SystemExit("{}: supported JDK range must be [17,18)".format(pom))
+
+expected_reference = "${java.supported.version.range}"
+actual_references = []
+for plugin in root.findall("m:build/m:plugins/m:plugin", namespace):
+    artifact_id = plugin.find("m:artifactId", namespace)
+    if artifact_id is None or artifact_id.text != "maven-enforcer-plugin":
+        continue
+    for rule in plugin.findall(
+        "m:executions/m:execution/m:configuration/m:rules/m:requireJavaVersion",
+        namespace,
+    ):
+        version = rule.find("m:version", namespace)
+        if version is not None:
+            actual_references.append((version.text or "").strip())
+
+if actual_references != [expected_reference]:
+    raise SystemExit(
+        "{}: requireJavaVersion must consume {} exactly once; found {}".format(
+            pom, expected_reference, actual_references
+        )
+    )
+PY
+}
+
 assert_surefire_execution_scope() {
     local pom="$1"
     shift
@@ -104,6 +149,7 @@ if missing:
 PY
 }
 
+assert_supported_java_contract "${SOURCE_ROOT}/pom.xml"
 assert_default_test_is_tolerant "${SOURCE_ROOT}/pom.xml"
 assert_surefire_execution_scope \
     "${SOURCE_ROOT}/hugegraph-server/hugegraph-test/pom.xml" \
diff --git a/hugegraph-store/AGENTS.md b/hugegraph-store/AGENTS.md
index c5d47c0295..432b8023ae 100644
--- a/hugegraph-store/AGENTS.md
+++ b/hugegraph-store/AGENTS.md
@@ -7,7 +7,7 @@ This file provides guidance to an AI coding tool when working with code in this
 HugeGraph Store is a distributed storage backend for Apache HugeGraph, using RocksDB as the underlying storage engine with Raft consensus protocol for distributed coordination. It is designed for production-scale deployments requiring high availability and horizontal scalability.
 
 **Technology Stack**:
-- Java 17+
+- Java 17 (currently supported release)
 - RocksDB: Embedded key-value storage engine
 - Raft (JRaft): Distributed consensus protocol
 - gRPC: Inter-node communication
diff --git a/hugegraph-store/README.md b/hugegraph-store/README.md
index 881feccc7e..24e091283c 100644
--- a/hugegraph-store/README.md
+++ b/hugegraph-store/README.md
@@ -23,7 +23,7 @@ HugeGraph Store is a distributed storage backend for HugeGraph that provides hig
 - **Storage Engine**: RocksDB 7.7.3 (optimized for graph workloads)
 - **Consensus Protocol**: Apache JRaft (Ant Financial's Raft implementation)
 - **RPC Framework**: gRPC + Protocol Buffers
-- **Deployment**: Java 17+, Docker/Kubernetes support
+- **Deployment**: Java 17 (currently supported release), Docker/Kubernetes support
 
 ### When to Use HugeGraph Store
 
@@ -87,7 +87,7 @@ For detailed architecture, Raft consensus mechanisms, and partition management,
 
 ### Prerequisites
 
-- **Java**: 17 or higher
+- **Java**: 17 (currently the only supported release)
 - **Maven**: 3.6.3 or higher
 - **HugeGraph PD Cluster**: Store requires a running PD cluster for metadata coordination (see [PD README](../hugegraph-pd/README.md))
 - **Disk Space**: At least 10GB per Store node for data and Raft logs
diff --git a/hugegraph-store/docs/deployment-guide.md b/hugegraph-store/docs/deployment-guide.md
index 7cc9ecbccd..9e148be70a 100644
--- a/hugegraph-store/docs/deployment-guide.md
+++ b/hugegraph-store/docs/deployment-guide.md
@@ -384,7 +384,7 @@ graph.name=hugegraph
 **On all nodes**:
 
 ```bash
-# Check Java version (17+ required)
+# Check Java version (Java 17 is the only currently supported release)
 java -version
 
 # Check Maven (for building from source)
diff --git a/hugegraph-store/docs/development-guide.md b/hugegraph-store/docs/development-guide.md
index a02f686312..bdc36e07ef 100644
--- a/hugegraph-store/docs/development-guide.md
+++ b/hugegraph-store/docs/development-guide.md
@@ -18,7 +18,7 @@ Comprehensive guide for developing, testing, and contributing to HugeGraph Store
 ### Prerequisites
 
 **Required**:
-- Java: 17 or higher (OpenJDK or Oracle JDK)
+- Java: 17 (currently the only supported release; OpenJDK or Oracle JDK)
 - Maven: 3.6.3 or higher
 - Git: Latest version
 - IDE: IntelliJ IDEA (recommended) or Eclipse
diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh
index 2609112493..15006bb777 100755
--- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh
+++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh
@@ -167,7 +167,7 @@ case "$GC_OPTION" in
                       -XX:InitiatingHeapOccupancyPercent=50 -XX:G1RSetUpdatingPauseTimePercent=5"
         ;;
     zgc|ZGC)
-        echo "Using ZGC as the default garbage collector (Only support Java 17+)"
+        echo "Using ZGC as the default garbage collector (requires Java 17 or later)"
         JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseZGC -XX:+UnlockExperimentalVMOptions \
                                       -XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \
                                       -XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \
diff --git a/pom.xml b/pom.xml
index 40be0a4316..ac246f9ed7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -93,6 +93,9 @@
         1.18.30
         hugegraph
         17
+        
+        [17,18)
         3.15.0
         3.5.6
         
                                 
                                 
-                                    [17,)
+                                    ${java.supported.version.range}
                                 
                                 
                                     [3.6.3,)