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

JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new 2f57fd6270a Speed up AINode CI by consolidating tests and caching 
PyInstaller output (#17687)
2f57fd6270a is described below

commit 2f57fd6270a14fd7601b95f6071bab30f6957835
Author: Jackie Tien <[email protected]>
AuthorDate: Sun May 17 08:20:45 2026 +0800

    Speed up AINode CI by consolidating tests and caching PyInstaller output 
(#17687)
---
 CLAUDE.md                                          |   1 +
 .../iotdb/ainode/it/AINodeCallInferenceIT.java     | 135 -----
 .../iotdb/ainode/it/AINodeClusterConfigIT.java     |  71 ++-
 .../iotdb/ainode/it/AINodeDeviceManageIT.java      |  96 ----
 .../apache/iotdb/ainode/it/AINodeForecastIT.java   | 195 --------
 .../ainode/it/AINodeInstanceManagementIT.java      | 179 -------
 .../iotdb/ainode/it/AINodeModelManageIT.java       | 215 --------
 .../iotdb/ainode/it/AINodeSharedClusterIT.java     | 552 +++++++++++++++++++++
 iotdb-core/ainode/build_binary.py                  | 115 ++++-
 9 files changed, 711 insertions(+), 848 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 897731068d5..01cc03fb7db 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -180,6 +180,7 @@ The project uses compile-time i18n via the 
`build-helper-maven-plugin`. The prop
 ### Code Style
 
 - **Always run `mvn spotless:apply` after editing Java files**: Spotless runs 
`spotless:check` automatically during the `compile` phase. Format violations 
cause an immediate BUILD FAILURE. Make it a habit to run `mvn spotless:apply 
-pl <module>` right after editing, not at the end. For files under 
`integration-test/`, add `-P with-integration-tests`.
+- **Always run `black` and `isort` after editing Python files under 
`iotdb-core/ainode/`**: The AINode Code Style Check CI runs `black --check .` 
and `isort --check-only --profile black .` on that directory. Run `cd 
iotdb-core/ainode && black . && isort --profile black .` before committing. 
Requires `pip install black==25.1.0 isort==6.0.1`.
 - **Gson version compatibility**: `JsonObject.isEmpty()` / 
`JsonArray.isEmpty()` may not be available in the Gson version used by this 
project. Use `size() > 0` instead and add a comment explaining why.
 
 ## Git Commit
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeCallInferenceIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeCallInferenceIT.java
deleted file mode 100644
index 852827aa8ab..00000000000
--- 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeCallInferenceIT.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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.iotdb.ainode.it;
-
-import org.apache.iotdb.ainode.utils.AINodeTestUtils;
-import org.apache.iotdb.it.env.EnvFactory;
-import org.apache.iotdb.it.framework.IoTDBTestRunner;
-import org.apache.iotdb.itbase.category.AIClusterIT;
-import org.apache.iotdb.itbase.env.BaseEnv;
-
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.runner.RunWith;
-
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.BUILTIN_MODEL_MAP;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.checkHeader;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.errorTest;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.prepareDataInTree;
-
-@RunWith(IoTDBTestRunner.class)
-@Category({AIClusterIT.class})
-public class AINodeCallInferenceIT {
-
-  private static final String CALL_INFERENCE_SQL_TEMPLATE =
-      "CALL INFERENCE(%s, \"SELECT s%d FROM root.AI LIMIT %d\", 
generateTime=true, outputLength=%d)";
-  private static final String CALL_INFERENCE_BY_DEFAULT_SQL_TEMPLATE =
-      "CALL INFERENCE(%s, \"SELECT s%d FROM root.AI LIMIT 256\")";
-  private static final int DEFAULT_INPUT_LENGTH = 256;
-  private static final int DEFAULT_OUTPUT_LENGTH = 48;
-
-  @BeforeClass
-  public static void setUp() throws Exception {
-    // Init 1C1D1A cluster environment
-    EnvFactory.getEnv().initClusterEnvironment(1, 1);
-    prepareDataInTree();
-  }
-
-  @AfterClass
-  public static void tearDown() throws Exception {
-    EnvFactory.getEnv().cleanClusterEnvironment();
-  }
-
-  @Test
-  public void callInferenceTest() throws SQLException {
-    for (AINodeTestUtils.FakeModelInfo modelInfo : BUILTIN_MODEL_MAP.values()) 
{
-      try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
-          Statement statement = connection.createStatement()) {
-        callInferenceTest(statement, modelInfo);
-        callInferenceByDefaultTest(statement, modelInfo);
-        callInferenceErrorTest(statement, modelInfo);
-      }
-    }
-  }
-
-  public static void callInferenceTest(Statement statement, 
AINodeTestUtils.FakeModelInfo modelInfo)
-      throws SQLException {
-    // Invoke call inference for specified models, there should exist result.
-    for (int i = 0; i < 4; i++) {
-      String callInferenceSQL =
-          String.format(
-              CALL_INFERENCE_SQL_TEMPLATE,
-              modelInfo.getModelId(),
-              i,
-              DEFAULT_INPUT_LENGTH,
-              DEFAULT_OUTPUT_LENGTH);
-      try (ResultSet resultSet = statement.executeQuery(callInferenceSQL)) {
-        ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
-        checkHeader(resultSetMetaData, "Time,output");
-        int count = 0;
-        while (resultSet.next()) {
-          count++;
-        }
-        // Ensure the call inference return results
-        Assert.assertEquals(DEFAULT_OUTPUT_LENGTH, count);
-      }
-    }
-  }
-
-  public static void callInferenceByDefaultTest(
-      Statement statement, AINodeTestUtils.FakeModelInfo modelInfo) throws 
SQLException {
-    // Invoke call inference for specified models, there should exist result.
-    for (int i = 0; i < 4; i++) {
-      String callInferenceSQL =
-          String.format(CALL_INFERENCE_BY_DEFAULT_SQL_TEMPLATE, 
modelInfo.getModelId(), i);
-      try (ResultSet resultSet = statement.executeQuery(callInferenceSQL)) {
-        ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
-        checkHeader(resultSetMetaData, "output");
-        int count = 0;
-        while (resultSet.next()) {
-          count++;
-        }
-        // Ensure the call inference return results
-        Assert.assertTrue(count > 0);
-      }
-    }
-  }
-
-  public static void callInferenceErrorTest(
-      Statement statement, AINodeTestUtils.FakeModelInfo modelInfo) {
-    String multiVariateSQL =
-        String.format(
-            "CALL INFERENCE(%s, \"SELECT s0,s1 FROM root.AI LIMIT 128\", 
generateTime=true, outputLength=10)",
-            modelInfo.getModelId());
-    errorTest(
-        statement,
-        multiVariateSQL,
-        "701: Call inference function should not contain more than one input 
column, found [2] input columns.");
-  }
-}
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeClusterConfigIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeClusterConfigIT.java
index e148be6b20a..de6d3c48be3 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeClusterConfigIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeClusterConfigIT.java
@@ -24,16 +24,15 @@ import org.apache.iotdb.it.framework.IoTDBTestRunner;
 import org.apache.iotdb.itbase.category.AIClusterIT;
 import org.apache.iotdb.itbase.env.BaseEnv;
 
