| from __future__ import annotations |
| |
| import json |
| import sys |
| import tempfile |
| import unittest |
| from pathlib import Path |
| from unittest.mock import Mock, patch |
| |
| ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(ROOT / "src")) |
| |
| from iotdb_mcp_server.session_manager import IoTDBSessionManager # noqa: E402 |
| from iotdb_mcp_server.session_manager import ( # noqa: E402 |
| create_table_session_pool, |
| create_tree_session_pool, |
| is_iotdb_connection_error, |
| make_tree_pool_config, |
| ) |
| from iotdb_mcp_server.target_lifecycle import ( # noqa: E402 |
| IoTDBTargetCandidateStore, |
| candidate_target, |
| verify_target_once, |
| ) |
| from iotdb_mcp_server.target_registry import ( # noqa: E402 |
| IoTDBTargetRegistry, |
| target_from_mapping, |
| ) |
| |
| |
| def _seed_session_pool(pool, *sessions) -> None: |
| queue = getattr(pool, "_SessionPool__queue") |
| for session in sessions: |
| queue.put(session) |
| setattr(pool, "_SessionPool__pool_size", len(sessions)) |
| |
| |
| class TargetLifecycleTest(unittest.TestCase): |
| def test_empty_registry_is_valid_and_not_resolvable(self) -> None: |
| registry = IoTDBTargetRegistry({}) |
| |
| self.assertIsNone(registry.default_target_id) |
| self.assertEqual(registry.list_targets(), []) |
| with self.assertRaisesRegex(KeyError, "No verified IoTDB target"): |
| registry.resolve() |
| |
| def test_from_env_ignores_unverified_persisted_targets(self) -> None: |
| payload = { |
| "default_target_id": "unverified", |
| "targets": { |
| "unverified": { |
| "target_id": "unverified", |
| "host": "192.168.99.15", |
| "port": 6667, |
| "user": "root", |
| "password": "stale", |
| }, |
| "verified": { |
| "target_id": "verified", |
| "host": "192.168.99.20", |
| "port": 6667, |
| "user": "root", |
| "password": "known-good", |
| "verified_at": "2026-07-10T00:00:00+00:00", |
| }, |
| }, |
| } |
| with tempfile.TemporaryDirectory() as directory: |
| path = Path(directory) / "targets.json" |
| path.write_text(json.dumps(payload), encoding="utf-8") |
| |
| registry = IoTDBTargetRegistry.from_env( |
| {"TIMESEEK_IOTDB_TARGETS_FILE": str(path)} |
| ) |
| |
| self.assertEqual( |
| [target["target_id"] for target in registry.list_targets()], |
| ["verified"], |
| ) |
| self.assertEqual(registry.default_target_id, "verified") |
| |
| def test_candidate_rejects_credentials_and_is_one_time(self) -> None: |
| store = IoTDBTargetCandidateStore() |
| with self.assertRaisesRegex(ValueError, "cannot contain credentials"): |
| store.prepare({"host": "192.168.99.15", "password": "secret"}) |
| |
| candidate = store.prepare({"host": "192.168.99.15", "port": 6667}) |
| self.assertNotIn("password", candidate.as_dict()["template"]) |
| self.assertEqual(store.consume(candidate.candidate_id), candidate) |
| with self.assertRaisesRegex(KeyError, "already consumed"): |
| store.consume(candidate.candidate_id) |
| |
| def test_failed_candidate_requires_user_confirmed_retry(self) -> None: |
| store = IoTDBTargetCandidateStore(retry_guard_seconds=60) |
| candidate = store.prepare({"host": "192.168.99.15", "port": 6667}) |
| store.consume(candidate.candidate_id) |
| store.record_failure(candidate) |
| |
| with self.assertRaisesRegex(PermissionError, "explicitly requests"): |
| store.prepare({"host": "192.168.99.15", "port": 6667}) |
| retried = store.prepare( |
| {"host": "192.168.99.15", "port": 6667}, |
| user_confirmed_retry=True, |
| ) |
| self.assertTrue(retried.candidate_id.startswith("candidate-")) |
| |
| def test_successful_verification_marks_target_publishable(self) -> None: |
| store = IoTDBTargetCandidateStore() |
| candidate = store.prepare({"host": "192.168.99.15", "port": 6667}) |
| target = candidate_target( |
| store.consume(candidate.candidate_id), |
| user="root", |
| password="known-good", |
| ) |
| session = Mock() |
| |
| with patch("iotdb_mcp_server.target_lifecycle.Session", return_value=session): |
| verified = verify_target_once(target) |
| |
| session.open.assert_called_once() |
| session.execute_query_statement.assert_called_once_with("SHOW VERSION") |
| session.close.assert_called_once() |
| self.assertTrue(verified.verified_at) |
| self.assertEqual( |
| verified.last_known_good_credential["password"], "known-good" |
| ) |
| |
| def test_connection_error_evicts_last_target(self) -> None: |
| target = target_from_mapping( |
| { |
| "target_id": "cloud", |
| "host": "192.168.99.15", |
| "port": 6667, |
| "user": "root", |
| "password": "known-good", |
| "sql_dialect": "tree", |
| } |
| ) |
| manager = IoTDBSessionManager( |
| IoTDBTargetRegistry({target.target_id: target}, target.target_id) |
| ) |
| callback = Mock() |
| manager.set_connection_error_callback(callback) |
| raw_pool = Mock() |
| raw_pool.get_session.side_effect = ConnectionError("connection refused") |
| |
| with patch( |
| "iotdb_mcp_server.session_manager.create_tree_session_pool", |
| return_value=raw_pool, |
| ): |
| pool = manager.tree_pool(target.target_id) |
| with self.assertRaises(ConnectionError): |
| pool.get_session() |
| |
| self.assertEqual(manager.registry.list_targets(), []) |
| callback.assert_called_once() |
| |
| def test_pool_config_uses_single_primary_endpoint(self) -> None: |
| target = target_from_mapping( |
| { |
| "target_id": "cloud", |
| "host": "192.168.99.15", |
| "port": 6667, |
| "node_urls": ["192.168.99.15:6667", "192.168.99.16:6667"], |
| "user": "root", |
| "password": "known-good", |
| } |
| ) |
| |
| pool_config = make_tree_pool_config(target) |
| |
| self.assertEqual(pool_config.host, "192.168.99.15") |
| self.assertEqual(pool_config.port, 6667) |
| self.assertFalse(pool_config.node_urls) |
| |
| def test_generic_sql_error_does_not_count_as_connection_error(self) -> None: |
| self.assertFalse(is_iotdb_connection_error(RuntimeError("syntax error"))) |
| self.assertTrue(is_iotdb_connection_error(RuntimeError("Status code 801"))) |
| |
| def test_pool_wait_timeout_does_not_count_as_connection_error(self) -> None: |
| self.assertFalse( |
| is_iotdb_connection_error( |
| TimeoutError("Wait to get session timeout in SessionPool") |
| ) |
| ) |
| |
| def test_managed_tree_session_is_returned_to_pool(self) -> None: |
| target = target_from_mapping( |
| { |
| "target_id": "cloud", |
| "host": "192.168.99.15", |
| "port": 6667, |
| "user": "root", |
| "password": "known-good", |
| "sql_dialect": "tree", |
| } |
| ) |
| manager = IoTDBSessionManager( |
| IoTDBTargetRegistry({target.target_id: target}, target.target_id) |
| ) |
| manager.set_connection_error_callback(Mock()) |
| raw_pool = Mock() |
| raw_session = Mock() |
| raw_pool.get_session.return_value = raw_session |
| |
| with patch( |
| "iotdb_mcp_server.session_manager.create_tree_session_pool", |
| return_value=raw_pool, |
| ): |
| session = manager.tree_pool(target.target_id).get_session() |
| session.close() |
| session.close() |
| |
| raw_pool.put_back.assert_called_once_with(raw_session) |
| raw_session.close.assert_not_called() |
| |
| def test_closed_table_pool_discards_all_borrowed_sessions(self) -> None: |
| target = target_from_mapping( |
| { |
| "target_id": "table-cloud", |
| "host": "192.168.99.15", |
| "port": 6667, |
| "user": "root", |
| "password": "known-good", |
| "database": "test", |
| "sql_dialect": "table", |
| } |
| ) |
| manager = IoTDBSessionManager( |
| IoTDBTargetRegistry({target.target_id: target}, target.target_id) |
| ) |
| callback = Mock() |
| manager.set_connection_error_callback(callback) |
| |
| driver_session_a = Mock() |
| driver_session_b = Mock() |
| database_error = ConnectionError("original database failure") |
| successful_result = object() |
| driver_session_a.execute_query_statement.side_effect = database_error |
| driver_session_b.execute_query_statement.return_value = successful_result |
| table_pool = create_table_session_pool(target, max_pool_size=2) |
| driver_pool = table_pool._session_pool |
| _seed_session_pool(driver_pool, driver_session_a, driver_session_b) |
| |
| with patch( |
| "iotdb_mcp_server.session_manager.create_table_session_pool", |
| return_value=table_pool, |
| ): |
| pool = manager.table_pool(target.target_id, max_pool_size=2) |
| session_a = pool.get_session() |
| session_b = pool.get_session() |
| self.assertIs( |
| session_b.execute_query_statement("SHOW TABLES"), |
| successful_result, |
| ) |
| with self.assertRaisesRegex( |
| ConnectionError, |
| "original database failure", |
| ): |
| try: |
| session_a.execute_query_statement("SHOW TABLES") |
| finally: |
| session_a.close() |
| session_b.close() |
| session_b.close() |
| |
| self.assertTrue(getattr(driver_pool, "_SessionPool__closed")) |
| driver_session_a.close.assert_called_once() |
| driver_session_b.close.assert_called_once() |
| callback.assert_called_once_with( |
| target.target_id, |
| database_error, |
| manager.registry, |
| ) |
| |
| def test_closed_tree_pool_discards_all_borrowed_sessions(self) -> None: |
| target = target_from_mapping( |
| { |
| "target_id": "tree-cloud", |
| "host": "192.168.99.15", |
| "port": 6667, |
| "user": "root", |
| "password": "known-good", |
| "sql_dialect": "tree", |
| } |
| ) |
| manager = IoTDBSessionManager( |
| IoTDBTargetRegistry({target.target_id: target}, target.target_id) |
| ) |
| callback = Mock() |
| manager.set_connection_error_callback(callback) |
| |
| raw_session_a = Mock() |
| raw_session_b = Mock() |
| database_error = ConnectionError("original database failure") |
| successful_result = object() |
| raw_session_a.execute_query_statement.side_effect = database_error |
| raw_session_b.execute_query_statement.return_value = successful_result |
| raw_pool = create_tree_session_pool(target, max_pool_size=2) |
| _seed_session_pool(raw_pool, raw_session_a, raw_session_b) |
| |
| with patch( |
| "iotdb_mcp_server.session_manager.create_tree_session_pool", |
| return_value=raw_pool, |
| ): |
| pool = manager.tree_pool(target.target_id, max_pool_size=2) |
| session_a = pool.get_session() |
| session_b = pool.get_session() |
| self.assertIs( |
| session_b.execute_query_statement("SHOW DATABASES"), |
| successful_result, |
| ) |
| with self.assertRaisesRegex( |
| ConnectionError, |
| "original database failure", |
| ): |
| try: |
| session_a.execute_query_statement("SHOW DATABASES") |
| finally: |
| session_a.close() |
| session_b.close() |
| session_b.close() |
| |
| self.assertTrue(getattr(raw_pool, "_SessionPool__closed")) |
| raw_session_a.close.assert_called_once() |
| raw_session_b.close.assert_called_once() |
| callback.assert_called_once_with( |
| target.target_id, |
| database_error, |
| manager.registry, |
| ) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |