4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
235 lines
8.3 KiB
Python
235 lines
8.3 KiB
Python
"""Tests for the RDS investigation tools."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
from integrations.rds.tools.rds_describe_instance_tool import describe_rds_instance
|
|
from integrations.rds.tools.rds_events_tool import describe_rds_events
|
|
|
|
|
|
class _FakeAWSBackend:
|
|
"""Minimal AWSBackend stand-in that records calls for assertion."""
|
|
|
|
def __init__(self, db_response: dict[str, Any], events_response: dict[str, Any]) -> None:
|
|
self.db_response = db_response
|
|
self.events_response = events_response
|
|
self.db_calls: list[dict[str, Any]] = []
|
|
self.events_calls: list[dict[str, Any]] = []
|
|
|
|
def describe_db_instances(
|
|
self,
|
|
db_instance_identifier: str,
|
|
region: str = "",
|
|
**_: Any,
|
|
) -> dict[str, Any]:
|
|
self.db_calls.append({"db_instance_identifier": db_instance_identifier, "region": region})
|
|
return self.db_response
|
|
|
|
def describe_db_events(
|
|
self,
|
|
db_instance_identifier: str,
|
|
duration_minutes: int = 60,
|
|
region: str = "",
|
|
**_: Any,
|
|
) -> dict[str, Any]:
|
|
self.events_calls.append(
|
|
{
|
|
"db_instance_identifier": db_instance_identifier,
|
|
"duration_minutes": duration_minutes,
|
|
"region": region,
|
|
}
|
|
)
|
|
return self.events_response
|
|
|
|
|
|
@patch("integrations.rds.tools.rds_describe_instance_tool.execute_aws_sdk_call")
|
|
def test_describe_rds_instance_success(mock_call) -> None:
|
|
mock_call.return_value = {
|
|
"success": True,
|
|
"data": {
|
|
"DBInstances": [
|
|
{
|
|
"DBInstanceStatus": "available",
|
|
"Engine": "postgres",
|
|
"EngineVersion": "15.4",
|
|
"DBInstanceClass": "db.t4g.micro",
|
|
"MultiAZ": True,
|
|
"PubliclyAccessible": False,
|
|
"StorageType": "gp3",
|
|
"AllocatedStorage": 20,
|
|
"Endpoint": {"Address": "prod.abc.rds.aws", "Port": 5432},
|
|
"AvailabilityZone": "us-east-1a",
|
|
"PreferredBackupWindow": "03:00-04:00",
|
|
"BackupRetentionPeriod": 7,
|
|
}
|
|
]
|
|
},
|
|
}
|
|
|
|
result = describe_rds_instance("prod-db", region="us-east-1")
|
|
assert result["available"] is True
|
|
assert result["status"] == "available"
|
|
assert result["engine"] == "postgres"
|
|
assert result["multi_az"] is True
|
|
assert result["endpoint"]["port"] == 5432
|
|
|
|
|
|
@patch("integrations.rds.tools.rds_describe_instance_tool.execute_aws_sdk_call")
|
|
def test_describe_rds_instance_not_found(mock_call) -> None:
|
|
mock_call.return_value = {"success": True, "data": {"DBInstances": []}}
|
|
|
|
result = describe_rds_instance("missing-db")
|
|
assert result["available"] is False
|
|
assert "No RDS instance" in result["error"]
|
|
|
|
|
|
@patch("integrations.rds.tools.rds_describe_instance_tool.execute_aws_sdk_call")
|
|
def test_describe_rds_instance_aws_failure(mock_call) -> None:
|
|
mock_call.return_value = {"success": False, "error": "AccessDenied"}
|
|
|
|
result = describe_rds_instance("prod-db")
|
|
assert result["available"] is False
|
|
assert result["error"] == "Failed to describe the RDS instance. Check server logs for details."
|
|
|
|
|
|
@patch("integrations.rds.tools.rds_describe_instance_tool.execute_aws_sdk_call")
|
|
def test_describe_rds_instance_multiple_instances_warns(mock_call, caplog) -> None:
|
|
"""When AWS returns >1 instance, use the first and emit a warning."""
|
|
mock_call.return_value = {
|
|
"success": True,
|
|
"data": {
|
|
"DBInstances": [
|
|
{
|
|
"DBInstanceStatus": "available",
|
|
"Engine": "postgres",
|
|
"EngineVersion": "15.4",
|
|
"DBInstanceClass": "db.t4g.micro",
|
|
"Endpoint": {"Address": "prod.abc.rds.aws", "Port": 5432},
|
|
},
|
|
{
|
|
"DBInstanceStatus": "available",
|
|
"Engine": "mysql",
|
|
"EngineVersion": "8.0",
|
|
"DBInstanceClass": "db.r6g.large",
|
|
"Endpoint": {"Address": "replica.abc.rds.aws", "Port": 3306},
|
|
},
|
|
]
|
|
},
|
|
}
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
result = describe_rds_instance("prod-db")
|
|
|
|
assert result["available"] is True
|
|
assert result["engine"] == "postgres" # first instance used
|
|
assert "returned 2 instances" in caplog.text
|
|
|
|
|
|
@patch("integrations.rds.tools.rds_events_tool.execute_aws_sdk_call")
|
|
def test_describe_rds_events_success(mock_call) -> None:
|
|
mock_call.return_value = {
|
|
"success": True,
|
|
"data": {
|
|
"Events": [
|
|
{
|
|
"Date": "2026-05-05T12:00:00Z",
|
|
"Message": "Multi-AZ instance failover started",
|
|
"EventCategories": ["failover"],
|
|
"SourceType": "db-instance",
|
|
}
|
|
]
|
|
},
|
|
}
|
|
|
|
result = describe_rds_events("prod-db", duration_minutes=120)
|
|
assert result["available"] is True
|
|
assert result["total_events"] == 1
|
|
assert result["events"][0]["categories"] == ["failover"]
|
|
|
|
|
|
@patch("integrations.rds.tools.rds_events_tool.execute_aws_sdk_call")
|
|
def test_describe_rds_events_no_events(mock_call) -> None:
|
|
mock_call.return_value = {"success": True, "data": {"Events": []}}
|
|
|
|
result = describe_rds_events("prod-db")
|
|
assert result["available"] is True
|
|
assert result["total_events"] == 0
|
|
assert result["events"] == []
|
|
|
|
|
|
@patch("integrations.rds.tools.rds_events_tool.execute_aws_sdk_call")
|
|
def test_describe_rds_events_failure(mock_call) -> None:
|
|
mock_call.return_value = {"success": False, "error": "ThrottlingException"}
|
|
|
|
result = describe_rds_events("prod-db")
|
|
assert result["available"] is False
|
|
assert result["error"] == "Failed to describe RDS events. Check server logs for details."
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Synthetic-mode short-circuit: when an aws_backend handle is provided the
|
|
# tool MUST route through it instead of boto3. This guards against the
|
|
# regression where synthetic scenarios silently leaked describe_db_instances
|
|
# calls to whatever real AWS account the developer was authenticated against.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@patch("integrations.rds.tools.rds_describe_instance_tool.execute_aws_sdk_call")
|
|
def test_describe_rds_instance_short_circuits_to_aws_backend(mock_call) -> None:
|
|
backend = _FakeAWSBackend(
|
|
db_response={
|
|
"source": "rds",
|
|
"available": True,
|
|
"db_instance_identifier": "synthetic-db",
|
|
"engine": "mysql",
|
|
"error": None,
|
|
},
|
|
events_response={},
|
|
)
|
|
|
|
result = describe_rds_instance(
|
|
"synthetic-db",
|
|
region="us-east-1",
|
|
aws_backend=backend,
|
|
)
|
|
|
|
mock_call.assert_not_called()
|
|
assert backend.db_calls == [{"db_instance_identifier": "synthetic-db", "region": "us-east-1"}]
|
|
assert result["engine"] == "mysql"
|
|
|
|
|
|
@patch("integrations.rds.tools.rds_events_tool.execute_aws_sdk_call")
|
|
def test_describe_rds_events_short_circuits_to_aws_backend(mock_call) -> None:
|
|
backend = _FakeAWSBackend(
|
|
db_response={},
|
|
events_response={
|
|
"source": "rds",
|
|
"available": True,
|
|
"db_instance_identifier": "synthetic-db",
|
|
"duration_minutes": 90,
|
|
"total_events": 1,
|
|
"events": [{"date": "2026-05-05T12:00:00Z", "message": "ok"}],
|
|
"error": None,
|
|
},
|
|
)
|
|
|
|
result = describe_rds_events(
|
|
"synthetic-db",
|
|
region="us-east-1",
|
|
duration_minutes=90,
|
|
aws_backend=backend,
|
|
)
|
|
|
|
mock_call.assert_not_called()
|
|
assert backend.events_calls == [
|
|
{
|
|
"db_instance_identifier": "synthetic-db",
|
|
"duration_minutes": 90,
|
|
"region": "us-east-1",
|
|
}
|
|
]
|
|
assert result["total_events"] == 1
|