-import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Assert;
-import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
 import org.junit.runner.RunWith;
 
 import java.sql.Connection;
 import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
 import java.sql.Statement;
 
@@ -44,39 +43,56 @@ import static org.junit.Assert.assertEquals;
 @Category({AIClusterIT.class})
 public class AINodeClusterConfigIT {
 
-  @Before
-  public void setUp() throws Exception {
-    // Init 1C1D1A cluster environment
+  @BeforeClass
+  public static void setUp() throws Exception {
     EnvFactory.getEnv().initClusterEnvironment(1, 1);
   }
 
-  @After
-  public void tearDown() throws Exception {
+  @AfterClass
+  public static void tearDown() throws Exception {
     EnvFactory.getEnv().cleanClusterEnvironment();
   }
 
   @Test
-  public void aiNodeRegisterAndRemoveTestInTree() throws SQLException {
+  public void aiNodeRegisterAndRemoveTest() throws SQLException {
+    String show_sql = "SHOW AINODES";
+    String title = "NodeID,Status,InternalAddress,InternalPort";
+
+    // Verify AINode exists via both dialects before removal
     try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
         Statement statement = connection.createStatement()) {
-      aiNodeRegisterAndRemoveTest(statement);
+      verifyAINodeExists(statement, show_sql, title);
+    }
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      verifyAINodeExists(statement, show_sql, title);
     }
-  }
 
-  @Test
-  public void aiNodeRegisterAndRemoveTestInTable() throws SQLException {
+    // Remove AINode
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      statement.execute("REMOVE AINODE");
+      waitForAINodeRemoval(statement, show_sql, title);
+    }
+
+    // Verify removal is visible via table dialect as well
     try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
         Statement statement = connection.createStatement()) {
-      aiNodeRegisterAndRemoveTest(statement);
+      try (ResultSet resultSet = statement.executeQuery(show_sql)) {
+        checkHeader(resultSet.getMetaData(), title);
+        int count = 0;
+        while (resultSet.next()) {
+          count++;
+        }
+        assertEquals(0, count);
+      }
     }
   }
 
