diff --git a/backend/app/api/routes/sse.py b/backend/app/api/routes/sse.py index 6b1b406f..81fd39ba 100644 --- a/backend/app/api/routes/sse.py +++ b/backend/app/api/routes/sse.py @@ -4,11 +4,11 @@ from sse_starlette.sse import EventSourceResponse from app.domain.sse import SSEHealthDomain +from app.schemas_pydantic.notification import NotificationResponse from app.schemas_pydantic.sse import ( ShutdownStatusResponse, SSEExecutionEventData, SSEHealthResponse, - SSENotificationEventData, ) from app.services.auth_service import AuthService from app.services.sse.sse_service import SSEService @@ -16,7 +16,7 @@ router = APIRouter(prefix="/events", tags=["sse"], route_class=DishkaRoute) -@router.get("/notifications/stream", responses={200: {"model": SSENotificationEventData}}) +@router.get("/notifications/stream", responses={200: {"model": NotificationResponse}}) async def notification_stream( request: Request, sse_service: FromDishka[SSEService], @@ -25,7 +25,10 @@ async def notification_stream( """Stream notifications for authenticated user.""" current_user = await auth_service.get_current_user(request) - return EventSourceResponse(sse_service.create_notification_stream(user_id=current_user.user_id)) + return EventSourceResponse( + sse_service.create_notification_stream(user_id=current_user.user_id), + ping=30, + ) @router.get("/executions/{execution_id}", responses={200: {"model": SSEExecutionEventData}}) diff --git a/backend/app/core/metrics/__init__.py b/backend/app/core/metrics/__init__.py index 16f45150..77d1687d 100644 --- a/backend/app/core/metrics/__init__.py +++ b/backend/app/core/metrics/__init__.py @@ -1,4 +1,4 @@ -from app.core.metrics.base import BaseMetrics, MetricsConfig +from app.core.metrics.base import BaseMetrics from app.core.metrics.connections import ConnectionMetrics from app.core.metrics.coordinator import CoordinatorMetrics from app.core.metrics.database import DatabaseMetrics @@ -14,7 +14,6 @@ __all__ = [ "BaseMetrics", - "MetricsConfig", "ConnectionMetrics", "CoordinatorMetrics", "DatabaseMetrics", diff --git a/backend/app/core/metrics/base.py b/backend/app/core/metrics/base.py index 911ed583..2e87d01c 100644 --- a/backend/app/core/metrics/base.py +++ b/backend/app/core/metrics/base.py @@ -1,76 +1,27 @@ -from dataclasses import dataclass - -from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter -from opentelemetry.metrics import Meter, NoOpMeterProvider -from opentelemetry.sdk.metrics import MeterProvider as SdkMeterProvider -from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader -from opentelemetry.sdk.resources import Resource +from opentelemetry import metrics from app.settings import Settings -@dataclass -class MetricsConfig: - service_name: str = "integr8scode-backend" - service_version: str = "1.0.0" - otlp_endpoint: str | None = None - export_interval_millis: int = 10000 - console_export_interval_millis: int = 60000 - - class BaseMetrics: def __init__(self, settings: Settings, meter_name: str | None = None): - """Initialize base metrics with its own meter. + """Initialize base metrics with a meter from the global MeterProvider. + + The global MeterProvider is configured once by ``setup_metrics``. + If it hasn't been called (e.g. in tests), the default no-op provider is used. Args: - settings: Application settings. + settings: Application settings (kept for DI compatibility). meter_name: Optional name for the meter. Defaults to class name. """ - config = MetricsConfig( - service_name=settings.TRACING_SERVICE_NAME or "integr8scode-backend", - service_version="1.0.0", - otlp_endpoint=settings.OTEL_EXPORTER_OTLP_ENDPOINT, - ) - meter_name = meter_name or self.__class__.__name__ - self._meter = self._create_meter(settings, config, meter_name) + self._meter = metrics.get_meter(meter_name) self._create_instruments() - def _create_meter(self, settings: Settings, config: MetricsConfig, meter_name: str) -> Meter: - """Create a new meter instance for this collector. - - Args: - settings: Application settings - config: Metrics configuration - meter_name: Name for this meter - - Returns: - A new meter instance - """ - # If tracing/metrics disabled or no OTLP endpoint configured, use NoOp meter - if not config.otlp_endpoint: - return NoOpMeterProvider().get_meter(meter_name) - - resource = Resource.create( - {"service.name": config.service_name, "service.version": config.service_version, "meter.name": meter_name} - ) - - reader = PeriodicExportingMetricReader( - exporter=OTLPMetricExporter(endpoint=config.otlp_endpoint), - export_interval_millis=config.export_interval_millis, - ) - - # Each collector gets its own MeterProvider - meter_provider = SdkMeterProvider(resource=resource, metric_readers=[reader]) - - # Return a meter from this provider - return meter_provider.get_meter(meter_name) - def _create_instruments(self) -> None: """Create metric instruments. Override in subclasses.""" pass def close(self) -> None: """Close the metrics collector and clean up resources.""" - # Subclasses can override if they need cleanup pass diff --git a/backend/app/core/middlewares/metrics.py b/backend/app/core/middlewares/metrics.py index 784dc174..93a00f98 100644 --- a/backend/app/core/middlewares/metrics.py +++ b/backend/app/core/middlewares/metrics.py @@ -4,7 +4,6 @@ import time import psutil -from fastapi import FastAPI from opentelemetry import metrics from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter from opentelemetry.metrics import CallbackOptions, Observation @@ -118,13 +117,22 @@ def _get_path_template(path: str) -> str: return path -def setup_metrics(app: FastAPI, settings: Settings, logger: logging.Logger) -> None: - """Set up OpenTelemetry metrics with OTLP exporter.""" - if not settings.OTEL_EXPORTER_OTLP_ENDPOINT: - logger.warning("OTEL_EXPORTER_OTLP_ENDPOINT not configured, skipping metrics setup") +def setup_metrics(settings: Settings, logger: logging.Logger) -> None: + """Set up the global OpenTelemetry MeterProvider with OTLP exporter. + + This is the single initialization point for metrics export. ``BaseMetrics`` + subclasses and ``MetricsMiddleware`` obtain meters via the global API + (``opentelemetry.metrics.get_meter``), so this must run before them. + When skipped (tests / missing endpoint), the default no-op provider is used. + """ + if settings.TESTING or not settings.OTEL_EXPORTER_OTLP_ENDPOINT: + logger.info( + "Metrics OTLP export disabled (testing=%s, endpoint=%s)", + settings.TESTING, + settings.OTEL_EXPORTER_OTLP_ENDPOINT, + ) return - # Configure OpenTelemetry resource resource = Resource.create( { SERVICE_NAME: settings.SERVICE_NAME, @@ -133,31 +141,21 @@ def setup_metrics(app: FastAPI, settings: Settings, logger: logging.Logger) -> N } ) - # Configure OTLP exporter (sends to OpenTelemetry Collector or compatible backend) - # Default endpoint is localhost:4317 for gRPC otlp_exporter = OTLPMetricExporter(endpoint=settings.OTEL_EXPORTER_OTLP_ENDPOINT, insecure=True) - # Create metric reader with 60 second export interval metric_reader = PeriodicExportingMetricReader( exporter=otlp_exporter, export_interval_millis=60000, ) - # Set up the meter provider meter_provider = MeterProvider( resource=resource, metric_readers=[metric_reader], ) - # Set the global meter provider metrics.set_meter_provider(meter_provider) - - # Create system metrics create_system_metrics() - # Add the metrics middleware (disabled for now to avoid DNS issues) - # app.add_middleware(MetricsMiddleware) - logger.info("OpenTelemetry metrics configured with OTLP exporter") diff --git a/backend/app/domain/enums/__init__.py b/backend/app/domain/enums/__init__.py index 31458a8e..145ec7db 100644 --- a/backend/app/domain/enums/__init__.py +++ b/backend/app/domain/enums/__init__.py @@ -7,7 +7,7 @@ NotificationStatus, ) from app.domain.enums.saga import SagaState -from app.domain.enums.sse import SSEControlEvent, SSENotificationEvent +from app.domain.enums.sse import SSEControlEvent from app.domain.enums.user import UserRole __all__ = [ @@ -30,7 +30,6 @@ "SagaState", # SSE "SSEControlEvent", - "SSENotificationEvent", # User "UserRole", ] diff --git a/backend/app/domain/enums/sse.py b/backend/app/domain/enums/sse.py index 85885634..1cead85a 100644 --- a/backend/app/domain/enums/sse.py +++ b/backend/app/domain/enums/sse.py @@ -17,12 +17,3 @@ class SSEControlEvent(StringEnum): SHUTDOWN = "shutdown" STATUS = "status" ERROR = "error" - - -class SSENotificationEvent(StringEnum): - """Event types for notification SSE streams.""" - - CONNECTED = "connected" - SUBSCRIBED = "subscribed" - HEARTBEAT = "heartbeat" - NOTIFICATION = "notification" diff --git a/backend/app/main.py b/backend/app/main.py index 5dfc1e7b..914d31b0 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -68,7 +68,7 @@ def create_app(settings: Settings | None = None) -> FastAPI: container = create_app_container(settings) setup_dishka(container, app) - setup_metrics(app, settings, logger) + setup_metrics(settings, logger) app.add_middleware(MetricsMiddleware) app.add_middleware(RateLimitMiddleware, settings=settings) app.add_middleware(CSRFMiddleware, container=container) diff --git a/backend/app/schemas_pydantic/sse.py b/backend/app/schemas_pydantic/sse.py index 14a4969c..74948cca 100644 --- a/backend/app/schemas_pydantic/sse.py +++ b/backend/app/schemas_pydantic/sse.py @@ -6,7 +6,7 @@ from app.domain.enums.events import EventType from app.domain.enums.execution import ExecutionStatus from app.domain.enums.notification import NotificationSeverity, NotificationStatus -from app.domain.enums.sse import SSEControlEvent, SSEHealthStatus, SSENotificationEvent +from app.domain.enums.sse import SSEControlEvent, SSEHealthStatus from app.schemas_pydantic.execution import ExecutionResult, ResourceUsage # Type variable for generic Redis message parsing @@ -63,31 +63,6 @@ class RedisSSEMessage(BaseModel): data: dict[str, Any] = Field(description="Full event data from BaseEvent.model_dump()") -class SSENotificationEventData(BaseModel): - """Typed model for SSE notification stream event payload. - - This represents the JSON data sent inside each SSE message for notification streams. - """ - - # Always present - identifies the event type - event_type: SSENotificationEvent = Field(description="SSE notification event type") - - # Present in control events (connected, heartbeat) - user_id: str | None = Field(default=None, description="User ID for the notification stream") - timestamp: datetime | None = Field(default=None, description="Event timestamp") - message: str | None = Field(default=None, description="Human-readable message") - - # Present only in notification events - notification_id: str | None = Field(default=None, description="Unique notification ID") - severity: NotificationSeverity | None = Field(default=None, description="Notification severity level") - status: NotificationStatus | None = Field(default=None, description="Notification delivery status") - tags: list[str] | None = Field(default=None, description="Notification tags") - subject: str | None = Field(default=None, description="Notification subject/title") - body: str | None = Field(default=None, description="Notification body content") - action_url: str | None = Field(default=None, description="Optional action URL") - created_at: datetime | None = Field(default=None, description="Creation timestamp") - - class RedisNotificationMessage(BaseModel): """Message structure published to Redis for notification SSE delivery.""" diff --git a/backend/app/services/sse/sse_service.py b/backend/app/services/sse/sse_service.py index 9cdb13ee..29b67bc8 100644 --- a/backend/app/services/sse/sse_service.py +++ b/backend/app/services/sse/sse_service.py @@ -7,14 +7,15 @@ from app.core.metrics import ConnectionMetrics from app.db.repositories.sse_repository import SSERepository from app.domain.enums.events import EventType -from app.domain.enums.sse import SSEControlEvent, SSEHealthStatus, SSENotificationEvent +from app.domain.enums.notification import NotificationChannel +from app.domain.enums.sse import SSEControlEvent, SSEHealthStatus from app.domain.sse import SSEHealthDomain from app.schemas_pydantic.execution import ExecutionResult +from app.schemas_pydantic.notification import NotificationResponse from app.schemas_pydantic.sse import ( RedisNotificationMessage, RedisSSEMessage, SSEExecutionEventData, - SSENotificationEventData, ) from app.services.sse.kafka_redis_bridge import SSEKafkaRedisBridge from app.services.sse.redis_bus import SSERedisBus @@ -197,66 +198,32 @@ async def _build_sse_event_from_redis(self, execution_id: str, msg: RedisSSEMess async def create_notification_stream(self, user_id: str) -> AsyncGenerator[dict[str, Any], None]: subscription = None - try: - # Start opening subscription concurrently, then yield handshake - sub_task = asyncio.create_task(self.sse_bus.open_notification_subscription(user_id)) - yield self._format_notification_event( - SSENotificationEventData( - event_type=SSENotificationEvent.CONNECTED, - user_id=user_id, - timestamp=datetime.now(timezone.utc), - message="Connected to notification stream", - ) - ) + subscription = await self.sse_bus.open_notification_subscription(user_id) + self.logger.info("Notification subscription opened", extra={"user_id": user_id}) - # Complete Redis subscription after handshake - subscription = await sub_task - - # Signal that subscription is ready - safe to publish notifications now - yield self._format_notification_event( - SSENotificationEventData( - event_type=SSENotificationEvent.SUBSCRIBED, - user_id=user_id, - timestamp=datetime.now(timezone.utc), - message="Redis subscription established", - ) - ) - - last_heartbeat = datetime.now(timezone.utc) while not self.shutdown_manager.is_shutting_down(): - # Heartbeat - now = datetime.now(timezone.utc) - if (now - last_heartbeat).total_seconds() >= self.heartbeat_interval: - yield self._format_notification_event( - SSENotificationEventData( - event_type=SSENotificationEvent.HEARTBEAT, - user_id=user_id, - timestamp=now, - message="Notification stream active", - ) - ) - last_heartbeat = now - - # Forward notification messages as SSE data redis_msg = await subscription.get(RedisNotificationMessage) - if redis_msg: - yield self._format_notification_event( - SSENotificationEventData( - event_type=SSENotificationEvent.NOTIFICATION, - notification_id=redis_msg.notification_id, - severity=redis_msg.severity, - status=redis_msg.status, - tags=redis_msg.tags, - subject=redis_msg.subject, - body=redis_msg.body, - action_url=redis_msg.action_url, - created_at=redis_msg.created_at, - ) - ) + if not redis_msg: + continue + + notification = NotificationResponse( + notification_id=redis_msg.notification_id, + channel=NotificationChannel.IN_APP, + status=redis_msg.status, + subject=redis_msg.subject, + body=redis_msg.body, + action_url=redis_msg.action_url, + created_at=redis_msg.created_at, + read_at=None, + severity=redis_msg.severity, + tags=redis_msg.tags, + ) + yield {"event": "notification", "data": notification.model_dump_json()} finally: if subscription is not None: await asyncio.shield(subscription.close()) + self.logger.info("Notification stream closed", extra={"user_id": user_id}) async def get_health_status(self) -> SSEHealthDomain: router_stats = self.router.get_stats() @@ -274,7 +241,3 @@ async def get_health_status(self) -> SSEHealthDomain: def _format_sse_event(self, event: SSEExecutionEventData) -> dict[str, Any]: """Format typed SSE event for sse-starlette.""" return {"data": event.model_dump_json(exclude_none=True)} - - def _format_notification_event(self, event: SSENotificationEventData) -> dict[str, Any]: - """Format typed notification SSE event for sse-starlette.""" - return {"data": event.model_dump_json(exclude_none=True)} diff --git a/backend/config.test.toml b/backend/config.test.toml index 42274458..f3d5306d 100644 --- a/backend/config.test.toml +++ b/backend/config.test.toml @@ -2,6 +2,7 @@ # Differences from config.toml: lower timeouts, faster bcrypt, relaxed rate limits # Secrets (SECRET_KEY, MONGODB_URL) live in secrets.toml — use secrets.test.toml in CI. +TESTING = true PROJECT_NAME = "integr8scode" DATABASE_NAME = "integr8scode_db" ALGORITHM = "HS256" diff --git a/backend/tests/unit/services/sse/test_sse_service.py b/backend/tests/unit/services/sse/test_sse_service.py index 3c86a15a..eef1dc2c 100644 --- a/backend/tests/unit/services/sse/test_sse_service.py +++ b/backend/tests/unit/services/sse/test_sse_service.py @@ -198,28 +198,21 @@ async def test_execution_stream_result_stored_includes_result_payload(connection @pytest.mark.asyncio -async def test_notification_stream_connected_and_heartbeat_and_message(connection_metrics: ConnectionMetrics) -> None: +async def test_notification_stream_yields_notification_and_shuts_down(connection_metrics: ConnectionMetrics) -> None: + """Notification stream yields {"event": "notification", "data": ...} for each message. + + No control events (connected/subscribed/heartbeat) — those are handled by + the SSE protocol layer (sse-starlette ping, EventSourcePlus onResponse). + """ repo = _FakeRepo() bus = _FakeBus() sm = _FakeShutdown() - settings = _make_fake_settings() - settings.SSE_HEARTBEAT_INTERVAL = 0 # emit immediately - svc = SSEService(repository=repo, router=_FakeRouter(), sse_bus=bus, shutdown_manager=sm, settings=settings, - logger=_test_logger, connection_metrics=connection_metrics) + svc = SSEService(repository=repo, router=_FakeRouter(), sse_bus=bus, shutdown_manager=sm, + settings=_make_fake_settings(), logger=_test_logger, connection_metrics=connection_metrics) agen = svc.create_notification_stream("u1") - connected = await agen.__anext__() - assert _decode(connected)["event_type"] == "connected" - - # Should emit subscribed after Redis subscription is ready - subscribed = await agen.__anext__() - assert _decode(subscribed)["event_type"] == "subscribed" - - # With 0 interval, next yield should be heartbeat - hb = await agen.__anext__() - assert _decode(hb)["event_type"] == "heartbeat" - # Push a notification payload (must match RedisNotificationMessage schema) + # Push a notification payload before advancing (avoids blocking on empty queue) await bus.notif_sub.push({ "notification_id": "n1", "severity": "low", @@ -230,16 +223,24 @@ async def test_notification_stream_connected_and_heartbeat_and_message(connectio "action_url": "", "created_at": "2025-01-01T00:00:00Z", }) - notif = await agen.__anext__() - assert _decode(notif)["event_type"] == "notification" - # Stop the stream by initiating shutdown and advancing once more (loop checks flag) + notif = await asyncio.wait_for(agen.__anext__(), timeout=2.0) + # New format: SSE event field + JSON data (no event_type wrapper) + assert notif["event"] == "notification" + data = json.loads(notif["data"]) + assert data["notification_id"] == "n1" + assert data["subject"] == "s" + assert data["channel"] == "in_app" + + # Stop the stream by initiating shutdown sm.initiate() - # It may loop until it sees the flag; push a None to release get(timeout) + # Push None to unblock the subscription.get() timeout loop await bus.notif_sub.push(None) - # Give the generator a chance to observe the flag and finish with pytest.raises(StopAsyncIteration): - await asyncio.wait_for(agen.__anext__(), timeout=0.2) + await asyncio.wait_for(agen.__anext__(), timeout=2.0) + + # Subscription should be closed during cleanup + assert bus.notif_sub.closed is True @pytest.mark.asyncio diff --git a/docs/reference/openapi.json b/docs/reference/openapi.json index e81eb2fb..d21477bc 100644 --- a/docs/reference/openapi.json +++ b/docs/reference/openapi.json @@ -1808,7 +1808,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SSENotificationEventData" + "$ref": "#/components/schemas/NotificationResponse" } } } @@ -5616,9 +5616,8 @@ "title": "Memory Request" }, "priority": { - "type": "integer", - "title": "Priority", - "default": 5 + "$ref": "#/components/schemas/QueuePriority", + "default": "normal" } }, "type": "object", @@ -8843,9 +8842,8 @@ "title": "Estimated Wait Seconds" }, "priority": { - "type": "integer", - "title": "Priority", - "default": 5 + "$ref": "#/components/schemas/QueuePriority", + "default": "normal" } }, "type": "object", @@ -9387,9 +9385,8 @@ "title": "Memory Request" }, "priority": { - "type": "integer", - "title": "Priority", - "default": 5 + "$ref": "#/components/schemas/QueuePriority", + "default": "normal" } }, "type": "object", @@ -11542,6 +11539,18 @@ "title": "PublishEventResponse", "description": "Response model for publishing events" }, + "QueuePriority": { + "type": "string", + "enum": [ + "critical", + "high", + "normal", + "low", + "background" + ], + "title": "QueuePriority", + "description": "Execution priority, ordered highest to lowest." + }, "QuotaExceededEvent": { "properties": { "event_id": { @@ -13350,166 +13359,6 @@ "title": "SSEHealthStatus", "description": "Health status for SSE service." }, - "SSENotificationEvent": { - "type": "string", - "enum": [ - "connected", - "subscribed", - "heartbeat", - "notification" - ], - "title": "SSENotificationEvent", - "description": "Event types for notification SSE streams." - }, - "SSENotificationEventData": { - "properties": { - "event_type": { - "$ref": "#/components/schemas/SSENotificationEvent", - "description": "SSE notification event type" - }, - "user_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "User Id", - "description": "User ID for the notification stream" - }, - "timestamp": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Timestamp", - "description": "Event timestamp" - }, - "message": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Message", - "description": "Human-readable message" - }, - "notification_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Notification Id", - "description": "Unique notification ID" - }, - "severity": { - "anyOf": [ - { - "$ref": "#/components/schemas/NotificationSeverity" - }, - { - "type": "null" - } - ], - "description": "Notification severity level" - }, - "status": { - "anyOf": [ - { - "$ref": "#/components/schemas/NotificationStatus" - }, - { - "type": "null" - } - ], - "description": "Notification delivery status" - }, - "tags": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Tags", - "description": "Notification tags" - }, - "subject": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Subject", - "description": "Notification subject/title" - }, - "body": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Body", - "description": "Notification body content" - }, - "action_url": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Action Url", - "description": "Optional action URL" - }, - "created_at": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Created At", - "description": "Creation timestamp" - } - }, - "type": "object", - "required": [ - "event_type" - ], - "title": "SSENotificationEventData", - "description": "Typed model for SSE notification stream event payload.\n\nThis represents the JSON data sent inside each SSE message for notification streams." - }, "SagaCancellationResponse": { "properties": { "success": { diff --git a/frontend/src/components/admin/events/UserOverviewModal.svelte b/frontend/src/components/admin/events/UserOverviewModal.svelte index 2fc90a72..a0782356 100644 --- a/frontend/src/components/admin/events/UserOverviewModal.svelte +++ b/frontend/src/components/admin/events/UserOverviewModal.svelte @@ -1,16 +1,10 @@ diff --git a/frontend/src/lib/__tests__/api-interceptors.test.ts b/frontend/src/lib/__tests__/api-interceptors.test.ts new file mode 100644 index 00000000..4870dd9d --- /dev/null +++ b/frontend/src/lib/__tests__/api-interceptors.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from 'vitest'; +import { getErrorMessage, unwrap, unwrapOr } from '../api-interceptors'; + +describe('getErrorMessage', () => { + it.each([ + ['null', null, undefined, 'An error occurred'], + ['undefined', undefined, undefined, 'An error occurred'], + ['zero', 0, undefined, 'An error occurred'], + ['empty string', '', undefined, 'An error occurred'], + ['null with custom fallback', null, 'custom', 'custom'], + ['number', 42, undefined, 'An error occurred'], + ['boolean', true, undefined, 'An error occurred'], + ['object without detail/message', { foo: 'bar' }, undefined, 'An error occurred'], + ] as [string, unknown, string | undefined, string][])('returns fallback for %s', (_label, input, fallback, expected) => { + expect(getErrorMessage(input, fallback)).toBe(expected); + }); + + it.each([ + ['string error', 'something broke', 'something broke'], + ['Error instance', new Error('boom'), 'boom'], + ['object with .detail string', { detail: 'Not found' }, 'Not found'], + ['object with .message string', { message: 'Oops' }, 'Oops'], + ['object with both (detail wins)', { detail: 'detail wins', message: 'msg' }, 'detail wins'], + ['ValidationError[] with locs', { + detail: [ + { loc: ['body', 'email'], msg: 'invalid email', type: 'value_error' }, + { loc: ['body', 'name'], msg: 'required', type: 'value_error' }, + ], + }, 'email: invalid email, name: required'], + ['ValidationError[] with empty loc', { + detail: [{ loc: [], msg: 'bad', type: 'value_error' }], + }, 'field: bad'], + ] as [string, unknown, string][])('extracts message from %s', (_label, input, expected) => { + expect(getErrorMessage(input)).toBe(expected); + }); +}); + +describe('unwrap', () => { + it.each([ + ['number', { data: 42 }, 42], + ['zero', { data: 0 }, 0], + ['empty string', { data: '' }, ''], + ['false', { data: false }, false], + ] as [string, { data: unknown }, unknown][])('returns data for %s', (_label, result, expected) => { + expect(unwrap(result)).toBe(expected); + }); + + it.each([ + ['error present', { data: 42, error: new Error('fail') }], + ['data undefined', {}], + ] as [string, { data?: unknown; error?: unknown }][])('throws when %s', (_label, result) => { + expect(() => unwrap(result)).toThrow(); + }); +}); + +describe('unwrapOr', () => { + it.each([ + ['data present', { data: 'value' }, 'fb', 'value'], + ['data is 0', { data: 0 }, 99, 0], + ['data is empty string', { data: '' }, 'fb', ''], + ['error present', { data: 'v', error: new Error() }, 'fb', 'fb'], + ['data undefined', {}, 'fb', 'fb'], + ] as [string, { data?: unknown; error?: unknown }, unknown, unknown][])('returns correct value when %s', (_label, result, fallback, expected) => { + expect(unwrapOr(result, fallback)).toBe(expected); + }); +}); diff --git a/frontend/src/lib/__tests__/formatters.test.ts b/frontend/src/lib/__tests__/formatters.test.ts new file mode 100644 index 00000000..6f289317 --- /dev/null +++ b/frontend/src/lib/__tests__/formatters.test.ts @@ -0,0 +1,182 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + formatDate, + formatTimestamp, + formatDuration, + formatDurationBetween, + formatRelativeTime, + formatBytes, + formatNumber, + truncate, +} from '../formatters'; + +describe('formatDate', () => { + it.each([ + [null, 'N/A'], + [undefined, 'N/A'], + ['', 'N/A'], + ['not-a-date', 'N/A'], + ] as const)('returns %j for %j input', (input, expected) => { + expect(formatDate(input as string | null | undefined)).toBe(expected); + }); + + it('formats ISO string to DD/MM/YYYY HH:mm', () => { + const date = new Date(2025, 0, 15, 14, 30); + expect(formatDate(date.toISOString())).toBe('15/01/2025 14:30'); + }); + + it('accepts Date object', () => { + expect(formatDate(new Date(2025, 5, 1, 9, 5))).toBe('01/06/2025 09:05'); + }); +}); + +describe('formatTimestamp', () => { + it.each([null, undefined, 'garbage'] as const)('returns N/A for %j', (input) => { + expect(formatTimestamp(input as string | null | undefined)).toBe('N/A'); + }); + + it('formats ISO string', () => { + const date = new Date(2025, 0, 15); + expect(formatTimestamp(date.toISOString())).toBe(date.toLocaleString()); + }); + + it('accepts custom Intl options', () => { + const date = new Date(2025, 0, 15); + const opts: Intl.DateTimeFormatOptions = { year: 'numeric' }; + const expected = new Intl.DateTimeFormat(undefined, opts).format(date); + expect(formatTimestamp(date.toISOString(), opts)).toBe(expected); + }); + + it('accepts Date object', () => { + const date = new Date(2025, 0, 15); + expect(formatTimestamp(date)).toBe(date.toLocaleString()); + }); +}); + +describe('formatDuration', () => { + it.each([ + [0, '0ms'], + [0.5, '500ms'], + [30, '30.0s'], + [90, '1m 30s'], + [120, '2m'], + [3660, '1h 1m'], + [3600, '1h'], + ])('formats %d seconds as %j', (input, expected) => { + expect(formatDuration(input)).toBe(expected); + }); + + it.each([null, undefined, -5])('returns N/A for %j', (input) => { + expect(formatDuration(input as number | null | undefined)).toBe('N/A'); + }); +}); + +describe('formatDurationBetween', () => { + it('computes duration between two timestamps', () => { + const start = new Date(2025, 0, 1, 12, 0, 0); + const end = new Date(2025, 0, 1, 12, 1, 30); + expect(formatDurationBetween(start, end)).toBe('1m 30s'); + }); + + it.each([ + [null, '2025-01-01T12:00:00Z'], + ['2025-01-01T12:00:00Z', null], + ['garbage', '2025-01-01T12:00:00Z'], + ] as const)('returns N/A for (%j, %j)', (start, end) => { + expect(formatDurationBetween( + start as string | null | undefined, + end as string | null | undefined, + )).toBe('N/A'); + }); + + it('returns N/A when end is before start', () => { + const start = new Date(2025, 0, 1, 13, 0, 0); + const end = new Date(2025, 0, 1, 12, 0, 0); + expect(formatDurationBetween(start, end)).toBe('N/A'); + }); +}); + +describe('formatRelativeTime', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2025, 6, 15, 12, 0, 0)); + }); + afterEach(() => vi.useRealTimers()); + + it.each([ + [new Date(2025, 6, 15, 11, 59, 30), 'just now'], // 30s ago + [new Date(2025, 6, 15, 11, 55, 0), '5m ago'], // 5m ago + [new Date(2025, 6, 15, 9, 0, 0), '3h ago'], // 3h ago + [new Date(2025, 6, 13, 12, 0, 0), '2d ago'], // 2d ago + ])('formats %j as %j', (date, expected) => { + expect(formatRelativeTime(date.toISOString())).toBe(expected); + }); + + it('returns locale date for >7 days ago', () => { + const result = formatRelativeTime(new Date(2025, 6, 1).toISOString()); + expect(result).not.toMatch(/ago$/); + expect(result).not.toBe('N/A'); + }); + + it('returns locale date for future dates', () => { + const result = formatRelativeTime(new Date(2025, 6, 20).toISOString()); + expect(result).not.toMatch(/ago$/); + }); + + it.each([null, 'not-valid'])('returns N/A for %j', (input) => { + expect(formatRelativeTime(input as string | null | undefined)).toBe('N/A'); + }); +}); + +describe('formatBytes', () => { + it.each([ + [0, undefined, '0 B'], + [500, undefined, '500 B'], + [1536, undefined, '1.5 KB'], + [1048576, undefined, '1 MB'], + [1536, 0, '2 KB'], + ] as const)('formats %d bytes (decimals=%j) as %j', (bytes, decimals, expected) => { + expect(formatBytes(bytes, decimals)).toBe(expected); + }); + + it.each([null, undefined, -1])('returns N/A for %j', (input) => { + expect(formatBytes(input as number | null | undefined)).toBe('N/A'); + }); +}); + +describe('formatNumber', () => { + it('formats zero', () => { + const expected = new Intl.NumberFormat().format(0); + expect(formatNumber(0)).toBe(expected); + }); + + it('formats with locale separators', () => { + const expected = new Intl.NumberFormat().format(1234567); + expect(formatNumber(1234567)).toBe(expected); + }); + + it.each([null, undefined])('returns N/A for %j', (input) => { + expect(formatNumber(input as number | null | undefined)).toBe('N/A'); + }); +}); + +describe('truncate', () => { + it.each([ + ['hello', 50, 'hello'], + ['12345', 5, '12345'], + [null, 50, ''], + ['', 50, ''], + ] as const)('truncate(%j, %d) → %j', (str, max, expected) => { + expect(truncate(str as string | null | undefined, max)).toBe(expected); + }); + + it('truncates long string with ellipsis', () => { + const result = truncate('a'.repeat(60), 50); + expect(result).toHaveLength(50); + expect(result).toMatch(/\.\.\.$/); + }); + + it('respects custom maxLength', () => { + expect(truncate('abcdefghij', 7)).toBe('abcd...'); + }); +}); diff --git a/frontend/src/lib/admin/__tests__/autoRefresh.test.ts b/frontend/src/lib/admin/__tests__/autoRefresh.test.ts new file mode 100644 index 00000000..a2e608c0 --- /dev/null +++ b/frontend/src/lib/admin/__tests__/autoRefresh.test.ts @@ -0,0 +1,140 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { effect_root } from 'svelte/internal/client'; + +// Mock onDestroy — no component lifecycle in unit tests +vi.mock('svelte', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, onDestroy: vi.fn() }; +}); + +const { createAutoRefresh } = await import('../autoRefresh.svelte'); + +describe('createAutoRefresh', () => { + beforeEach(() => vi.useFakeTimers()); + afterEach(() => vi.useRealTimers()); + + function make(overrides: Record = {}) { + const onRefresh = vi.fn(); + let ar!: ReturnType; + const teardown = effect_root(() => { + ar = createAutoRefresh({ + onRefresh, + autoCleanup: false, + initialEnabled: false, + ...overrides, + }); + }); + return { ar, onRefresh, teardown }; + } + + describe('initial state', () => { + it.each([ + [{ initialEnabled: true }, true], + [{ initialEnabled: false }, false], + ])('enabled=%j → %s', (opts, expected) => { + const { ar, teardown } = make(opts); + expect(ar.enabled).toBe(expected); + ar.cleanup(); + teardown(); + }); + + it('defaults rate to 5', () => { + const { ar, teardown } = make(); + expect(ar.rate).toBe(5); + ar.cleanup(); + teardown(); + }); + + it('exposes default rate options', () => { + const { ar, teardown } = make(); + expect(ar.rateOptions).toEqual([ + { value: 5, label: '5 seconds' }, + { value: 10, label: '10 seconds' }, + { value: 30, label: '30 seconds' }, + { value: 60, label: '1 minute' }, + ]); + ar.cleanup(); + teardown(); + }); + + it('accepts custom rate options', () => { + const custom = [{ value: 15, label: '15s' }]; + const { ar, teardown } = make({ rateOptions: custom }); + expect(ar.rateOptions).toEqual(custom); + ar.cleanup(); + teardown(); + }); + }); + + describe('start / stop / cleanup', () => { + it('start fires onRefresh at the configured rate', () => { + const { ar, onRefresh, teardown } = make({ initialRate: 2 }); + ar.enabled = true; + ar.start(); + + vi.advanceTimersByTime(2000); + expect(onRefresh).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(2000); + expect(onRefresh).toHaveBeenCalledTimes(2); + + ar.cleanup(); + teardown(); + }); + + it('does not fire when disabled', () => { + const { ar, onRefresh, teardown } = make({ initialEnabled: false }); + ar.start(); + + vi.advanceTimersByTime(10_000); + expect(onRefresh).not.toHaveBeenCalled(); + + ar.cleanup(); + teardown(); + }); + + it('stop halts the interval', () => { + const { ar, onRefresh, teardown } = make({ initialRate: 1 }); + ar.enabled = true; + ar.start(); + + vi.advanceTimersByTime(1000); + expect(onRefresh).toHaveBeenCalledTimes(1); + + ar.stop(); + vi.advanceTimersByTime(5000); + expect(onRefresh).toHaveBeenCalledTimes(1); + + teardown(); + }); + + it('restart restarts the interval', () => { + const { ar, onRefresh, teardown } = make({ initialRate: 1 }); + ar.enabled = true; + ar.start(); + + vi.advanceTimersByTime(500); + ar.restart(); // resets timer + vi.advanceTimersByTime(500); + expect(onRefresh).not.toHaveBeenCalled(); // only 500ms since restart + + vi.advanceTimersByTime(500); + expect(onRefresh).toHaveBeenCalledTimes(1); + + ar.cleanup(); + teardown(); + }); + + it('cleanup stops the interval', () => { + const { ar, onRefresh, teardown } = make({ initialRate: 1 }); + ar.enabled = true; + ar.start(); + + ar.cleanup(); + vi.advanceTimersByTime(5000); + expect(onRefresh).not.toHaveBeenCalled(); + + teardown(); + }); + }); +}); diff --git a/frontend/src/lib/admin/__tests__/pagination.test.ts b/frontend/src/lib/admin/__tests__/pagination.test.ts new file mode 100644 index 00000000..a2894a68 --- /dev/null +++ b/frontend/src/lib/admin/__tests__/pagination.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect, vi } from 'vitest'; + +const { createPaginationState } = await import('../pagination.svelte'); + +describe('createPaginationState', () => { + describe('defaults', () => { + it('initialises with default values', () => { + const p = createPaginationState(); + expect(p.currentPage).toBe(1); + expect(p.pageSize).toBe(10); + expect(p.totalItems).toBe(0); + expect(p.totalPages).toBe(1); + expect(p.skip).toBe(0); + }); + }); + + describe('custom options', () => { + it.each([ + [{ initialPage: 3, initialPageSize: 20 }, 3, 20], + [{ initialPage: 1, initialPageSize: 5 }, 1, 5], + ])('createPaginationState(%j) → page=%d, size=%d', (opts, page, size) => { + const p = createPaginationState(opts); + expect(p.currentPage).toBe(page); + expect(p.pageSize).toBe(size); + }); + }); + + describe('totalPages', () => { + it.each([ + [0, 10, 1], // empty → 1 page + [5, 10, 1], // fewer than page size + [10, 10, 1], // exactly one page + [11, 10, 2], // one extra item + [100, 20, 5], // even split + [101, 20, 6], // one extra + ])('totalItems=%d, pageSize=%d → totalPages=%d', (items, size, expected) => { + const p = createPaginationState({ initialPageSize: size }); + p.totalItems = items; + expect(p.totalPages).toBe(expected); + }); + }); + + describe('skip', () => { + it.each([ + [1, 10, 0], + [2, 10, 10], + [3, 20, 40], + [5, 5, 20], + ])('page=%d, size=%d → skip=%d', (page, size, expected) => { + const p = createPaginationState({ initialPage: page, initialPageSize: size }); + expect(p.skip).toBe(expected); + }); + }); + + describe('handlePageChange', () => { + it('updates currentPage and calls onLoad', () => { + const p = createPaginationState(); + const onLoad = vi.fn(); + p.handlePageChange(5, onLoad); + expect(p.currentPage).toBe(5); + expect(onLoad).toHaveBeenCalledOnce(); + }); + + it('works without onLoad callback', () => { + const p = createPaginationState(); + p.handlePageChange(3); + expect(p.currentPage).toBe(3); + }); + }); + + describe('handlePageSizeChange', () => { + it('updates pageSize, resets to page 1, and calls onLoad', () => { + const p = createPaginationState({ initialPage: 5 }); + const onLoad = vi.fn(); + p.handlePageSizeChange(50, onLoad); + expect(p.pageSize).toBe(50); + expect(p.currentPage).toBe(1); + expect(onLoad).toHaveBeenCalledOnce(); + }); + }); + + describe('reset', () => { + it('restores initial values', () => { + const p = createPaginationState({ initialPage: 2, initialPageSize: 25 }); + p.currentPage = 10; + p.pageSize = 100; + p.totalItems = 500; + + p.reset(); + + expect(p.currentPage).toBe(2); + expect(p.pageSize).toBe(25); + expect(p.totalItems).toBe(0); + }); + }); +}); diff --git a/frontend/src/lib/api/index.ts b/frontend/src/lib/api/index.ts index 1f20e0cf..fbbd685c 100644 --- a/frontend/src/lib/api/index.ts +++ b/frontend/src/lib/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts export { aggregateEventsApiV1EventsAggregatePost, browseEventsApiV1AdminEventsBrowsePost, cancelExecutionApiV1ExecutionsExecutionIdCancelPost, cancelReplaySessionApiV1ReplaySessionsSessionIdCancelPost, cancelSagaApiV1SagasSagaIdCancelPost, cleanupOldSessionsApiV1ReplayCleanupPost, createExecutionApiV1ExecutePost, createReplaySessionApiV1ReplaySessionsPost, createSavedScriptApiV1ScriptsPost, createUserApiV1AdminUsersPost, deleteEventApiV1AdminEventsEventIdDelete, deleteEventApiV1EventsEventIdDelete, deleteExecutionApiV1ExecutionsExecutionIdDelete, deleteNotificationApiV1NotificationsNotificationIdDelete, deleteSavedScriptApiV1ScriptsScriptIdDelete, deleteUserApiV1AdminUsersUserIdDelete, discardDlqMessageApiV1DlqMessagesEventIdDelete, executionEventsApiV1EventsExecutionsExecutionIdGet, exportEventsCsvApiV1AdminEventsExportCsvGet, exportEventsJsonApiV1AdminEventsExportJsonGet, getCurrentRequestEventsApiV1EventsCurrentRequestGet, getCurrentUserProfileApiV1AuthMeGet, getDlqMessageApiV1DlqMessagesEventIdGet, getDlqMessagesApiV1DlqMessagesGet, getDlqStatisticsApiV1DlqStatsGet, getDlqTopicsApiV1DlqTopicsGet, getEventApiV1EventsEventIdGet, getEventDetailApiV1AdminEventsEventIdGet, getEventsByCorrelationApiV1EventsCorrelationCorrelationIdGet, getEventStatisticsApiV1EventsStatisticsGet, getEventStatsApiV1AdminEventsStatsGet, getExampleScriptsApiV1ExampleScriptsGet, getExecutionEventsApiV1EventsExecutionsExecutionIdEventsGet, getExecutionEventsApiV1ExecutionsExecutionIdEventsGet, getExecutionSagasApiV1SagasExecutionExecutionIdGet, getK8sResourceLimitsApiV1K8sLimitsGet, getNotificationsApiV1NotificationsGet, getReplaySessionApiV1ReplaySessionsSessionIdGet, getReplayStatusApiV1AdminEventsReplaySessionIdStatusGet, getResultApiV1ExecutionsExecutionIdResultGet, getSagaStatusApiV1SagasSagaIdGet, getSavedScriptApiV1ScriptsScriptIdGet, getSettingsHistoryApiV1UserSettingsHistoryGet, getSubscriptionsApiV1NotificationsSubscriptionsGet, getSystemSettingsApiV1AdminSettingsGet, getUnreadCountApiV1NotificationsUnreadCountGet, getUserApiV1AdminUsersUserIdGet, getUserEventsApiV1EventsUserGet, getUserExecutionsApiV1UserExecutionsGet, getUserOverviewApiV1AdminUsersUserIdOverviewGet, getUserRateLimitsApiV1AdminUsersUserIdRateLimitsGet, getUserSettingsApiV1UserSettingsGet, listEventTypesApiV1EventsTypesListGet, listReplaySessionsApiV1ReplaySessionsGet, listSagasApiV1SagasGet, listSavedScriptsApiV1ScriptsGet, listUsersApiV1AdminUsersGet, livenessApiV1HealthLiveGet, loginApiV1AuthLoginPost, logoutApiV1AuthLogoutPost, markAllReadApiV1NotificationsMarkAllReadPost, markNotificationReadApiV1NotificationsNotificationIdReadPut, notificationStreamApiV1EventsNotificationsStreamGet, type Options, pauseReplaySessionApiV1ReplaySessionsSessionIdPausePost, publishCustomEventApiV1EventsPublishPost, queryEventsApiV1EventsQueryPost, readinessApiV1HealthReadyGet, receiveGrafanaAlertsApiV1AlertsGrafanaPost, registerApiV1AuthRegisterPost, replayAggregateEventsApiV1EventsReplayAggregateIdPost, replayEventsApiV1AdminEventsReplayPost, resetSystemSettingsApiV1AdminSettingsResetPost, resetUserPasswordApiV1AdminUsersUserIdResetPasswordPost, resetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPost, restoreSettingsApiV1UserSettingsRestorePost, resumeReplaySessionApiV1ReplaySessionsSessionIdResumePost, retryDlqMessagesApiV1DlqRetryPost, retryExecutionApiV1ExecutionsExecutionIdRetryPost, setRetryPolicyApiV1DlqRetryPolicyPost, sseHealthApiV1EventsHealthGet, startReplaySessionApiV1ReplaySessionsSessionIdStartPost, testGrafanaAlertEndpointApiV1AlertsGrafanaTestGet, updateCustomSettingApiV1UserSettingsCustomKeyPut, updateEditorSettingsApiV1UserSettingsEditorPut, updateNotificationSettingsApiV1UserSettingsNotificationsPut, updateSavedScriptApiV1ScriptsScriptIdPut, updateSubscriptionApiV1NotificationsSubscriptionsChannelPut, updateSystemSettingsApiV1AdminSettingsPut, updateThemeApiV1UserSettingsThemePut, updateUserApiV1AdminUsersUserIdPut, updateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPut, updateUserSettingsApiV1UserSettingsPut, verifyTokenApiV1AuthVerifyTokenGet } from './sdk.gen'; -export type { AdminUserOverview, AgeStatistics, AggregateEventsApiV1EventsAggregatePostData, AggregateEventsApiV1EventsAggregatePostError, AggregateEventsApiV1EventsAggregatePostErrors, AggregateEventsApiV1EventsAggregatePostResponse, AggregateEventsApiV1EventsAggregatePostResponses, AlertResponse, AllocateResourcesCommandEvent, AuthFailedEvent, BodyLoginApiV1AuthLoginPost, BrowseEventsApiV1AdminEventsBrowsePostData, BrowseEventsApiV1AdminEventsBrowsePostError, BrowseEventsApiV1AdminEventsBrowsePostErrors, BrowseEventsApiV1AdminEventsBrowsePostResponse, BrowseEventsApiV1AdminEventsBrowsePostResponses, CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, CancelExecutionApiV1ExecutionsExecutionIdCancelPostError, CancelExecutionApiV1ExecutionsExecutionIdCancelPostErrors, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponse, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponses, CancelExecutionRequest, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostError, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostErrors, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponse, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponses, CancelResponse, CancelSagaApiV1SagasSagaIdCancelPostData, CancelSagaApiV1SagasSagaIdCancelPostError, CancelSagaApiV1SagasSagaIdCancelPostErrors, CancelSagaApiV1SagasSagaIdCancelPostResponse, CancelSagaApiV1SagasSagaIdCancelPostResponses, CleanupOldSessionsApiV1ReplayCleanupPostData, CleanupOldSessionsApiV1ReplayCleanupPostError, CleanupOldSessionsApiV1ReplayCleanupPostErrors, CleanupOldSessionsApiV1ReplayCleanupPostResponse, CleanupOldSessionsApiV1ReplayCleanupPostResponses, CleanupResponse, ClientOptions, ContainerStatusInfo, CreateExecutionApiV1ExecutePostData, CreateExecutionApiV1ExecutePostError, CreateExecutionApiV1ExecutePostErrors, CreateExecutionApiV1ExecutePostResponse, CreateExecutionApiV1ExecutePostResponses, CreatePodCommandEvent, CreateReplaySessionApiV1ReplaySessionsPostData, CreateReplaySessionApiV1ReplaySessionsPostError, CreateReplaySessionApiV1ReplaySessionsPostErrors, CreateReplaySessionApiV1ReplaySessionsPostResponse, CreateReplaySessionApiV1ReplaySessionsPostResponses, CreateSavedScriptApiV1ScriptsPostData, CreateSavedScriptApiV1ScriptsPostError, CreateSavedScriptApiV1ScriptsPostErrors, CreateSavedScriptApiV1ScriptsPostResponse, CreateSavedScriptApiV1ScriptsPostResponses, CreateUserApiV1AdminUsersPostData, CreateUserApiV1AdminUsersPostError, CreateUserApiV1AdminUsersPostErrors, CreateUserApiV1AdminUsersPostResponse, CreateUserApiV1AdminUsersPostResponses, DeleteEventApiV1AdminEventsEventIdDeleteData, DeleteEventApiV1AdminEventsEventIdDeleteError, DeleteEventApiV1AdminEventsEventIdDeleteErrors, DeleteEventApiV1AdminEventsEventIdDeleteResponse, DeleteEventApiV1AdminEventsEventIdDeleteResponses, DeleteEventApiV1EventsEventIdDeleteData, DeleteEventApiV1EventsEventIdDeleteError, DeleteEventApiV1EventsEventIdDeleteErrors, DeleteEventApiV1EventsEventIdDeleteResponse, DeleteEventApiV1EventsEventIdDeleteResponses, DeleteEventResponse, DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, DeleteExecutionApiV1ExecutionsExecutionIdDeleteError, DeleteExecutionApiV1ExecutionsExecutionIdDeleteErrors, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponse, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponses, DeleteNotificationApiV1NotificationsNotificationIdDeleteData, DeleteNotificationApiV1NotificationsNotificationIdDeleteError, DeleteNotificationApiV1NotificationsNotificationIdDeleteErrors, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponse, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses, DeleteNotificationResponse, DeletePodCommandEvent, DeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, DeleteSavedScriptApiV1ScriptsScriptIdDeleteError, DeleteSavedScriptApiV1ScriptsScriptIdDeleteErrors, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponses, DeleteUserApiV1AdminUsersUserIdDeleteData, DeleteUserApiV1AdminUsersUserIdDeleteError, DeleteUserApiV1AdminUsersUserIdDeleteErrors, DeleteUserApiV1AdminUsersUserIdDeleteResponse, DeleteUserApiV1AdminUsersUserIdDeleteResponses, DeleteUserResponse, DerivedCounts, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteData, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteError, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteErrors, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteResponse, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteResponses, DlqBatchRetryResponse, DlqMessageDetail, DlqMessageDiscardedEvent, DlqMessageReceivedEvent, DlqMessageResponse, DlqMessageRetriedEvent, DlqMessagesResponse, DlqMessageStatus, DlqRetryResult, DlqStats, DlqTopicSummaryResponse, EditorSettings, EndpointGroup, EndpointUsageStats, Environment, EventAggregationRequest, EventBrowseRequest, EventBrowseResponse, EventDeleteResponse, EventDetailResponse, EventFilter, EventFilterRequest, EventListResponse, EventMetadata, EventReplayRequest, EventReplayResponse, EventReplayStatusResponse, EventReplayStatusResponseWritable, EventStatistics, EventStatsResponse, EventSummary, EventType, EventTypeCountSchema, EventTypeStatistic, ExampleScripts, ExecutionAcceptedEvent, ExecutionCancelledEvent, ExecutionCompletedEvent, ExecutionErrorType, ExecutionEventsApiV1EventsExecutionsExecutionIdGetData, ExecutionEventsApiV1EventsExecutionsExecutionIdGetError, ExecutionEventsApiV1EventsExecutionsExecutionIdGetErrors, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponse, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponses, ExecutionFailedEvent, ExecutionLimitsSchema, ExecutionListResponse, ExecutionQueuedEvent, ExecutionRequest, ExecutionRequestedEvent, ExecutionResponse, ExecutionResult, ExecutionRunningEvent, ExecutionStartedEvent, ExecutionStatus, ExecutionTimeoutEvent, ExportEventsCsvApiV1AdminEventsExportCsvGetData, ExportEventsCsvApiV1AdminEventsExportCsvGetError, ExportEventsCsvApiV1AdminEventsExportCsvGetErrors, ExportEventsCsvApiV1AdminEventsExportCsvGetResponses, ExportEventsJsonApiV1AdminEventsExportJsonGetData, ExportEventsJsonApiV1AdminEventsExportJsonGetError, ExportEventsJsonApiV1AdminEventsExportJsonGetErrors, ExportEventsJsonApiV1AdminEventsExportJsonGetResponses, GetCurrentRequestEventsApiV1EventsCurrentRequestGetData, GetCurrentRequestEventsApiV1EventsCurrentRequestGetError, GetCurrentRequestEventsApiV1EventsCurrentRequestGetErrors, GetCurrentRequestEventsApiV1EventsCurrentRequestGetResponse, GetCurrentRequestEventsApiV1EventsCurrentRequestGetResponses, GetCurrentUserProfileApiV1AuthMeGetData, GetCurrentUserProfileApiV1AuthMeGetResponse, GetCurrentUserProfileApiV1AuthMeGetResponses, GetDlqMessageApiV1DlqMessagesEventIdGetData, GetDlqMessageApiV1DlqMessagesEventIdGetError, GetDlqMessageApiV1DlqMessagesEventIdGetErrors, GetDlqMessageApiV1DlqMessagesEventIdGetResponse, GetDlqMessageApiV1DlqMessagesEventIdGetResponses, GetDlqMessagesApiV1DlqMessagesGetData, GetDlqMessagesApiV1DlqMessagesGetError, GetDlqMessagesApiV1DlqMessagesGetErrors, GetDlqMessagesApiV1DlqMessagesGetResponse, GetDlqMessagesApiV1DlqMessagesGetResponses, GetDlqStatisticsApiV1DlqStatsGetData, GetDlqStatisticsApiV1DlqStatsGetResponse, GetDlqStatisticsApiV1DlqStatsGetResponses, GetDlqTopicsApiV1DlqTopicsGetData, GetDlqTopicsApiV1DlqTopicsGetResponse, GetDlqTopicsApiV1DlqTopicsGetResponses, GetEventApiV1EventsEventIdGetData, GetEventApiV1EventsEventIdGetError, GetEventApiV1EventsEventIdGetErrors, GetEventApiV1EventsEventIdGetResponse, GetEventApiV1EventsEventIdGetResponses, GetEventDetailApiV1AdminEventsEventIdGetData, GetEventDetailApiV1AdminEventsEventIdGetError, GetEventDetailApiV1AdminEventsEventIdGetErrors, GetEventDetailApiV1AdminEventsEventIdGetResponse, GetEventDetailApiV1AdminEventsEventIdGetResponses, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetData, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetError, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetErrors, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetResponse, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetResponses, GetEventStatisticsApiV1EventsStatisticsGetData, GetEventStatisticsApiV1EventsStatisticsGetError, GetEventStatisticsApiV1EventsStatisticsGetErrors, GetEventStatisticsApiV1EventsStatisticsGetResponse, GetEventStatisticsApiV1EventsStatisticsGetResponses, GetEventStatsApiV1AdminEventsStatsGetData, GetEventStatsApiV1AdminEventsStatsGetError, GetEventStatsApiV1AdminEventsStatsGetErrors, GetEventStatsApiV1AdminEventsStatsGetResponse, GetEventStatsApiV1AdminEventsStatsGetResponses, GetExampleScriptsApiV1ExampleScriptsGetData, GetExampleScriptsApiV1ExampleScriptsGetResponse, GetExampleScriptsApiV1ExampleScriptsGetResponses, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetError, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetResponse, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetResponses, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetError, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses, GetExecutionSagasApiV1SagasExecutionExecutionIdGetData, GetExecutionSagasApiV1SagasExecutionExecutionIdGetError, GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors, GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse, GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponses, GetK8sResourceLimitsApiV1K8sLimitsGetData, GetK8sResourceLimitsApiV1K8sLimitsGetResponse, GetK8sResourceLimitsApiV1K8sLimitsGetResponses, GetNotificationsApiV1NotificationsGetData, GetNotificationsApiV1NotificationsGetError, GetNotificationsApiV1NotificationsGetErrors, GetNotificationsApiV1NotificationsGetResponse, GetNotificationsApiV1NotificationsGetResponses, GetReplaySessionApiV1ReplaySessionsSessionIdGetData, GetReplaySessionApiV1ReplaySessionsSessionIdGetError, GetReplaySessionApiV1ReplaySessionsSessionIdGetErrors, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponse, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponses, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetData, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetError, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetErrors, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponse, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponses, GetResultApiV1ExecutionsExecutionIdResultGetData, GetResultApiV1ExecutionsExecutionIdResultGetError, GetResultApiV1ExecutionsExecutionIdResultGetErrors, GetResultApiV1ExecutionsExecutionIdResultGetResponse, GetResultApiV1ExecutionsExecutionIdResultGetResponses, GetSagaStatusApiV1SagasSagaIdGetData, GetSagaStatusApiV1SagasSagaIdGetError, GetSagaStatusApiV1SagasSagaIdGetErrors, GetSagaStatusApiV1SagasSagaIdGetResponse, GetSagaStatusApiV1SagasSagaIdGetResponses, GetSavedScriptApiV1ScriptsScriptIdGetData, GetSavedScriptApiV1ScriptsScriptIdGetError, GetSavedScriptApiV1ScriptsScriptIdGetErrors, GetSavedScriptApiV1ScriptsScriptIdGetResponse, GetSavedScriptApiV1ScriptsScriptIdGetResponses, GetSettingsHistoryApiV1UserSettingsHistoryGetData, GetSettingsHistoryApiV1UserSettingsHistoryGetError, GetSettingsHistoryApiV1UserSettingsHistoryGetErrors, GetSettingsHistoryApiV1UserSettingsHistoryGetResponse, GetSettingsHistoryApiV1UserSettingsHistoryGetResponses, GetSubscriptionsApiV1NotificationsSubscriptionsGetData, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponse, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponses, GetSystemSettingsApiV1AdminSettingsGetData, GetSystemSettingsApiV1AdminSettingsGetResponse, GetSystemSettingsApiV1AdminSettingsGetResponses, GetUnreadCountApiV1NotificationsUnreadCountGetData, GetUnreadCountApiV1NotificationsUnreadCountGetResponse, GetUnreadCountApiV1NotificationsUnreadCountGetResponses, GetUserApiV1AdminUsersUserIdGetData, GetUserApiV1AdminUsersUserIdGetError, GetUserApiV1AdminUsersUserIdGetErrors, GetUserApiV1AdminUsersUserIdGetResponse, GetUserApiV1AdminUsersUserIdGetResponses, GetUserEventsApiV1EventsUserGetData, GetUserEventsApiV1EventsUserGetError, GetUserEventsApiV1EventsUserGetErrors, GetUserEventsApiV1EventsUserGetResponse, GetUserEventsApiV1EventsUserGetResponses, GetUserExecutionsApiV1UserExecutionsGetData, GetUserExecutionsApiV1UserExecutionsGetError, GetUserExecutionsApiV1UserExecutionsGetErrors, GetUserExecutionsApiV1UserExecutionsGetResponse, GetUserExecutionsApiV1UserExecutionsGetResponses, GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, GetUserOverviewApiV1AdminUsersUserIdOverviewGetError, GetUserOverviewApiV1AdminUsersUserIdOverviewGetErrors, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponse, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponses, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetData, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetError, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetErrors, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetResponse, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetResponses, GetUserSettingsApiV1UserSettingsGetData, GetUserSettingsApiV1UserSettingsGetResponse, GetUserSettingsApiV1UserSettingsGetResponses, GrafanaAlertItem, GrafanaWebhook, HourlyEventCountSchema, HttpValidationError, KafkaTopic, LanguageInfo, ListEventTypesApiV1EventsTypesListGetData, ListEventTypesApiV1EventsTypesListGetResponse, ListEventTypesApiV1EventsTypesListGetResponses, ListReplaySessionsApiV1ReplaySessionsGetData, ListReplaySessionsApiV1ReplaySessionsGetError, ListReplaySessionsApiV1ReplaySessionsGetErrors, ListReplaySessionsApiV1ReplaySessionsGetResponse, ListReplaySessionsApiV1ReplaySessionsGetResponses, ListSagasApiV1SagasGetData, ListSagasApiV1SagasGetError, ListSagasApiV1SagasGetErrors, ListSagasApiV1SagasGetResponse, ListSagasApiV1SagasGetResponses, ListSavedScriptsApiV1ScriptsGetData, ListSavedScriptsApiV1ScriptsGetResponse, ListSavedScriptsApiV1ScriptsGetResponses, ListUsersApiV1AdminUsersGetData, ListUsersApiV1AdminUsersGetError, ListUsersApiV1AdminUsersGetErrors, ListUsersApiV1AdminUsersGetResponse, ListUsersApiV1AdminUsersGetResponses, LivenessApiV1HealthLiveGetData, LivenessApiV1HealthLiveGetResponse, LivenessApiV1HealthLiveGetResponses, LivenessResponse, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostError, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponse, LoginApiV1AuthLoginPostResponses, LoginMethod, LoginResponse, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostResponse, LogoutApiV1AuthLogoutPostResponses, ManualRetryRequest, MarkAllReadApiV1NotificationsMarkAllReadPostData, MarkAllReadApiV1NotificationsMarkAllReadPostResponse, MarkAllReadApiV1NotificationsMarkAllReadPostResponses, MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, MarkNotificationReadApiV1NotificationsNotificationIdReadPutError, MarkNotificationReadApiV1NotificationsNotificationIdReadPutErrors, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponse, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponses, MessageResponse, MonitoringSettingsSchema, NotificationAllReadEvent, NotificationChannel, NotificationClickedEvent, NotificationCreatedEvent, NotificationDeliveredEvent, NotificationFailedEvent, NotificationListResponse, NotificationPreferencesUpdatedEvent, NotificationReadEvent, NotificationResponse, NotificationSentEvent, NotificationSettings, NotificationSeverity, NotificationStatus, NotificationStreamApiV1EventsNotificationsStreamGetData, NotificationStreamApiV1EventsNotificationsStreamGetResponse, NotificationStreamApiV1EventsNotificationsStreamGetResponses, NotificationSubscription, PasswordResetRequest, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostError, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostErrors, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponse, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponses, PodCreatedEvent, PodDeletedEvent, PodFailedEvent, PodRunningEvent, PodScheduledEvent, PodSucceededEvent, PodTerminatedEvent, PublishCustomEventApiV1EventsPublishPostData, PublishCustomEventApiV1EventsPublishPostError, PublishCustomEventApiV1EventsPublishPostErrors, PublishCustomEventApiV1EventsPublishPostResponse, PublishCustomEventApiV1EventsPublishPostResponses, PublishEventRequest, PublishEventResponse, QueryEventsApiV1EventsQueryPostData, QueryEventsApiV1EventsQueryPostError, QueryEventsApiV1EventsQueryPostErrors, QueryEventsApiV1EventsQueryPostResponse, QueryEventsApiV1EventsQueryPostResponses, QuotaExceededEvent, RateLimitAlgorithm, RateLimitExceededEvent, RateLimitRuleRequest, RateLimitRuleResponse, RateLimitSummary, RateLimitUpdateRequest, RateLimitUpdateResponse, ReadinessApiV1HealthReadyGetData, ReadinessApiV1HealthReadyGetResponse, ReadinessApiV1HealthReadyGetResponses, ReadinessResponse, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostData, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostError, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostErrors, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostResponse, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostResponses, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostError, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponse, RegisterApiV1AuthRegisterPostResponses, ReleaseResourcesCommandEvent, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostData, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostError, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostErrors, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostResponse, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostResponses, ReplayAggregateResponse, ReplayConfigSchema, ReplayError, ReplayEventsApiV1AdminEventsReplayPostData, ReplayEventsApiV1AdminEventsReplayPostError, ReplayEventsApiV1AdminEventsReplayPostErrors, ReplayEventsApiV1AdminEventsReplayPostResponse, ReplayEventsApiV1AdminEventsReplayPostResponses, ReplayFilter, ReplayFilterSchema, ReplayRequest, ReplayResponse, ReplaySession, ReplayStatus, ReplayTarget, ReplayType, ResetSystemSettingsApiV1AdminSettingsResetPostData, ResetSystemSettingsApiV1AdminSettingsResetPostResponse, ResetSystemSettingsApiV1AdminSettingsResetPostResponses, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostError, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostErrors, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponse, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponses, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostData, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostError, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostErrors, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostResponse, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostResponses, ResourceLimitExceededEvent, ResourceLimits, ResourceUsage, ResourceUsageDomain, RestoreSettingsApiV1UserSettingsRestorePostData, RestoreSettingsApiV1UserSettingsRestorePostError, RestoreSettingsApiV1UserSettingsRestorePostErrors, RestoreSettingsApiV1UserSettingsRestorePostResponse, RestoreSettingsApiV1UserSettingsRestorePostResponses, RestoreSettingsRequest, ResultFailedEvent, ResultStoredEvent, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostError, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostErrors, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponse, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponses, RetryDlqMessagesApiV1DlqRetryPostData, RetryDlqMessagesApiV1DlqRetryPostError, RetryDlqMessagesApiV1DlqRetryPostErrors, RetryDlqMessagesApiV1DlqRetryPostResponse, RetryDlqMessagesApiV1DlqRetryPostResponses, RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, RetryExecutionApiV1ExecutionsExecutionIdRetryPostError, RetryExecutionApiV1ExecutionsExecutionIdRetryPostErrors, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponse, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponses, RetryExecutionRequest, RetryPolicyRequest, RetryStrategy, SagaCancellationResponse, SagaCancelledEvent, SagaCompensatedEvent, SagaCompensatingEvent, SagaCompletedEvent, SagaFailedEvent, SagaListResponse, SagaStartedEvent, SagaState, SagaStatusResponse, SavedScriptCreateRequest, SavedScriptResponse, SavedScriptUpdate, ScriptDeletedEvent, ScriptSavedEvent, ScriptSharedEvent, SecuritySettingsSchema, SecurityViolationEvent, ServiceEventCountSchema, ServiceRecoveredEvent, ServiceUnhealthyEvent, SessionSummary, SessionSummaryWritable, SetRetryPolicyApiV1DlqRetryPolicyPostData, SetRetryPolicyApiV1DlqRetryPolicyPostError, SetRetryPolicyApiV1DlqRetryPolicyPostErrors, SetRetryPolicyApiV1DlqRetryPolicyPostResponse, SetRetryPolicyApiV1DlqRetryPolicyPostResponses, SettingsHistoryEntry, SettingsHistoryResponse, ShutdownStatusResponse, SortOrder, SseControlEvent, SseExecutionEventData, SseHealthApiV1EventsHealthGetData, SseHealthApiV1EventsHealthGetResponse, SseHealthApiV1EventsHealthGetResponses, SseHealthResponse, SseHealthStatus, SseNotificationEvent, SseNotificationEventData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostError, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostErrors, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponse, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponses, StorageType, SubscriptionsResponse, SubscriptionUpdate, SystemErrorEvent, SystemSettings, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetData, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetResponse, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetResponses, Theme, ThemeUpdateRequest, TokenValidationResponse, TopicStatistic, UnreadCountResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, UpdateCustomSettingApiV1UserSettingsCustomKeyPutError, UpdateCustomSettingApiV1UserSettingsCustomKeyPutErrors, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponses, UpdateEditorSettingsApiV1UserSettingsEditorPutData, UpdateEditorSettingsApiV1UserSettingsEditorPutError, UpdateEditorSettingsApiV1UserSettingsEditorPutErrors, UpdateEditorSettingsApiV1UserSettingsEditorPutResponse, UpdateEditorSettingsApiV1UserSettingsEditorPutResponses, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutError, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutErrors, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponse, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponses, UpdateSavedScriptApiV1ScriptsScriptIdPutData, UpdateSavedScriptApiV1ScriptsScriptIdPutError, UpdateSavedScriptApiV1ScriptsScriptIdPutErrors, UpdateSavedScriptApiV1ScriptsScriptIdPutResponse, UpdateSavedScriptApiV1ScriptsScriptIdPutResponses, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutError, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutErrors, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponse, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponses, UpdateSystemSettingsApiV1AdminSettingsPutData, UpdateSystemSettingsApiV1AdminSettingsPutError, UpdateSystemSettingsApiV1AdminSettingsPutErrors, UpdateSystemSettingsApiV1AdminSettingsPutResponse, UpdateSystemSettingsApiV1AdminSettingsPutResponses, UpdateThemeApiV1UserSettingsThemePutData, UpdateThemeApiV1UserSettingsThemePutError, UpdateThemeApiV1UserSettingsThemePutErrors, UpdateThemeApiV1UserSettingsThemePutResponse, UpdateThemeApiV1UserSettingsThemePutResponses, UpdateUserApiV1AdminUsersUserIdPutData, UpdateUserApiV1AdminUsersUserIdPutError, UpdateUserApiV1AdminUsersUserIdPutErrors, UpdateUserApiV1AdminUsersUserIdPutResponse, UpdateUserApiV1AdminUsersUserIdPutResponses, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutData, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutError, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutErrors, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutResponse, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutResponses, UpdateUserSettingsApiV1UserSettingsPutData, UpdateUserSettingsApiV1UserSettingsPutError, UpdateUserSettingsApiV1UserSettingsPutErrors, UpdateUserSettingsApiV1UserSettingsPutResponse, UpdateUserSettingsApiV1UserSettingsPutResponses, UserCreate, UserDeletedEvent, UserEventCountSchema, UserListResponse, UserLoggedInEvent, UserLoggedOutEvent, UserLoginEvent, UserRateLimitConfigResponse, UserRateLimitsResponse, UserRegisteredEvent, UserResponse, UserRole, UserSettings, UserSettingsUpdate, UserSettingsUpdatedEvent, UserUpdate, UserUpdatedEvent, ValidationError, VerifyTokenApiV1AuthVerifyTokenGetData, VerifyTokenApiV1AuthVerifyTokenGetResponse, VerifyTokenApiV1AuthVerifyTokenGetResponses } from './types.gen'; +export type { AdminUserOverview, AgeStatistics, AggregateEventsApiV1EventsAggregatePostData, AggregateEventsApiV1EventsAggregatePostError, AggregateEventsApiV1EventsAggregatePostErrors, AggregateEventsApiV1EventsAggregatePostResponse, AggregateEventsApiV1EventsAggregatePostResponses, AlertResponse, AllocateResourcesCommandEvent, AuthFailedEvent, BodyLoginApiV1AuthLoginPost, BrowseEventsApiV1AdminEventsBrowsePostData, BrowseEventsApiV1AdminEventsBrowsePostError, BrowseEventsApiV1AdminEventsBrowsePostErrors, BrowseEventsApiV1AdminEventsBrowsePostResponse, BrowseEventsApiV1AdminEventsBrowsePostResponses, CancelExecutionApiV1ExecutionsExecutionIdCancelPostData, CancelExecutionApiV1ExecutionsExecutionIdCancelPostError, CancelExecutionApiV1ExecutionsExecutionIdCancelPostErrors, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponse, CancelExecutionApiV1ExecutionsExecutionIdCancelPostResponses, CancelExecutionRequest, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostData, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostError, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostErrors, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponse, CancelReplaySessionApiV1ReplaySessionsSessionIdCancelPostResponses, CancelResponse, CancelSagaApiV1SagasSagaIdCancelPostData, CancelSagaApiV1SagasSagaIdCancelPostError, CancelSagaApiV1SagasSagaIdCancelPostErrors, CancelSagaApiV1SagasSagaIdCancelPostResponse, CancelSagaApiV1SagasSagaIdCancelPostResponses, CleanupOldSessionsApiV1ReplayCleanupPostData, CleanupOldSessionsApiV1ReplayCleanupPostError, CleanupOldSessionsApiV1ReplayCleanupPostErrors, CleanupOldSessionsApiV1ReplayCleanupPostResponse, CleanupOldSessionsApiV1ReplayCleanupPostResponses, CleanupResponse, ClientOptions, ContainerStatusInfo, CreateExecutionApiV1ExecutePostData, CreateExecutionApiV1ExecutePostError, CreateExecutionApiV1ExecutePostErrors, CreateExecutionApiV1ExecutePostResponse, CreateExecutionApiV1ExecutePostResponses, CreatePodCommandEvent, CreateReplaySessionApiV1ReplaySessionsPostData, CreateReplaySessionApiV1ReplaySessionsPostError, CreateReplaySessionApiV1ReplaySessionsPostErrors, CreateReplaySessionApiV1ReplaySessionsPostResponse, CreateReplaySessionApiV1ReplaySessionsPostResponses, CreateSavedScriptApiV1ScriptsPostData, CreateSavedScriptApiV1ScriptsPostError, CreateSavedScriptApiV1ScriptsPostErrors, CreateSavedScriptApiV1ScriptsPostResponse, CreateSavedScriptApiV1ScriptsPostResponses, CreateUserApiV1AdminUsersPostData, CreateUserApiV1AdminUsersPostError, CreateUserApiV1AdminUsersPostErrors, CreateUserApiV1AdminUsersPostResponse, CreateUserApiV1AdminUsersPostResponses, DeleteEventApiV1AdminEventsEventIdDeleteData, DeleteEventApiV1AdminEventsEventIdDeleteError, DeleteEventApiV1AdminEventsEventIdDeleteErrors, DeleteEventApiV1AdminEventsEventIdDeleteResponse, DeleteEventApiV1AdminEventsEventIdDeleteResponses, DeleteEventApiV1EventsEventIdDeleteData, DeleteEventApiV1EventsEventIdDeleteError, DeleteEventApiV1EventsEventIdDeleteErrors, DeleteEventApiV1EventsEventIdDeleteResponse, DeleteEventApiV1EventsEventIdDeleteResponses, DeleteEventResponse, DeleteExecutionApiV1ExecutionsExecutionIdDeleteData, DeleteExecutionApiV1ExecutionsExecutionIdDeleteError, DeleteExecutionApiV1ExecutionsExecutionIdDeleteErrors, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponse, DeleteExecutionApiV1ExecutionsExecutionIdDeleteResponses, DeleteNotificationApiV1NotificationsNotificationIdDeleteData, DeleteNotificationApiV1NotificationsNotificationIdDeleteError, DeleteNotificationApiV1NotificationsNotificationIdDeleteErrors, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponse, DeleteNotificationApiV1NotificationsNotificationIdDeleteResponses, DeleteNotificationResponse, DeletePodCommandEvent, DeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteData, DeleteSavedScriptApiV1ScriptsScriptIdDeleteError, DeleteSavedScriptApiV1ScriptsScriptIdDeleteErrors, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponse, DeleteSavedScriptApiV1ScriptsScriptIdDeleteResponses, DeleteUserApiV1AdminUsersUserIdDeleteData, DeleteUserApiV1AdminUsersUserIdDeleteError, DeleteUserApiV1AdminUsersUserIdDeleteErrors, DeleteUserApiV1AdminUsersUserIdDeleteResponse, DeleteUserApiV1AdminUsersUserIdDeleteResponses, DeleteUserResponse, DerivedCounts, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteData, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteError, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteErrors, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteResponse, DiscardDlqMessageApiV1DlqMessagesEventIdDeleteResponses, DlqBatchRetryResponse, DlqMessageDetail, DlqMessageDiscardedEvent, DlqMessageReceivedEvent, DlqMessageResponse, DlqMessageRetriedEvent, DlqMessagesResponse, DlqMessageStatus, DlqRetryResult, DlqStats, DlqTopicSummaryResponse, EditorSettings, EndpointGroup, EndpointUsageStats, Environment, EventAggregationRequest, EventBrowseRequest, EventBrowseResponse, EventDeleteResponse, EventDetailResponse, EventFilter, EventFilterRequest, EventListResponse, EventMetadata, EventReplayRequest, EventReplayResponse, EventReplayStatusResponse, EventReplayStatusResponseWritable, EventStatistics, EventStatsResponse, EventSummary, EventType, EventTypeCountSchema, EventTypeStatistic, ExampleScripts, ExecutionAcceptedEvent, ExecutionCancelledEvent, ExecutionCompletedEvent, ExecutionErrorType, ExecutionEventsApiV1EventsExecutionsExecutionIdGetData, ExecutionEventsApiV1EventsExecutionsExecutionIdGetError, ExecutionEventsApiV1EventsExecutionsExecutionIdGetErrors, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponse, ExecutionEventsApiV1EventsExecutionsExecutionIdGetResponses, ExecutionFailedEvent, ExecutionLimitsSchema, ExecutionListResponse, ExecutionQueuedEvent, ExecutionRequest, ExecutionRequestedEvent, ExecutionResponse, ExecutionResult, ExecutionRunningEvent, ExecutionStartedEvent, ExecutionStatus, ExecutionTimeoutEvent, ExportEventsCsvApiV1AdminEventsExportCsvGetData, ExportEventsCsvApiV1AdminEventsExportCsvGetError, ExportEventsCsvApiV1AdminEventsExportCsvGetErrors, ExportEventsCsvApiV1AdminEventsExportCsvGetResponses, ExportEventsJsonApiV1AdminEventsExportJsonGetData, ExportEventsJsonApiV1AdminEventsExportJsonGetError, ExportEventsJsonApiV1AdminEventsExportJsonGetErrors, ExportEventsJsonApiV1AdminEventsExportJsonGetResponses, GetCurrentRequestEventsApiV1EventsCurrentRequestGetData, GetCurrentRequestEventsApiV1EventsCurrentRequestGetError, GetCurrentRequestEventsApiV1EventsCurrentRequestGetErrors, GetCurrentRequestEventsApiV1EventsCurrentRequestGetResponse, GetCurrentRequestEventsApiV1EventsCurrentRequestGetResponses, GetCurrentUserProfileApiV1AuthMeGetData, GetCurrentUserProfileApiV1AuthMeGetResponse, GetCurrentUserProfileApiV1AuthMeGetResponses, GetDlqMessageApiV1DlqMessagesEventIdGetData, GetDlqMessageApiV1DlqMessagesEventIdGetError, GetDlqMessageApiV1DlqMessagesEventIdGetErrors, GetDlqMessageApiV1DlqMessagesEventIdGetResponse, GetDlqMessageApiV1DlqMessagesEventIdGetResponses, GetDlqMessagesApiV1DlqMessagesGetData, GetDlqMessagesApiV1DlqMessagesGetError, GetDlqMessagesApiV1DlqMessagesGetErrors, GetDlqMessagesApiV1DlqMessagesGetResponse, GetDlqMessagesApiV1DlqMessagesGetResponses, GetDlqStatisticsApiV1DlqStatsGetData, GetDlqStatisticsApiV1DlqStatsGetResponse, GetDlqStatisticsApiV1DlqStatsGetResponses, GetDlqTopicsApiV1DlqTopicsGetData, GetDlqTopicsApiV1DlqTopicsGetResponse, GetDlqTopicsApiV1DlqTopicsGetResponses, GetEventApiV1EventsEventIdGetData, GetEventApiV1EventsEventIdGetError, GetEventApiV1EventsEventIdGetErrors, GetEventApiV1EventsEventIdGetResponse, GetEventApiV1EventsEventIdGetResponses, GetEventDetailApiV1AdminEventsEventIdGetData, GetEventDetailApiV1AdminEventsEventIdGetError, GetEventDetailApiV1AdminEventsEventIdGetErrors, GetEventDetailApiV1AdminEventsEventIdGetResponse, GetEventDetailApiV1AdminEventsEventIdGetResponses, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetData, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetError, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetErrors, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetResponse, GetEventsByCorrelationApiV1EventsCorrelationCorrelationIdGetResponses, GetEventStatisticsApiV1EventsStatisticsGetData, GetEventStatisticsApiV1EventsStatisticsGetError, GetEventStatisticsApiV1EventsStatisticsGetErrors, GetEventStatisticsApiV1EventsStatisticsGetResponse, GetEventStatisticsApiV1EventsStatisticsGetResponses, GetEventStatsApiV1AdminEventsStatsGetData, GetEventStatsApiV1AdminEventsStatsGetError, GetEventStatsApiV1AdminEventsStatsGetErrors, GetEventStatsApiV1AdminEventsStatsGetResponse, GetEventStatsApiV1AdminEventsStatsGetResponses, GetExampleScriptsApiV1ExampleScriptsGetData, GetExampleScriptsApiV1ExampleScriptsGetResponse, GetExampleScriptsApiV1ExampleScriptsGetResponses, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetError, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetResponse, GetExecutionEventsApiV1EventsExecutionsExecutionIdEventsGetResponses, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetData, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetError, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetErrors, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse, GetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponses, GetExecutionSagasApiV1SagasExecutionExecutionIdGetData, GetExecutionSagasApiV1SagasExecutionExecutionIdGetError, GetExecutionSagasApiV1SagasExecutionExecutionIdGetErrors, GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse, GetExecutionSagasApiV1SagasExecutionExecutionIdGetResponses, GetK8sResourceLimitsApiV1K8sLimitsGetData, GetK8sResourceLimitsApiV1K8sLimitsGetResponse, GetK8sResourceLimitsApiV1K8sLimitsGetResponses, GetNotificationsApiV1NotificationsGetData, GetNotificationsApiV1NotificationsGetError, GetNotificationsApiV1NotificationsGetErrors, GetNotificationsApiV1NotificationsGetResponse, GetNotificationsApiV1NotificationsGetResponses, GetReplaySessionApiV1ReplaySessionsSessionIdGetData, GetReplaySessionApiV1ReplaySessionsSessionIdGetError, GetReplaySessionApiV1ReplaySessionsSessionIdGetErrors, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponse, GetReplaySessionApiV1ReplaySessionsSessionIdGetResponses, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetData, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetError, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetErrors, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponse, GetReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponses, GetResultApiV1ExecutionsExecutionIdResultGetData, GetResultApiV1ExecutionsExecutionIdResultGetError, GetResultApiV1ExecutionsExecutionIdResultGetErrors, GetResultApiV1ExecutionsExecutionIdResultGetResponse, GetResultApiV1ExecutionsExecutionIdResultGetResponses, GetSagaStatusApiV1SagasSagaIdGetData, GetSagaStatusApiV1SagasSagaIdGetError, GetSagaStatusApiV1SagasSagaIdGetErrors, GetSagaStatusApiV1SagasSagaIdGetResponse, GetSagaStatusApiV1SagasSagaIdGetResponses, GetSavedScriptApiV1ScriptsScriptIdGetData, GetSavedScriptApiV1ScriptsScriptIdGetError, GetSavedScriptApiV1ScriptsScriptIdGetErrors, GetSavedScriptApiV1ScriptsScriptIdGetResponse, GetSavedScriptApiV1ScriptsScriptIdGetResponses, GetSettingsHistoryApiV1UserSettingsHistoryGetData, GetSettingsHistoryApiV1UserSettingsHistoryGetError, GetSettingsHistoryApiV1UserSettingsHistoryGetErrors, GetSettingsHistoryApiV1UserSettingsHistoryGetResponse, GetSettingsHistoryApiV1UserSettingsHistoryGetResponses, GetSubscriptionsApiV1NotificationsSubscriptionsGetData, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponse, GetSubscriptionsApiV1NotificationsSubscriptionsGetResponses, GetSystemSettingsApiV1AdminSettingsGetData, GetSystemSettingsApiV1AdminSettingsGetResponse, GetSystemSettingsApiV1AdminSettingsGetResponses, GetUnreadCountApiV1NotificationsUnreadCountGetData, GetUnreadCountApiV1NotificationsUnreadCountGetResponse, GetUnreadCountApiV1NotificationsUnreadCountGetResponses, GetUserApiV1AdminUsersUserIdGetData, GetUserApiV1AdminUsersUserIdGetError, GetUserApiV1AdminUsersUserIdGetErrors, GetUserApiV1AdminUsersUserIdGetResponse, GetUserApiV1AdminUsersUserIdGetResponses, GetUserEventsApiV1EventsUserGetData, GetUserEventsApiV1EventsUserGetError, GetUserEventsApiV1EventsUserGetErrors, GetUserEventsApiV1EventsUserGetResponse, GetUserEventsApiV1EventsUserGetResponses, GetUserExecutionsApiV1UserExecutionsGetData, GetUserExecutionsApiV1UserExecutionsGetError, GetUserExecutionsApiV1UserExecutionsGetErrors, GetUserExecutionsApiV1UserExecutionsGetResponse, GetUserExecutionsApiV1UserExecutionsGetResponses, GetUserOverviewApiV1AdminUsersUserIdOverviewGetData, GetUserOverviewApiV1AdminUsersUserIdOverviewGetError, GetUserOverviewApiV1AdminUsersUserIdOverviewGetErrors, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponse, GetUserOverviewApiV1AdminUsersUserIdOverviewGetResponses, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetData, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetError, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetErrors, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetResponse, GetUserRateLimitsApiV1AdminUsersUserIdRateLimitsGetResponses, GetUserSettingsApiV1UserSettingsGetData, GetUserSettingsApiV1UserSettingsGetResponse, GetUserSettingsApiV1UserSettingsGetResponses, GrafanaAlertItem, GrafanaWebhook, HourlyEventCountSchema, HttpValidationError, KafkaTopic, LanguageInfo, ListEventTypesApiV1EventsTypesListGetData, ListEventTypesApiV1EventsTypesListGetResponse, ListEventTypesApiV1EventsTypesListGetResponses, ListReplaySessionsApiV1ReplaySessionsGetData, ListReplaySessionsApiV1ReplaySessionsGetError, ListReplaySessionsApiV1ReplaySessionsGetErrors, ListReplaySessionsApiV1ReplaySessionsGetResponse, ListReplaySessionsApiV1ReplaySessionsGetResponses, ListSagasApiV1SagasGetData, ListSagasApiV1SagasGetError, ListSagasApiV1SagasGetErrors, ListSagasApiV1SagasGetResponse, ListSagasApiV1SagasGetResponses, ListSavedScriptsApiV1ScriptsGetData, ListSavedScriptsApiV1ScriptsGetResponse, ListSavedScriptsApiV1ScriptsGetResponses, ListUsersApiV1AdminUsersGetData, ListUsersApiV1AdminUsersGetError, ListUsersApiV1AdminUsersGetErrors, ListUsersApiV1AdminUsersGetResponse, ListUsersApiV1AdminUsersGetResponses, LivenessApiV1HealthLiveGetData, LivenessApiV1HealthLiveGetResponse, LivenessApiV1HealthLiveGetResponses, LivenessResponse, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostError, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponse, LoginApiV1AuthLoginPostResponses, LoginMethod, LoginResponse, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostResponse, LogoutApiV1AuthLogoutPostResponses, ManualRetryRequest, MarkAllReadApiV1NotificationsMarkAllReadPostData, MarkAllReadApiV1NotificationsMarkAllReadPostResponse, MarkAllReadApiV1NotificationsMarkAllReadPostResponses, MarkNotificationReadApiV1NotificationsNotificationIdReadPutData, MarkNotificationReadApiV1NotificationsNotificationIdReadPutError, MarkNotificationReadApiV1NotificationsNotificationIdReadPutErrors, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponse, MarkNotificationReadApiV1NotificationsNotificationIdReadPutResponses, MessageResponse, MonitoringSettingsSchema, NotificationAllReadEvent, NotificationChannel, NotificationClickedEvent, NotificationCreatedEvent, NotificationDeliveredEvent, NotificationFailedEvent, NotificationListResponse, NotificationPreferencesUpdatedEvent, NotificationReadEvent, NotificationResponse, NotificationSentEvent, NotificationSettings, NotificationSeverity, NotificationStatus, NotificationStreamApiV1EventsNotificationsStreamGetData, NotificationStreamApiV1EventsNotificationsStreamGetResponse, NotificationStreamApiV1EventsNotificationsStreamGetResponses, NotificationSubscription, PasswordResetRequest, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostData, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostError, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostErrors, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponse, PauseReplaySessionApiV1ReplaySessionsSessionIdPausePostResponses, PodCreatedEvent, PodDeletedEvent, PodFailedEvent, PodRunningEvent, PodScheduledEvent, PodSucceededEvent, PodTerminatedEvent, PublishCustomEventApiV1EventsPublishPostData, PublishCustomEventApiV1EventsPublishPostError, PublishCustomEventApiV1EventsPublishPostErrors, PublishCustomEventApiV1EventsPublishPostResponse, PublishCustomEventApiV1EventsPublishPostResponses, PublishEventRequest, PublishEventResponse, QueryEventsApiV1EventsQueryPostData, QueryEventsApiV1EventsQueryPostError, QueryEventsApiV1EventsQueryPostErrors, QueryEventsApiV1EventsQueryPostResponse, QueryEventsApiV1EventsQueryPostResponses, QueuePriority, QuotaExceededEvent, RateLimitAlgorithm, RateLimitExceededEvent, RateLimitRuleRequest, RateLimitRuleResponse, RateLimitSummary, RateLimitUpdateRequest, RateLimitUpdateResponse, ReadinessApiV1HealthReadyGetData, ReadinessApiV1HealthReadyGetResponse, ReadinessApiV1HealthReadyGetResponses, ReadinessResponse, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostData, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostError, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostErrors, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostResponse, ReceiveGrafanaAlertsApiV1AlertsGrafanaPostResponses, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostError, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponse, RegisterApiV1AuthRegisterPostResponses, ReleaseResourcesCommandEvent, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostData, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostError, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostErrors, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostResponse, ReplayAggregateEventsApiV1EventsReplayAggregateIdPostResponses, ReplayAggregateResponse, ReplayConfigSchema, ReplayError, ReplayEventsApiV1AdminEventsReplayPostData, ReplayEventsApiV1AdminEventsReplayPostError, ReplayEventsApiV1AdminEventsReplayPostErrors, ReplayEventsApiV1AdminEventsReplayPostResponse, ReplayEventsApiV1AdminEventsReplayPostResponses, ReplayFilter, ReplayFilterSchema, ReplayRequest, ReplayResponse, ReplaySession, ReplayStatus, ReplayTarget, ReplayType, ResetSystemSettingsApiV1AdminSettingsResetPostData, ResetSystemSettingsApiV1AdminSettingsResetPostResponse, ResetSystemSettingsApiV1AdminSettingsResetPostResponses, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostData, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostError, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostErrors, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponse, ResetUserPasswordApiV1AdminUsersUserIdResetPasswordPostResponses, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostData, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostError, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostErrors, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostResponse, ResetUserRateLimitsApiV1AdminUsersUserIdRateLimitsResetPostResponses, ResourceLimitExceededEvent, ResourceLimits, ResourceUsage, ResourceUsageDomain, RestoreSettingsApiV1UserSettingsRestorePostData, RestoreSettingsApiV1UserSettingsRestorePostError, RestoreSettingsApiV1UserSettingsRestorePostErrors, RestoreSettingsApiV1UserSettingsRestorePostResponse, RestoreSettingsApiV1UserSettingsRestorePostResponses, RestoreSettingsRequest, ResultFailedEvent, ResultStoredEvent, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostData, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostError, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostErrors, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponse, ResumeReplaySessionApiV1ReplaySessionsSessionIdResumePostResponses, RetryDlqMessagesApiV1DlqRetryPostData, RetryDlqMessagesApiV1DlqRetryPostError, RetryDlqMessagesApiV1DlqRetryPostErrors, RetryDlqMessagesApiV1DlqRetryPostResponse, RetryDlqMessagesApiV1DlqRetryPostResponses, RetryExecutionApiV1ExecutionsExecutionIdRetryPostData, RetryExecutionApiV1ExecutionsExecutionIdRetryPostError, RetryExecutionApiV1ExecutionsExecutionIdRetryPostErrors, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponse, RetryExecutionApiV1ExecutionsExecutionIdRetryPostResponses, RetryExecutionRequest, RetryPolicyRequest, RetryStrategy, SagaCancellationResponse, SagaCancelledEvent, SagaCompensatedEvent, SagaCompensatingEvent, SagaCompletedEvent, SagaFailedEvent, SagaListResponse, SagaStartedEvent, SagaState, SagaStatusResponse, SavedScriptCreateRequest, SavedScriptResponse, SavedScriptUpdate, ScriptDeletedEvent, ScriptSavedEvent, ScriptSharedEvent, SecuritySettingsSchema, SecurityViolationEvent, ServiceEventCountSchema, ServiceRecoveredEvent, ServiceUnhealthyEvent, SessionSummary, SessionSummaryWritable, SetRetryPolicyApiV1DlqRetryPolicyPostData, SetRetryPolicyApiV1DlqRetryPolicyPostError, SetRetryPolicyApiV1DlqRetryPolicyPostErrors, SetRetryPolicyApiV1DlqRetryPolicyPostResponse, SetRetryPolicyApiV1DlqRetryPolicyPostResponses, SettingsHistoryEntry, SettingsHistoryResponse, ShutdownStatusResponse, SortOrder, SseControlEvent, SseExecutionEventData, SseHealthApiV1EventsHealthGetData, SseHealthApiV1EventsHealthGetResponse, SseHealthApiV1EventsHealthGetResponses, SseHealthResponse, SseHealthStatus, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostData, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostError, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostErrors, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponse, StartReplaySessionApiV1ReplaySessionsSessionIdStartPostResponses, StorageType, SubscriptionsResponse, SubscriptionUpdate, SystemErrorEvent, SystemSettings, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetData, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetResponse, TestGrafanaAlertEndpointApiV1AlertsGrafanaTestGetResponses, Theme, ThemeUpdateRequest, TokenValidationResponse, TopicStatistic, UnreadCountResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutData, UpdateCustomSettingApiV1UserSettingsCustomKeyPutError, UpdateCustomSettingApiV1UserSettingsCustomKeyPutErrors, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponse, UpdateCustomSettingApiV1UserSettingsCustomKeyPutResponses, UpdateEditorSettingsApiV1UserSettingsEditorPutData, UpdateEditorSettingsApiV1UserSettingsEditorPutError, UpdateEditorSettingsApiV1UserSettingsEditorPutErrors, UpdateEditorSettingsApiV1UserSettingsEditorPutResponse, UpdateEditorSettingsApiV1UserSettingsEditorPutResponses, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutData, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutError, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutErrors, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponse, UpdateNotificationSettingsApiV1UserSettingsNotificationsPutResponses, UpdateSavedScriptApiV1ScriptsScriptIdPutData, UpdateSavedScriptApiV1ScriptsScriptIdPutError, UpdateSavedScriptApiV1ScriptsScriptIdPutErrors, UpdateSavedScriptApiV1ScriptsScriptIdPutResponse, UpdateSavedScriptApiV1ScriptsScriptIdPutResponses, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutData, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutError, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutErrors, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponse, UpdateSubscriptionApiV1NotificationsSubscriptionsChannelPutResponses, UpdateSystemSettingsApiV1AdminSettingsPutData, UpdateSystemSettingsApiV1AdminSettingsPutError, UpdateSystemSettingsApiV1AdminSettingsPutErrors, UpdateSystemSettingsApiV1AdminSettingsPutResponse, UpdateSystemSettingsApiV1AdminSettingsPutResponses, UpdateThemeApiV1UserSettingsThemePutData, UpdateThemeApiV1UserSettingsThemePutError, UpdateThemeApiV1UserSettingsThemePutErrors, UpdateThemeApiV1UserSettingsThemePutResponse, UpdateThemeApiV1UserSettingsThemePutResponses, UpdateUserApiV1AdminUsersUserIdPutData, UpdateUserApiV1AdminUsersUserIdPutError, UpdateUserApiV1AdminUsersUserIdPutErrors, UpdateUserApiV1AdminUsersUserIdPutResponse, UpdateUserApiV1AdminUsersUserIdPutResponses, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutData, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutError, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutErrors, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutResponse, UpdateUserRateLimitsApiV1AdminUsersUserIdRateLimitsPutResponses, UpdateUserSettingsApiV1UserSettingsPutData, UpdateUserSettingsApiV1UserSettingsPutError, UpdateUserSettingsApiV1UserSettingsPutErrors, UpdateUserSettingsApiV1UserSettingsPutResponse, UpdateUserSettingsApiV1UserSettingsPutResponses, UserCreate, UserDeletedEvent, UserEventCountSchema, UserListResponse, UserLoggedInEvent, UserLoggedOutEvent, UserLoginEvent, UserRateLimitConfigResponse, UserRateLimitsResponse, UserRegisteredEvent, UserResponse, UserRole, UserSettings, UserSettingsUpdate, UserSettingsUpdatedEvent, UserUpdate, UserUpdatedEvent, ValidationError, VerifyTokenApiV1AuthVerifyTokenGetData, VerifyTokenApiV1AuthVerifyTokenGetResponse, VerifyTokenApiV1AuthVerifyTokenGetResponses } from './types.gen'; diff --git a/frontend/src/lib/api/types.gen.ts b/frontend/src/lib/api/types.gen.ts index ef3c492e..a5f20f14 100644 --- a/frontend/src/lib/api/types.gen.ts +++ b/frontend/src/lib/api/types.gen.ts @@ -439,10 +439,7 @@ export type CreatePodCommandEvent = { * Memory Request */ memory_request?: string; - /** - * Priority - */ - priority?: number; + priority?: QueuePriority; }; /** @@ -2230,10 +2227,7 @@ export type ExecutionAcceptedEvent = { * Estimated Wait Seconds */ estimated_wait_seconds?: number | null; - /** - * Priority - */ - priority?: number; + priority?: QueuePriority; }; /** @@ -2577,10 +2571,7 @@ export type ExecutionRequestedEvent = { * Memory Request */ memory_request?: string; - /** - * Priority - */ - priority?: number; + priority?: QueuePriority; }; /** @@ -3878,6 +3869,13 @@ export type PublishEventResponse = { timestamp: string; }; +/** + * QueuePriority + * + * Execution priority, ordered highest to lowest. + */ +export type QueuePriority = 'critical' | 'high' | 'normal' | 'low' | 'background'; + /** * QuotaExceededEvent */ @@ -4938,89 +4936,6 @@ export type SseHealthResponse = { */ export type SseHealthStatus = 'healthy' | 'draining'; -/** - * SSENotificationEvent - * - * Event types for notification SSE streams. - */ -export type SseNotificationEvent = 'connected' | 'subscribed' | 'heartbeat' | 'notification'; - -/** - * SSENotificationEventData - * - * Typed model for SSE notification stream event payload. - * - * This represents the JSON data sent inside each SSE message for notification streams. - */ -export type SseNotificationEventData = { - /** - * SSE notification event type - */ - event_type: SseNotificationEvent; - /** - * User Id - * - * User ID for the notification stream - */ - user_id?: string | null; - /** - * Timestamp - * - * Event timestamp - */ - timestamp?: string | null; - /** - * Message - * - * Human-readable message - */ - message?: string | null; - /** - * Notification Id - * - * Unique notification ID - */ - notification_id?: string | null; - /** - * Notification severity level - */ - severity?: NotificationSeverity | null; - /** - * Notification delivery status - */ - status?: NotificationStatus | null; - /** - * Tags - * - * Notification tags - */ - tags?: Array | null; - /** - * Subject - * - * Notification subject/title - */ - subject?: string | null; - /** - * Body - * - * Notification body content - */ - body?: string | null; - /** - * Action Url - * - * Optional action URL - */ - action_url?: string | null; - /** - * Created At - * - * Creation timestamp - */ - created_at?: string | null; -}; - /** * SagaCancellationResponse * @@ -7912,7 +7827,7 @@ export type NotificationStreamApiV1EventsNotificationsStreamGetResponses = { /** * Successful Response */ - 200: SseNotificationEventData; + 200: NotificationResponse; }; export type NotificationStreamApiV1EventsNotificationsStreamGetResponse = NotificationStreamApiV1EventsNotificationsStreamGetResponses[keyof NotificationStreamApiV1EventsNotificationsStreamGetResponses]; diff --git a/frontend/src/lib/editor/__tests__/execution.test.ts b/frontend/src/lib/editor/__tests__/execution.test.ts new file mode 100644 index 00000000..e10d9dd3 --- /dev/null +++ b/frontend/src/lib/editor/__tests__/execution.test.ts @@ -0,0 +1,222 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import type { ExecutionResult } from '$lib/api'; + +// ── Mocks ────────────────────────────────────────────────────────────── +const mockCreateExecution = vi.fn(); +const mockGetResult = vi.fn(); + +vi.mock('$lib/api', () => ({ + createExecutionApiV1ExecutePost: (...a: unknown[]) => mockCreateExecution(...a), + getResultApiV1ExecutionsExecutionIdResultGet: (...a: unknown[]) => mockGetResult(...a), +})); + +vi.mock('$lib/api-interceptors', () => ({ + getErrorMessage: (_err: unknown, fallback: string) => fallback, +})); + +const { createExecutionState } = await import('../execution.svelte'); + +// ── Helpers ──────────────────────────────────────────────────────────── +const RESULT: ExecutionResult = { + execution_id: 'exec-1', + status: 'completed', + stdout: 'hello', + stderr: '', + exit_code: 0, + lang: 'python', + lang_version: '3.12', + execution_time: 0.1, + memory_used_kb: 64, +}; + +/** Build an SSE ReadableStream from a list of "data: …" lines. */ +function sseStream(lines: string[]): ReadableStream { + const text = lines.map((l) => `data: ${l}\n\n`).join(''); + const encoder = new TextEncoder(); + return new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(text)); + controller.close(); + }, + }); +} + +function mockFetchSSE(lines: string[]) { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + status: 200, + body: sseStream(lines), + }), + ); +} + +// ── Tests ────────────────────────────────────────────────────────────── +describe('createExecutionState', () => { + beforeEach(() => { + mockCreateExecution.mockReset(); + mockGetResult.mockReset(); + }); + + afterEach(() => vi.unstubAllGlobals()); + + describe('initial state', () => { + it.each([ + ['phase', 'idle'], + ['result', null], + ['error', null], + ['isExecuting', false], + ] as const)('%s is %j', (key, expected) => { + const s = createExecutionState(); + expect(s[key]).toBe(expected); + }); + }); + + describe('execute → stream result_stored', () => { + it('yields result from SSE stream', async () => { + mockCreateExecution.mockResolvedValue({ + data: { execution_id: 'exec-1', status: 'queued' }, + error: null, + }); + + mockFetchSSE([ + JSON.stringify({ event_type: 'status', status: 'running' }), + JSON.stringify({ event_type: 'result_stored', result: RESULT }), + ]); + + const s = createExecutionState(); + await s.execute('print("hi")', 'python', '3.12'); + + expect(s.result).toEqual(RESULT); + expect(s.phase).toBe('idle'); + expect(s.error).toBeNull(); + }); + }); + + describe('execute → terminal failure falls back to fetchResult', () => { + it.each(['execution_failed', 'execution_timeout', 'result_failed'])( + 'fetches result on %s event', + async (eventType) => { + mockCreateExecution.mockResolvedValue({ + data: { execution_id: 'exec-1', status: 'queued' }, + error: null, + }); + + mockFetchSSE([JSON.stringify({ event_type: eventType })]); + mockGetResult.mockResolvedValue({ data: RESULT, error: null }); + + const s = createExecutionState(); + await s.execute('x', 'python', '3.12'); + + expect(mockGetResult).toHaveBeenCalledOnce(); + expect(s.result).toEqual(RESULT); + }, + ); + }); + + describe('execute → stream ends without terminal event', () => { + it('falls back to fetchResult', async () => { + mockCreateExecution.mockResolvedValue({ + data: { execution_id: 'exec-1', status: 'queued' }, + error: null, + }); + + mockFetchSSE([JSON.stringify({ event_type: 'status', status: 'running' })]); + mockGetResult.mockResolvedValue({ data: RESULT, error: null }); + + const s = createExecutionState(); + await s.execute('x', 'python', '3.12'); + + expect(mockGetResult).toHaveBeenCalledOnce(); + expect(s.result).toEqual(RESULT); + }); + }); + + describe('execute → non-OK fetch falls back to fetchResult', () => { + it('fetches result on 500', async () => { + mockCreateExecution.mockResolvedValue({ + data: { execution_id: 'exec-1', status: 'queued' }, + error: null, + }); + + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 500 })); + mockGetResult.mockResolvedValue({ data: RESULT, error: null }); + + const s = createExecutionState(); + await s.execute('x', 'python', '3.12'); + + expect(s.result).toEqual(RESULT); + }); + + it('sets error on 401', async () => { + mockCreateExecution.mockResolvedValue({ + data: { execution_id: 'exec-1', status: 'queued' }, + error: null, + }); + + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 401 })); + + const s = createExecutionState(); + await s.execute('x', 'python', '3.12'); + + expect(s.error).toBe('Error executing script.'); + }); + }); + + describe('execute → API error', () => { + it('sets error when createExecution fails', async () => { + mockCreateExecution.mockResolvedValue({ + data: null, + error: { detail: 'rate limited' }, + }); + + const s = createExecutionState(); + await s.execute('x', 'python', '3.12'); + + expect(s.error).toBe('Error executing script.'); + expect(s.phase).toBe('idle'); + }); + }); + + describe('abort', () => { + it('resets phase to idle', () => { + const s = createExecutionState(); + s.abort(); + expect(s.phase).toBe('idle'); + }); + }); + + describe('reset', () => { + it('clears all state', async () => { + mockCreateExecution.mockResolvedValue({ data: null, error: 'fail' }); + + const s = createExecutionState(); + await s.execute('x', 'python', '3.12'); + + s.reset(); + expect(s.phase).toBe('idle'); + expect(s.result).toBeNull(); + expect(s.error).toBeNull(); + }); + }); + + describe('malformed SSE', () => { + it('skips invalid JSON and continues', async () => { + mockCreateExecution.mockResolvedValue({ + data: { execution_id: 'exec-1', status: 'queued' }, + error: null, + }); + + mockFetchSSE([ + '{broken', + JSON.stringify({ event_type: 'result_stored', result: RESULT }), + ]); + + const s = createExecutionState(); + await s.execute('x', 'python', '3.12'); + + expect(s.result).toEqual(RESULT); + }); + }); +}); diff --git a/frontend/src/lib/editor/__tests__/languages.test.ts b/frontend/src/lib/editor/__tests__/languages.test.ts new file mode 100644 index 00000000..09cf87cd --- /dev/null +++ b/frontend/src/lib/editor/__tests__/languages.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest'; +import { getLanguageExtension } from '$lib/editor/languages'; + +describe('getLanguageExtension', () => { + it.each(['python', 'node', 'go', 'ruby', 'bash'])( + 'returns an extension for "%s"', + (lang) => { + const ext = getLanguageExtension(lang); + expect(ext).toBeDefined(); + }, + ); + + it('returns python extension for unknown language', () => { + const fallback = getLanguageExtension('unknown-lang'); + const python = getLanguageExtension('python'); + // Both should return a python() extension — compare structure + expect(typeof fallback).toBe(typeof python); + }); + + it('returns python extension for empty string', () => { + const ext = getLanguageExtension(''); + expect(ext).toBeDefined(); + }); + + it('returns distinct extensions for different languages', () => { + const py = getLanguageExtension('python'); + const js = getLanguageExtension('node'); + // Different language extensions should not be referentially equal + expect(py).not.toBe(js); + }); +}); diff --git a/frontend/src/lib/notifications/__tests__/stream.test.ts b/frontend/src/lib/notifications/__tests__/stream.test.ts new file mode 100644 index 00000000..81e442bb --- /dev/null +++ b/frontend/src/lib/notifications/__tests__/stream.test.ts @@ -0,0 +1,140 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { NotificationResponse } from '$lib/api'; + +// Capture the listen callbacks so tests can invoke them directly +let listenCallbacks: Record void> = {}; +const mockAbort = vi.fn(); + +vi.mock('event-source-plus', () => { + const EventSourcePlus = function () { + // noop constructor + }; + EventSourcePlus.prototype.listen = function (cbs: Record void>) { + listenCallbacks = cbs; + return { abort: mockAbort }; + }; + return { EventSourcePlus }; +}); + +// Must import after mocking +const { notificationStream } = await import('../stream.svelte'); + +function validPayload(overrides: Partial = {}): NotificationResponse { + return { + notification_id: 'n-1', + channel: 'in_app', + status: 'delivered', + subject: 'Test', + body: 'Hello', + action_url: '/action', + created_at: '2025-01-01T00:00:00Z', + read_at: null, + severity: 'medium', + tags: ['tag1'], + ...overrides, + }; +} + +describe('NotificationStream', () => { + let callback: ReturnType; + + beforeEach(() => { + callback = vi.fn(); + listenCallbacks = {}; + mockAbort.mockClear(); + notificationStream.disconnect(); + }); + + it('sets connected=true on onResponse', () => { + notificationStream.connect(callback); + listenCallbacks.onResponse!(); + expect(notificationStream.connected).toBe(true); + expect(notificationStream.error).toBeNull(); + }); + + it('calls callback with parsed NotificationResponse', () => { + notificationStream.connect(callback); + listenCallbacks.onMessage!({ event: 'notification', data: JSON.stringify(validPayload()) }); + + expect(callback).toHaveBeenCalledOnce(); + const notification = callback.mock.calls[0][0]; + expect(notification).toMatchObject({ + notification_id: 'n-1', + channel: 'in_app', + status: 'delivered', + subject: 'Test', + body: 'Hello', + read_at: null, + severity: 'medium', + tags: ['tag1'], + action_url: '/action', + }); + }); + + it('ignores messages with non-notification event type', () => { + notificationStream.connect(callback); + listenCallbacks.onMessage!({ event: 'ping', data: '{}' }); + listenCallbacks.onMessage!({ event: '', data: '{}' }); + expect(callback).not.toHaveBeenCalled(); + }); + + it('handles invalid JSON without crashing', () => { + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}); + notificationStream.connect(callback); + listenCallbacks.onMessage!({ event: 'notification', data: '{broken' }); + expect(callback).not.toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); + }); + + it('sets error on request error', () => { + notificationStream.connect(callback); + listenCallbacks.onResponse!(); + listenCallbacks.onRequestError!({ error: new Error('Network down') }); + expect(notificationStream.connected).toBe(false); + expect(notificationStream.error).toBe('Network down'); + }); + + it('falls back to "Connection failed" when error has no message', () => { + notificationStream.connect(callback); + listenCallbacks.onRequestError!({ error: null }); + expect(notificationStream.error).toBe('Connection failed'); + }); + + it('disconnects on 401 response error', () => { + notificationStream.connect(callback); + listenCallbacks.onResponse!(); + listenCallbacks.onResponseError!({ response: { status: 401 } }); + expect(notificationStream.connected).toBe(false); + expect(notificationStream.error).toBe('Unauthorized'); + expect(mockAbort).toHaveBeenCalled(); + }); + + it('sets connected=false on non-401 response error', () => { + notificationStream.connect(callback); + listenCallbacks.onResponse!(); + listenCallbacks.onResponseError!({ response: { status: 500 } }); + expect(notificationStream.connected).toBe(false); + expect(notificationStream.error).toBeNull(); + }); + + it('disconnect aborts controller and resets state', () => { + notificationStream.connect(callback); + listenCallbacks.onResponse!(); + notificationStream.disconnect(); + expect(mockAbort).toHaveBeenCalled(); + expect(notificationStream.connected).toBe(false); + }); + + it('fires browser Notification when permission is granted', () => { + const MockNotification = vi.fn(); + Object.defineProperty(MockNotification, 'permission', { value: 'granted' }); + vi.stubGlobal('Notification', MockNotification); + + notificationStream.connect(callback); + listenCallbacks.onMessage!({ event: 'notification', data: JSON.stringify(validPayload()) }); + + expect(MockNotification).toHaveBeenCalledWith('Test', { body: 'Hello', icon: '/favicon.png' }); + vi.unstubAllGlobals(); + }); +}); diff --git a/frontend/src/lib/notifications/stream.svelte.ts b/frontend/src/lib/notifications/stream.svelte.ts index ab642951..ed8fad4b 100644 --- a/frontend/src/lib/notifications/stream.svelte.ts +++ b/frontend/src/lib/notifications/stream.svelte.ts @@ -1,13 +1,8 @@ import { EventSourcePlus } from 'event-source-plus'; -import type { NotificationResponse, SseNotificationEventData } from '$lib/api'; +import type { NotificationResponse } from '$lib/api'; type NotificationCallback = (data: NotificationResponse) => void; -// Validates SSE notification has all required fields for UI -function isCompleteNotification(data: SseNotificationEventData): boolean { - return !!(data.notification_id && data.subject && data.body && data.status && data.created_at); -} - class NotificationStream { #controller: ReturnType | null = null; #onNotification: NotificationCallback | null = null; @@ -34,18 +29,14 @@ class NotificationStream { this.error = null; console.log('Notification stream connected'); }, - onMessage: (event) => { + onMessage: (message) => { + if (message.event !== 'notification') return; try { - const data: SseNotificationEventData = JSON.parse(event.data); - - // Only process actual notification events with complete data - if (data.event_type === 'notification' && isCompleteNotification(data)) { - // SSE data matches NotificationResponse except 'channel' (unused by UI) - this.#onNotification?.(data as unknown as NotificationResponse); + const data: NotificationResponse = JSON.parse(message.data); + this.#onNotification?.(data); - if (typeof Notification !== 'undefined' && Notification.permission === 'granted') { - new Notification(data.subject!, { body: data.body!, icon: '/favicon.png' }); - } + if (typeof Notification !== 'undefined' && Notification.permission === 'granted') { + new Notification(data.subject, { body: data.body, icon: '/favicon.png' }); } } catch (err) { console.error('Error processing notification:', err); diff --git a/frontend/src/stores/__tests__/userSettings.test.ts b/frontend/src/stores/__tests__/userSettings.test.ts new file mode 100644 index 00000000..05e81ce6 --- /dev/null +++ b/frontend/src/stores/__tests__/userSettings.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { get } from 'svelte/store'; +import { userSettings, editorSettings, setUserSettings, clearUserSettings } from '$stores/userSettings'; +import type { UserSettings } from '$lib/api'; + +const DEFAULTS = { + theme: 'auto', + font_size: 14, + tab_size: 4, + use_tabs: false, + word_wrap: true, + show_line_numbers: true, +}; + +describe('userSettings store', () => { + beforeEach(() => clearUserSettings()); + + it('starts as null', () => { + expect(get(userSettings)).toBeNull(); + }); + + it.each([ + ['object', { editor: { font_size: 16 } } as UserSettings], + ['null', null], + ])('setUserSettings accepts %s', (_, value) => { + setUserSettings(value); + expect(get(userSettings)).toEqual(value); + }); + + it('clearUserSettings resets to null', () => { + setUserSettings({ editor: { font_size: 20 } } as UserSettings); + clearUserSettings(); + expect(get(userSettings)).toBeNull(); + }); + + describe('editorSettings (derived)', () => { + it('returns defaults when userSettings is null', () => { + expect(get(editorSettings)).toEqual(DEFAULTS); + }); + + it.each([ + ['partial override', { font_size: 20, tab_size: 2 }, { ...DEFAULTS, font_size: 20, tab_size: 2 }], + ['full override', { theme: 'dark', font_size: 18, tab_size: 8, use_tabs: true, word_wrap: false, show_line_numbers: false }, + { theme: 'dark', font_size: 18, tab_size: 8, use_tabs: true, word_wrap: false, show_line_numbers: false }], + ])('merges %s with defaults', (_, editor, expected) => { + setUserSettings({ editor } as UserSettings); + expect(get(editorSettings)).toEqual(expected); + }); + + it('reverts to defaults when cleared', () => { + setUserSettings({ editor: { font_size: 20 } } as UserSettings); + clearUserSettings(); + expect(get(editorSettings)).toEqual(DEFAULTS); + }); + }); +}); diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 00000000..f7bdd44c --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,6 @@ +export default { + compilerOptions: { + dev: true, + runes: true, + }, +}; diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index e72b5e2d..0bb4a2a6 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -4,13 +4,7 @@ import { svelteTesting } from '@testing-library/svelte/vite'; export default defineConfig({ plugins: [ - svelte({ - hot: !process.env.VITEST, - compilerOptions: { - dev: true, - runes: true, - }, - }), + svelte({ hot: !process.env.VITEST }), svelteTesting(), ], test: {