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
This commit is contained in:
218
agent/tests/test_agent/test_agent_context.py
Normal file
218
agent/tests/test_agent/test_agent_context.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user