chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
"""Unit tests for the data_validation utilities."""
|
||||
|
||||
from core.tool_framework.utils.data_validation import (
|
||||
MetricsValidator,
|
||||
_validate_data_list,
|
||||
validate_host_metrics,
|
||||
)
|
||||
|
||||
|
||||
def test_impossible_percentages():
|
||||
"""Test that percentages over 100 are flagged as impossible."""
|
||||
validator = MetricsValidator()
|
||||
|
||||
# Disk percent > 100
|
||||
payload = {"disk": {"percent": 150}}
|
||||
result = validator.validate_metrics(payload)
|
||||
|
||||
assert result["disk"]["percent_invalid"] is True
|
||||
assert result["disk"]["percent_raw"] == 150
|
||||
assert result["disk"]["percent"] is None
|
||||
|
||||
# Check that data quality issues are attached
|
||||
issues = result["data_quality_issues"]
|
||||
assert len(issues) == 1
|
||||
assert issues[0]["field"] == "disk.percent"
|
||||
assert issues[0]["issue"] == "impossible_percentage"
|
||||
assert issues[0]["severity"] == "error"
|
||||
|
||||
|
||||
def test_cpu_suspicious_percentage():
|
||||
"""Test that CPU > 1000% is flagged as suspicious but not strictly impossible."""
|
||||
validator = MetricsValidator()
|
||||
result = validator.validate_metrics({"cpu": {"percent": 1500}})
|
||||
|
||||
assert result["cpu"]["percent_suspicious"] is True
|
||||
assert result["cpu"]["percent_raw"] == 1500
|
||||
assert result["data_quality_issues"][0]["issue"] == "suspicious_value"
|
||||
assert result["data_quality_issues"][0]["severity"] == "warning"
|
||||
|
||||
|
||||
def test_byte_to_gb_and_mb_inference():
|
||||
"""Test memory unit inference for bytes masquerading as percentages."""
|
||||
validator = MetricsValidator()
|
||||
|
||||
# 8 GB in bytes
|
||||
gb_bytes = 8 * (1024**3)
|
||||
result_gb = validator.validate_metrics({"memory": {"percent": gb_bytes}})
|
||||
|
||||
assert result_gb["memory"]["percent_invalid"] is True
|
||||
interpretation_gb = result_gb["memory"]["percent_interpretation"]
|
||||
assert interpretation_gb["likely_unit"] == "bytes"
|
||||
assert interpretation_gb["likely_value_gb"] == 8.0
|
||||
|
||||
# 500 MB in bytes
|
||||
mb_bytes = 500 * (1024**2)
|
||||
result_mb = validator.validate_metrics({"memory": {"percent": mb_bytes}})
|
||||
|
||||
assert result_mb["memory"]["percent_invalid"] is True
|
||||
interpretation_mb = result_mb["memory"]["percent_interpretation"]
|
||||
assert interpretation_mb["likely_unit"] == "bytes"
|
||||
assert interpretation_mb["likely_value_mb"] == 500.0
|
||||
|
||||
|
||||
def test_class_flat_metric_payload():
|
||||
"""Test class validation on flat API response structures."""
|
||||
payload = {"cpu": 95, "ram": 8589934592, "disk": 50}
|
||||
validator = MetricsValidator()
|
||||
result = validator.validate_metrics(payload)
|
||||
|
||||
# Ram should be flagged as invalid and inferred as 8 GB
|
||||
assert result["ram_invalid"] is True
|
||||
assert result["ram_interpretation"]["likely_unit"] == "bytes"
|
||||
assert result["ram_interpretation"]["likely_value_gb"] == 8.0
|
||||
|
||||
# Issues should be attached at the root level
|
||||
assert "data_quality_issues" in result
|
||||
assert result["data_quality_issues"][0]["field"] == "ram"
|
||||
|
||||
|
||||
def test_wrapper_flat_metric_payload():
|
||||
"""Test wrapper validation on flat API response structures."""
|
||||
payload = {"cpu": 95, "ram": 8589934592, "disk": 50}
|
||||
result = validate_host_metrics(payload)
|
||||
|
||||
# Ram should be flagged as invalid and inferred as 8 GB
|
||||
assert result["ram_invalid"] is True
|
||||
assert result["ram_interpretation"]["likely_unit"] == "bytes"
|
||||
assert result["ram_interpretation"]["likely_value_gb"] == 8.0
|
||||
|
||||
# Issues should be attached at the root level
|
||||
assert "data_quality_issues" in result
|
||||
assert result["data_quality_issues"][0]["field"] == "ram"
|
||||
|
||||
|
||||
def test_nested_metric_payload():
|
||||
"""Test validation on deeply nested API response structures."""
|
||||
payload = {"memory": {"percent": 8589934592}, "cpu": {"percent": 95}}
|
||||
validator = MetricsValidator()
|
||||
result = validator.validate_metrics(payload)
|
||||
|
||||
assert result["memory"]["percent_invalid"] is True
|
||||
assert "percent_interpretation" in result["memory"]
|
||||
assert result["data_quality_issues"][0]["field"] == "memory.percent"
|
||||
|
||||
|
||||
def test_class_list_structure_payload():
|
||||
"""Test class validation on list-based API response structures."""
|
||||
payload = {
|
||||
"success": True,
|
||||
"data": [
|
||||
{"cpu": 95, "ram": 8589934592, "disk": 50},
|
||||
{"cpu": 10, "ram": 45, "disk": 20}, # Valid payload
|
||||
],
|
||||
}
|
||||
validator = MetricsValidator()
|
||||
result = validator.validate_metrics(payload)
|
||||
|
||||
# First item in data array should have validation flags
|
||||
assert result["data"][0]["ram_invalid"] is True
|
||||
assert result["data"][0]["ram_interpretation"]["likely_unit"] == "bytes"
|
||||
|
||||
# Second item should remain untouched
|
||||
assert "ram_invalid" not in result["data"][1]
|
||||
|
||||
# Root level should aggregate the data_quality_issues
|
||||
assert "data_quality_issues" in result
|
||||
assert len(result["data_quality_issues"]) == 1
|
||||
assert result["data_quality_issues"][0]["field"] == "ram"
|
||||
|
||||
|
||||
def test_wrapper_list_structure_payload():
|
||||
"""Test wrapper validation on lists (Currently expected to fail due to bug)."""
|
||||
payload = {
|
||||
"success": True,
|
||||
"data": [{"cpu": 95, "ram": 8589934592, "disk": 50}, {"cpu": 10, "ram": 45, "disk": 20}],
|
||||
}
|
||||
result = validate_host_metrics(payload)
|
||||
|
||||
# This assertion WILL fail because of the bug, but xfail tells pytest we expect it to!
|
||||
assert "data_quality_issues" in result
|
||||
|
||||
|
||||
def test_invalid_format():
|
||||
"""Test fallback when metrics is not a dictionary."""
|
||||
result = validate_host_metrics("this is just a string, not a dict")
|
||||
assert result["validated"] is False
|
||||
assert result["data_quality_issues"][0]["issue"] == "invalid_format"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _validate_data_list helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_validate_data_list_strips_issues_from_each_point() -> None:
|
||||
"""_validate_data_list must strip data_quality_issues from each point and collect them."""
|
||||
validator = MetricsValidator()
|
||||
data = [
|
||||
{"cpu": 95, "ram": 8589934592, "disk": 50},
|
||||
{"cpu": 10, "ram": 45, "disk": 20},
|
||||
]
|
||||
validated_points, all_issues = _validate_data_list(data, validator.validate_metrics)
|
||||
|
||||
assert len(validated_points) == 2
|
||||
# Issues stripped from each point
|
||||
assert "data_quality_issues" not in validated_points[0]
|
||||
assert "data_quality_issues" not in validated_points[1]
|
||||
# Issues collected at the top level
|
||||
assert len(all_issues) >= 1
|
||||
fields = [issue["field"] for issue in all_issues]
|
||||
assert "ram" in fields
|
||||
|
||||
|
||||
def test_validate_data_list_passes_through_non_dict_items() -> None:
|
||||
"""Non-dict items in the list must be passed through unchanged."""
|
||||
validated_points, issues = _validate_data_list(["plain string", 42], lambda x: x) # type: ignore[arg-type]
|
||||
assert validated_points == ["plain string", 42]
|
||||
assert issues == []
|
||||
|
||||
|
||||
def test_validate_data_list_empty_input() -> None:
|
||||
"""Empty list produces empty outputs."""
|
||||
validated_points, issues = _validate_data_list([], lambda x: x)
|
||||
assert validated_points == []
|
||||
assert issues == []
|
||||
Reference in New Issue
Block a user