Files
cleveragents-core/robot/helper_database_resources.py
T
freemo c054675167
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 18s
CI / build (pull_request) Successful in 17s
CI / typecheck (pull_request) Successful in 38s
CI / security (pull_request) Successful in 40s
CI / integration_tests (pull_request) Successful in 4m5s
CI / unit_tests (pull_request) Successful in 4m56s
CI / coverage (pull_request) Successful in 5m13s
CI / docker (pull_request) Successful in 1m10s
CI / lint (push) Successful in 13s
CI / quality (push) Successful in 19s
CI / build (push) Successful in 17s
CI / security (push) Successful in 37s
CI / typecheck (push) Successful in 40s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m59s
CI / integration_tests (push) Successful in 3m24s
CI / docker (push) Successful in 40s
CI / coverage (push) Successful in 5m14s
CI / benchmark-publish (push) Successful in 17m55s
CI / benchmark-regression (pull_request) Successful in 34m15s
feat(resource): add database resources
Implement database resource types (postgres, mysql, sqlite, duckdb)
with connection args, auth handling, and transaction-based sandbox
strategy using BEGIN/ROLLBACK/COMMIT wrappers.

Key changes:
- Add DatabaseResourceHandler with 4 database type definitions
- Implement TransactionSandbox for transaction_rollback strategy
- Wire TransactionSandbox into SandboxFactory
- Register database types in bootstrap_builtin_types
- Add connection validation with credential masking
- Add Behave BDD tests, Robot integration tests, ASV benchmarks

ISSUES CLOSED: #342
2026-03-10 19:29:27 +00:00

191 lines
6.0 KiB
Python

"""Helper utilities for database resource Robot smoke tests.
Each command prints a single ``<command>-ok`` token on success so the
calling Robot test can assert on ``stdout``. Only uses sqlite3 (stdlib)
for concrete database testing.
"""
from __future__ import annotations
import os
import sqlite3
import sys
import tempfile
from cleveragents.infrastructure.sandbox.factory import SandboxFactory
from cleveragents.infrastructure.sandbox.transaction_sandbox import (
TransactionSandbox,
)
from cleveragents.resource.handlers.database import (
DATABASE_TYPE_DEFS,
DATABASE_TYPE_NAMES,
DatabaseResourceHandler,
resolve_connection_args,
validate_connection,
)
from cleveragents.resource.handlers.protocol import ResourceHandler
from cleveragents.shared.redaction import mask_database_url
def _protocol_check() -> None:
"""Verify DatabaseResourceHandler satisfies ResourceHandler."""
handler = DatabaseResourceHandler()
assert isinstance(handler, ResourceHandler), (
"DatabaseResourceHandler not a ResourceHandler"
)
print("protocol-check-ok")
def _type_defs() -> None:
"""Verify all 4 database type definitions are present."""
names = {td["name"] for td in DATABASE_TYPE_DEFS}
for expected in ("postgres", "mysql", "sqlite", "duckdb"):
assert expected in names, f"Missing type def: {expected}"
assert names == DATABASE_TYPE_NAMES
print("type-defs-ok")
def _sqlite_validate() -> None:
"""Validate SQLite connection with a temp file."""
fd, path = tempfile.mkstemp(suffix=".db")
os.close(fd)
try:
success, msg = validate_connection("sqlite", {"path": path})
assert success, f"SQLite validation failed: {msg}"
print("sqlite-validate-ok")
finally:
os.unlink(path)
def _tx_commit() -> None:
"""Test transaction sandbox commit persists data."""
fd, path = tempfile.mkstemp(suffix=".db")
os.close(fd)
try:
# Setup table
conn = sqlite3.connect(path)
conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)")
conn.commit()
conn.close()
# Use sandbox
sandbox = TransactionSandbox(resource_id="robot-tx-1", original_path=path)
sandbox.create(plan_id="PLAN-ROBOT-COMMIT")
sandbox.execute("INSERT INTO items (id, name) VALUES (1, 'committed')")
result = sandbox.commit("robot commit")
assert result.success
sandbox.cleanup()
# Verify data persisted
conn = sqlite3.connect(path)
rows = conn.execute("SELECT name FROM items WHERE id = 1").fetchall()
conn.close()
assert len(rows) == 1
assert rows[0][0] == "committed"
print("tx-commit-ok")
finally:
os.unlink(path)
def _tx_rollback() -> None:
"""Test transaction sandbox rollback discards data."""
fd, path = tempfile.mkstemp(suffix=".db")
os.close(fd)
try:
# Setup table
conn = sqlite3.connect(path)
conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)")
conn.commit()
conn.close()
# Use sandbox
sandbox = TransactionSandbox(resource_id="robot-tx-2", original_path=path)
sandbox.create(plan_id="PLAN-ROBOT-ROLLBACK")
sandbox.execute("INSERT INTO items (id, name) VALUES (2, 'rolled-back')")
# Transition to ACTIVE by executing, then rollback
sandbox.rollback()
sandbox.cleanup()
# Verify data NOT persisted
conn = sqlite3.connect(path)
rows = conn.execute("SELECT name FROM items WHERE id = 2").fetchall()
conn.close()
assert len(rows) == 0
print("tx-rollback-ok")
finally:
os.unlink(path)
def _cred_mask() -> None:
"""Verify credential masking in validation messages."""
props = {"connection-string": "postgresql://admin:supersecret@db:5432/app"}
resolved = resolve_connection_args(props, "postgres")
_success, msg = validate_connection("postgres", resolved)
assert "supersecret" not in msg, f"Secret leaked: {msg}"
# Also test mask_database_url directly
masked = mask_database_url("postgresql://admin:supersecret@db:5432/app")
assert "supersecret" not in masked
print("cred-mask-ok")
def _factory_routing() -> None:
"""Verify SandboxFactory routes transaction_rollback correctly."""
factory = SandboxFactory()
sandbox = factory.create_sandbox(
resource_id="factory-test",
original_path=":memory:",
sandbox_strategy="transaction_rollback",
)
assert isinstance(sandbox, TransactionSandbox)
assert SandboxFactory.is_supported("transaction_rollback")
print("factory-routing-ok")
def _read_only() -> None:
"""Verify read-only sandbox prevents writes."""
fd, path = tempfile.mkstemp(suffix=".db")
os.close(fd)
try:
conn = sqlite3.connect(path)
conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)")
conn.commit()
conn.close()
sandbox = TransactionSandbox(
resource_id="ro-test", original_path=path, read_only=True
)
sandbox.create(plan_id="PLAN-RO-ROBOT")
rejected = False
try:
sandbox.execute("INSERT INTO items (id, name) VALUES (1, 'forbidden')")
except Exception:
rejected = True
finally:
sandbox.cleanup()
assert rejected, "Write was not rejected in read-only mode"
print("read-only-ok")
finally:
os.unlink(path)
_COMMANDS = {
"protocol-check": _protocol_check,
"type-defs": _type_defs,
"sqlite-validate": _sqlite_validate,
"tx-commit": _tx_commit,
"tx-rollback": _tx_rollback,
"cred-mask": _cred_mask,
"factory-routing": _factory_routing,
"read-only": _read_only,
}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(
f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>",
file=sys.stderr,
)
sys.exit(1)
_COMMANDS[sys.argv[1]]()