diff --git a/src/telemetry/configuration_snapshot.py b/src/telemetry/configuration_snapshot.py index e0e9a9fe2..815572aab 100644 --- a/src/telemetry/configuration_snapshot.py +++ b/src/telemetry/configuration_snapshot.py @@ -79,9 +79,11 @@ class ListFieldSpec: FieldSpec("service.workers", MaskingType.PASSTHROUGH), FieldSpec("service.host", MaskingType.SENSITIVE), FieldSpec("service.port", MaskingType.PASSTHROUGH), + FieldSpec("service.base_url", MaskingType.SENSITIVE), FieldSpec("service.auth_enabled", MaskingType.PASSTHROUGH), FieldSpec("service.color_log", MaskingType.PASSTHROUGH), FieldSpec("service.access_log", MaskingType.PASSTHROUGH), + FieldSpec("service.root_path", MaskingType.SENSITIVE), FieldSpec("service.tls_config.tls_certificate_path", MaskingType.SENSITIVE), FieldSpec("service.tls_config.tls_key_path", MaskingType.SENSITIVE), FieldSpec("service.tls_config.tls_key_password", MaskingType.SENSITIVE), @@ -94,11 +96,32 @@ class ListFieldSpec: FieldSpec("llama_stack.url", MaskingType.SENSITIVE), FieldSpec("llama_stack.api_key", MaskingType.SENSITIVE), FieldSpec("llama_stack.library_client_config_path", MaskingType.SENSITIVE), + FieldSpec("llama_stack.timeout", MaskingType.PASSTHROUGH), + FieldSpec("llama_stack.max_retries", MaskingType.PASSTHROUGH), + FieldSpec("llama_stack.retry_delay", MaskingType.PASSTHROUGH), + FieldSpec("llama_stack.allow_degraded_mode", MaskingType.PASSTHROUGH), + FieldSpec("llama_stack.config.baseline", MaskingType.PASSTHROUGH), + FieldSpec("llama_stack.config.profile", MaskingType.SENSITIVE), + FieldSpec("llama_stack.config.native_override", MaskingType.SENSITIVE), FieldSpec("inference.default_model", MaskingType.PASSTHROUGH), FieldSpec("inference.default_provider", MaskingType.PASSTHROUGH), + FieldSpec("inference.context_windows", MaskingType.PASSTHROUGH), + FieldSpec("inference.max_infer_iters", MaskingType.PASSTHROUGH), + FieldSpec("inference.max_tool_calls", MaskingType.PASSTHROUGH), + ListFieldSpec( + "inference.providers", + item_fields=( + FieldSpec("type", MaskingType.PASSTHROUGH), + FieldSpec("id", MaskingType.PASSTHROUGH), + FieldSpec("api_key_env", MaskingType.SENSITIVE), + FieldSpec("allowed_models", MaskingType.PASSTHROUGH), + ), + ), # Authentication & Authorization FieldSpec("authentication.module", MaskingType.PASSTHROUGH), FieldSpec("authentication.skip_tls_verification", MaskingType.PASSTHROUGH), + FieldSpec("authentication.skip_for_health_probes", MaskingType.PASSTHROUGH), + FieldSpec("authentication.skip_for_metrics", MaskingType.PASSTHROUGH), FieldSpec("authentication.k8s_cluster_api", MaskingType.SENSITIVE), FieldSpec("authentication.k8s_ca_cert_path", MaskingType.SENSITIVE), FieldSpec("authentication.jwk_config.url", MaskingType.SENSITIVE), @@ -120,6 +143,26 @@ class ListFieldSpec: FieldSpec("negate", MaskingType.PASSTHROUGH), ), ), + FieldSpec("authentication.api_key_config.api_key", MaskingType.SENSITIVE), + FieldSpec( + "authentication.rh_identity_config.required_entitlements", + MaskingType.SENSITIVE, + ), + FieldSpec( + "authentication.rh_identity_config.max_header_size", + MaskingType.PASSTHROUGH, + ), + FieldSpec( + "authentication.trusted_proxy_config.user_header", + MaskingType.PASSTHROUGH, + ), + ListFieldSpec( + "authentication.trusted_proxy_config.allowed_service_accounts", + item_fields=( + FieldSpec("namespace", MaskingType.SENSITIVE), + FieldSpec("name", MaskingType.SENSITIVE), + ), + ), ListFieldSpec( "authorization.access_rules", item_fields=( @@ -127,6 +170,11 @@ class ListFieldSpec: FieldSpec("actions", MaskingType.PASSTHROUGH), ), ), + # Azure Entra ID + FieldSpec("azure_entra_id.tenant_id", MaskingType.SENSITIVE), + FieldSpec("azure_entra_id.client_id", MaskingType.SENSITIVE), + FieldSpec("azure_entra_id.client_secret", MaskingType.SENSITIVE), + FieldSpec("azure_entra_id.scope", MaskingType.PASSTHROUGH), # User Data Collection Features FieldSpec("user_data_collection.feedback_enabled", MaskingType.PASSTHROUGH), FieldSpec("user_data_collection.feedback_storage", MaskingType.SENSITIVE), @@ -135,7 +183,10 @@ class ListFieldSpec: # AI/ML Capabilities Configuration FieldSpec("customization.system_prompt", MaskingType.SENSITIVE), FieldSpec("customization.system_prompt_path", MaskingType.SENSITIVE), + FieldSpec("customization.profile_path", MaskingType.SENSITIVE), FieldSpec("customization.disable_query_system_prompt", MaskingType.PASSTHROUGH), + FieldSpec("customization.disable_shield_ids_override", MaskingType.PASSTHROUGH), + FieldSpec("customization.agent_card_path", MaskingType.SENSITIVE), # Database & Storage Configuration FieldSpec("database.sqlite.db_path", MaskingType.SENSITIVE), FieldSpec("database.postgres.host", MaskingType.SENSITIVE), @@ -147,6 +198,128 @@ class ListFieldSpec: FieldSpec("database.postgres.ssl_mode", MaskingType.PASSTHROUGH), FieldSpec("database.postgres.gss_encmode", MaskingType.PASSTHROUGH), FieldSpec("database.postgres.ca_cert_path", MaskingType.SENSITIVE), + # Conversation Cache + FieldSpec("conversation_cache.type", MaskingType.PASSTHROUGH), + FieldSpec("conversation_cache.memory.max_entries", MaskingType.PASSTHROUGH), + FieldSpec("conversation_cache.sqlite.db_path", MaskingType.SENSITIVE), + FieldSpec("conversation_cache.postgres.host", MaskingType.SENSITIVE), + FieldSpec("conversation_cache.postgres.port", MaskingType.PASSTHROUGH), + FieldSpec("conversation_cache.postgres.db", MaskingType.SENSITIVE), + FieldSpec("conversation_cache.postgres.user", MaskingType.SENSITIVE), + FieldSpec("conversation_cache.postgres.password", MaskingType.SENSITIVE), + FieldSpec("conversation_cache.postgres.namespace", MaskingType.SENSITIVE), + FieldSpec("conversation_cache.postgres.ssl_mode", MaskingType.PASSTHROUGH), + FieldSpec("conversation_cache.postgres.gss_encmode", MaskingType.PASSTHROUGH), + FieldSpec("conversation_cache.postgres.ca_cert_path", MaskingType.SENSITIVE), + # Conversation Compaction + FieldSpec("compaction.enabled", MaskingType.PASSTHROUGH), + FieldSpec("compaction.threshold_ratio", MaskingType.PASSTHROUGH), + FieldSpec("compaction.token_floor", MaskingType.PASSTHROUGH), + FieldSpec("compaction.buffer_turns", MaskingType.PASSTHROUGH), + FieldSpec("compaction.buffer_max_ratio", MaskingType.PASSTHROUGH), + # Quota Handlers + FieldSpec("quota_handlers.sqlite.db_path", MaskingType.SENSITIVE), + FieldSpec("quota_handlers.postgres.host", MaskingType.SENSITIVE), + FieldSpec("quota_handlers.postgres.port", MaskingType.PASSTHROUGH), + FieldSpec("quota_handlers.postgres.db", MaskingType.SENSITIVE), + FieldSpec("quota_handlers.postgres.user", MaskingType.SENSITIVE), + FieldSpec("quota_handlers.postgres.password", MaskingType.SENSITIVE), + FieldSpec("quota_handlers.postgres.namespace", MaskingType.SENSITIVE), + FieldSpec("quota_handlers.postgres.ssl_mode", MaskingType.PASSTHROUGH), + FieldSpec("quota_handlers.postgres.gss_encmode", MaskingType.PASSTHROUGH), + FieldSpec("quota_handlers.postgres.ca_cert_path", MaskingType.SENSITIVE), + ListFieldSpec( + "quota_handlers.limiters", + item_fields=( + FieldSpec("type", MaskingType.PASSTHROUGH), + FieldSpec("name", MaskingType.PASSTHROUGH), + FieldSpec("initial_quota", MaskingType.PASSTHROUGH), + FieldSpec("quota_increase", MaskingType.PASSTHROUGH), + FieldSpec("period", MaskingType.PASSTHROUGH), + ), + ), + FieldSpec("quota_handlers.scheduler.period", MaskingType.PASSTHROUGH), + FieldSpec( + "quota_handlers.scheduler.database_reconnection_count", + MaskingType.PASSTHROUGH, + ), + FieldSpec( + "quota_handlers.scheduler.database_reconnection_delay", + MaskingType.PASSTHROUGH, + ), + FieldSpec("quota_handlers.enable_token_history", MaskingType.PASSTHROUGH), + # BYOK RAG + ListFieldSpec( + "byok_rag", + item_fields=( + FieldSpec("rag_id", MaskingType.PASSTHROUGH), + FieldSpec("rag_type", MaskingType.PASSTHROUGH), + FieldSpec("embedding_model", MaskingType.PASSTHROUGH), + FieldSpec("embedding_dimension", MaskingType.PASSTHROUGH), + FieldSpec("vector_db_id", MaskingType.PASSTHROUGH), + FieldSpec("db_path", MaskingType.SENSITIVE), + FieldSpec("score_multiplier", MaskingType.PASSTHROUGH), + FieldSpec("host", MaskingType.SENSITIVE), + FieldSpec("port", MaskingType.SENSITIVE), + FieldSpec("db", MaskingType.SENSITIVE), + FieldSpec("user", MaskingType.SENSITIVE), + FieldSpec("password", MaskingType.SENSITIVE), + ), + ), + # A2A State + FieldSpec("a2a_state.sqlite.db_path", MaskingType.SENSITIVE), + FieldSpec("a2a_state.postgres.host", MaskingType.SENSITIVE), + FieldSpec("a2a_state.postgres.port", MaskingType.PASSTHROUGH), + FieldSpec("a2a_state.postgres.db", MaskingType.SENSITIVE), + FieldSpec("a2a_state.postgres.user", MaskingType.SENSITIVE), + FieldSpec("a2a_state.postgres.password", MaskingType.SENSITIVE), + FieldSpec("a2a_state.postgres.namespace", MaskingType.SENSITIVE), + FieldSpec("a2a_state.postgres.ssl_mode", MaskingType.PASSTHROUGH), + FieldSpec("a2a_state.postgres.gss_encmode", MaskingType.PASSTHROUGH), + FieldSpec("a2a_state.postgres.ca_cert_path", MaskingType.SENSITIVE), + # A2A Agents + ListFieldSpec( + "a2a_agents.agents", + item_fields=( + FieldSpec("name", MaskingType.PASSTHROUGH), + FieldSpec("url", MaskingType.SENSITIVE), + FieldSpec("auth_token", MaskingType.SENSITIVE), + FieldSpec("timeout", MaskingType.PASSTHROUGH), + FieldSpec("max_retries", MaskingType.PASSTHROUGH), + ), + ), + # Splunk + FieldSpec("splunk.enabled", MaskingType.PASSTHROUGH), + FieldSpec("splunk.url", MaskingType.SENSITIVE), + FieldSpec("splunk.token_path", MaskingType.SENSITIVE), + FieldSpec("splunk.index", MaskingType.SENSITIVE), + FieldSpec("splunk.source", MaskingType.PASSTHROUGH), + FieldSpec("splunk.timeout", MaskingType.PASSTHROUGH), + FieldSpec("splunk.verify_ssl", MaskingType.PASSTHROUGH), + # RAG Strategy + FieldSpec("rag.inline", MaskingType.PASSTHROUGH), + FieldSpec("rag.tool", MaskingType.PASSTHROUGH), + # OKP + FieldSpec("okp.rhokp_url", MaskingType.SENSITIVE), + FieldSpec("okp.offline", MaskingType.PASSTHROUGH), + FieldSpec("okp.chunk_filter_query", MaskingType.SENSITIVE), + # Reranker + FieldSpec("reranker.enabled", MaskingType.PASSTHROUGH), + FieldSpec("reranker.model", MaskingType.PASSTHROUGH), + # Approvals + FieldSpec("approvals.approval_timeout_seconds", MaskingType.PASSTHROUGH), + FieldSpec("approvals.approval_retention_days", MaskingType.PASSTHROUGH), + # rlsapi v1 + FieldSpec("rlsapi_v1.allow_verbose_infer", MaskingType.PASSTHROUGH), + FieldSpec("rlsapi_v1.quota_subject", MaskingType.PASSTHROUGH), + # Saved Prompts + FieldSpec("saved_prompts.max_prompts_per_user", MaskingType.PASSTHROUGH), + FieldSpec("saved_prompts.max_display_name_length", MaskingType.PASSTHROUGH), + FieldSpec("saved_prompts.max_content_length", MaskingType.PASSTHROUGH), + # Skills + FieldSpec("skills.paths", MaskingType.SENSITIVE), + # Deployment Environment + FieldSpec("deployment_environment", MaskingType.PASSTHROUGH), # Integration & Connectivity ListFieldSpec( "mcp_servers", @@ -154,6 +327,10 @@ class ListFieldSpec: FieldSpec("name", MaskingType.PASSTHROUGH), FieldSpec("provider_id", MaskingType.PASSTHROUGH), FieldSpec("url", MaskingType.SENSITIVE), + FieldSpec("authorization_headers", MaskingType.SENSITIVE), + FieldSpec("headers", MaskingType.SENSITIVE), + FieldSpec("require_approval", MaskingType.PASSTHROUGH), + FieldSpec("timeout", MaskingType.PASSTHROUGH), ), ), ) @@ -303,7 +480,7 @@ def mask_value(value: Any, masking: MaskingType) -> Any: The masked or serialized value. """ if masking == MaskingType.SENSITIVE: - if value is None: + if value is None or value == "": return NOT_CONFIGURED return CONFIGURED return _serialize_passthrough(value) diff --git a/tests/unit/telemetry/conftest.py b/tests/unit/telemetry/conftest.py index 6b2db6a82..7905ce04d 100644 --- a/tests/unit/telemetry/conftest.py +++ b/tests/unit/telemetry/conftest.py @@ -8,25 +8,50 @@ from pydantic import SecretStr from models.config import ( + A2AAgentEndpointConfiguration, + A2AAgentsConfiguration, + A2AStateConfiguration, AccessRule, Action, + APIKeyTokenConfiguration, + ApprovalsConfiguration, AuthenticationConfiguration, AuthorizationConfiguration, + AzureEntraIdConfiguration, + ByokRag, + CompactionConfiguration, Configuration, + ConversationHistoryConfiguration, CORSConfiguration, Customization, DatabaseConfiguration, InferenceConfiguration, + InMemoryCacheConfig, JsonPathOperator, JwkConfiguration, JwtConfiguration, JwtRoleRule, LlamaStackConfiguration, ModelContextProtocolServer, + OkpConfiguration, PostgreSQLDatabaseConfiguration, + QuotaHandlersConfiguration, + QuotaLimiterConfiguration, + QuotaSchedulerConfiguration, + RagConfiguration, + RerankerConfiguration, + RHIdentityConfiguration, + RlsapiV1Configuration, + SavedPromptsConfiguration, ServiceConfiguration, + SkillsConfiguration, + SplunkConfiguration, SQLiteDatabaseConfiguration, TLSConfiguration, + TrustedProxyConfiguration, + TrustedProxyServiceAccount, + UnifiedInferenceProvider, + UnifiedLlamaStackConfig, UserDataCollection, ) @@ -58,6 +83,55 @@ PII_PG_NAMESPACE = "production_ns" PII_PG_CA_CERT = "/etc/ssl/postgres/ca.crt" PII_MCP_URL = "https://mcp.internal.corp.com:9090" +PII_MCP_AUTH_HEADER_VALUE = "/etc/secrets/mcp-token.txt" +PII_BASE_URL = "https://lightspeed.internal.corp.com" +PII_ROOT_PATH = "/api/v1/lightspeed" +PII_PROFILE_PATH = "/opt/lightspeed/custom_profile.py" +PII_AGENT_CARD_PATH = "/opt/lightspeed/agent_card.yaml" +PII_CACHE_SQLITE_PATH = "/var/lib/lightspeed/cache.sqlite" +PII_CACHE_PG_HOST = "cache-db.internal.corp.com" +PII_CACHE_PG_DB = "lightspeed_cache" +PII_CACHE_PG_USER = "cache_admin" +PII_CACHE_PG_PASS = "CacheP@ss!Secret" +PII_CACHE_PG_NAMESPACE = "cache_ns" +PII_CACHE_PG_CA_CERT = "/etc/ssl/cache/ca.crt" +PII_QUOTA_SQLITE_PATH = "/var/lib/lightspeed/quota.sqlite" +PII_QUOTA_PG_HOST = "quota-db.internal.corp.com" +PII_QUOTA_PG_DB = "lightspeed_quota" +PII_QUOTA_PG_USER = "quota_admin" +PII_QUOTA_PG_PASS = "QuotaP@ss!Secret" +PII_QUOTA_PG_NAMESPACE = "quota_ns" +PII_QUOTA_PG_CA_CERT = "/etc/ssl/quota/ca.crt" +PII_BYOK_DB_PATH = "/var/lib/lightspeed/byok_rag.db" +PII_BYOK_HOST = "byok-db.internal.corp.com" +PII_BYOK_PORT = "5433" +PII_BYOK_DB = "byok_vectors" +PII_BYOK_USER = "byok_admin" +PII_BYOK_PASS = "ByokP@ss!Secret" +PII_A2A_SQLITE_PATH = "/var/lib/lightspeed/a2a.sqlite" +PII_A2A_PG_HOST = "a2a-db.internal.corp.com" +PII_A2A_PG_DB = "lightspeed_a2a" +PII_A2A_PG_USER = "a2a_admin" +PII_A2A_PG_PASS = "A2aP@ss!Secret" +PII_A2A_PG_NAMESPACE = "a2a_ns" +PII_A2A_PG_CA_CERT = "/etc/ssl/a2a/ca.crt" +PII_A2A_AGENT_URL = "https://a2a-agent.internal.corp.com:8443" +PII_A2A_AGENT_TOKEN = "a2a-secret-bearer-token-xyz" +PII_SPLUNK_URL = "https://splunk-hec.internal.corp.com:8088" +PII_SPLUNK_TOKEN_PATH = "/etc/secrets/splunk-token.txt" +PII_SPLUNK_INDEX = "lightspeed_prod_index" +PII_OKP_URL = "https://okp.internal.corp.com:9443" +PII_OKP_CHUNK_FILTER = "product:ansible AND product:*openshift*" +PII_AZURE_TENANT_ID = "azure-tenant-id-secret-12345" +PII_AZURE_CLIENT_ID = "azure-client-id-secret-67890" +PII_AZURE_CLIENT_SECRET = "azure-client-secret-abcdef" +PII_RH_IDENTITY_ENTITLEMENTS = "insights,openshift" +PII_TRUSTED_PROXY_SA_NS = "proxy-namespace-secret" +PII_TRUSTED_PROXY_SA_NAME = "proxy-sa-secret-name" +PII_SKILLS_PATH = "/opt/lightspeed/skills" +PII_LS_PROFILE = "/opt/llama-stack/custom-profile.yaml" +PII_LS_NATIVE_OVERRIDE = "override-secret-value" +PII_PROVIDER_API_KEY_ENV = "OPENAI_API_KEY" ALL_PII_VALUES = [ PII_HOST, @@ -84,6 +158,55 @@ PII_PG_NAMESPACE, PII_PG_CA_CERT, PII_MCP_URL, + PII_MCP_AUTH_HEADER_VALUE, + PII_BASE_URL, + PII_ROOT_PATH, + PII_PROFILE_PATH, + PII_AGENT_CARD_PATH, + PII_CACHE_SQLITE_PATH, + PII_CACHE_PG_HOST, + PII_CACHE_PG_DB, + PII_CACHE_PG_USER, + PII_CACHE_PG_PASS, + PII_CACHE_PG_NAMESPACE, + PII_CACHE_PG_CA_CERT, + PII_QUOTA_SQLITE_PATH, + PII_QUOTA_PG_HOST, + PII_QUOTA_PG_DB, + PII_QUOTA_PG_USER, + PII_QUOTA_PG_PASS, + PII_QUOTA_PG_NAMESPACE, + PII_QUOTA_PG_CA_CERT, + PII_BYOK_DB_PATH, + PII_BYOK_HOST, + PII_BYOK_PORT, + PII_BYOK_DB, + PII_BYOK_USER, + PII_BYOK_PASS, + PII_A2A_SQLITE_PATH, + PII_A2A_PG_HOST, + PII_A2A_PG_DB, + PII_A2A_PG_USER, + PII_A2A_PG_PASS, + PII_A2A_PG_NAMESPACE, + PII_A2A_PG_CA_CERT, + PII_A2A_AGENT_URL, + PII_A2A_AGENT_TOKEN, + PII_SPLUNK_URL, + PII_SPLUNK_TOKEN_PATH, + PII_SPLUNK_INDEX, + PII_OKP_URL, + PII_OKP_CHUNK_FILTER, + PII_AZURE_TENANT_ID, + PII_AZURE_CLIENT_ID, + PII_AZURE_CLIENT_SECRET, + PII_RH_IDENTITY_ENTITLEMENTS, + PII_TRUSTED_PROXY_SA_NS, + PII_TRUSTED_PROXY_SA_NAME, + PII_SKILLS_PATH, + PII_LS_PROFILE, + PII_LS_NATIVE_OVERRIDE, + PII_PROVIDER_API_KEY_ENV, ] SAMPLE_LLAMA_STACK_CONFIG: dict[str, Any] = { @@ -179,12 +302,12 @@ def build_fully_populated_config() -> Configuration: service=ServiceConfiguration.model_construct( host=PII_HOST, port=8080, - base_url=None, + base_url=PII_BASE_URL, workers=4, auth_enabled=True, color_log=True, access_log=False, - root_path="", + root_path=PII_ROOT_PATH, tls_config=TLSConfiguration.model_construct( tls_certificate_path=Path(PII_TLS_CERT), tls_key_path=Path(PII_TLS_KEY), @@ -203,15 +326,36 @@ def build_fully_populated_config() -> Configuration: use_as_library_client=False, library_client_config_path=PII_LIB_CONFIG, timeout=180, + max_retries=5, + retry_delay=2, + allow_degraded_mode=True, + config=UnifiedLlamaStackConfig.model_construct( + baseline="default", + profile=PII_LS_PROFILE, + native_override={"key": PII_LS_NATIVE_OVERRIDE}, + ), ), inference=InferenceConfiguration.model_construct( default_model="gpt-4o-mini", default_provider="openai", + context_windows={"openai/gpt-4o-mini": 128000}, + max_infer_iters=10, + max_tool_calls=30, + providers=[ + UnifiedInferenceProvider.model_construct( + type="openai", + id="openai-provider", + api_key_env=PII_PROVIDER_API_KEY_ENV, + allowed_models=["gpt-4o-mini", "gpt-4o"], + extra={}, + ), + ], ), authentication=AuthenticationConfiguration.model_construct( module="jwk_token", skip_tls_verification=False, - skip_for_health_probes=False, + skip_for_health_probes=True, + skip_for_metrics=True, k8s_cluster_api=PII_K8S_API, k8s_ca_cert_path=Path(PII_K8S_CERT), jwk_config=JwkConfiguration.model_construct( @@ -231,8 +375,22 @@ def build_fully_populated_config() -> Configuration: ], ), ), - api_key_config=None, - rh_identity_config=None, + api_key_config=APIKeyTokenConfiguration.model_construct( + api_key=SecretStr(PII_API_KEY), + ), + rh_identity_config=RHIdentityConfiguration.model_construct( + required_entitlements=[PII_RH_IDENTITY_ENTITLEMENTS], + max_header_size=16384, + ), + trusted_proxy_config=TrustedProxyConfiguration.model_construct( + user_header="X-Forwarded-User", + allowed_service_accounts=[ + TrustedProxyServiceAccount.model_construct( + namespace=PII_TRUSTED_PROXY_SA_NS, + name=PII_TRUSTED_PROXY_SA_NAME, + ), + ], + ), ), authorization=AuthorizationConfiguration.model_construct( access_rules=[ @@ -255,10 +413,11 @@ def build_fully_populated_config() -> Configuration: customization=Customization.model_construct( system_prompt=PII_SYSTEM_PROMPT, system_prompt_path=Path(PII_PROMPT_PATH), + profile_path=PII_PROFILE_PATH, disable_query_system_prompt=False, - profile_path=None, + disable_shield_ids_override=True, custom_profile=None, - agent_card_path=None, + agent_card_path=Path(PII_AGENT_CARD_PATH), agent_card_config=None, ), database=DatabaseConfiguration.model_construct( @@ -277,23 +436,161 @@ def build_fully_populated_config() -> Configuration: ca_cert_path=Path(PII_PG_CA_CERT), ), ), + conversation_cache=ConversationHistoryConfiguration.model_construct( + type="postgres", + memory=InMemoryCacheConfig.model_construct(max_entries=1000), + sqlite=SQLiteDatabaseConfiguration.model_construct( + db_path=PII_CACHE_SQLITE_PATH, + ), + postgres=PostgreSQLDatabaseConfiguration.model_construct( + host=PII_CACHE_PG_HOST, + port=5432, + db=PII_CACHE_PG_DB, + user=PII_CACHE_PG_USER, + password=SecretStr(PII_CACHE_PG_PASS), + namespace=PII_CACHE_PG_NAMESPACE, + ssl_mode="verify-full", + gss_encmode="prefer", + ca_cert_path=Path(PII_CACHE_PG_CA_CERT), + ), + ), + compaction=CompactionConfiguration.model_construct( + enabled=True, + threshold_ratio=0.8, + token_floor=8192, + buffer_turns=6, + buffer_max_ratio=0.4, + ), + quota_handlers=QuotaHandlersConfiguration.model_construct( + sqlite=SQLiteDatabaseConfiguration.model_construct( + db_path=PII_QUOTA_SQLITE_PATH, + ), + postgres=PostgreSQLDatabaseConfiguration.model_construct( + host=PII_QUOTA_PG_HOST, + port=5432, + db=PII_QUOTA_PG_DB, + user=PII_QUOTA_PG_USER, + password=SecretStr(PII_QUOTA_PG_PASS), + namespace=PII_QUOTA_PG_NAMESPACE, + ssl_mode="verify-full", + gss_encmode="prefer", + ca_cert_path=Path(PII_QUOTA_PG_CA_CERT), + ), + limiters=[ + QuotaLimiterConfiguration.model_construct( + type="user_limiter", + name="daily-user-limit", + initial_quota=10000, + quota_increase=0, + period="1 day", + ), + ], + scheduler=QuotaSchedulerConfiguration.model_construct( + period=5, + database_reconnection_count=10, + database_reconnection_delay=2, + ), + enable_token_history=True, + ), + byok_rag=[ + ByokRag.model_construct( + rag_id="my-rag", + rag_type="inline::faiss", + embedding_model="all-MiniLM-L6-v2", + embedding_dimension=384, + vector_db_id="my-vector-db", + db_path=PII_BYOK_DB_PATH, + score_multiplier=1.5, + host=PII_BYOK_HOST, + port=PII_BYOK_PORT, + db=PII_BYOK_DB, + user=PII_BYOK_USER, + password=SecretStr(PII_BYOK_PASS), + ), + ], + a2a_state=A2AStateConfiguration.model_construct( + sqlite=SQLiteDatabaseConfiguration.model_construct( + db_path=PII_A2A_SQLITE_PATH, + ), + postgres=PostgreSQLDatabaseConfiguration.model_construct( + host=PII_A2A_PG_HOST, + port=5432, + db=PII_A2A_PG_DB, + user=PII_A2A_PG_USER, + password=SecretStr(PII_A2A_PG_PASS), + namespace=PII_A2A_PG_NAMESPACE, + ssl_mode="verify-full", + gss_encmode="prefer", + ca_cert_path=Path(PII_A2A_PG_CA_CERT), + ), + ), + a2a_agents=A2AAgentsConfiguration.model_construct( + agents=[ + A2AAgentEndpointConfiguration.model_construct( + name="external-agent", + url=PII_A2A_AGENT_URL, + auth_token=SecretStr(PII_A2A_AGENT_TOKEN), + timeout=30, + max_retries=3, + ), + ], + ), mcp_servers=[ ModelContextProtocolServer.model_construct( name="my-mcp-server", provider_id="model-context-protocol", url=PII_MCP_URL, - authorization_headers={}, - timeout=None, + authorization_headers={"Authorization": PII_MCP_AUTH_HEADER_VALUE}, + headers=["x-rh-identity"], + require_approval="always", + timeout=60, ), ], - conversation_cache=None, - byok_rag=[], - a2a_state=None, - quota_handlers=None, - azure_entra_id=None, - splunk=None, + azure_entra_id=AzureEntraIdConfiguration.model_construct( + tenant_id=SecretStr(PII_AZURE_TENANT_ID), + client_id=SecretStr(PII_AZURE_CLIENT_ID), + client_secret=SecretStr(PII_AZURE_CLIENT_SECRET), + scope="https://cognitiveservices.azure.com/.default", + ), + splunk=SplunkConfiguration.model_construct( + enabled=True, + url=PII_SPLUNK_URL, + token_path=Path(PII_SPLUNK_TOKEN_PATH), + index=PII_SPLUNK_INDEX, + source="lightspeed-stack", + timeout=5, + verify_ssl=True, + ), + rag=RagConfiguration.model_construct( + inline=["okp", "my-rag"], + tool=["my-rag"], + ), + okp=OkpConfiguration.model_construct( + rhokp_url=PII_OKP_URL, + offline=True, + chunk_filter_query=PII_OKP_CHUNK_FILTER, + ), + reranker=RerankerConfiguration.model_construct( + enabled=True, + model="cross-encoder/ms-marco-MiniLM-L6-v2", + ), + approvals=ApprovalsConfiguration.model_construct( + approval_timeout_seconds=600, + approval_retention_days=90, + ), + rlsapi_v1=RlsapiV1Configuration.model_construct( + allow_verbose_infer=True, + quota_subject="user_id", + ), + saved_prompts=SavedPromptsConfiguration.model_construct( + max_prompts_per_user=100, + max_display_name_length=200, + max_content_length=5000, + ), + skills=SkillsConfiguration.model_construct( + paths=[Path(PII_SKILLS_PATH)], + ), deployment_environment="production", - solr=None, ) @@ -332,20 +629,30 @@ def build_minimal_config() -> Configuration: use_as_library_client=True, library_client_config_path=None, timeout=180, + max_retries=5, + retry_delay=2, + allow_degraded_mode=False, + config=None, ), inference=InferenceConfiguration.model_construct( default_model=None, default_provider=None, + context_windows={}, + max_infer_iters=10, + max_tool_calls=30, + providers=[], ), authentication=AuthenticationConfiguration.model_construct( module="noop", skip_tls_verification=False, skip_for_health_probes=False, + skip_for_metrics=False, k8s_cluster_api=None, k8s_ca_cert_path=None, jwk_config=None, api_key_config=None, rh_identity_config=None, + trusted_proxy_config=None, ), authorization=None, user_data_collection=UserDataCollection.model_construct( @@ -363,13 +670,47 @@ def build_minimal_config() -> Configuration: ), mcp_servers=[], conversation_cache=None, + compaction=CompactionConfiguration.model_construct( + enabled=False, + threshold_ratio=0.7, + token_floor=4096, + buffer_turns=4, + buffer_max_ratio=0.3, + ), byok_rag=[], a2a_state=None, + a2a_agents=None, quota_handlers=None, azure_entra_id=None, splunk=None, + rag=RagConfiguration.model_construct( + inline=[], + tool=[], + ), + okp=OkpConfiguration.model_construct( + rhokp_url=None, + offline=True, + chunk_filter_query=None, + ), + reranker=RerankerConfiguration.model_construct( + enabled=False, + model="cross-encoder/ms-marco-MiniLM-L6-v2", + ), + approvals=ApprovalsConfiguration.model_construct( + approval_timeout_seconds=300, + approval_retention_days=30, + ), + rlsapi_v1=RlsapiV1Configuration.model_construct( + allow_verbose_infer=False, + quota_subject=None, + ), + saved_prompts=SavedPromptsConfiguration.model_construct( + max_prompts_per_user=50, + max_display_name_length=255, + max_content_length=10000, + ), + skills=None, deployment_environment="development", - solr=None, ) diff --git a/tests/unit/telemetry/test_configuration_snapshot.py b/tests/unit/telemetry/test_configuration_snapshot.py index 85dac1d64..eaf1bfc87 100644 --- a/tests/unit/telemetry/test_configuration_snapshot.py +++ b/tests/unit/telemetry/test_configuration_snapshot.py @@ -1,5 +1,7 @@ """Tests for configuration snapshot with PII masking.""" +# pylint: disable=too-many-lines,too-many-public-methods + import json from enum import Enum from pathlib import Path, PurePosixPath @@ -188,8 +190,8 @@ def test_sensitive_with_path(self) -> None: ) def test_sensitive_with_empty_string(self) -> None: - """Test sensitive masking with empty string returns 'configured'.""" - assert mask_value("", MaskingType.SENSITIVE) == CONFIGURED + """Test sensitive masking with empty string returns 'not_configured'.""" + assert mask_value("", MaskingType.SENSITIVE) == NOT_CONFIGURED def test_passthrough_bool(self) -> None: """Test passthrough returns bool as-is.""" @@ -448,6 +450,10 @@ def test_list_field_mcp_servers(self) -> None: assert mcp[0]["name"] == "my-mcp-server" assert mcp[0]["provider_id"] == "model-context-protocol" assert mcp[0]["url"] == CONFIGURED + assert mcp[0]["authorization_headers"] == CONFIGURED + assert mcp[0]["headers"] == CONFIGURED + assert mcp[0]["require_approval"] == "always" + assert mcp[0]["timeout"] == 60 def test_empty_mcp_servers(self) -> None: """Test empty MCP servers list.""" @@ -490,6 +496,464 @@ def test_database_ssl_mode_passthrough(self) -> None: assert snapshot["database"]["postgres"]["ssl_mode"] == "verify-full" assert snapshot["database"]["postgres"]["gss_encmode"] == "prefer" + def test_service_base_url_masked(self) -> None: + """Test service base_url is masked as sensitive.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["service"]["base_url"] == CONFIGURED + + def test_service_base_url_none(self) -> None: + """Test service base_url when not configured.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["service"]["base_url"] == NOT_CONFIGURED + + def test_service_root_path_masked(self) -> None: + """Test service root_path is masked as sensitive.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["service"]["root_path"] == CONFIGURED + + def test_llama_stack_timeout_passthrough(self) -> None: + """Test llama_stack timeout passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["llama_stack"]["timeout"] == 180 + + def test_llama_stack_max_retries_passthrough(self) -> None: + """Test llama_stack max_retries passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["llama_stack"]["max_retries"] == 5 + + def test_llama_stack_retry_delay_passthrough(self) -> None: + """Test llama_stack retry_delay passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["llama_stack"]["retry_delay"] == 2 + + def test_llama_stack_allow_degraded_mode_passthrough(self) -> None: + """Test llama_stack allow_degraded_mode passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["llama_stack"]["allow_degraded_mode"] is True + + def test_llama_stack_config_baseline_passthrough(self) -> None: + """Test llama_stack config baseline passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["llama_stack"]["config"]["baseline"] == "default" + + def test_llama_stack_config_profile_masked(self) -> None: + """Test llama_stack config profile is masked as sensitive.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["llama_stack"]["config"]["profile"] == CONFIGURED + + def test_llama_stack_config_native_override_masked(self) -> None: + """Test llama_stack config native_override is masked as sensitive.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["llama_stack"]["config"]["native_override"] == CONFIGURED + + def test_llama_stack_config_none(self) -> None: + """Test llama_stack config fields when config is None.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["llama_stack"]["config"]["baseline"] is None + assert snapshot["llama_stack"]["config"]["profile"] == NOT_CONFIGURED + assert snapshot["llama_stack"]["config"]["native_override"] == NOT_CONFIGURED + + def test_inference_context_windows_passthrough(self) -> None: + """Test inference context_windows passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["inference"]["context_windows"] == { + "openai/gpt-4o-mini": 128000 + } + + def test_inference_max_infer_iters_passthrough(self) -> None: + """Test inference max_infer_iters passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["inference"]["max_infer_iters"] == 10 + + def test_inference_max_tool_calls_passthrough(self) -> None: + """Test inference max_tool_calls passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["inference"]["max_tool_calls"] == 30 + + def test_inference_providers_extraction(self) -> None: + """Test inference providers list extraction with masking.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + providers = snapshot["inference"]["providers"] + assert isinstance(providers, list) + assert len(providers) == 1 + assert providers[0]["type"] == "openai" + assert providers[0]["id"] == "openai-provider" + assert providers[0]["api_key_env"] == CONFIGURED + assert providers[0]["allowed_models"] == ["gpt-4o-mini", "gpt-4o"] + + def test_inference_providers_empty(self) -> None: + """Test inference providers when empty.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["inference"]["providers"] == [] + + def test_authentication_skip_for_health_probes(self) -> None: + """Test authentication skip_for_health_probes passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["authentication"]["skip_for_health_probes"] is True + + def test_authentication_skip_for_metrics(self) -> None: + """Test authentication skip_for_metrics passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["authentication"]["skip_for_metrics"] is True + + def test_authentication_api_key_config_masked(self) -> None: + """Test authentication api_key_config.api_key is masked.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["authentication"]["api_key_config"]["api_key"] == CONFIGURED + + def test_authentication_api_key_config_none(self) -> None: + """Test authentication api_key_config when not configured.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["authentication"]["api_key_config"]["api_key"] == NOT_CONFIGURED + + def test_authentication_rh_identity_config(self) -> None: + """Test authentication rh_identity_config fields.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert ( + snapshot["authentication"]["rh_identity_config"]["required_entitlements"] + == CONFIGURED + ) + assert ( + snapshot["authentication"]["rh_identity_config"]["max_header_size"] == 16384 + ) + + def test_authentication_rh_identity_config_none(self) -> None: + """Test authentication rh_identity_config when not configured.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert ( + snapshot["authentication"]["rh_identity_config"]["required_entitlements"] + == NOT_CONFIGURED + ) + assert ( + snapshot["authentication"]["rh_identity_config"]["max_header_size"] is None + ) + + def test_authentication_trusted_proxy_config(self) -> None: + """Test authentication trusted_proxy_config fields.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert ( + snapshot["authentication"]["trusted_proxy_config"]["user_header"] + == "X-Forwarded-User" + ) + accounts = snapshot["authentication"]["trusted_proxy_config"][ + "allowed_service_accounts" + ] + assert isinstance(accounts, list) + assert len(accounts) == 1 + assert accounts[0]["namespace"] == CONFIGURED + assert accounts[0]["name"] == CONFIGURED + + def test_authentication_trusted_proxy_config_none(self) -> None: + """Test authentication trusted_proxy_config when not configured.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["authentication"]["trusted_proxy_config"]["user_header"] is None + assert ( + snapshot["authentication"]["trusted_proxy_config"][ + "allowed_service_accounts" + ] + == NOT_CONFIGURED + ) + + def test_azure_entra_id_fields(self) -> None: + """Test azure_entra_id fields are properly masked.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["azure_entra_id"]["tenant_id"] == CONFIGURED + assert snapshot["azure_entra_id"]["client_id"] == CONFIGURED + assert snapshot["azure_entra_id"]["client_secret"] == CONFIGURED + assert ( + snapshot["azure_entra_id"]["scope"] + == "https://cognitiveservices.azure.com/.default" + ) + + def test_azure_entra_id_none(self) -> None: + """Test azure_entra_id when not configured.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["azure_entra_id"]["tenant_id"] == NOT_CONFIGURED + assert snapshot["azure_entra_id"]["client_id"] == NOT_CONFIGURED + assert snapshot["azure_entra_id"]["client_secret"] == NOT_CONFIGURED + assert snapshot["azure_entra_id"]["scope"] is None + + def test_customization_profile_path_masked(self) -> None: + """Test customization profile_path is masked as sensitive.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["customization"]["profile_path"] == CONFIGURED + + def test_customization_disable_shield_ids_override(self) -> None: + """Test customization disable_shield_ids_override passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["customization"]["disable_shield_ids_override"] is True + + def test_customization_agent_card_path_masked(self) -> None: + """Test customization agent_card_path is masked as sensitive.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["customization"]["agent_card_path"] == CONFIGURED + + def test_conversation_cache_fields(self) -> None: + """Test conversation_cache fields extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + cache = snapshot["conversation_cache"] + assert cache["type"] == "postgres" + assert cache["memory"]["max_entries"] == 1000 + assert cache["sqlite"]["db_path"] == CONFIGURED + assert cache["postgres"]["host"] == CONFIGURED + assert cache["postgres"]["port"] == 5432 + assert cache["postgres"]["db"] == CONFIGURED + assert cache["postgres"]["user"] == CONFIGURED + assert cache["postgres"]["password"] == CONFIGURED + assert cache["postgres"]["namespace"] == CONFIGURED + assert cache["postgres"]["ssl_mode"] == "verify-full" + assert cache["postgres"]["gss_encmode"] == "prefer" + assert cache["postgres"]["ca_cert_path"] == CONFIGURED + + def test_conversation_cache_none(self) -> None: + """Test conversation_cache when not configured.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["conversation_cache"]["type"] is None + assert snapshot["conversation_cache"]["memory"]["max_entries"] is None + assert snapshot["conversation_cache"]["sqlite"]["db_path"] == NOT_CONFIGURED + assert snapshot["conversation_cache"]["postgres"]["host"] == NOT_CONFIGURED + + def test_compaction_fields(self) -> None: + """Test compaction fields extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + compaction = snapshot["compaction"] + assert compaction["enabled"] is True + assert compaction["threshold_ratio"] == 0.8 + assert compaction["token_floor"] == 8192 + assert compaction["buffer_turns"] == 6 + assert compaction["buffer_max_ratio"] == 0.4 + + def test_compaction_defaults(self) -> None: + """Test compaction fields with default values.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + compaction = snapshot["compaction"] + assert compaction["enabled"] is False + assert compaction["threshold_ratio"] == 0.7 + assert compaction["token_floor"] == 4096 + assert compaction["buffer_turns"] == 4 + assert compaction["buffer_max_ratio"] == 0.3 + + def test_quota_handlers_fields(self) -> None: + """Test quota_handlers fields extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + qh = snapshot["quota_handlers"] + assert qh["sqlite"]["db_path"] == CONFIGURED + assert qh["postgres"]["host"] == CONFIGURED + assert qh["postgres"]["port"] == 5432 + assert qh["postgres"]["db"] == CONFIGURED + assert qh["postgres"]["user"] == CONFIGURED + assert qh["postgres"]["password"] == CONFIGURED + assert qh["postgres"]["namespace"] == CONFIGURED + assert qh["postgres"]["ssl_mode"] == "verify-full" + assert qh["postgres"]["gss_encmode"] == "prefer" + assert qh["postgres"]["ca_cert_path"] == CONFIGURED + assert qh["enable_token_history"] is True + + def test_quota_handlers_limiters(self) -> None: + """Test quota_handlers limiters list extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + limiters = snapshot["quota_handlers"]["limiters"] + assert isinstance(limiters, list) + assert len(limiters) == 1 + assert limiters[0]["type"] == "user_limiter" + assert limiters[0]["name"] == "daily-user-limit" + assert limiters[0]["initial_quota"] == 10000 + assert limiters[0]["quota_increase"] == 0 + assert limiters[0]["period"] == "1 day" + + def test_quota_handlers_scheduler(self) -> None: + """Test quota_handlers scheduler fields.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + scheduler = snapshot["quota_handlers"]["scheduler"] + assert scheduler["period"] == 5 + assert scheduler["database_reconnection_count"] == 10 + assert scheduler["database_reconnection_delay"] == 2 + + def test_quota_handlers_none(self) -> None: + """Test quota_handlers when not configured.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["quota_handlers"]["sqlite"]["db_path"] == NOT_CONFIGURED + assert snapshot["quota_handlers"]["postgres"]["host"] == NOT_CONFIGURED + + def test_byok_rag_extraction(self) -> None: + """Test byok_rag list extraction with masking.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + byok = snapshot["byok_rag"] + assert isinstance(byok, list) + assert len(byok) == 1 + assert byok[0]["rag_id"] == "my-rag" + assert byok[0]["rag_type"] == "inline::faiss" + assert byok[0]["embedding_model"] == "all-MiniLM-L6-v2" + assert byok[0]["embedding_dimension"] == 384 + assert byok[0]["vector_db_id"] == "my-vector-db" + assert byok[0]["db_path"] == CONFIGURED + assert byok[0]["score_multiplier"] == 1.5 + assert byok[0]["host"] == CONFIGURED + assert byok[0]["port"] == CONFIGURED + assert byok[0]["db"] == CONFIGURED + assert byok[0]["user"] == CONFIGURED + assert byok[0]["password"] == CONFIGURED + + def test_byok_rag_empty(self) -> None: + """Test byok_rag when empty.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["byok_rag"] == [] + + def test_a2a_state_fields(self) -> None: + """Test a2a_state fields extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + a2a = snapshot["a2a_state"] + assert a2a["sqlite"]["db_path"] == CONFIGURED + assert a2a["postgres"]["host"] == CONFIGURED + assert a2a["postgres"]["port"] == 5432 + assert a2a["postgres"]["db"] == CONFIGURED + assert a2a["postgres"]["user"] == CONFIGURED + assert a2a["postgres"]["password"] == CONFIGURED + assert a2a["postgres"]["namespace"] == CONFIGURED + assert a2a["postgres"]["ssl_mode"] == "verify-full" + assert a2a["postgres"]["gss_encmode"] == "prefer" + assert a2a["postgres"]["ca_cert_path"] == CONFIGURED + + def test_a2a_state_none(self) -> None: + """Test a2a_state when not configured.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["a2a_state"]["sqlite"]["db_path"] == NOT_CONFIGURED + assert snapshot["a2a_state"]["postgres"]["host"] == NOT_CONFIGURED + + def test_a2a_agents_extraction(self) -> None: + """Test a2a_agents list extraction with masking.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + agents = snapshot["a2a_agents"]["agents"] + assert isinstance(agents, list) + assert len(agents) == 1 + assert agents[0]["name"] == "external-agent" + assert agents[0]["url"] == CONFIGURED + assert agents[0]["auth_token"] == CONFIGURED + assert agents[0]["timeout"] == 30 + assert agents[0]["max_retries"] == 3 + + def test_a2a_agents_none(self) -> None: + """Test a2a_agents when not configured.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["a2a_agents"]["agents"] == NOT_CONFIGURED + + def test_splunk_fields(self) -> None: + """Test splunk fields extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + splunk = snapshot["splunk"] + assert splunk["enabled"] is True + assert splunk["url"] == CONFIGURED + assert splunk["token_path"] == CONFIGURED + assert splunk["index"] == CONFIGURED + assert splunk["source"] == "lightspeed-stack" + assert splunk["timeout"] == 5 + assert splunk["verify_ssl"] is True + + def test_splunk_none(self) -> None: + """Test splunk when not configured.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["splunk"]["enabled"] is None + assert snapshot["splunk"]["url"] == NOT_CONFIGURED + assert snapshot["splunk"]["token_path"] == NOT_CONFIGURED + assert snapshot["splunk"]["index"] == NOT_CONFIGURED + assert snapshot["splunk"]["source"] is None + + def test_rag_fields(self) -> None: + """Test rag strategy fields extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["rag"]["inline"] == ["okp", "my-rag"] + assert snapshot["rag"]["tool"] == ["my-rag"] + + def test_rag_defaults(self) -> None: + """Test rag strategy fields with defaults.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["rag"]["inline"] == [] + assert snapshot["rag"]["tool"] == [] + + def test_okp_fields(self) -> None: + """Test okp fields extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["okp"]["rhokp_url"] == CONFIGURED + assert snapshot["okp"]["offline"] is True + assert snapshot["okp"]["chunk_filter_query"] == CONFIGURED + + def test_okp_defaults(self) -> None: + """Test okp fields with defaults.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["okp"]["rhokp_url"] == NOT_CONFIGURED + assert snapshot["okp"]["offline"] is True + assert snapshot["okp"]["chunk_filter_query"] == NOT_CONFIGURED + + def test_reranker_fields(self) -> None: + """Test reranker fields extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["reranker"]["enabled"] is True + assert snapshot["reranker"]["model"] == "cross-encoder/ms-marco-MiniLM-L6-v2" + + def test_reranker_defaults(self) -> None: + """Test reranker fields with defaults.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["reranker"]["enabled"] is False + assert snapshot["reranker"]["model"] == "cross-encoder/ms-marco-MiniLM-L6-v2" + + def test_approvals_fields(self) -> None: + """Test approvals fields extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["approvals"]["approval_timeout_seconds"] == 600 + assert snapshot["approvals"]["approval_retention_days"] == 90 + + def test_approvals_defaults(self) -> None: + """Test approvals fields with defaults.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["approvals"]["approval_timeout_seconds"] == 300 + assert snapshot["approvals"]["approval_retention_days"] == 30 + + def test_rlsapi_v1_fields(self) -> None: + """Test rlsapi_v1 fields extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["rlsapi_v1"]["allow_verbose_infer"] is True + assert snapshot["rlsapi_v1"]["quota_subject"] == "user_id" + + def test_rlsapi_v1_defaults(self) -> None: + """Test rlsapi_v1 fields with defaults.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["rlsapi_v1"]["allow_verbose_infer"] is False + assert snapshot["rlsapi_v1"]["quota_subject"] is None + + def test_saved_prompts_fields(self) -> None: + """Test saved_prompts fields extraction.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["saved_prompts"]["max_prompts_per_user"] == 100 + assert snapshot["saved_prompts"]["max_display_name_length"] == 200 + assert snapshot["saved_prompts"]["max_content_length"] == 5000 + + def test_saved_prompts_defaults(self) -> None: + """Test saved_prompts fields with defaults.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["saved_prompts"]["max_prompts_per_user"] == 50 + assert snapshot["saved_prompts"]["max_display_name_length"] == 255 + assert snapshot["saved_prompts"]["max_content_length"] == 10000 + + def test_skills_paths_masked(self) -> None: + """Test skills paths is masked as sensitive.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["skills"]["paths"] == CONFIGURED + + def test_skills_none(self) -> None: + """Test skills when not configured.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["skills"]["paths"] == NOT_CONFIGURED + + def test_deployment_environment_passthrough(self) -> None: + """Test deployment_environment passes through.""" + snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) + assert snapshot["deployment_environment"] == "production" + + def test_deployment_environment_default(self) -> None: + """Test deployment_environment with default value.""" + snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) + assert snapshot["deployment_environment"] == "development" + # ============================================================================= # Tests: build_llama_stack_snapshot diff --git a/tests/unit/telemetry/test_configuration_snapshot_new_sections.py b/tests/unit/telemetry/test_configuration_snapshot_new_sections.py new file mode 100644 index 000000000..940ef1cf2 --- /dev/null +++ b/tests/unit/telemetry/test_configuration_snapshot_new_sections.py @@ -0,0 +1,1133 @@ +"""Tests for configuration snapshot coverage of new configuration sections. + +Verifies that the ~118 new fields across 20 configuration sections are +properly represented in the telemetry configuration snapshot, with correct +masking for sensitive fields and passthrough for non-sensitive fields. + +Spec: LCORE-2923 — Add missing configuration fields to telemetry configuration +snapshot. +""" + +import json +from typing import Any + +import pytest + +from telemetry.configuration_snapshot import ( + CONFIGURED, + LIGHTSPEED_STACK_FIELDS, + NOT_CONFIGURED, + FieldSpec, + ListFieldSpec, + MaskingType, + build_lightspeed_stack_snapshot, +) +from tests.unit.telemetry.conftest import ( + ALL_PII_VALUES, + PII_A2A_AGENT_TOKEN, + PII_A2A_AGENT_URL, + PII_A2A_PG_CA_CERT, + PII_A2A_PG_DB, + PII_A2A_PG_HOST, + PII_A2A_PG_NAMESPACE, + PII_A2A_PG_PASS, + PII_A2A_PG_USER, + PII_A2A_SQLITE_PATH, + PII_AGENT_CARD_PATH, + PII_AZURE_CLIENT_ID, + PII_AZURE_CLIENT_SECRET, + PII_AZURE_TENANT_ID, + PII_BYOK_DB, + PII_BYOK_DB_PATH, + PII_BYOK_HOST, + PII_BYOK_PASS, + PII_BYOK_PORT, + PII_BYOK_USER, + PII_CACHE_PG_CA_CERT, + PII_CACHE_PG_DB, + PII_CACHE_PG_HOST, + PII_CACHE_PG_NAMESPACE, + PII_CACHE_PG_PASS, + PII_CACHE_PG_USER, + PII_CACHE_SQLITE_PATH, + PII_LS_NATIVE_OVERRIDE, + PII_LS_PROFILE, + PII_MCP_AUTH_HEADER_VALUE, + PII_MCP_URL, + PII_OKP_CHUNK_FILTER, + PII_OKP_URL, + PII_PROFILE_PATH, + PII_QUOTA_PG_CA_CERT, + PII_QUOTA_PG_DB, + PII_QUOTA_PG_HOST, + PII_QUOTA_PG_NAMESPACE, + PII_QUOTA_PG_PASS, + PII_QUOTA_PG_USER, + PII_QUOTA_SQLITE_PATH, + PII_RH_IDENTITY_ENTITLEMENTS, + PII_SKILLS_PATH, + PII_SPLUNK_INDEX, + PII_SPLUNK_TOKEN_PATH, + PII_SPLUNK_URL, + PII_TRUSTED_PROXY_SA_NAME, + PII_TRUSTED_PROXY_SA_NS, + build_fully_populated_config, + build_minimal_config, +) + +# ============================================================================= +# Helper: build snapshot once for fully-populated config +# ============================================================================= + + +@pytest.fixture(name="full_snapshot") +def full_snapshot_fixture() -> dict[str, Any]: + """Build a snapshot from the fully-populated config. + + Returns: + The lightspeed-stack snapshot dict. + """ + return build_lightspeed_stack_snapshot(build_fully_populated_config()) + + +@pytest.fixture(name="minimal_snapshot") +def minimal_snapshot_fixture() -> dict[str, Any]: + """Build a snapshot from the minimal config. + + Returns: + The lightspeed-stack snapshot dict. + """ + return build_lightspeed_stack_snapshot(build_minimal_config()) + + +# ============================================================================= +# Tests: Compaction section (5 fields) +# ============================================================================= + + +class TestCompactionSection: + """Tests for compaction configuration section in snapshot.""" + + def test_compaction_enabled_passthrough(self, full_snapshot: dict) -> None: + """Test compaction.enabled passes through as its actual boolean value.""" + assert full_snapshot["compaction"]["enabled"] is True + + def test_compaction_threshold_ratio_passthrough(self, full_snapshot: dict) -> None: + """Test compaction.threshold_ratio passes through as its actual float.""" + assert full_snapshot["compaction"]["threshold_ratio"] == 0.8 + + def test_compaction_token_floor_passthrough(self, full_snapshot: dict) -> None: + """Test compaction.token_floor passes through as its actual int.""" + assert full_snapshot["compaction"]["token_floor"] == 8192 + + def test_compaction_buffer_turns_passthrough(self, full_snapshot: dict) -> None: + """Test compaction.buffer_turns passes through as its actual int.""" + assert full_snapshot["compaction"]["buffer_turns"] == 6 + + def test_compaction_buffer_max_ratio_passthrough(self, full_snapshot: dict) -> None: + """Test compaction.buffer_max_ratio passes through as its actual float.""" + assert full_snapshot["compaction"]["buffer_max_ratio"] == 0.4 + + def test_compaction_section_present_in_minimal( + self, minimal_snapshot: dict + ) -> None: + """Test compaction section is present even in minimal config.""" + assert "compaction" in minimal_snapshot + + +# ============================================================================= +# Tests: Conversation cache section (12 fields) +# ============================================================================= + + +class TestConversationCacheSection: + """Tests for conversation_cache configuration section in snapshot.""" + + def test_cache_type_passthrough(self, full_snapshot: dict) -> None: + """Test conversation_cache.type passes through as its actual value.""" + assert full_snapshot["conversation_cache"]["type"] == "postgres" + + def test_cache_memory_max_entries_passthrough(self, full_snapshot: dict) -> None: + """Test conversation_cache.memory.max_entries passes through.""" + assert full_snapshot["conversation_cache"]["memory"]["max_entries"] == 1000 + + def test_cache_sqlite_db_path_masked(self, full_snapshot: dict) -> None: + """Test conversation_cache.sqlite.db_path is masked as sensitive.""" + assert full_snapshot["conversation_cache"]["sqlite"]["db_path"] == CONFIGURED + + def test_cache_postgres_host_masked(self, full_snapshot: dict) -> None: + """Test conversation_cache.postgres.host is masked as sensitive.""" + assert full_snapshot["conversation_cache"]["postgres"]["host"] == CONFIGURED + + def test_cache_postgres_db_masked(self, full_snapshot: dict) -> None: + """Test conversation_cache.postgres.db is masked as sensitive.""" + assert full_snapshot["conversation_cache"]["postgres"]["db"] == CONFIGURED + + def test_cache_postgres_user_masked(self, full_snapshot: dict) -> None: + """Test conversation_cache.postgres.user is masked as sensitive.""" + assert full_snapshot["conversation_cache"]["postgres"]["user"] == CONFIGURED + + def test_cache_postgres_password_masked(self, full_snapshot: dict) -> None: + """Test conversation_cache.postgres.password is masked as sensitive.""" + assert full_snapshot["conversation_cache"]["postgres"]["password"] == CONFIGURED + + def test_cache_postgres_namespace_masked(self, full_snapshot: dict) -> None: + """Test conversation_cache.postgres.namespace is masked as sensitive.""" + assert ( + full_snapshot["conversation_cache"]["postgres"]["namespace"] == CONFIGURED + ) + + def test_cache_postgres_port_passthrough(self, full_snapshot: dict) -> None: + """Test conversation_cache.postgres.port passes through as its actual value.""" + assert full_snapshot["conversation_cache"]["postgres"]["port"] == 5432 + + def test_cache_postgres_ssl_mode_passthrough(self, full_snapshot: dict) -> None: + """Test conversation_cache.postgres.ssl_mode passes through.""" + assert ( + full_snapshot["conversation_cache"]["postgres"]["ssl_mode"] == "verify-full" + ) + + def test_cache_postgres_ca_cert_path_masked(self, full_snapshot: dict) -> None: + """Test conversation_cache.postgres.ca_cert_path is masked as sensitive.""" + assert ( + full_snapshot["conversation_cache"]["postgres"]["ca_cert_path"] + == CONFIGURED + ) + + def test_cache_not_configured_when_none(self, minimal_snapshot: dict) -> None: + """Test conversation_cache shows not_configured when None.""" + # When conversation_cache is None, the snapshot should reflect that + cache = minimal_snapshot.get("conversation_cache") + if cache is not None: + assert cache.get("type") in (None, NOT_CONFIGURED) + + +# ============================================================================= +# Tests: Quota handlers section (19 fields) +# ============================================================================= + + +class TestQuotaHandlersSection: + """Tests for quota_handlers configuration section in snapshot.""" + + def test_quota_sqlite_db_path_masked(self, full_snapshot: dict) -> None: + """Test quota_handlers.sqlite.db_path is masked as sensitive.""" + assert full_snapshot["quota_handlers"]["sqlite"]["db_path"] == CONFIGURED + + def test_quota_postgres_host_masked(self, full_snapshot: dict) -> None: + """Test quota_handlers.postgres.host is masked as sensitive.""" + assert full_snapshot["quota_handlers"]["postgres"]["host"] == CONFIGURED + + def test_quota_postgres_db_masked(self, full_snapshot: dict) -> None: + """Test quota_handlers.postgres.db is masked as sensitive.""" + assert full_snapshot["quota_handlers"]["postgres"]["db"] == CONFIGURED + + def test_quota_postgres_user_masked(self, full_snapshot: dict) -> None: + """Test quota_handlers.postgres.user is masked as sensitive.""" + assert full_snapshot["quota_handlers"]["postgres"]["user"] == CONFIGURED + + def test_quota_postgres_password_masked(self, full_snapshot: dict) -> None: + """Test quota_handlers.postgres.password is masked as sensitive.""" + assert full_snapshot["quota_handlers"]["postgres"]["password"] == CONFIGURED + + def test_quota_postgres_namespace_masked(self, full_snapshot: dict) -> None: + """Test quota_handlers.postgres.namespace is masked as sensitive.""" + assert full_snapshot["quota_handlers"]["postgres"]["namespace"] == CONFIGURED + + def test_quota_postgres_port_passthrough(self, full_snapshot: dict) -> None: + """Test quota_handlers.postgres.port passes through.""" + assert full_snapshot["quota_handlers"]["postgres"]["port"] == 5432 + + def test_quota_postgres_ssl_mode_passthrough(self, full_snapshot: dict) -> None: + """Test quota_handlers.postgres.ssl_mode passes through.""" + assert full_snapshot["quota_handlers"]["postgres"]["ssl_mode"] == "verify-full" + + def test_quota_postgres_ca_cert_path_masked(self, full_snapshot: dict) -> None: + """Test quota_handlers.postgres.ca_cert_path is masked as sensitive.""" + assert full_snapshot["quota_handlers"]["postgres"]["ca_cert_path"] == CONFIGURED + + def test_quota_enable_token_history_passthrough(self, full_snapshot: dict) -> None: + """Test quota_handlers.enable_token_history passes through.""" + assert full_snapshot["quota_handlers"]["enable_token_history"] is True + + def test_quota_scheduler_period_passthrough(self, full_snapshot: dict) -> None: + """Test quota_handlers.scheduler.period passes through.""" + assert full_snapshot["quota_handlers"]["scheduler"]["period"] == 5 + + def test_quota_scheduler_reconnection_count_passthrough( + self, full_snapshot: dict + ) -> None: + """Test quota_handlers.scheduler.database_reconnection_count passes through.""" + assert ( + full_snapshot["quota_handlers"]["scheduler"]["database_reconnection_count"] + == 10 + ) + + def test_quota_scheduler_reconnection_delay_passthrough( + self, full_snapshot: dict + ) -> None: + """Test quota_handlers.scheduler.database_reconnection_delay passes through.""" + assert ( + full_snapshot["quota_handlers"]["scheduler"]["database_reconnection_delay"] + == 2 + ) + + def test_quota_not_configured_when_none(self, minimal_snapshot: dict) -> None: + """Test quota_handlers shows not_configured when None.""" + qh = minimal_snapshot.get("quota_handlers") + if qh is not None: + # When quota_handlers is None, sub-fields should be not_configured + assert qh.get("sqlite", {}).get("db_path") in ( + None, + NOT_CONFIGURED, + ) + + +# ============================================================================= +# Tests: BYOK RAG section (12 fields per item) +# ============================================================================= + + +class TestByokRagSection: + """Tests for byok_rag[] list configuration section in snapshot.""" + + def test_byok_rag_is_list(self, full_snapshot: dict) -> None: + """Test byok_rag is a list in the snapshot.""" + assert isinstance(full_snapshot["byok_rag"], list) + + def test_byok_rag_has_items(self, full_snapshot: dict) -> None: + """Test byok_rag list has the expected number of items.""" + assert len(full_snapshot["byok_rag"]) == 1 + + def test_byok_rag_id_passthrough(self, full_snapshot: dict) -> None: + """Test byok_rag[].rag_id passes through as its actual value.""" + assert full_snapshot["byok_rag"][0]["rag_id"] == "my-rag" + + def test_byok_rag_type_passthrough(self, full_snapshot: dict) -> None: + """Test byok_rag[].rag_type passes through.""" + assert full_snapshot["byok_rag"][0]["rag_type"] == "inline::faiss" + + def test_byok_embedding_model_passthrough(self, full_snapshot: dict) -> None: + """Test byok_rag[].embedding_model passes through.""" + assert full_snapshot["byok_rag"][0]["embedding_model"] == "all-MiniLM-L6-v2" + + def test_byok_embedding_dimension_passthrough(self, full_snapshot: dict) -> None: + """Test byok_rag[].embedding_dimension passes through.""" + assert full_snapshot["byok_rag"][0]["embedding_dimension"] == 384 + + def test_byok_vector_db_id_passthrough(self, full_snapshot: dict) -> None: + """Test byok_rag[].vector_db_id passes through.""" + assert full_snapshot["byok_rag"][0]["vector_db_id"] == "my-vector-db" + + def test_byok_db_path_masked(self, full_snapshot: dict) -> None: + """Test byok_rag[].db_path is masked as sensitive.""" + assert full_snapshot["byok_rag"][0]["db_path"] == CONFIGURED + + def test_byok_score_multiplier_passthrough(self, full_snapshot: dict) -> None: + """Test byok_rag[].score_multiplier passes through.""" + assert full_snapshot["byok_rag"][0]["score_multiplier"] == 1.5 + + def test_byok_host_masked(self, full_snapshot: dict) -> None: + """Test byok_rag[].host is masked as sensitive.""" + assert full_snapshot["byok_rag"][0]["host"] == CONFIGURED + + def test_byok_user_masked(self, full_snapshot: dict) -> None: + """Test byok_rag[].user is masked as sensitive.""" + assert full_snapshot["byok_rag"][0]["user"] == CONFIGURED + + def test_byok_password_masked(self, full_snapshot: dict) -> None: + """Test byok_rag[].password is masked as sensitive.""" + assert full_snapshot["byok_rag"][0]["password"] == CONFIGURED + + def test_byok_empty_list(self, minimal_snapshot: dict) -> None: + """Test byok_rag is empty list when no BYOK RAGs configured.""" + assert minimal_snapshot["byok_rag"] == [] + + +# ============================================================================= +# Tests: A2A state section (10 fields) +# ============================================================================= + + +class TestA2AStateSection: + """Tests for a2a_state configuration section in snapshot.""" + + def test_a2a_state_sqlite_db_path_masked(self, full_snapshot: dict) -> None: + """Test a2a_state.sqlite.db_path is masked as sensitive.""" + assert full_snapshot["a2a_state"]["sqlite"]["db_path"] == CONFIGURED + + def test_a2a_state_postgres_host_masked(self, full_snapshot: dict) -> None: + """Test a2a_state.postgres.host is masked as sensitive.""" + assert full_snapshot["a2a_state"]["postgres"]["host"] == CONFIGURED + + def test_a2a_state_postgres_db_masked(self, full_snapshot: dict) -> None: + """Test a2a_state.postgres.db is masked as sensitive.""" + assert full_snapshot["a2a_state"]["postgres"]["db"] == CONFIGURED + + def test_a2a_state_postgres_user_masked(self, full_snapshot: dict) -> None: + """Test a2a_state.postgres.user is masked as sensitive.""" + assert full_snapshot["a2a_state"]["postgres"]["user"] == CONFIGURED + + def test_a2a_state_postgres_password_masked(self, full_snapshot: dict) -> None: + """Test a2a_state.postgres.password is masked as sensitive.""" + assert full_snapshot["a2a_state"]["postgres"]["password"] == CONFIGURED + + def test_a2a_state_postgres_namespace_masked(self, full_snapshot: dict) -> None: + """Test a2a_state.postgres.namespace is masked as sensitive.""" + assert full_snapshot["a2a_state"]["postgres"]["namespace"] == CONFIGURED + + def test_a2a_state_postgres_port_passthrough(self, full_snapshot: dict) -> None: + """Test a2a_state.postgres.port passes through.""" + assert full_snapshot["a2a_state"]["postgres"]["port"] == 5432 + + def test_a2a_state_postgres_ssl_mode_passthrough(self, full_snapshot: dict) -> None: + """Test a2a_state.postgres.ssl_mode passes through.""" + assert full_snapshot["a2a_state"]["postgres"]["ssl_mode"] == "verify-full" + + def test_a2a_state_postgres_ca_cert_path_masked(self, full_snapshot: dict) -> None: + """Test a2a_state.postgres.ca_cert_path is masked as sensitive.""" + assert full_snapshot["a2a_state"]["postgres"]["ca_cert_path"] == CONFIGURED + + def test_a2a_state_not_configured_when_none(self, minimal_snapshot: dict) -> None: + """Test a2a_state shows not_configured when None.""" + a2a = minimal_snapshot.get("a2a_state") + if a2a is not None: + assert a2a.get("sqlite", {}).get("db_path") in ( + None, + NOT_CONFIGURED, + ) + + +# ============================================================================= +# Tests: Splunk section (7 fields) +# ============================================================================= + + +class TestSplunkSection: + """Tests for splunk configuration section in snapshot.""" + + def test_splunk_enabled_passthrough(self, full_snapshot: dict) -> None: + """Test splunk.enabled passes through as its actual boolean value.""" + assert full_snapshot["splunk"]["enabled"] is True + + def test_splunk_url_masked(self, full_snapshot: dict) -> None: + """Test splunk.url is masked as sensitive.""" + assert full_snapshot["splunk"]["url"] == CONFIGURED + + def test_splunk_token_path_masked(self, full_snapshot: dict) -> None: + """Test splunk.token_path is masked as sensitive.""" + assert full_snapshot["splunk"]["token_path"] == CONFIGURED + + def test_splunk_index_masked(self, full_snapshot: dict) -> None: + """Test splunk.index is masked as sensitive.""" + assert full_snapshot["splunk"]["index"] == CONFIGURED + + def test_splunk_source_passthrough(self, full_snapshot: dict) -> None: + """Test splunk.source passes through as its actual value.""" + assert full_snapshot["splunk"]["source"] == "lightspeed-stack" + + def test_splunk_timeout_passthrough(self, full_snapshot: dict) -> None: + """Test splunk.timeout passes through as its actual value.""" + assert full_snapshot["splunk"]["timeout"] == 5 + + def test_splunk_verify_ssl_passthrough(self, full_snapshot: dict) -> None: + """Test splunk.verify_ssl passes through as its actual boolean value.""" + assert full_snapshot["splunk"]["verify_ssl"] is True + + def test_splunk_not_configured_when_none(self, minimal_snapshot: dict) -> None: + """Test splunk section when not configured.""" + splunk = minimal_snapshot.get("splunk") + if splunk is not None: + assert splunk.get("url") in (None, NOT_CONFIGURED) + + +# ============================================================================= +# Tests: Inference section (7 new subfields) +# ============================================================================= + + +class TestInferenceNewSubfields: + """Tests for new inference configuration subfields in snapshot.""" + + def test_inference_context_windows_passthrough(self, full_snapshot: dict) -> None: + """Test inference.context_windows passes through as its actual value.""" + assert full_snapshot["inference"]["context_windows"] == { + "openai/gpt-4o-mini": 128000 + } + + def test_inference_max_infer_iters_passthrough(self, full_snapshot: dict) -> None: + """Test inference.max_infer_iters passes through.""" + assert full_snapshot["inference"]["max_infer_iters"] == 10 + + def test_inference_max_tool_calls_passthrough(self, full_snapshot: dict) -> None: + """Test inference.max_tool_calls passes through.""" + assert full_snapshot["inference"]["max_tool_calls"] == 30 + + +# ============================================================================= +# Tests: Llama Stack section (7 new subfields) +# ============================================================================= + + +class TestLlamaStackNewSubfields: + """Tests for new llama_stack configuration subfields in snapshot.""" + + def test_llama_stack_timeout_passthrough(self, full_snapshot: dict) -> None: + """Test llama_stack.timeout passes through as its actual value.""" + assert full_snapshot["llama_stack"]["timeout"] == 180 + + def test_llama_stack_max_retries_passthrough(self, full_snapshot: dict) -> None: + """Test llama_stack.max_retries passes through.""" + assert full_snapshot["llama_stack"]["max_retries"] == 5 + + def test_llama_stack_retry_delay_passthrough(self, full_snapshot: dict) -> None: + """Test llama_stack.retry_delay passes through.""" + assert full_snapshot["llama_stack"]["retry_delay"] == 2 + + def test_llama_stack_allow_degraded_mode_passthrough( + self, full_snapshot: dict + ) -> None: + """Test llama_stack.allow_degraded_mode passes through.""" + assert full_snapshot["llama_stack"]["allow_degraded_mode"] is True + + def test_llama_stack_config_baseline_passthrough(self, full_snapshot: dict) -> None: + """Test llama_stack.config.baseline passes through.""" + assert full_snapshot["llama_stack"]["config"]["baseline"] == "default" + + def test_llama_stack_config_profile_masked(self, full_snapshot: dict) -> None: + """Test llama_stack.config.profile is masked as sensitive.""" + assert full_snapshot["llama_stack"]["config"]["profile"] == CONFIGURED + + def test_llama_stack_config_native_override_masked( + self, full_snapshot: dict + ) -> None: + """Test llama_stack.config.native_override is masked as sensitive.""" + assert full_snapshot["llama_stack"]["config"]["native_override"] == CONFIGURED + + +# ============================================================================= +# Tests: Authentication section (9 new subfields) +# ============================================================================= + + +class TestAuthenticationNewSubfields: + """Tests for new authentication configuration subfields in snapshot.""" + + def test_auth_skip_for_health_probes_passthrough(self, full_snapshot: dict) -> None: + """Test authentication.skip_for_health_probes passes through.""" + assert full_snapshot["authentication"]["skip_for_health_probes"] is True + + def test_auth_skip_for_metrics_passthrough(self, full_snapshot: dict) -> None: + """Test authentication.skip_for_metrics passes through.""" + assert full_snapshot["authentication"]["skip_for_metrics"] is True + + def test_auth_api_key_config_api_key_masked(self, full_snapshot: dict) -> None: + """Test authentication.api_key_config.api_key is masked as sensitive.""" + assert ( + full_snapshot["authentication"]["api_key_config"]["api_key"] == CONFIGURED + ) + + def test_auth_rh_identity_entitlements_masked(self, full_snapshot: dict) -> None: + """Test authentication.rh_identity_config.required_entitlements is masked.""" + assert ( + full_snapshot["authentication"]["rh_identity_config"][ + "required_entitlements" + ] + == CONFIGURED + ) + + def test_auth_rh_identity_max_header_size_passthrough( + self, full_snapshot: dict + ) -> None: + """Test authentication.rh_identity_config.max_header_size passes through.""" + assert ( + full_snapshot["authentication"]["rh_identity_config"]["max_header_size"] + == 16384 + ) + + def test_auth_trusted_proxy_user_header_passthrough( + self, full_snapshot: dict + ) -> None: + """Test authentication.trusted_proxy_config.user_header passes through.""" + assert ( + full_snapshot["authentication"]["trusted_proxy_config"]["user_header"] + == "X-Forwarded-User" + ) + + def test_auth_trusted_proxy_sa_namespace_masked(self, full_snapshot: dict) -> None: + """Test trusted_proxy allowed_service_accounts namespace is masked.""" + sa_list = full_snapshot["authentication"]["trusted_proxy_config"][ + "allowed_service_accounts" + ] + assert isinstance(sa_list, list) + assert len(sa_list) == 1 + assert sa_list[0]["namespace"] == CONFIGURED + + def test_auth_trusted_proxy_sa_name_masked(self, full_snapshot: dict) -> None: + """Test trusted_proxy allowed_service_accounts name is masked.""" + sa_list = full_snapshot["authentication"]["trusted_proxy_config"][ + "allowed_service_accounts" + ] + assert sa_list[0]["name"] == CONFIGURED + + +# ============================================================================= +# Tests: Azure Entra ID section (4 fields) +# ============================================================================= + + +class TestAzureEntraIdSection: + """Tests for azure_entra_id configuration section in snapshot.""" + + def test_azure_tenant_id_masked(self, full_snapshot: dict) -> None: + """Test azure_entra_id.tenant_id is masked as sensitive.""" + assert full_snapshot["azure_entra_id"]["tenant_id"] == CONFIGURED + + def test_azure_client_id_masked(self, full_snapshot: dict) -> None: + """Test azure_entra_id.client_id is masked as sensitive.""" + assert full_snapshot["azure_entra_id"]["client_id"] == CONFIGURED + + def test_azure_client_secret_masked(self, full_snapshot: dict) -> None: + """Test azure_entra_id.client_secret is masked as sensitive.""" + assert full_snapshot["azure_entra_id"]["client_secret"] == CONFIGURED + + def test_azure_scope_passthrough(self, full_snapshot: dict) -> None: + """Test azure_entra_id.scope passes through as its actual value.""" + assert ( + full_snapshot["azure_entra_id"]["scope"] + == "https://cognitiveservices.azure.com/.default" + ) + + def test_azure_not_configured_when_none(self, minimal_snapshot: dict) -> None: + """Test azure_entra_id shows not_configured when None.""" + azure = minimal_snapshot.get("azure_entra_id") + if azure is not None: + assert azure.get("tenant_id") in (None, NOT_CONFIGURED) + + +# ============================================================================= +# Tests: Customization section (4 new subfields) +# ============================================================================= + + +class TestCustomizationNewSubfields: + """Tests for new customization configuration subfields in snapshot.""" + + def test_customization_profile_path_masked(self, full_snapshot: dict) -> None: + """Test customization.profile_path is masked as sensitive.""" + assert full_snapshot["customization"]["profile_path"] == CONFIGURED + + def test_customization_agent_card_path_masked(self, full_snapshot: dict) -> None: + """Test customization.agent_card_path is masked as sensitive.""" + assert full_snapshot["customization"]["agent_card_path"] == CONFIGURED + + def test_customization_disable_shield_ids_override_passthrough( + self, full_snapshot: dict + ) -> None: + """Test customization.disable_shield_ids_override passes through.""" + assert full_snapshot["customization"]["disable_shield_ids_override"] is True + + +# ============================================================================= +# Tests: OKP section (3 fields) +# ============================================================================= + + +class TestOkpSection: + """Tests for okp configuration section in snapshot.""" + + def test_okp_offline_passthrough(self, full_snapshot: dict) -> None: + """Test okp.offline passes through as its actual boolean value.""" + assert full_snapshot["okp"]["offline"] is True + + def test_okp_url_masked(self, full_snapshot: dict) -> None: + """Test okp.rhokp_url is masked as sensitive.""" + assert full_snapshot["okp"]["rhokp_url"] == CONFIGURED + + def test_okp_chunk_filter_query_masked(self, full_snapshot: dict) -> None: + """Test okp.chunk_filter_query is masked as sensitive.""" + assert full_snapshot["okp"]["chunk_filter_query"] == CONFIGURED + + +# ============================================================================= +# Tests: RAG section (2 fields) +# ============================================================================= + + +class TestRagSection: + """Tests for rag configuration section in snapshot.""" + + def test_rag_inline_passthrough(self, full_snapshot: dict) -> None: + """Test rag.inline passes through as its actual list value.""" + assert full_snapshot["rag"]["inline"] == ["okp", "my-rag"] + + def test_rag_tool_passthrough(self, full_snapshot: dict) -> None: + """Test rag.tool passes through as its actual list value.""" + assert full_snapshot["rag"]["tool"] == ["my-rag"] + + +# ============================================================================= +# Tests: Reranker section (2 fields) +# ============================================================================= + + +class TestRerankerSection: + """Tests for reranker configuration section in snapshot.""" + + def test_reranker_enabled_passthrough(self, full_snapshot: dict) -> None: + """Test reranker.enabled passes through as its actual boolean value.""" + assert full_snapshot["reranker"]["enabled"] is True + + def test_reranker_model_passthrough(self, full_snapshot: dict) -> None: + """Test reranker.model passes through as its actual value.""" + assert ( + full_snapshot["reranker"]["model"] == "cross-encoder/ms-marco-MiniLM-L6-v2" + ) + + +# ============================================================================= +# Tests: Approvals section (2 fields) +# ============================================================================= + + +class TestApprovalsSection: + """Tests for approvals configuration section in snapshot.""" + + def test_approvals_timeout_passthrough(self, full_snapshot: dict) -> None: + """Test approvals.approval_timeout_seconds passes through.""" + assert full_snapshot["approvals"]["approval_timeout_seconds"] == 600 + + def test_approvals_retention_passthrough(self, full_snapshot: dict) -> None: + """Test approvals.approval_retention_days passes through.""" + assert full_snapshot["approvals"]["approval_retention_days"] == 90 + + +# ============================================================================= +# Tests: RLSAPIv1 section (2 fields) +# ============================================================================= + + +class TestRlsapiV1Section: + """Tests for rlsapi_v1 configuration section in snapshot.""" + + def test_rlsapi_allow_verbose_infer_passthrough(self, full_snapshot: dict) -> None: + """Test rlsapi_v1.allow_verbose_infer passes through.""" + assert full_snapshot["rlsapi_v1"]["allow_verbose_infer"] is True + + def test_rlsapi_quota_subject_passthrough(self, full_snapshot: dict) -> None: + """Test rlsapi_v1.quota_subject passes through.""" + assert full_snapshot["rlsapi_v1"]["quota_subject"] == "user_id" + + +# ============================================================================= +# Tests: Saved prompts section (3 fields) +# ============================================================================= + + +class TestSavedPromptsSection: + """Tests for saved_prompts configuration section in snapshot.""" + + def test_saved_prompts_max_per_user_passthrough(self, full_snapshot: dict) -> None: + """Test saved_prompts.max_prompts_per_user passes through.""" + assert full_snapshot["saved_prompts"]["max_prompts_per_user"] == 100 + + def test_saved_prompts_max_display_name_length_passthrough( + self, full_snapshot: dict + ) -> None: + """Test saved_prompts.max_display_name_length passes through.""" + assert full_snapshot["saved_prompts"]["max_display_name_length"] == 200 + + def test_saved_prompts_max_content_length_passthrough( + self, full_snapshot: dict + ) -> None: + """Test saved_prompts.max_content_length passes through.""" + assert full_snapshot["saved_prompts"]["max_content_length"] == 5000 + + +# ============================================================================= +# Tests: Skills section (1 field) +# ============================================================================= + + +class TestSkillsSection: + """Tests for skills configuration section in snapshot.""" + + def test_skills_paths_masked(self, full_snapshot: dict) -> None: + """Test skills.paths is masked as sensitive (contains file paths).""" + assert full_snapshot["skills"]["paths"] == CONFIGURED + + +# ============================================================================= +# Tests: Deployment environment section (1 field) +# ============================================================================= + + +class TestDeploymentEnvironmentSection: + """Tests for deployment_environment configuration field in snapshot.""" + + def test_deployment_environment_passthrough(self, full_snapshot: dict) -> None: + """Test deployment_environment passes through as its actual value.""" + assert full_snapshot["deployment_environment"] == "production" + + def test_deployment_environment_minimal(self, minimal_snapshot: dict) -> None: + """Test deployment_environment in minimal config.""" + assert minimal_snapshot["deployment_environment"] == "development" + + +# ============================================================================= +# Tests: MCP servers extra item fields (4 extra fields) +# ============================================================================= + + +class TestMcpServersExtraFields: + """Tests for new MCP server item fields in snapshot.""" + + def test_mcp_authorization_headers_masked(self, full_snapshot: dict) -> None: + """Test mcp_servers[].authorization_headers is masked as sensitive.""" + mcp = full_snapshot["mcp_servers"] + assert isinstance(mcp, list) + assert len(mcp) == 1 + assert mcp[0]["authorization_headers"] == CONFIGURED + + def test_mcp_headers_masked(self, full_snapshot: dict) -> None: + """Test mcp_servers[].headers is masked as sensitive.""" + mcp = full_snapshot["mcp_servers"] + assert mcp[0]["headers"] == CONFIGURED + + def test_mcp_require_approval_passthrough(self, full_snapshot: dict) -> None: + """Test mcp_servers[].require_approval passes through.""" + mcp = full_snapshot["mcp_servers"] + assert mcp[0]["require_approval"] == "always" + + def test_mcp_timeout_passthrough(self, full_snapshot: dict) -> None: + """Test mcp_servers[].timeout passes through.""" + mcp = full_snapshot["mcp_servers"] + assert mcp[0]["timeout"] == 60 + + +# ============================================================================= +# Tests: Completeness — all 20 sections present in registry +# ============================================================================= + + +class TestSectionCompleteness: + """Tests verifying all 20 new configuration sections are in the registry.""" + + @staticmethod + def _registry_paths() -> set[str]: + """Extract all top-level section names from the field registry.""" + paths: set[str] = set() + for spec in LIGHTSPEED_STACK_FIELDS: + if isinstance(spec, FieldSpec): + paths.add(spec.path.split(".")[0]) + elif isinstance(spec, ListFieldSpec): + paths.add(spec.path.split(".")[0]) + return paths + + @pytest.mark.parametrize( + "section", + [ + "compaction", + "conversation_cache", + "quota_handlers", + "byok_rag", + "a2a_state", + "splunk", + "inference", + "llama_stack", + "authentication", + "azure_entra_id", + "customization", + "okp", + "rag", + "reranker", + "approvals", + "rlsapi_v1", + "saved_prompts", + "skills", + "deployment_environment", + "mcp_servers", + ], + ) + def test_section_in_registry(self, section: str) -> None: + """Test that each new section has at least one field in the registry.""" + paths = self._registry_paths() + assert ( + section in paths + ), f"Section '{section}' not found in LIGHTSPEED_STACK_FIELDS registry" + + @pytest.mark.parametrize( + "section", + [ + "compaction", + "conversation_cache", + "quota_handlers", + "byok_rag", + "a2a_state", + "splunk", + "inference", + "llama_stack", + "authentication", + "azure_entra_id", + "customization", + "okp", + "rag", + "reranker", + "approvals", + "rlsapi_v1", + "saved_prompts", + "skills", + "deployment_environment", + "mcp_servers", + ], + ) + def test_section_in_snapshot_output( + self, section: str, full_snapshot: dict + ) -> None: + """Test that each new section appears in the snapshot output.""" + assert ( + section in full_snapshot + ), f"Section '{section}' not found in snapshot output" + + +# ============================================================================= +# Tests: PII leak prevention for new sections +# ============================================================================= + + +class TestNewSectionsPiiLeakPrevention: + """Critical tests proving PII is not leaked in new section snapshots.""" + + def test_no_pii_in_full_snapshot(self, full_snapshot: dict) -> None: + """Verify no PII leaks in the full snapshot JSON for new sections.""" + json_str = json.dumps(full_snapshot) + for pii_value in ALL_PII_VALUES: + assert pii_value not in json_str, f"PII leaked in snapshot: '{pii_value}'" + + def test_no_cache_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no conversation cache PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + cache_pii = [ + PII_CACHE_SQLITE_PATH, + PII_CACHE_PG_HOST, + PII_CACHE_PG_DB, + PII_CACHE_PG_USER, + PII_CACHE_PG_PASS, + PII_CACHE_PG_NAMESPACE, + PII_CACHE_PG_CA_CERT, + ] + for pii in cache_pii: + assert pii not in json_str, f"Cache PII leaked: '{pii}'" + + def test_no_quota_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no quota handler PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + quota_pii = [ + PII_QUOTA_SQLITE_PATH, + PII_QUOTA_PG_HOST, + PII_QUOTA_PG_DB, + PII_QUOTA_PG_USER, + PII_QUOTA_PG_PASS, + PII_QUOTA_PG_NAMESPACE, + PII_QUOTA_PG_CA_CERT, + ] + for pii in quota_pii: + assert pii not in json_str, f"Quota PII leaked: '{pii}'" + + def test_no_byok_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no BYOK RAG PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + byok_pii = [ + PII_BYOK_DB_PATH, + PII_BYOK_HOST, + PII_BYOK_PORT, + PII_BYOK_DB, + PII_BYOK_USER, + PII_BYOK_PASS, + ] + for pii in byok_pii: + assert pii not in json_str, f"BYOK PII leaked: '{pii}'" + + def test_no_a2a_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no A2A state PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + a2a_pii = [ + PII_A2A_SQLITE_PATH, + PII_A2A_PG_HOST, + PII_A2A_PG_DB, + PII_A2A_PG_USER, + PII_A2A_PG_PASS, + PII_A2A_PG_NAMESPACE, + PII_A2A_PG_CA_CERT, + PII_A2A_AGENT_URL, + PII_A2A_AGENT_TOKEN, + ] + for pii in a2a_pii: + assert pii not in json_str, f"A2A PII leaked: '{pii}'" + + def test_no_splunk_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no Splunk PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + splunk_pii = [PII_SPLUNK_URL, PII_SPLUNK_TOKEN_PATH, PII_SPLUNK_INDEX] + for pii in splunk_pii: + assert pii not in json_str, f"Splunk PII leaked: '{pii}'" + + def test_no_azure_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no Azure Entra ID PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + azure_pii = [ + PII_AZURE_TENANT_ID, + PII_AZURE_CLIENT_ID, + PII_AZURE_CLIENT_SECRET, + ] + for pii in azure_pii: + assert pii not in json_str, f"Azure PII leaked: '{pii}'" + + def test_no_okp_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no OKP PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + okp_pii = [PII_OKP_URL, PII_OKP_CHUNK_FILTER] + for pii in okp_pii: + assert pii not in json_str, f"OKP PII leaked: '{pii}'" + + def test_no_mcp_auth_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no MCP authorization header PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + assert PII_MCP_AUTH_HEADER_VALUE not in json_str, "MCP auth header PII leaked" + assert PII_MCP_URL not in json_str, "MCP URL PII leaked" + + def test_no_skills_path_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no skills path PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + assert PII_SKILLS_PATH not in json_str, "Skills path PII leaked" + + def test_no_customization_path_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no customization path PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + assert PII_PROFILE_PATH not in json_str, "Profile path PII leaked" + assert PII_AGENT_CARD_PATH not in json_str, "Agent card path PII leaked" + + def test_no_auth_subfield_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no authentication subfield PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + auth_pii = [ + PII_RH_IDENTITY_ENTITLEMENTS, + PII_TRUSTED_PROXY_SA_NS, + PII_TRUSTED_PROXY_SA_NAME, + ] + for pii in auth_pii: + assert pii not in json_str, f"Auth subfield PII leaked: '{pii}'" + + def test_no_llama_stack_config_pii_leaked(self, full_snapshot: dict) -> None: + """Verify no llama_stack config PII values appear in snapshot.""" + json_str = json.dumps(full_snapshot) + ls_pii = [PII_LS_PROFILE, PII_LS_NATIVE_OVERRIDE] + for pii in ls_pii: + assert pii not in json_str, f"Llama Stack config PII leaked: '{pii}'" + + +# ============================================================================= +# Tests: Edge cases +# ============================================================================= + + +class TestEdgeCases: + """Tests for edge cases with new configuration sections.""" + + def test_empty_byok_rag_list(self, minimal_snapshot: dict) -> None: + """Test empty byok_rag list produces empty list in snapshot.""" + assert minimal_snapshot["byok_rag"] == [] + + def test_empty_mcp_servers_list(self, minimal_snapshot: dict) -> None: + """Test empty mcp_servers list produces empty list in snapshot.""" + assert minimal_snapshot["mcp_servers"] == [] + + def test_none_optional_sections_produce_not_configured( + self, minimal_snapshot: dict + ) -> None: + """Test that None optional sections produce not_configured markers.""" + # These sections are None in minimal config + json_str = json.dumps(minimal_snapshot) + # The snapshot should be JSON-serializable even with None sections + assert isinstance(json.loads(json_str), dict) + + def test_snapshot_is_json_serializable_with_all_sections( + self, full_snapshot: dict + ) -> None: + """Test the full snapshot with all sections is JSON-serializable.""" + json_str = json.dumps(full_snapshot) + parsed = json.loads(json_str) + assert isinstance(parsed, dict) + + def test_snapshot_only_contains_allowlisted_fields( + self, full_snapshot: dict + ) -> None: + """Verify snapshot does not contain any fields outside the allowlist.""" + allowed_top_keys = set() + for spec in LIGHTSPEED_STACK_FIELDS: + if isinstance(spec, FieldSpec): + allowed_top_keys.add(spec.path.split(".")[0]) + elif isinstance(spec, ListFieldSpec): + allowed_top_keys.add(spec.path.split(".")[0]) + unexpected = set(full_snapshot.keys()) - allowed_top_keys + assert ( + not unexpected + ), f"Snapshot contains unexpected top-level keys: {unexpected}" + + def test_minimal_config_no_pii_leak(self, minimal_snapshot: dict) -> None: + """Verify minimal config snapshot has no PII leaks.""" + json_str = json.dumps(minimal_snapshot) + # Minimal config uses "localhost" and "/tmp/..." paths which are + # not in ALL_PII_VALUES, so this verifies no unexpected PII + for pii_value in ALL_PII_VALUES: + assert ( + pii_value not in json_str + ), f"PII leaked in minimal snapshot: '{pii_value}'" + + +# ============================================================================= +# Tests: Registry field count validation +# ============================================================================= + + +class TestRegistryFieldCount: + """Tests validating the field registry has sufficient coverage.""" + + def test_registry_has_minimum_field_count(self) -> None: + """Verify the registry has at least the expected number of fields. + + The spec requires ~118 new fields plus the existing fields. + This test ensures the registry is not accidentally truncated. + """ + # Count all field paths (including sub-fields in ListFieldSpec) + total_fields = 0 + for spec in LIGHTSPEED_STACK_FIELDS: + if isinstance(spec, FieldSpec): + total_fields += 1 + elif isinstance(spec, ListFieldSpec): + total_fields += len(spec.item_fields) + # The original registry had ~30 fields, plus ~118 new = ~148 minimum + # Use a conservative lower bound + assert ( + total_fields >= 100 + ), f"Registry has only {total_fields} fields, expected at least 100" + + def test_all_field_specs_have_valid_masking(self) -> None: + """Verify all field specs in the registry have valid MaskingType.""" + for spec in LIGHTSPEED_STACK_FIELDS: + if isinstance(spec, FieldSpec): + assert isinstance( + spec.masking, MaskingType + ), f"Invalid masking for {spec.path}" + elif isinstance(spec, ListFieldSpec): + for sub in spec.item_fields: + assert isinstance( + sub.masking, MaskingType + ), f"Invalid masking for {spec.path}.{sub.path}" + + def test_no_duplicate_paths(self) -> None: + """Verify no duplicate paths in the registry.""" + paths = [s.path for s in LIGHTSPEED_STACK_FIELDS] + assert len(paths) == len( + set(paths) + ), f"Duplicate paths: {set(p for p in paths if paths.count(p) > 1)}"