-  private void aiNodeRegisterAndRemoveTest(Statement statement) throws 
SQLException {
-    String show_sql = "SHOW AINODES";
-    String title = "NodeID,Status,InternalAddress,InternalPort";
-    try (ResultSet resultSet = statement.executeQuery(show_sql)) {
-      ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
-      checkHeader(resultSetMetaData, title);
+  private static void verifyAINodeExists(Statement statement, String showSql, 
String title)
+      throws SQLException {
+    try (ResultSet resultSet = statement.executeQuery(showSql)) {
+      checkHeader(resultSet.getMetaData(), title);
       int count = 0;
       while (resultSet.next()) {
         assertEquals("2", resultSet.getString(1));
@@ -85,22 +101,23 @@ public class AINodeClusterConfigIT {
       }
       assertEquals(1, count);
     }
-    String remove_sql = "REMOVE AINODE";
-    statement.execute(remove_sql);
+  }
+
+  private static void waitForAINodeRemoval(Statement statement, String 
showSql, String title)
+      throws SQLException {
     for (int retry = 0; retry < 500; retry++) {
-      try (ResultSet resultSet = statement.executeQuery(show_sql)) {
-        ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
-        checkHeader(resultSetMetaData, title);
+      try (ResultSet resultSet = statement.executeQuery(showSql)) {
+        checkHeader(resultSet.getMetaData(), title);
         int count = 0;
         while (resultSet.next()) {
           count++;
         }
         if (count == 0) {
-          return; // Successfully removed the AI node
+          return;
         }
       }
       try {
-        Thread.sleep(1000); // Wait before retrying
+        Thread.sleep(1000);
       } catch (InterruptedException e) {
         Thread.currentThread().interrupt();
       }
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeDeviceManageIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeDeviceManageIT.java
deleted file mode 100644
index bbffd3cffb0..00000000000
--- 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeDeviceManageIT.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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.iotdb.ainode.it;
-
-import org.apache.iotdb.it.env.EnvFactory;
-import org.apache.iotdb.it.framework.IoTDBTestRunner;
-import org.apache.iotdb.itbase.category.AIClusterIT;
-import org.apache.iotdb.itbase.env.BaseEnv;
-
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.runner.RunWith;
-
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.List;
-
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.checkHeader;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.prepareDataInTable;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.prepareDataInTree;
-
-@RunWith(IoTDBTestRunner.class)
-@Category({AIClusterIT.class})
-public class AINodeDeviceManageIT {
-
-  @BeforeClass
-  public static void setUp() throws Exception {
-    // Init 1C1D1A cluster environment
-    EnvFactory.getEnv().initClusterEnvironment(1, 1);
-    prepareDataInTree();
-    prepareDataInTable();
-  }
-
-  @AfterClass
-  public static void tearDown() throws Exception {
-    EnvFactory.getEnv().cleanClusterEnvironment();
-  }
-
-  @Test
-  public void showAIDeviceTestInTree() throws SQLException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      showAIDevicesTest(statement);
-    }
-  }
-
-  @Test
-  public void showAIDeviceTestInTable() throws SQLException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      showAIDevicesTest(statement);
-    }
-  }
-
-  private void showAIDevicesTest(Statement statement) throws SQLException {
-    final String showSql = "SHOW AI_DEVICES";
-    final List<String> expectedDeviceIdList = new 
LinkedList<>(Arrays.asList("0", "1", "cpu"));
-    final List<String> expectedDeviceTypeList =
-        new LinkedList<>(Arrays.asList("cuda", "cuda", "cpu"));
-    try (ResultSet resultSet = statement.executeQuery(showSql)) {
-      ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
-      checkHeader(resultSetMetaData, "DeviceId,DeviceType");
-      while (resultSet.next()) {
-        String deviceId = resultSet.getString(1);
-        String deviceType = resultSet.getString(2);
-        Assert.assertEquals(expectedDeviceIdList.remove(0), deviceId);
-        Assert.assertEquals(expectedDeviceTypeList.remove(0), deviceType);
-      }
-    }
-  }
-}
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeForecastIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeForecastIT.java
deleted file mode 100644
index eb4a981389e..00000000000
--- 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeForecastIT.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * 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.iotdb.ainode.it;
-
-import org.apache.iotdb.ainode.utils.AINodeTestUtils;
-import org.apache.iotdb.it.env.EnvFactory;
-import org.apache.iotdb.it.framework.IoTDBTestRunner;
-import org.apache.iotdb.itbase.category.AIClusterIT;
-import org.apache.iotdb.itbase.env.BaseEnv;
-
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.runner.RunWith;
-
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.BUILTIN_MODEL_MAP;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.errorTest;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.prepareDataInTable;
-
-@RunWith(IoTDBTestRunner.class)
-@Category({AIClusterIT.class})
-public class AINodeForecastIT {
-
-  private static final String FORECAST_TABLE_FUNCTION_SQL_TEMPLATE =
-      "SELECT * FROM FORECAST("
-          + "model_id=>'%s', "
-          + "targets=>(SELECT time, s%d FROM db.AI WHERE time<%d ORDER BY time 
DESC LIMIT %d) ORDER BY time, "
-          + "output_start_time=>%d, "
-          + "output_length=>%d, "
-          + "output_interval=>%d, "
-          + "timecol=>'%s'"
-          + ")";
-
-  @BeforeClass
-  public static void setUp() throws Exception {
-    // Init 1C1D1A cluster environment
-    EnvFactory.getEnv().initClusterEnvironment(1, 1);
-    prepareDataInTable();
-  }
-
-  @AfterClass
-  public static void tearDown() throws Exception {
-    EnvFactory.getEnv().cleanClusterEnvironment();
-  }
-
-  @Test
-  public void forecastTableFunctionTest() throws SQLException {
-    for (AINodeTestUtils.FakeModelInfo modelInfo : BUILTIN_MODEL_MAP.values()) 
{
-      try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
-          Statement statement = connection.createStatement()) {
-        forecastTableFunctionTest(statement, modelInfo);
-      }
-    }
-  }
-
-  public static void forecastTableFunctionTest(
-      Statement statement, AINodeTestUtils.FakeModelInfo modelInfo) throws 
SQLException {
-    // Invoke forecast table function for specified models, there should exist 
result.
-    for (int i = 0; i < 4; i++) {
-      String forecastTableFunctionSQL =
-          String.format(
-              FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
-              modelInfo.getModelId(),
-              i,
-              5760,
-              2880,
-              5760,
-              96,
-              1,
-              "time");
-      try (ResultSet resultSet = 
statement.executeQuery(forecastTableFunctionSQL)) {
-        int count = 0;
-        while (resultSet.next()) {
-          count++;
-        }
-        // Ensure the forecast sentence return results
-        Assert.assertTrue(count > 0);
-      }
-    }
-  }
-
-  @Test
-  public void forecastTableFunctionErrorTest() throws SQLException {
-    for (AINodeTestUtils.FakeModelInfo modelInfo : BUILTIN_MODEL_MAP.values()) 
{
-      try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
-          Statement statement = connection.createStatement()) {
-        forecastTableFunctionErrorTest(statement, modelInfo);
-      }
-    }
-  }
-
-  public static void forecastTableFunctionErrorTest(
-      Statement statement, AINodeTestUtils.FakeModelInfo modelInfo) throws 
SQLException {
-    // OUTPUT_START_TIME error
-    String invalidOutputStartTimeSQL =
-        String.format(
-            FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
-            modelInfo.getModelId(),
-            0,
-            5760,
-            2880,
-            5759,
-            96,
-            1,
-            "time");
-    errorTest(
-        statement,
-        invalidOutputStartTimeSQL,
-        "701: The OUTPUT_START_TIME should be greater than the maximum 
timestamp of target time series. Expected greater than [5759] but found 
[5759].");
-
-    // OUTPUT_LENGTH error
-    String invalidOutputLengthSQLWithZero =
-        String.format(
-            FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
-            modelInfo.getModelId(),
-            0,
-            5760,
-            2880,
-            5760,
-            0,
-            1,
-            "time");
-    errorTest(
-        statement, invalidOutputLengthSQLWithZero, "701: OUTPUT_LENGTH should 
be greater than 0");
-
-    String invalidOutputLengthSQLWithOutOfRange =
-        String.format(
-            FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
-            modelInfo.getModelId(),
-            0,
-            5760,
-            2880,
-            5760,
-            2881,
-            1,
-            "time");
-    errorTest(
-        statement,
-        invalidOutputLengthSQLWithOutOfRange,
-        "1599: Error occurred while executing forecast:[Attribute 
output_length expect value between 1 and 2880, got 2881 instead.]");
-
-    // OUTPUT_INTERVAL error
-    String invalidOutputIntervalSQL =
-        String.format(
-            FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
-            modelInfo.getModelId(),
-            0,
-            5760,
-            2880,
-            5760,
-            96,
-            -1,
-            "time");
-    errorTest(statement, invalidOutputIntervalSQL, "701: OUTPUT_INTERVAL 
should be greater than 0");
-
-    // TIMECOL error
-    String invalidTimecolSQL2 =
-        String.format(
-            FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
-            modelInfo.getModelId(),
-            0,
-            5760,
-            2880,
-            5760,
-            96,
-            1,
-            "s0");
-    errorTest(
-        statement, invalidTimecolSQL2, "701: The type of the column [s0] is 
not as expected.");
-  }
-}
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeInstanceManagementIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeInstanceManagementIT.java
deleted file mode 100644
index 8356a055311..00000000000
--- 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeInstanceManagementIT.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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.iotdb.ainode.it;
-
-import org.apache.iotdb.it.env.EnvFactory;
-import org.apache.iotdb.it.framework.IoTDBTestRunner;
-import org.apache.iotdb.itbase.category.AIClusterIT;
-import org.apache.iotdb.itbase.env.BaseEnv;
-
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.runner.RunWith;
-
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.checkHeader;
-import static 
org.apache.iotdb.ainode.utils.AINodeTestUtils.checkModelNotOnSpecifiedDevice;
-import static 
org.apache.iotdb.ainode.utils.AINodeTestUtils.checkModelOnSpecifiedDevice;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.errorTest;
-
-@RunWith(IoTDBTestRunner.class)
-@Category({AIClusterIT.class})
-public class AINodeInstanceManagementIT {
-
-  private static final String TARGET_DEVICES_STR = "0,1";
-  private static final Set<String> TARGET_DEVICES =
-      new HashSet<>(Arrays.asList(TARGET_DEVICES_STR.split(",")));
-
-  @BeforeClass
-  public static void setUp() throws Exception {
-    // Init 1C1D1A cluster environment
-    EnvFactory.getEnv().initClusterEnvironment(1, 1);
-  }
-
-  @AfterClass
-  public static void tearDown() throws Exception {
-    EnvFactory.getEnv().cleanClusterEnvironment();
-  }
-
-  @Test
-  public void basicManagementTestInTreeModel() throws SQLException, 
InterruptedException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      basicManagementTest(statement);
-    }
-  }
-
-  @Test
-  public void basicManagementTestInTableModel() throws SQLException, 
InterruptedException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      basicManagementTest(statement);
-    }
-  }
-
-  private void basicManagementTest(Statement statement) throws SQLException, 
InterruptedException {
-    // Ensure resources
-    try (ResultSet resultSet = statement.executeQuery("SHOW AI_DEVICES")) {
-      ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
-      checkHeader(resultSetMetaData, "DeviceId,DeviceType");
-      final Set<String> resultDevices = new HashSet<>();
-      while (resultSet.next()) {
-        resultDevices.add(resultSet.getString("DeviceId"));
-      }
-      Set<String> expected = new HashSet<>(TARGET_DEVICES);
-      expected.add("cpu");
-      Assert.assertEquals(expected, resultDevices);
-    }
-
-    // Load sundial to each device
-    statement.execute(String.format("LOAD MODEL sundial TO DEVICES '%s'", 
TARGET_DEVICES_STR));
-    checkModelOnSpecifiedDevice(statement, "sundial", TARGET_DEVICES_STR);
-    // Unload sundial from each device
-    statement.execute(String.format("UNLOAD MODEL sundial FROM DEVICES '%s'", 
TARGET_DEVICES_STR));
-    checkModelNotOnSpecifiedDevice(statement, "sundial", TARGET_DEVICES_STR);
-
-    // Load timer_xl to each device
-    statement.execute(String.format("LOAD MODEL timer_xl TO DEVICES '%s'", 
TARGET_DEVICES_STR));
-    checkModelOnSpecifiedDevice(statement, "timer_xl", TARGET_DEVICES_STR);
-    // Unload timer_xl from each device
-    statement.execute(String.format("UNLOAD MODEL timer_xl FROM DEVICES '%s'", 
TARGET_DEVICES_STR));
-    checkModelNotOnSpecifiedDevice(statement, "timer_xl", TARGET_DEVICES_STR);
-  }
-
-  private static final int LOOP_CNT = 10;
-
-  //  @Test
-  public void repeatLoadAndUnloadTest() throws SQLException, 
InterruptedException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      for (int i = 0; i < LOOP_CNT; i++) {
-        statement.execute(String.format("LOAD MODEL sundial TO DEVICES '%s'", 
TARGET_DEVICES_STR));
-        checkModelOnSpecifiedDevice(statement, "sundial", TARGET_DEVICES_STR);
-        statement.execute(
-            String.format("UNLOAD MODEL sundial FROM DEVICES '%s'", 
TARGET_DEVICES_STR));
-        checkModelNotOnSpecifiedDevice(statement, "sundial", 
TARGET_DEVICES_STR);
-      }
-    }
-  }
-
-  //  @Test
-  public void concurrentLoadAndUnloadTest() throws SQLException, 
InterruptedException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      for (int i = 0; i < LOOP_CNT; i++) {
-        statement.execute(String.format("LOAD MODEL sundial TO DEVICES '%s'", 
TARGET_DEVICES_STR));
-        statement.execute(
-            String.format("UNLOAD MODEL sundial FROM DEVICES '%s'", 
TARGET_DEVICES_STR));
-      }
-      checkModelNotOnSpecifiedDevice(statement, "sundial", TARGET_DEVICES_STR);
-    }
-  }
-
-  @Test
-  public void failTestInTreeModel() throws SQLException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      failTest(statement);
-    }
-  }
-
-  @Test
-  public void failTestInTableModel() throws SQLException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      failTest(statement);
-    }
-  }
-
-  private void failTest(Statement statement) {
-    errorTest(
-        statement,
-        "LOAD MODEL unknown TO DEVICES 'cpu,0,1'",
-        "1504: Model [unknown] is not registered yet. You can use 'SHOW 
MODELS' to retrieve the available models.");
-    errorTest(
-        statement,
-        "LOAD MODEL sundial TO DEVICES '999'",
-        "1508: AIDevice ID [999] is not available. You can use 'SHOW 
AI_DEVICES' to retrieve the available devices.");
-    errorTest(
-        statement,
-        "UNLOAD MODEL sundial FROM DEVICES '999'",
-        "1508: AIDevice ID [999] is not available. You can use 'SHOW 
AI_DEVICES' to retrieve the available devices.");
-    errorTest(
-        statement,
-        "LOAD MODEL sundial TO DEVICES '0,0'",
-        "1509: Device ID list contains duplicate entries.");
-    errorTest(
-        statement,
-        "UNLOAD MODEL sundial FROM DEVICES '0,0'",
-        "1510: Device ID list contains duplicate entries.");
-  }
-}
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeModelManageIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeModelManageIT.java
deleted file mode 100644
index 42dc8db520e..00000000000
--- 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeModelManageIT.java
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * 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.iotdb.ainode.it;
-
-import org.apache.iotdb.ainode.utils.AINodeTestUtils;
-import org.apache.iotdb.ainode.utils.AINodeTestUtils.FakeModelInfo;
-import org.apache.iotdb.it.env.EnvFactory;
-import org.apache.iotdb.it.framework.IoTDBTestRunner;
-import org.apache.iotdb.itbase.category.AIClusterIT;
-import org.apache.iotdb.itbase.env.BaseEnv;
-
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.runner.RunWith;
-
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.concurrent.TimeUnit;
-
-import static 
org.apache.iotdb.ainode.it.AINodeCallInferenceIT.callInferenceTest;
-import static 
org.apache.iotdb.ainode.it.AINodeForecastIT.forecastTableFunctionTest;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.checkHeader;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.errorTest;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.prepareDataInTable;
-import static org.apache.iotdb.ainode.utils.AINodeTestUtils.prepareDataInTree;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-@RunWith(IoTDBTestRunner.class)
-@Category({AIClusterIT.class})
-public class AINodeModelManageIT {
-
-  @BeforeClass
-  public static void setUp() throws Exception {
-    // Init 1C1D1A cluster environment
-    EnvFactory.getEnv().initClusterEnvironment(1, 1);
-    prepareDataInTree();
-    prepareDataInTable();
-  }
-
-  @AfterClass
-  public static void tearDown() throws Exception {
-    EnvFactory.getEnv().cleanClusterEnvironment();
-  }
-
-  @Test
-  public void userDefinedModelManagementTestInTree() throws SQLException, 
InterruptedException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      // Test transformers model (chronos2) in tree.
-      AINodeTestUtils.FakeModelInfo modelInfo =
-          new FakeModelInfo("user_chronos", "custom_t5", "user_defined", 
"active");
-      registerUserDefinedModel(statement, modelInfo, "file:///data/chronos2");
-      callInferenceTest(statement, modelInfo);
-      dropUserDefinedModel(statement, modelInfo.getModelId());
-
-      // Test PytorchModelHubMixin model (mantis) in tree.
-      modelInfo = new FakeModelInfo("user_mantis", "custom_mantis", 
"user_defined", "active");
-      registerUserDefinedModel(statement, modelInfo, "file:///data/mantis");
-      dropUserDefinedModel(statement, modelInfo.getModelId());
-    }
-  }
-
-  @Test
-  public void userDefinedModelManagementTestInTable() throws SQLException, 
InterruptedException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      // Test transformers model (chronos2) in table.
-      AINodeTestUtils.FakeModelInfo modelInfo =
-          new FakeModelInfo("user_chronos", "custom_t5", "user_defined", 
"active");
-      registerUserDefinedModel(statement, modelInfo, "file:///data/chronos2");
-      forecastTableFunctionTest(statement, modelInfo);
-      dropUserDefinedModel(statement, modelInfo.getModelId());
-
-      // Test PytorchModelHubMixin model (mantis) in table.
-      modelInfo = new FakeModelInfo("user_mantis", "custom_mantis", 
"user_defined", "active");
-      registerUserDefinedModel(statement, modelInfo, "file:///data/mantis");
-      dropUserDefinedModel(statement, modelInfo.getModelId());
-    }
-  }
-
-  public static void registerUserDefinedModel(
-      Statement statement, AINodeTestUtils.FakeModelInfo modelInfo, String uri)
-      throws SQLException, InterruptedException {
-    String modelId = modelInfo.getModelId();
-    String modelType = modelInfo.getModelType();
-    String category = modelInfo.getCategory();
-    final String CREATE_MODEL_TEMPLATE = "create model %s using uri \"%s\"";
-    final String alterConfigSQL = "set configuration 
\"trusted_uri_pattern\"='.*'";
-    final String registerSql = String.format(CREATE_MODEL_TEMPLATE, modelId, 
uri);
-    final String showSql = String.format("SHOW MODELS %s", modelId);
-    statement.execute(alterConfigSQL);
-    statement.execute(registerSql);
-    boolean loading = true;
-    for (int retryCnt = 0; retryCnt < 100; retryCnt++) {
-      try (ResultSet resultSet = statement.executeQuery(showSql)) {
-        ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
-        checkHeader(resultSetMetaData, "ModelId,ModelType,Category,State");
-        while (resultSet.next()) {
-          String resultModelId = resultSet.getString(1);
-          String resultModelType = resultSet.getString(2);
-          String resultCategory = resultSet.getString(3);
-          String state = resultSet.getString(4);
-          assertEquals(modelId, resultModelId);
-          assertEquals(modelType, resultModelType);
-          assertEquals(category, resultCategory);
-          if (state.equals("active")) {
-            loading = false;
-          } else if (state.equals("loading")) {
-            break;
-          } else {
-            fail("Unexpected status of model: " + state);
-          }
-        }
-      }
-      if (!loading) {
-        break; // Model is loaded successfully
-      }
-      TimeUnit.SECONDS.sleep(1);
-    }
-    assertFalse(loading);
-  }
-
-  public static void dropUserDefinedModel(Statement statement, String modelId) 
throws SQLException {
-    final String showSql = String.format("SHOW MODELS %s", modelId);
-    final String dropSql = String.format("DROP MODEL %s", modelId);
-    statement.execute(dropSql);
-    try (ResultSet resultSet = statement.executeQuery(showSql)) {
-      ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
-      checkHeader(resultSetMetaData, "ModelId,ModelType,Category,State");
-      int count = 0;
-      while (resultSet.next()) {
-        count++;
-      }
-      assertEquals(0, count);
-    }
-  }
-
-  @Test
-  public void dropBuiltInModelErrorTestInTree() throws SQLException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      errorTest(statement, "drop model sundial", "1506: Cannot delete built-in 
model: sundial");
-    }
-  }
-
-  @Test
-  public void dropBuiltInModelErrorTestInTable() throws SQLException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      errorTest(statement, "drop model sundial", "1506: Cannot delete built-in 
model: sundial");
-    }
-  }
-
-  @Test
-  public void showBuiltInModelTestInTree() throws SQLException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
-        Statement statement = connection.createStatement()) {
-      showBuiltInModelTest(statement);
-    }
-  }
-
-  @Test
-  public void showBuiltInModelTestInTable() throws SQLException {
-    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
-        Statement statement = connection.createStatement(); ) {
-      showBuiltInModelTest(statement);
-    }
-  }
-
-  private void showBuiltInModelTest(Statement statement) throws SQLException {
-    int built_in_model_count = 0;
-    final String showSql = "SHOW MODELS";
-    try (ResultSet resultSet = statement.executeQuery(showSql)) {
-      ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
-      checkHeader(resultSetMetaData, "ModelId,ModelType,Category,State");
-      while (resultSet.next()) {
-        built_in_model_count++;
-        FakeModelInfo modelInfo =
-            new FakeModelInfo(
-                resultSet.getString(1),
-                resultSet.getString(2),
-                resultSet.getString(3),
-                resultSet.getString(4));
-        
assertTrue(AINodeTestUtils.BUILTIN_MODEL_MAP.containsKey(modelInfo.getModelId()));
-        
assertEquals(AINodeTestUtils.BUILTIN_MODEL_MAP.get(modelInfo.getModelId()), 
modelInfo);
-      }
-    }
-    assertEquals(AINodeTestUtils.BUILTIN_MODEL_MAP.size(), 
built_in_model_count);
-  }
-}
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeSharedClusterIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeSharedClusterIT.java
new file mode 100644
index 00000000000..7a71682f167
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/ainode/it/AINodeSharedClusterIT.java
@@ -0,0 +1,552 @@
+/*
+ * 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.iotdb.ainode.it;
+
+import org.apache.iotdb.ainode.utils.AINodeTestUtils;
+import org.apache.iotdb.ainode.utils.AINodeTestUtils.FakeModelInfo;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.AIClusterIT;
+import org.apache.iotdb.itbase.env.BaseEnv;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.iotdb.ainode.utils.AINodeTestUtils.BUILTIN_MODEL_MAP;
+import static org.apache.iotdb.ainode.utils.AINodeTestUtils.checkHeader;
+import static 
org.apache.iotdb.ainode.utils.AINodeTestUtils.checkModelNotOnSpecifiedDevice;
+import static 
org.apache.iotdb.ainode.utils.AINodeTestUtils.checkModelOnSpecifiedDevice;
+import static org.apache.iotdb.ainode.utils.AINodeTestUtils.errorTest;
+import static org.apache.iotdb.ainode.utils.AINodeTestUtils.prepareDataInTable;
+import static org.apache.iotdb.ainode.utils.AINodeTestUtils.prepareDataInTree;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Consolidates AINodeDeviceManageIT, AINodeModelManageIT, 
AINodeCallInferenceIT, AINodeForecastIT,
+ * and AINodeInstanceManagementIT into a single class that shares one 1C1D1A 
cluster, avoiding 5
+ * redundant cluster startups (~20 min saved).
+ */
+@RunWith(IoTDBTestRunner.class)
+@Category({AIClusterIT.class})
+public class AINodeSharedClusterIT {
+
+  private static final String TARGET_DEVICES_STR = "0,1";
+  private static final Set<String> TARGET_DEVICES =
+      new HashSet<>(Arrays.asList(TARGET_DEVICES_STR.split(",")));
+
+  private static final String CALL_INFERENCE_SQL_TEMPLATE =
+      "CALL INFERENCE(%s, \"SELECT s%d FROM root.AI LIMIT %d\", 
generateTime=true, outputLength=%d)";
+  private static final String CALL_INFERENCE_BY_DEFAULT_SQL_TEMPLATE =
+      "CALL INFERENCE(%s, \"SELECT s%d FROM root.AI LIMIT 256\")";
+  private static final int DEFAULT_INPUT_LENGTH = 256;
+  private static final int DEFAULT_OUTPUT_LENGTH = 48;
+
+  private static final String FORECAST_TABLE_FUNCTION_SQL_TEMPLATE =
+      "SELECT * FROM FORECAST("
+          + "model_id=>'%s', "
+          + "targets=>(SELECT time, s%d FROM db.AI WHERE time<%d ORDER BY time 
DESC LIMIT %d) ORDER BY time, "
+          + "output_start_time=>%d, "
+          + "output_length=>%d, "
+          + "output_interval=>%d, "
+          + "timecol=>'%s'"
+          + ")";
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    EnvFactory.getEnv().initClusterEnvironment(1, 1);
+    prepareDataInTree();
+    prepareDataInTable();
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    EnvFactory.getEnv().cleanClusterEnvironment();
+  }
+
+  // ========== DeviceManage tests ==========
+
+  @Test
+  public void showAIDeviceTestInTree() throws SQLException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      showAIDevicesTest(statement);
+    }
+  }
+
+  @Test
+  public void showAIDeviceTestInTable() throws SQLException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      showAIDevicesTest(statement);
+    }
+  }
+
+  private void showAIDevicesTest(Statement statement) throws SQLException {
+    final String showSql = "SHOW AI_DEVICES";
+    final List<String> expectedDeviceIdList = new 
LinkedList<>(Arrays.asList("0", "1", "cpu"));
+    final List<String> expectedDeviceTypeList =
+        new LinkedList<>(Arrays.asList("cuda", "cuda", "cpu"));
+    try (ResultSet resultSet = statement.executeQuery(showSql)) {
+      ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
+      checkHeader(resultSetMetaData, "DeviceId,DeviceType");
+      while (resultSet.next()) {
+        String deviceId = resultSet.getString(1);
+        String deviceType = resultSet.getString(2);
+        Assert.assertEquals(expectedDeviceIdList.remove(0), deviceId);
+        Assert.assertEquals(expectedDeviceTypeList.remove(0), deviceType);
+      }
+    }
+  }
+
+  // ========== ModelManage tests ==========
+
+  @Test
+  public void userDefinedModelManagementTestInTree() throws SQLException, 
InterruptedException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      FakeModelInfo modelInfo =
+          new FakeModelInfo("user_chronos", "custom_t5", "user_defined", 
"active");
+      registerUserDefinedModel(statement, modelInfo, "file:///data/chronos2");
+      callInferenceTest(statement, modelInfo);
+      dropUserDefinedModel(statement, modelInfo.getModelId());
+
+      modelInfo = new FakeModelInfo("user_mantis", "custom_mantis", 
"user_defined", "active");
+      registerUserDefinedModel(statement, modelInfo, "file:///data/mantis");
+      dropUserDefinedModel(statement, modelInfo.getModelId());
+    }
+  }
+
+  @Test
+  public void userDefinedModelManagementTestInTable() throws SQLException, 
InterruptedException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      FakeModelInfo modelInfo =
+          new FakeModelInfo("user_chronos", "custom_t5", "user_defined", 
"active");
+      registerUserDefinedModel(statement, modelInfo, "file:///data/chronos2");
+      forecastTableFunctionTest(statement, modelInfo);
+      dropUserDefinedModel(statement, modelInfo.getModelId());
+
+      modelInfo = new FakeModelInfo("user_mantis", "custom_mantis", 
"user_defined", "active");
+      registerUserDefinedModel(statement, modelInfo, "file:///data/mantis");
+      dropUserDefinedModel(statement, modelInfo.getModelId());
+    }
+  }
+
+  @Test
+  public void dropBuiltInModelErrorTestInTree() throws SQLException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      errorTest(statement, "drop model sundial", "1506: Cannot delete built-in 
model: sundial");
+    }
+  }
+
+  @Test
+  public void dropBuiltInModelErrorTestInTable() throws SQLException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      errorTest(statement, "drop model sundial", "1506: Cannot delete built-in 
model: sundial");
+    }
+  }
+
+  @Test
+  public void showBuiltInModelTestInTree() throws SQLException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      showBuiltInModelTest(statement);
+    }
+  }
+
+  @Test
+  public void showBuiltInModelTestInTable() throws SQLException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      showBuiltInModelTest(statement);
+    }
+  }
+
+  private void showBuiltInModelTest(Statement statement) throws SQLException {
+    int built_in_model_count = 0;
+    final String showSql = "SHOW MODELS";
+    try (ResultSet resultSet = statement.executeQuery(showSql)) {
+      ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
+      checkHeader(resultSetMetaData, "ModelId,ModelType,Category,State");
+      while (resultSet.next()) {
+        built_in_model_count++;
+        FakeModelInfo modelInfo =
+            new FakeModelInfo(
+                resultSet.getString(1),
+                resultSet.getString(2),
+                resultSet.getString(3),
+                resultSet.getString(4));
+        assertTrue(BUILTIN_MODEL_MAP.containsKey(modelInfo.getModelId()));
+        assertEquals(BUILTIN_MODEL_MAP.get(modelInfo.getModelId()), modelInfo);
+      }
+    }
+    assertEquals(BUILTIN_MODEL_MAP.size(), built_in_model_count);
+  }
+
+  // ========== CallInference tests ==========
+
+  @Test
+  public void callInferenceTest() throws SQLException {
+    for (AINodeTestUtils.FakeModelInfo modelInfo : BUILTIN_MODEL_MAP.values()) 
{
+      try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
+          Statement statement = connection.createStatement()) {
+        callInferenceTest(statement, modelInfo);
+        callInferenceByDefaultTest(statement, modelInfo);
+        callInferenceErrorTest(statement, modelInfo);
+      }
+    }
+  }
+
+  public static void callInferenceTest(Statement statement, 
AINodeTestUtils.FakeModelInfo modelInfo)
+      throws SQLException {
+    for (int i = 0; i < 4; i++) {
+      String callInferenceSQL =
+          String.format(
+              CALL_INFERENCE_SQL_TEMPLATE,
+              modelInfo.getModelId(),
+              i,
+              DEFAULT_INPUT_LENGTH,
+              DEFAULT_OUTPUT_LENGTH);
+      try (ResultSet resultSet = statement.executeQuery(callInferenceSQL)) {
+        ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
+        checkHeader(resultSetMetaData, "Time,output");
+        int count = 0;
+        while (resultSet.next()) {
+          count++;
+        }
+        Assert.assertEquals(DEFAULT_OUTPUT_LENGTH, count);
+      }
+    }
+  }
+
+  public static void callInferenceByDefaultTest(
+      Statement statement, AINodeTestUtils.FakeModelInfo modelInfo) throws 
SQLException {
+    for (int i = 0; i < 4; i++) {
+      String callInferenceSQL =
+          String.format(CALL_INFERENCE_BY_DEFAULT_SQL_TEMPLATE, 
modelInfo.getModelId(), i);
+      try (ResultSet resultSet = statement.executeQuery(callInferenceSQL)) {
+        ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
+        checkHeader(resultSetMetaData, "output");
+        int count = 0;
+        while (resultSet.next()) {
+          count++;
+        }
+        Assert.assertTrue(count > 0);
+      }
+    }
+  }
+
+  public static void callInferenceErrorTest(
+      Statement statement, AINodeTestUtils.FakeModelInfo modelInfo) {
+    String multiVariateSQL =
+        String.format(
+            "CALL INFERENCE(%s, \"SELECT s0,s1 FROM root.AI LIMIT 128\", 
generateTime=true, outputLength=10)",
+            modelInfo.getModelId());
+    errorTest(
+        statement,
+        multiVariateSQL,
+        "701: Call inference function should not contain more than one input 
column, found [2] input columns.");
+  }
+
+  // ========== Forecast tests ==========
+
+  @Test
+  public void forecastTableFunctionTest() throws SQLException {
+    for (AINodeTestUtils.FakeModelInfo modelInfo : BUILTIN_MODEL_MAP.values()) 
{
+      try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+          Statement statement = connection.createStatement()) {
+        forecastTableFunctionTest(statement, modelInfo);
+      }
+    }
+  }
+
+  public static void forecastTableFunctionTest(
+      Statement statement, AINodeTestUtils.FakeModelInfo modelInfo) throws 
SQLException {
+    for (int i = 0; i < 4; i++) {
+      String forecastTableFunctionSQL =
+          String.format(
+              FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
+              modelInfo.getModelId(),
+              i,
+              5760,
+              2880,
+              5760,
+              96,
+              1,
+              "time");
+      try (ResultSet resultSet = 
statement.executeQuery(forecastTableFunctionSQL)) {
+        int count = 0;
+        while (resultSet.next()) {
+          count++;
+        }
+        Assert.assertTrue(count > 0);
+      }
+    }
+  }
+
+  @Test
+  public void forecastTableFunctionErrorTest() throws SQLException {
+    for (AINodeTestUtils.FakeModelInfo modelInfo : BUILTIN_MODEL_MAP.values()) 
{
+      try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+          Statement statement = connection.createStatement()) {
+        forecastTableFunctionErrorTest(statement, modelInfo);
+      }
+    }
+  }
+
+  public static void forecastTableFunctionErrorTest(
+      Statement statement, AINodeTestUtils.FakeModelInfo modelInfo) throws 
SQLException {
+    String invalidOutputStartTimeSQL =
+        String.format(
+            FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
+            modelInfo.getModelId(),
+            0,
+            5760,
+            2880,
+            5759,
+            96,
+            1,
+            "time");
+    errorTest(
+        statement,
+        invalidOutputStartTimeSQL,
+        "701: The OUTPUT_START_TIME should be greater than the maximum 
timestamp of target time series. Expected greater than [5759] but found 
[5759].");
+
+    String invalidOutputLengthSQLWithZero =
+        String.format(
+            FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
+            modelInfo.getModelId(),
+            0,
+            5760,
+            2880,
+            5760,
+            0,
+            1,
+            "time");
+    errorTest(
+        statement, invalidOutputLengthSQLWithZero, "701: OUTPUT_LENGTH should 
be greater than 0");
+
+    String invalidOutputLengthSQLWithOutOfRange =
+        String.format(
+            FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
+            modelInfo.getModelId(),
+            0,
+            5760,
+            2880,
+            5760,
+            2881,
+            1,
+            "time");
+    errorTest(
+        statement,
+        invalidOutputLengthSQLWithOutOfRange,
+        "1599: Error occurred while executing forecast:[Attribute 
output_length expect value between 1 and 2880, got 2881 instead.]");
+
+    String invalidOutputIntervalSQL =
+        String.format(
+            FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
+            modelInfo.getModelId(),
+            0,
+            5760,
+            2880,
+            5760,
+            96,
+            -1,
+            "time");
+    errorTest(statement, invalidOutputIntervalSQL, "701: OUTPUT_INTERVAL 
should be greater than 0");
+
+    String invalidTimecolSQL2 =
+        String.format(
+            FORECAST_TABLE_FUNCTION_SQL_TEMPLATE,
+            modelInfo.getModelId(),
+            0,
+            5760,
+            2880,
+            5760,
+            96,
+            1,
+            "s0");
+    errorTest(
+        statement, invalidTimecolSQL2, "701: The type of the column [s0] is 
not as expected.");
+  }
+
+  // ========== InstanceManagement tests ==========
+
+  @Test
+  public void instanceBasicManagementTestInTreeModel() throws SQLException, 
InterruptedException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      instanceBasicManagementTest(statement);
+    }
+  }
+
+  @Test
+  public void instanceBasicManagementTestInTableModel() throws SQLException, 
InterruptedException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      instanceBasicManagementTest(statement);
+    }
+  }
+
+  private void instanceBasicManagementTest(Statement statement)
+      throws SQLException, InterruptedException {
+    try (ResultSet resultSet = statement.executeQuery("SHOW AI_DEVICES")) {
+      ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
+      checkHeader(resultSetMetaData, "DeviceId,DeviceType");
+      final Set<String> resultDevices = new HashSet<>();
+      while (resultSet.next()) {
+        resultDevices.add(resultSet.getString("DeviceId"));
+      }
+      Set<String> expected = new HashSet<>(TARGET_DEVICES);
+      expected.add("cpu");
+      Assert.assertEquals(expected, resultDevices);
+    }
+
+    statement.execute(String.format("LOAD MODEL sundial TO DEVICES '%s'", 
TARGET_DEVICES_STR));
+    checkModelOnSpecifiedDevice(statement, "sundial", TARGET_DEVICES_STR);
+    statement.execute(String.format("UNLOAD MODEL sundial FROM DEVICES '%s'", 
TARGET_DEVICES_STR));
+    checkModelNotOnSpecifiedDevice(statement, "sundial", TARGET_DEVICES_STR);
+
+    statement.execute(String.format("LOAD MODEL timer_xl TO DEVICES '%s'", 
TARGET_DEVICES_STR));
+    checkModelOnSpecifiedDevice(statement, "timer_xl", TARGET_DEVICES_STR);
+    statement.execute(String.format("UNLOAD MODEL timer_xl FROM DEVICES '%s'", 
TARGET_DEVICES_STR));
+    checkModelNotOnSpecifiedDevice(statement, "timer_xl", TARGET_DEVICES_STR);
+  }
+
+  @Test
+  public void instanceFailTestInTreeModel() throws SQLException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      instanceFailTest(statement);
+    }
+  }
+
+  @Test
+  public void instanceFailTestInTableModel() throws SQLException {
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+        Statement statement = connection.createStatement()) {
+      instanceFailTest(statement);
+    }
+  }
+
+  private void instanceFailTest(Statement statement) {
+    errorTest(
+        statement,
+        "LOAD MODEL unknown TO DEVICES 'cpu,0,1'",
+        "1504: Model [unknown] is not registered yet. You can use 'SHOW 
MODELS' to retrieve the available models.");
+    errorTest(
+        statement,
+        "LOAD MODEL sundial TO DEVICES '999'",
+        "1508: AIDevice ID [999] is not available. You can use 'SHOW 
AI_DEVICES' to retrieve the available devices.");
+    errorTest(
+        statement,
+        "UNLOAD MODEL sundial FROM DEVICES '999'",
+        "1508: AIDevice ID [999] is not available. You can use 'SHOW 
AI_DEVICES' to retrieve the available devices.");
+    errorTest(
+        statement,
+        "LOAD MODEL sundial TO DEVICES '0,0'",
+        "1509: Device ID list contains duplicate entries.");
+    errorTest(
+        statement,
+        "UNLOAD MODEL sundial FROM DEVICES '0,0'",
+        "1510: Device ID list contains duplicate entries.");
+  }
+
+  // ========== Helper methods (from ModelManageIT) ==========
+
+  private static void registerUserDefinedModel(
+      Statement statement, AINodeTestUtils.FakeModelInfo modelInfo, String uri)
+      throws SQLException, InterruptedException {
+    String modelId = modelInfo.getModelId();
+    String modelType = modelInfo.getModelType();
+    String category = modelInfo.getCategory();
+    final String CREATE_MODEL_TEMPLATE = "create model %s using uri \"%s\"";
+    final String alterConfigSQL = "set configuration 
\"trusted_uri_pattern\"='.*'";
+    final String registerSql = String.format(CREATE_MODEL_TEMPLATE, modelId, 
uri);
+    final String showSql = String.format("SHOW MODELS %s", modelId);
+    statement.execute(alterConfigSQL);
+    statement.execute(registerSql);
+    boolean loading = true;
+    for (int retryCnt = 0; retryCnt < 100; retryCnt++) {
+      try (ResultSet resultSet = statement.executeQuery(showSql)) {
+        ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
+        checkHeader(resultSetMetaData, "ModelId,ModelType,Category,State");
+        while (resultSet.next()) {
+          String resultModelId = resultSet.getString(1);
+          String resultModelType = resultSet.getString(2);
+          String resultCategory = resultSet.getString(3);
+          String state = resultSet.getString(4);
+          assertEquals(modelId, resultModelId);
+          assertEquals(modelType, resultModelType);
+          assertEquals(category, resultCategory);
+          if (state.equals("active")) {
+            loading = false;
+          } else if (state.equals("loading")) {
+            break;
+          } else {
+            fail("Unexpected status of model: " + state);
+          }
+        }
+      }
+      if (!loading) {
+        break;
+      }
+      TimeUnit.SECONDS.sleep(1);
+    }
+    assertFalse(loading);
+  }
+
+  private static void dropUserDefinedModel(Statement statement, String modelId)
+      throws SQLException {
+    final String showSql = String.format("SHOW MODELS %s", modelId);
+    final String dropSql = String.format("DROP MODEL %s", modelId);
+    statement.execute(dropSql);
+    try (ResultSet resultSet = statement.executeQuery(showSql)) {
+      ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
+      checkHeader(resultSetMetaData, "ModelId,ModelType,Category,State");
+      int count = 0;
+      while (resultSet.next()) {
+        count++;
+      }
+      assertEquals(0, count);
+    }
+  }
+}
diff --git a/iotdb-core/ainode/build_binary.py 
b/iotdb-core/ainode/build_binary.py
index 56f19f43cf5..aa342bb82d1 100644
--- a/iotdb-core/ainode/build_binary.py
+++ b/iotdb-core/ainode/build_binary.py
@@ -21,7 +21,9 @@
 PyInstaller build script (Python version)
 """
 
+import hashlib
 import os
+import shutil
 import subprocess
 import sys
 from pathlib import Path
@@ -317,6 +319,106 @@ def poetry_install_with_accel(poetry_exe, script_dir, 
venv_env, accelerator):
     )
 
 
+def compute_source_hash(script_dir):
+    """
+    Compute a SHA256 hash over all files that affect the PyInstaller output.
+
+    Includes Python source files, the spec file, pyproject.toml, and 
poetry.lock.
+    """
+    hasher = hashlib.sha256()
+
+    # Include Python version so cache invalidates on interpreter upgrades
+    hasher.update(sys.version.encode())
+
+    hash_targets = []
+
+    excluded_dirs = {"build", "dist", "__pycache__"}
+
+    for pattern in ("**/*.py", "**/*.spec"):
+        for f in script_dir.glob(pattern):
+            if not any(
+                part in excluded_dirs for part in 
f.relative_to(script_dir).parts
+            ):
+                hash_targets.append(f)
+
+    for name in ("pyproject.toml", "poetry.lock"):
+        f = script_dir / name
+        if f.exists():
+            hash_targets.append(f)
+
+    # Also include the thrift/client-py sources that get copied in
+    client_py_dir = script_dir.parent.parent / "iotdb-client" / "client-py" / 
"iotdb"
+    if client_py_dir.is_dir():
+        hash_targets.extend(client_py_dir.rglob("*.py"))
+
+    hash_targets.sort(key=lambda p: str(p))
+
+    for f in hash_targets:
+        try:
+            rel = f.relative_to(script_dir)
+        except ValueError:
+            rel = f
+        hasher.update(str(rel).encode())
+        hasher.update(f.read_bytes())
+
+    return hasher.hexdigest()
+
+
+def get_dist_cache_dir():
+    """Get the directory used to cache PyInstaller dist output."""
+    return get_venv_base_dir() / "dist-cache"
+
+
+def try_restore_dist_cache(script_dir):
+    """
+    Try to restore the dist/ directory from cache.
+
+    Returns True if cache hit, False otherwise.
+    """
+    source_hash = compute_source_hash(script_dir)
+    cache_dir = get_dist_cache_dir()
+    hash_file = cache_dir / "source_hash"
+    cached_dist = cache_dir / "ainode"
+    dist_dir = script_dir / "dist" / "ainode"
+
+    print(f"Source hash: {source_hash}")
+
+    if hash_file.exists() and cached_dist.is_dir():
+        cached_hash = hash_file.read_text().strip()
+        if cached_hash == source_hash:
+            print("Cache hit — restoring dist/ from cache, skipping 
PyInstaller build")
+            dist_dir.parent.mkdir(parents=True, exist_ok=True)
+            if dist_dir.exists():
+                shutil.rmtree(dist_dir)
+            shutil.copytree(cached_dist, dist_dir, symlinks=True)
+            return True
+        else:
+            print("Cache miss — source hash changed, will rebuild")
+    else:
+        print("No dist cache found, will build from scratch")
+
+    return False
+
+
+def save_dist_cache(script_dir):
+    """Save the dist/ directory to cache after a successful build."""
+    source_hash = compute_source_hash(script_dir)
+    cache_dir = get_dist_cache_dir()
+    cached_dist = cache_dir / "ainode"
+    dist_dir = script_dir / "dist" / "ainode"
+
+    if not dist_dir.is_dir():
+        print("Warning: dist/ainode not found, skipping cache save")
+        return
+
+    cache_dir.mkdir(parents=True, exist_ok=True)
+    if cached_dist.exists():
+        shutil.rmtree(cached_dist)
+    shutil.copytree(dist_dir, cached_dist, symlinks=True)
+    (cache_dir / "source_hash").write_text(source_hash)
+    print(f"Saved dist cache (hash: {source_hash})")
+
+
 def build():
     """
     Execute the complete build process.
@@ -325,7 +427,9 @@ def build():
     1. Setup virtual environment (outside project directory)
     2. Update pip and install 2.2.1 poetry
     3. Install project dependencies (including PyInstaller from pyproject.toml)
-    4. Build executable using PyInstaller
+    4. Check dist cache — skip PyInstaller if source hasn't changed
+    5. Build executable using PyInstaller (if cache miss)
+    6. Save dist to cache
     """
     script_dir = Path(__file__).parent
 
@@ -344,6 +448,13 @@ def build():
     print("=" * 50)
     print()
 
+    if try_restore_dist_cache(script_dir):
+        print()
+        print("=" * 50)
+        print("Build completed (from cache)!")
+        print("=" * 50)
+        return
+
     print("Starting build...")
     print()
 
@@ -383,6 +494,8 @@ def build():
         print(f"\nError: Build failed: {e}")
         sys.exit(1)
 
+    save_dist_cache(script_dir)
+
     print()
     print("=" * 50)
     print("Build completed!")

Reply via email to