# 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