Files
ss-tools/agent/tests/test_agent/test_agent_context.py
busya b95df37cd5 refactor(agent): extract agent+shared into standalone packages with full GRACE semantic markup
- Move agent code from backend/src/agent/ to agent/src/ss_tools/agent/
- Extract shared stdlib-only utilities to shared/src/ss_tools/shared/
- Add #region/#endregion contracts to all ~140 functions (INV_1 compliance)
- Update docker files, entrypoint, build scripts for new package layout
- Backend now imports ss_tools.shared._llm_health (no gradio/langchain deps)
- Add specs for 036-039 feature plans
2026-07-07 15:18:24 +03:00

219 lines
7.4 KiB
Python

# agent/tests/test_agent/test_agent_context.py
# #region Test.Agent.Context [C:3] [TYPE Module] [SEMANTICS test,agent-chat,context,validation,uicontext]
# @BRIEF Contract tests for UIContext validation — enum checks, size limits, field lengths, boundary values.
# @RELATION BINDS_TO -> [AgentChat.Context.Validate]
# @TEST_FIXTURE: null -> returns {}
# @TEST_FIXTURE: dashboard+id+name -> passes
# @TEST_FIXTURE: malformed_objectType -> raises UIContextValidationError
# @TEST_FIXTURE: objectName>256 -> raises
# @TEST_FIXTURE: oversized>4KB -> raises
# @TEST_FIXTURE: contextVersion!=1 -> raises
# @TEST_FIXTURE: dataset_context -> passes
# @TEST_FIXTURE: migration_context -> passes
# @TEST_FIXTURE: non_numeric_objectId -> raises
# @TEST_FIXTURE: empty_route -> passes
import pytest
from ss_tools.agent._context import UIContextValidationError, validate_uicontext
# #region test_null_payload_returns_empty [C:2] [TYPE Function]
def test_null_payload_returns_empty_dict():
"""Null payloads should safely return an empty dict."""
assert validate_uicontext(None) == {}
# #endregion test_null_payload_returns_empty
# #region test_valid_dashboard_context_passes [C:2] [TYPE Function]
def test_valid_dashboard_context_passes():
"""Full dashboard UIContext with all fields should validate cleanly."""
payload = {
"objectType": "dashboard",
"objectId": "42",
"objectName": "Energy Report",
"envId": "ss-dev",
"route": "/dashboards/42",
"contextVersion": 1,
}
result = validate_uicontext(payload)
assert result == payload
# #endregion test_valid_dashboard_context_passes
# #region test_valid_dataset_context_passes [C:2] [TYPE Function]
def test_valid_dataset_context_passes():
"""Dataset UIContext should pass validation."""
payload = {
"objectType": "dataset",
"objectId": "15",
"objectName": None,
"envId": "staging",
"route": "/datasets/15",
"contextVersion": 1,
}
assert validate_uicontext(payload) == payload
# #endregion test_valid_dataset_context_passes
# #region test_valid_migration_context_passes [C:2] [TYPE Function]
def test_valid_migration_context_passes():
"""Migration UIContext should pass validation."""
payload = {
"objectType": "migration",
"objectId": None,
"objectName": None,
"envId": None,
"route": "/migrations",
"contextVersion": 1,
}
assert validate_uicontext(payload) == payload
# #endregion test_valid_migration_context_passes
# #region test_invalid_object_type_raises [C:2] [TYPE Function]
def test_invalid_object_type_raises_validation_error():
"""Unknown objectType values should be rejected."""
payload = {
"objectType": "chart",
"objectId": "42",
"envId": "ss-dev",
"route": "/dashboards/42",
"contextVersion": 1,
}
with pytest.raises(UIContextValidationError, match="objectType"):
validate_uicontext(payload)
# #endregion test_invalid_object_type_raises
# #region test_invalid_object_id_raises [C:2] [TYPE Function]
def test_non_numeric_object_id_raises():
"""ObjectId must be a numeric string or None."""
payload = {
"objectType": "dashboard",
"objectId": "abc-not-a-number",
"route": "/dashboards/42",
"contextVersion": 1,
}
with pytest.raises(UIContextValidationError, match="objectId"):
validate_uicontext(payload)
# #endregion test_invalid_object_id_raises
# #region test_object_name_exceeds_limit_raises [C:2] [TYPE Function]
def test_object_name_exceeds_256_chars_raises():
"""ObjectName longer than 256 characters should be rejected."""
payload = {
"objectType": "dashboard",
"objectId": "42",
"objectName": "A" * 257,
"route": "/dashboards/42",
"contextVersion": 1,
}
with pytest.raises(UIContextValidationError, match="objectName"):
validate_uicontext(payload)
# #endregion test_object_name_exceeds_limit_raises
# #region test_object_name_at_boundary_passes [C:2] [TYPE Function]
def test_object_name_at_256_chars_passes():
"""ObjectName at exactly 256 characters should pass."""
payload = {
"objectType": "dashboard",
"objectId": "42",
"objectName": "A" * 256,
"route": "/dashboards/42",
"contextVersion": 1,
}
assert validate_uicontext(payload) == payload
# #endregion test_object_name_at_boundary_passes
# #region test_invalid_context_version_raises [C:2] [TYPE Function]
def test_invalid_context_version_raises():
"""Only contextVersion=1 is currently supported."""
payload = {
"objectType": "dashboard",
"objectId": "42",
"route": "/dashboards/42",
"contextVersion": 2,
}
with pytest.raises(UIContextValidationError, match="contextVersion"):
validate_uicontext(payload)
# #endregion test_invalid_context_version_raises
# #region test_missing_context_version_raises [C:2] [TYPE Function]
def test_missing_context_version_raises():
"""ContextVersion should always be present and equal 1."""
payload = {
"objectType": "dashboard",
"objectId": "42",
"route": "/dashboards/42",
}
with pytest.raises(UIContextValidationError):
validate_uicontext(payload)
# #endregion test_missing_context_version_raises
# #region test_payload_exceeds_4kb_raises [C:2] [TYPE Function]
def test_payload_exceeds_4kb_raises():
"""Payloads larger than 4 KB should be rejected to prevent prompt injection."""
payload = {
"objectType": "dashboard",
"objectId": "42",
"route": "/dashboards/42",
"contextVersion": 1,
# Create a field that pushes the serialized JSON over 4096 bytes
"padding": "X" * 4096,
}
with pytest.raises(UIContextValidationError, match="exceeds 4 KB"):
validate_uicontext(payload)
# #endregion test_payload_exceeds_4kb_raises
# #region test_route_exceeds_512_chars_raises [C:2] [TYPE Function]
def test_route_exceeds_512_chars_raises():
"""Route length should be capped at 512 characters."""
payload = {
"objectType": "dashboard",
"objectId": "42",
"route": "/" + "a" * 512,
"contextVersion": 1,
}
with pytest.raises(UIContextValidationError, match="route"):
validate_uicontext(payload)
# #endregion test_route_exceeds_512_chars_raises
# #region test_env_id_none_and_string_accepted [C:2] [TYPE Function]
def test_env_id_none_and_string_accepted():
"""envId should accept None and string values."""
assert validate_uicontext({
"objectType": "dashboard", "objectId": "42",
"envId": None, "route": "/dashboards/42", "contextVersion": 1,
})["envId"] is None
assert validate_uicontext({
"objectType": "dashboard", "objectId": "42",
"envId": "ss-dev", "route": "/dashboards/42", "contextVersion": 1,
})["envId"] == "ss-dev"
# #endregion test_env_id_none_and_string_accepted
# #region test_without_object_type_passes [C:2] [TYPE Function]
def test_no_object_type_with_env_passes():
"""Context without objectType (general mode) should pass validation."""
payload = {
"objectType": None,
"objectId": None,
"envId": "ss-dev",
"route": "/settings",
"contextVersion": 1,
}
assert validate_uicontext(payload) == payload
# #endregion test_without_object_type_passes
# #endregion Test.Agent.Context