Skip to content

Commit 12c54ef

Browse files
authored
feat: MySQL Session IT (#164) (#187)
## AgentScope-Java Version 1.0.3-SNAPSHOT ## Description to PR #164 ## Checklist Please check the following items before code is ready to be reviewed. - [x] Code has been formatted with `mvn spotless:apply` - [x] All tests are passing (`mvn test`) - [x] Javadoc comments are complete and follow project conventions - [x] Related documentation has been updated (e.g. links, examples, etc.) - [x] Code is ready for review
1 parent a4d99e6 commit 12c54ef

2 files changed

Lines changed: 248 additions & 0 deletions

File tree

agentscope-extensions/agentscope-extensions-session-mysql/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@
4242
<groupId>com.mysql</groupId>
4343
<artifactId>mysql-connector-j</artifactId>
4444
</dependency>
45+
46+
<!-- In-memory database for E2E tests (run without a real MySQL instance in CI) -->
47+
<dependency>
48+
<groupId>com.h2database</groupId>
49+
<artifactId>h2</artifactId>
50+
<version>2.2.224</version>
51+
<scope>test</scope>
52+
</dependency>
4553
</dependencies>
4654
</project>
4755

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.agentscope.core.session.mysql.e2e;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertFalse;
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
import static org.junit.jupiter.api.Assertions.assertNull;
22+
import static org.junit.jupiter.api.Assertions.assertThrows;
23+
import static org.junit.jupiter.api.Assertions.assertTrue;
24+
25+
import io.agentscope.core.session.SessionInfo;
26+
import io.agentscope.core.session.mysql.MysqlSession;
27+
import io.agentscope.core.state.StateModule;
28+
import io.agentscope.core.state.StateModuleBase;
29+
import java.sql.Connection;
30+
import java.sql.SQLException;
31+
import java.sql.Statement;
32+
import java.util.List;
33+
import java.util.Map;
34+
import java.util.UUID;
35+
import javax.sql.DataSource;
36+
import org.h2.jdbcx.JdbcDataSource;
37+
import org.junit.jupiter.api.AfterEach;
38+
import org.junit.jupiter.api.DisplayName;
39+
import org.junit.jupiter.api.Tag;
40+
import org.junit.jupiter.api.Test;
41+
import org.junit.jupiter.api.parallel.Execution;
42+
import org.junit.jupiter.api.parallel.ExecutionMode;
43+
44+
/**
45+
* End-to-end tests for {@link MysqlSession} using an in-memory H2 database in MySQL compatibility
46+
* mode.
47+
*
48+
* <p>This makes the E2E tests runnable in CI without provisioning a real MySQL instance and
49+
* without requiring any environment variables.
50+
*/
51+
@Tag("e2e")
52+
@Execution(ExecutionMode.CONCURRENT)
53+
@DisplayName("Session MySQL Storage E2E Tests")
54+
class MysqlSessionE2ETest {
55+
56+
private String createdSchemaName;
57+
private DataSource dataSource;
58+
59+
@AfterEach
60+
void cleanupDatabase() {
61+
if (dataSource == null || createdSchemaName == null) {
62+
return;
63+
}
64+
try (Connection conn = dataSource.getConnection();
65+
Statement stmt = conn.createStatement()) {
66+
stmt.execute("DROP SCHEMA IF EXISTS " + createdSchemaName + " CASCADE");
67+
} catch (SQLException e) {
68+
// best-effort cleanup
69+
System.err.println(
70+
"Failed to drop e2e schema " + createdSchemaName + ": " + e.getMessage());
71+
} finally {
72+
createdSchemaName = null;
73+
dataSource = null;
74+
}
75+
}
76+
77+
@Test
78+
@DisplayName("Smoke: auto-create database/table + save/load/list/info/delete flow")
79+
void testMysqlSessionEndToEndFlow() {
80+
System.out.println("\n=== Test: MysqlSession E2E Flow ===");
81+
82+
dataSource = createH2DataSource();
83+
// H2 folds unquoted identifiers to upper-case. Keep schema/table names upper-case so that
84+
// INFORMATION_SCHEMA lookups in MysqlSession match exactly.
85+
String schemaName = generateSafeIdentifier("AGENTSCOPE_E2E").toUpperCase();
86+
String tableName = generateSafeIdentifier("AGENTSCOPE_SESSIONS").toUpperCase();
87+
createdSchemaName = schemaName;
88+
89+
initSchemaAndTable(dataSource, schemaName, tableName);
90+
MysqlSession session = new MysqlSession(dataSource, schemaName, tableName, false);
91+
92+
// Prepare state modules
93+
TestStateModule moduleA = new TestStateModule("moduleA");
94+
TestStateModule moduleB = new TestStateModule("moduleB");
95+
moduleA.setValue("hello");
96+
moduleB.setValue("world");
97+
98+
String sessionId = "mysql_e2e_session_" + UUID.randomUUID();
99+
Map<String, StateModule> modules = Map.of("moduleA", moduleA, "moduleB", moduleB);
100+
101+
// Save
102+
session.saveSessionState(sessionId, modules);
103+
assertTrue(session.sessionExists(sessionId));
104+
105+
// Load into fresh modules
106+
TestStateModule loadedA = new TestStateModule("moduleA");
107+
TestStateModule loadedB = new TestStateModule("moduleB");
108+
session.loadSessionState(sessionId, false, Map.of("moduleA", loadedA, "moduleB", loadedB));
109+
110+
assertEquals("hello", loadedA.getValue());
111+
assertEquals("world", loadedB.getValue());
112+
113+
// listSessions
114+
List<String> sessions = session.listSessions();
115+
assertTrue(sessions.contains(sessionId), "listSessions should contain saved session id");
116+
117+
// getSessionInfo
118+
SessionInfo info = session.getSessionInfo(sessionId);
119+
assertNotNull(info);
120+
assertEquals(sessionId, info.getSessionId());
121+
assertTrue(info.getSize() > 0, "SessionInfo.size should be > 0");
122+
assertEquals(2, info.getComponentCount(), "Should contain 2 components");
123+
assertTrue(info.getLastModified() > 0, "SessionInfo.lastModified should be > 0");
124+
125+
// deleteSession
126+
assertTrue(session.deleteSession(sessionId));
127+
assertFalse(session.sessionExists(sessionId));
128+
assertFalse(session.deleteSession(sessionId), "Delete again should return false");
129+
}
130+
131+
@Test
132+
@DisplayName("allowNotExist=true should silently ignore missing session")
133+
void testLoadAllowNotExistTrue() {
134+
System.out.println("\n=== Test: allowNotExist=true ===");
135+
136+
dataSource = createH2DataSource();
137+
String schemaName = generateSafeIdentifier("AGENTSCOPE_E2E").toUpperCase();
138+
String tableName = generateSafeIdentifier("AGENTSCOPE_SESSIONS").toUpperCase();
139+
createdSchemaName = schemaName;
140+
141+
initSchemaAndTable(dataSource, schemaName, tableName);
142+
MysqlSession session = new MysqlSession(dataSource, schemaName, tableName, false);
143+
144+
TestStateModule module = new TestStateModule("moduleA");
145+
session.loadSessionState("missing_" + UUID.randomUUID(), true, Map.of("moduleA", module));
146+
// Should not throw, and module should remain default
147+
assertNull(module.getValue());
148+
}
149+
150+
@Test
151+
@DisplayName("createIfNotExist=false should fail fast when database/table do not exist")
152+
void testCreateIfNotExistFalseFailsWhenMissing() {
153+
System.out.println("\n=== Test: createIfNotExist=false with missing schema ===");
154+
155+
dataSource = createH2DataSource();
156+
String schemaName = generateSafeIdentifier("AGENTSCOPE_E2E_MISSING").toUpperCase();
157+
String tableName = generateSafeIdentifier("AGENTSCOPE_SESSIONS_MISSING").toUpperCase();
158+
159+
// Do not set createdSchemaName because we didn't create it; cleanup not needed.
160+
assertThrows(
161+
IllegalStateException.class,
162+
() -> new MysqlSession(dataSource, schemaName, tableName, false));
163+
}
164+
165+
private static DataSource createH2DataSource() {
166+
String dbName = "mysql_session_e2e_" + UUID.randomUUID().toString().replace("-", "");
167+
JdbcDataSource ds = new JdbcDataSource();
168+
ds.setURL("jdbc:h2:mem:" + dbName + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
169+
ds.setUser("sa");
170+
ds.setPassword("");
171+
return ds;
172+
}
173+
174+
/**
175+
* Generates a safe MySQL identifier (letters/numbers/underscore) and keeps it <= 64 chars.
176+
*/
177+
private static String generateSafeIdentifier(String prefix) {
178+
String suffix = UUID.randomUUID().toString().replace("-", "_");
179+
String raw = prefix + "_" + suffix;
180+
// Ensure first char is a letter or underscore
181+
if (!Character.isLetter(raw.charAt(0)) && raw.charAt(0) != '_') {
182+
raw = "_" + raw;
183+
}
184+
if (raw.length() > 64) {
185+
raw = raw.substring(0, 64);
186+
}
187+
// Avoid trailing underscore-only truncation weirdness
188+
raw = raw.replaceAll("_+$", "_e2e");
189+
if (raw.length() > 64) {
190+
raw = raw.substring(0, 64);
191+
}
192+
return raw;
193+
}
194+
195+
private static void initSchemaAndTable(
196+
DataSource dataSource, String schemaName, String tableName) throws RuntimeException {
197+
try (Connection conn = dataSource.getConnection();
198+
Statement stmt = conn.createStatement()) {
199+
stmt.execute("CREATE SCHEMA IF NOT EXISTS " + schemaName);
200+
stmt.execute("SET SCHEMA " + schemaName);
201+
stmt.execute("DROP TABLE IF EXISTS " + tableName);
202+
// Keep DDL compatible with H2 while still exercising MysqlSession's DML (including
203+
// "ON DUPLICATE KEY UPDATE") in MySQL mode.
204+
stmt.execute(
205+
"CREATE TABLE "
206+
+ tableName
207+
+ " ("
208+
+ "session_id VARCHAR(255) PRIMARY KEY, "
209+
+ "state_data TEXT NOT NULL, "
210+
+ "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "
211+
+ "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
212+
+ ")");
213+
} catch (SQLException e) {
214+
throw new RuntimeException("Failed to init schema/table for H2 e2e", e);
215+
}
216+
}
217+
218+
private static class TestStateModule extends StateModuleBase {
219+
private final String componentName;
220+
private String value;
221+
222+
TestStateModule(String componentName) {
223+
this.componentName = componentName;
224+
registerState("value");
225+
}
226+
227+
@Override
228+
public String getComponentName() {
229+
return componentName;
230+
}
231+
232+
public String getValue() {
233+
return value;
234+
}
235+
236+
public void setValue(String value) {
237+
this.value = value;
238+
}
239+
}
240+
}

0 commit comments

Comments
 (0)