diff --git a/.github/workflows/deploy-testing.yml b/.github/workflows/deploy-testing.yml index 82277f69..4e4007c4 100644 --- a/.github/workflows/deploy-testing.yml +++ b/.github/workflows/deploy-testing.yml @@ -53,6 +53,35 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + build-agent: + name: Build & Push Agent Service + runs-on: ubuntu-latest + needs: [generate-tag] + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to DigitalOcean Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DO_REGISTRY_TOKEN }} + password: ${{ secrets.DO_REGISTRY_TOKEN }} + + - name: Build and Push Agent Service + uses: docker/build-push-action@v5 + with: + context: ./echo/agent + file: ./echo/agent/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/dbr-echo-agent:${{ needs.generate-tag.outputs.image-tag }} + ${{ env.REGISTRY }}/dbr-echo-agent:testing + cache-from: type=gha + cache-to: type=gha,mode=max + build-directus: name: Build & Push Directus runs-on: ubuntu-latest @@ -217,7 +246,7 @@ jobs: update-gitops: name: Update GitOps Repo runs-on: ubuntu-latest - needs: [generate-tag, build-api, build-directus] + needs: [generate-tag, build-api, build-agent, build-directus] steps: - name: Checkout GitOps repo uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 2ffa195e..90329330 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,16 @@ echo/server/dembrane/audio_lightrag/data/progress_tracker.csv echo/server/test.py echo/server/wandb* +# Python virtual environments +.venv/ +.venv-*/ +**/venv/ +**/.venv/ +**/.venv-*/ + +# Local documentation (not committed) +CLAUDE.md +IMPLEMENTATION_PLANS/ *.env notes_*.md diff --git a/contributors.yml b/contributors.yml index 424b955a..1963bd11 100644 --- a/contributors.yml +++ b/contributors.yml @@ -5,3 +5,5 @@ - RubenNair - vanpauli - MsVivienne +- dtrn2048 +- Charugundlavipul \ No newline at end of file diff --git a/echo/.devcontainer/docker-compose.yml b/echo/.devcontainer/docker-compose.yml index a95401a1..a570981a 100644 --- a/echo/.devcontainer/docker-compose.yml +++ b/echo/.devcontainer/docker-compose.yml @@ -80,6 +80,21 @@ services: - postgres - redis + agent: + build: + context: ../agent + dockerfile: Dockerfile + ports: + - 8001:8001 + environment: + - GEMINI_API_KEY=${GEMINI_API_KEY:-} + - ECHO_API_URL=http://devcontainer:8000/api + - AGENT_CORS_ORIGINS=http://localhost:5173,http://localhost:5174 + networks: + - dembrane-network + depends_on: + - devcontainer + devcontainer: build: context: ../server @@ -123,4 +138,4 @@ services: networks: dembrane-network: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/echo/.gitignore b/echo/.gitignore index d44a857c..1bf72605 100644 --- a/echo/.gitignore +++ b/echo/.gitignore @@ -14,6 +14,7 @@ directus/.env *.sql node_modules .next +cypress/test-suites/.cypress-cache/ celerybeat-schedule.db @@ -28,3 +29,5 @@ __queuestorage__echo/server/dembrane/workspace_script.py tools/translator echo-gitops/ + +cypress/reports/ \ No newline at end of file diff --git a/echo/AGENTS.md b/echo/AGENTS.md index 6c8f3046..050c5064 100644 --- a/echo/AGENTS.md +++ b/echo/AGENTS.md @@ -110,8 +110,43 @@ Convention: Use `ENABLE_*` naming pattern for feature flags. # Frontend cd frontend && pnpm i && pnpm dev -# Backend -cd server && uv sync && uv run uvicorn dembrane.main:app --reload +# Backend API +cd server && uv sync && uv run uvicorn dembrane.main:app --port 8000 --reload --loop asyncio + +# Agent service (required for agentic chat) +cd ../agent && uv sync && uv run uvicorn main:app --host 0.0.0.0 --port 8001 --reload +``` + +For full background processing (transcription/audio and non-agentic jobs), also run: + +```bash +cd server +uv run dramatiq-gevent --watch ./dembrane --queues network --processes 2 --threads 1 dembrane.tasks +uv run dramatiq --watch ./dembrane --queues cpu --processes 1 --threads 1 dembrane.tasks +``` + +Agentic chat execution is stream-first through `POST /api/agentic/runs/{run_id}/stream` and no longer enqueues an agentic Dramatiq actor. + +### Future Agent Tool Development + +- Add tool definitions in `agent/agent.py` (`@tool` functions in `create_agent_graph`). +- Add shared API client calls for tools in `agent/echo_client.py`. +- If a tool needs new data, expose it via `server/dembrane/api/agentic.py` and/or `server/dembrane/service/`. +- Add tests in `agent/tests/test_agent_tools.py` and `server/tests/test_agentic_api.py`. + +### Inspect Local Agentic Conversations + +Use the local script from repo root: + +```bash +bash echo/server/scripts/agentic/latest_runs.sh --chat-id --limit 3 --events 80 +``` + +Common variants: + +```bash +bash echo/server/scripts/agentic/latest_runs.sh --run-id --events 120 +bash echo/server/scripts/agentic/latest_runs.sh --chat-id --limit 1 --events 200 --json ``` ## Important Files diff --git a/echo/agent/.env.sample b/echo/agent/.env.sample new file mode 100644 index 00000000..4b818f20 --- /dev/null +++ b/echo/agent/.env.sample @@ -0,0 +1,5 @@ +GEMINI_API_KEY= +ECHO_API_URL=http://host.docker.internal:8000/api +LLM_MODEL=gemini-3-pro-preview +AGENT_GRAPH_RECURSION_LIMIT=80 +AGENT_CORS_ORIGINS=http://localhost:5173,http://localhost:5174 diff --git a/echo/agent/.gitignore b/echo/agent/.gitignore new file mode 100644 index 00000000..28c8676a --- /dev/null +++ b/echo/agent/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ diff --git a/echo/agent/Dockerfile b/echo/agent/Dockerfile new file mode 100644 index 00000000..37648e49 --- /dev/null +++ b/echo/agent/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.11-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +WORKDIR /app +COPY pyproject.toml ./ +RUN uv sync --frozen || uv sync + +COPY . . + +EXPOSE 8001 + +CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/echo/agent/README.md b/echo/agent/README.md new file mode 100644 index 00000000..10c14ccd --- /dev/null +++ b/echo/agent/README.md @@ -0,0 +1,37 @@ +# Echo Agent Service + +Isolated CopilotKit/LangGraph runtime for Agentic Chat. + +## Why This Service Exists + +- Keeps agent execution out of the frontend runtime. +- Avoids dependency conflicts with `echo/server`. +- Supports long-running execution and backend-owned run lifecycle. + +## Local Run + +```bash +cd echo/agent +cp .env.sample .env +# set GEMINI_API_KEY in .env +uv sync +uv run uvicorn main:app --host 0.0.0.0 --port 8001 --reload +``` + +## Docker Run + +```bash +cd echo/agent +docker build -t echo-agent:local . +docker run --rm -p 8001:8001 --env-file .env echo-agent:local +``` + +## Endpoints + +- `GET /health` +- `POST /copilotkit/{project_id}` + +## Notes + +- This service is intentionally scoped to one purpose: agentic chat execution. +- Auth, persistence, and notifications should be owned by `echo/server` gateway routes. diff --git a/echo/agent/agent.py b/echo/agent/agent.py new file mode 100644 index 00000000..959696c5 --- /dev/null +++ b/echo/agent/agent.py @@ -0,0 +1,665 @@ +from logging import getLogger +import re +from typing import Any, Callable + +from copilotkit.langgraph import CopilotKitState +from langchain_core.messages import HumanMessage, SystemMessage +from langchain_core.tools import tool +from langchain_google_genai import ChatGoogleGenerativeAI +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph import END, StateGraph +from langgraph.prebuilt import ToolNode + +from echo_client import EchoClient +from settings import get_settings + +logger = getLogger("agent") + +SYSTEM_PROMPT = """You are the Dembrane Echo assistant — a friendly, conversational AI that helps \ +users explore and understand their project's conversation data. + +Dembrane Echo is a platform for collective sense-making through recorded conversations. + +## Conversation style +- Be natural and conversational. Match the user's tone and energy. +- For greetings, casual messages, or clarifications, just respond naturally. \ +Do NOT launch into research or tool calls. +- Keep responses concise. Ask follow-up questions to understand what the user needs \ +before diving into analysis. +- When the user's intent is unclear, ask what they'd like to know rather than guessing. + +## Writing style +- Write naturally, like you're talking to a colleague. Vary your response format \ +based on what makes sense for the question. +- Be direct and conversational. Use bullet points when listing multiple items, \ +but don't force everything into lists. +- Flag genuine uncertainty with words like "suggests," "likely," "indicates." +- Keep it concise — don't over-explain or pad responses. +- Don't use the same rigid structure for every response. +- Don't use corporate jargon. + +## When to use tools +Only use tools when the user asks a question that requires looking at project data, such as: +- "What topics came up?" → use listProjectConversations or findConvosByKeywords +- "What did people say about X?" → search and retrieve transcripts +- "Summarize this project" → list conversations, read summaries + +Do NOT use tools for greetings, small talk, or meta-questions about how you work. + +## Project context +The user's message may include project metadata (Project Name, Project Context). \ +Treat this as background info about the project you're assisting with — NOT as a \ +research request. Focus on what the user is actually asking in their message. + +## Research guidelines (when doing research) +- Start by telling the user your plan briefly before making tool calls. +- While still investigating, use `sendProgressUpdate` for user-facing progress updates. +- Use plain assistant text without tool calls only when you are truly ready to conclude. +- Prefer `listProjectConversations` to get an overview before keyword searches. +- For `findConvosByKeywords`, prefer 2-4 focused keywords over long sentence-style queries. +- Avoid repetitive low-signal searches. Maximum 20 tool calls per turn. +- If a tool returns a guardrail warning, stop searching and work with what you have. +- After gathering evidence, give a clear, direct answer. + +## Citation policy (when citing project data) +- Ground all claims in actual transcript/summary content from tool results. +- Provide exact quotes when you have transcripts: "[Participant Name]: quoted text" \ +tagged as [conversation_id:]. +- Use quotes to support your points, but don't overwhelm with citations. +- When working from summaries only (no transcript retrieved), say so and suggest \ +you can retrieve the full transcript if they want exact wording. +- Never fabricate quotes, sources, or conversation IDs. + +## Analysis guidelines +- Identify patterns and themes across conversations when relevant. +- Compare different perspectives and viewpoints. +- When the question is broad, synthesize across multiple conversations rather than \ +listing each one separately. +- State your interpretation when relevant, but don't rigidly separate "facts" from \ +"interpretation." +""" + +AUTOMATIC_NUDGE_TOOL_CALL_INTERVAL = 4 +AUTOMATIC_NUDGE_TEMPLATE = ( + " You have made {tool_call_count} tool calls without sending an assistant update. " + "Call `sendProgressUpdate` now with a concise update and next steps, then continue research with another " + "tool call if evidence is still missing. Only return plain text with no tool call if you are concluding." +) +POST_NUDGE_CONTINUATION_SYSTEM_PROMPT = ( + "You just produced a text-only response immediately after an automatic nudge. " + "If the task is not complete, call `sendProgressUpdate` and then continue with the next tool call. " + "Only respond without tool calls when you are actually done." +) + + +def _build_llm() -> ChatGoogleGenerativeAI: + settings = get_settings() + if not settings.gemini_api_key: + raise ValueError("GEMINI_API_KEY is required") + + return ChatGoogleGenerativeAI( + model=settings.llm_model, + google_api_key=settings.gemini_api_key, + ) + + +def create_agent_graph( + project_id: str, + bearer_token: str, + llm: Any | None = None, + echo_client_factory: Callable[[str], EchoClient] | None = None, +): + if not bearer_token: + raise ValueError("bearer_token is required") + + keyword_search_cache: dict[tuple[str, int], dict[str, Any]] = {} + consecutive_empty_keyword_searches = 0 + project_conversation_cache: dict[str, dict[str, Any]] = {} + automatic_nudge_milestones: set[int] = set() + nudge_retry_milestones: set[int] = set() + last_tool_calls_without_assistant_update = 0 + + def _coerce_message_text(value: Any) -> str: + if isinstance(value, str): + return value.strip() + + if isinstance(value, list): + parts: list[str] = [] + for item in value: + if isinstance(item, str): + normalized = item.strip() + if normalized: + parts.append(normalized) + continue + if isinstance(item, dict): + text = item.get("text") + if isinstance(text, str) and text.strip(): + parts.append(text.strip()) + return "\n".join(parts).strip() + + return "" + + def _count_tool_calls_since_assistant_update(messages: list[Any]) -> int: + tool_calls_since_assistant_update = 0 + + for message in messages: + if getattr(message, "type", None) != "ai": + continue + + tool_calls = getattr(message, "tool_calls", None) + if isinstance(tool_calls, list) and len(tool_calls) > 0: + # Some model responses include both progress text and tool calls. + # Treat the text as an assistant update before counting new tool calls. + if _coerce_message_text(getattr(message, "content", None)): + tool_calls_since_assistant_update = 0 + tool_calls_since_assistant_update += len(tool_calls) + continue + + if _coerce_message_text(getattr(message, "content", None)): + tool_calls_since_assistant_update = 0 + + return tool_calls_since_assistant_update + + def _build_automatic_nudge(messages: list[Any]) -> tuple[str, int] | None: + nonlocal last_tool_calls_without_assistant_update + + tool_calls_without_assistant_update = _count_tool_calls_since_assistant_update(messages) + if tool_calls_without_assistant_update < last_tool_calls_without_assistant_update: + automatic_nudge_milestones.clear() + nudge_retry_milestones.clear() + + if tool_calls_without_assistant_update == 0: + automatic_nudge_milestones.clear() + nudge_retry_milestones.clear() + + last_tool_calls_without_assistant_update = tool_calls_without_assistant_update + if tool_calls_without_assistant_update < AUTOMATIC_NUDGE_TOOL_CALL_INTERVAL: + return None + + milestone = ( + tool_calls_without_assistant_update // AUTOMATIC_NUDGE_TOOL_CALL_INTERVAL + ) * AUTOMATIC_NUDGE_TOOL_CALL_INTERVAL + if milestone in automatic_nudge_milestones: + return None + + automatic_nudge_milestones.add(milestone) + return AUTOMATIC_NUDGE_TEMPLATE.format(tool_call_count=milestone), milestone + + def _message_has_tool_calls(message: Any) -> bool: + tool_calls = getattr(message, "tool_calls", None) + return isinstance(tool_calls, list) and len(tool_calls) > 0 + + def _keyword_guardrail_result( + *, + query: str, + code: str, + message: str, + attempts: int = 0, + stop_search: bool = False, + ) -> dict[str, Any]: + return { + "project_id": project_id, + "query": query, + "count": 0, + "conversations": [], + "guardrail": { + "code": code, + "message": message, + "attempts": attempts, + "stop_search": stop_search, + }, + } + + def _build_snippet( + *, + line: str, + offset: int, + needle_length: int, + context_window: int = 80, + ) -> str: + start = max(0, offset - context_window) + end = min(len(line), offset + needle_length + context_window) + snippet = line[start:end].strip() + if not snippet: + snippet = line.strip() + if start > 0 and snippet: + snippet = f"...{snippet}" + if end < len(line) and snippet: + snippet = f"{snippet}..." + return snippet + + def _grep_transcript_snippets( + *, + transcript: str, + query: str, + limit: int, + ) -> list[dict[str, Any]]: + normalized_query = query.strip().lower() + if not normalized_query: + return [] + + matches: list[dict[str, Any]] = [] + lines = transcript.splitlines() or [transcript] + + for line_index, line in enumerate(lines): + if not isinstance(line, str): + continue + + lowered = line.lower() + search_offset = 0 + while True: + match_offset = lowered.find(normalized_query, search_offset) + if match_offset < 0: + break + + matches.append( + { + "line_index": line_index, + "offset": match_offset, + "snippet": _build_snippet( + line=line, + offset=match_offset, + needle_length=len(normalized_query), + ), + } + ) + if len(matches) >= limit: + return matches + + search_offset = match_offset + max(1, len(normalized_query)) + + return matches + + def _create_echo_client() -> EchoClient: + if echo_client_factory: + return echo_client_factory(bearer_token) + return EchoClient(bearer_token=bearer_token) + + def _normalize_project_conversation( + raw: dict[str, Any], + *, + fallback_project_id: str | None = None, + ) -> dict[str, Any] | None: + conversation_id = raw.get("id") + if not isinstance(conversation_id, str): + conversation_id = raw.get("conversation_id") + if not isinstance(conversation_id, str) or not conversation_id: + return None + + conversation_project_id = raw.get("projectId") + if isinstance(conversation_project_id, dict): + conversation_project_id = conversation_project_id.get("id") + if not isinstance(conversation_project_id, str): + conversation_project_id = raw.get("project_id") + if isinstance(conversation_project_id, dict): + conversation_project_id = conversation_project_id.get("id") + if not isinstance(conversation_project_id, str): + conversation_project_id = fallback_project_id + if not isinstance(conversation_project_id, str) or conversation_project_id != project_id: + return None + + return { + "conversation_id": conversation_id, + "project_id": conversation_project_id, + "project_name": raw.get("projectName") or raw.get("project_name"), + "participant_name": raw.get("displayLabel") or raw.get("participant_name"), + "status": raw.get("status"), + "started_at": raw.get("startedAt") or raw.get("started_at"), + "last_chunk_at": raw.get("lastChunkAt") or raw.get("last_chunk_at"), + "summary": raw.get("summary"), + } + + def _cache_project_conversations(conversations: list[dict[str, Any]]) -> None: + for conversation in conversations: + conversation_id = conversation.get("conversation_id") + if isinstance(conversation_id, str) and conversation_id: + project_conversation_cache[conversation_id] = conversation + + def _extract_project_conversations( + payload: dict[str, Any], + *, + fallback_project_id: str | None = None, + ) -> list[dict[str, Any]]: + raw_conversations = payload.get("conversations", []) + if not isinstance(raw_conversations, list): + return [] + + conversations: list[dict[str, Any]] = [] + for raw in raw_conversations: + if not isinstance(raw, dict): + continue + normalized = _normalize_project_conversation( + raw, + fallback_project_id=fallback_project_id, + ) + if normalized is not None: + conversations.append(normalized) + return conversations + + def _extract_transcript_conversation_ids(payload: dict[str, Any]) -> list[str]: + raw_transcripts = payload.get("transcripts", []) + if not isinstance(raw_transcripts, list): + return [] + + conversation_ids: list[str] = [] + seen: set[str] = set() + for transcript in raw_transcripts: + if not isinstance(transcript, dict): + continue + conversation_id = transcript.get("conversationId") + if not isinstance(conversation_id, str) or not conversation_id: + continue + if conversation_id in seen: + continue + seen.add(conversation_id) + conversation_ids.append(conversation_id) + return conversation_ids + + async def _resolve_project_conversation_with_client( + client: EchoClient, + conversation_id: str, + ) -> dict[str, Any]: + cached = project_conversation_cache.get(conversation_id) + if cached is not None: + return cached + + list_payload = await client.list_project_conversations( + project_id=project_id, + limit=1, + conversation_id=conversation_id, + ) + listed = _extract_project_conversations( + list_payload, + fallback_project_id=project_id, + ) + if listed: + _cache_project_conversations(listed) + return listed[0] + + payload = await client.search_home(query=conversation_id, limit=20) + for candidate in _extract_project_conversations(payload): + if candidate.get("conversation_id") == conversation_id: + _cache_project_conversations([candidate]) + return candidate + raise ValueError("Conversation not found in current project scope") + + async def _search_project_conversations( + *, + query: str, + limit: int = 5, + ) -> list[dict[str, Any]]: + normalized_limit = max(1, min(limit, 20)) + client = _create_echo_client() + try: + payload = await client.search_home(query=query, limit=normalized_limit) + conversations = _extract_project_conversations(payload) + conversations_by_id = {item["conversation_id"]: item for item in conversations} + _cache_project_conversations(conversations) + + transcript_conversation_ids = _extract_transcript_conversation_ids(payload) + if len(conversations_by_id) < normalized_limit: + for conversation_id in transcript_conversation_ids: + if conversation_id in conversations_by_id: + continue + try: + resolved = await _resolve_project_conversation_with_client( + client=client, + conversation_id=conversation_id, + ) + except ValueError: + continue + + conversations_by_id[resolved["conversation_id"]] = resolved + if len(conversations_by_id) >= normalized_limit: + break + + final_conversations = list(conversations_by_id.values())[:normalized_limit] + _cache_project_conversations(final_conversations) + return final_conversations + finally: + await client.close() + + async def _resolve_project_conversation(conversation_id: str) -> dict[str, Any]: + client = _create_echo_client() + try: + return await _resolve_project_conversation_with_client( + client=client, + conversation_id=conversation_id, + ) + finally: + await client.close() + + @tool + async def get_project_scope() -> dict[str, Any]: + """Return the current project scope for this agent run.""" + return {"project_id": project_id} + + @tool + async def findConvosByKeywords(keywords: str, limit: int = 5) -> dict[str, Any]: + """Search project conversations by keywords and return summaries + metadata.""" + nonlocal consecutive_empty_keyword_searches + + normalized_keywords = keywords.strip() + normalized_limit = max(1, min(limit, 20)) + tokens = re.findall(r"[a-z0-9]+", normalized_keywords.lower()) + meaningful_tokens = [token for token in tokens if len(token) >= 4] + if len(meaningful_tokens) == 0: + return _keyword_guardrail_result( + query=normalized_keywords, + code="LOW_SIGNAL_QUERY", + message=( + "Low-signal keyword query. Use specific terms or listProjectConversations first." + ), + ) + + cache_key = (normalized_keywords.lower(), normalized_limit) + cached = keyword_search_cache.get(cache_key) + if cached is not None: + return { + **cached, + "cached": True, + } + + client = _create_echo_client() + try: + payload = await client.list_project_conversations( + project_id=project_id, + limit=normalized_limit, + transcript_query=normalized_keywords, + ) + finally: + await client.close() + + conversations = _extract_project_conversations( + payload, + fallback_project_id=project_id, + ) + _cache_project_conversations(conversations) + result = { + "project_id": project_id, + "query": normalized_keywords, + "count": len(conversations), + "conversations": conversations, + } + keyword_search_cache[cache_key] = result + + if len(conversations) == 0: + consecutive_empty_keyword_searches += 1 + if consecutive_empty_keyword_searches >= 3: + return _keyword_guardrail_result( + query=normalized_keywords, + code="NO_MATCHES_AFTER_RETRIES", + message=( + "No matches after multiple keyword searches. " + "Stop repeating findConvosByKeywords and answer from available context/evidence." + ), + attempts=consecutive_empty_keyword_searches, + stop_search=True, + ) + else: + consecutive_empty_keyword_searches = 0 + + return result + + @tool + async def listConvoSummary(conversation_id: str) -> dict[str, Any]: + """Return metadata + summary (nullable) for a single project conversation.""" + conversation = await _resolve_project_conversation(conversation_id) + return { + "project_id": project_id, + "conversation": conversation, + } + + @tool + async def listProjectConversations(limit: int = 20) -> dict[str, Any]: + """List conversations for the current project scope.""" + normalized_limit = max(1, min(limit, 100)) + client = _create_echo_client() + try: + payload = await client.list_project_conversations(project_id, normalized_limit) + finally: + await client.close() + + conversations = _extract_project_conversations( + payload, + fallback_project_id=project_id, + ) + _cache_project_conversations(conversations) + + return { + "project_id": project_id, + "count": int(payload.get("count") or len(conversations)), + "conversations": conversations, + } + + @tool + async def listConvoFullTranscript(conversation_id: str) -> dict[str, Any]: + """Return full transcript text for a single project conversation.""" + conversation = await _resolve_project_conversation(conversation_id) + + client = _create_echo_client() + try: + transcript = await client.get_conversation_transcript(conversation_id) + finally: + await client.close() + + return { + "project_id": project_id, + "conversation_id": conversation_id, + "participant_name": conversation.get("participant_name"), + "transcript": transcript, + } + + @tool + async def grepConvoSnippets(conversation_id: str, query: str, limit: int = 8) -> dict[str, Any]: + """Find matching transcript snippets for one project-scoped conversation.""" + normalized_query = query.strip() + if not normalized_query: + raise ValueError("query is required") + + normalized_limit = max(1, min(limit, 25)) + conversation = await _resolve_project_conversation(conversation_id) + + client = _create_echo_client() + try: + transcript = await client.get_conversation_transcript(conversation_id) + finally: + await client.close() + + matches = _grep_transcript_snippets( + transcript=transcript, + query=normalized_query, + limit=normalized_limit, + ) + return { + "project_id": project_id, + "conversation_id": conversation_id, + "participant_name": conversation.get("participant_name"), + "query": normalized_query, + "count": len(matches), + "matches": matches, + } + + @tool + async def sendProgressUpdate(update: str, next_steps: str = "") -> dict[str, Any]: + """Emit a user-visible progress update without concluding the run.""" + normalized_update = update.strip() + if not normalized_update: + raise ValueError("update is required") + + return { + "kind": "progress_update", + "update": normalized_update, + "next_steps": next_steps.strip(), + "visible_to_user": True, + } + + tools = [ + get_project_scope, + findConvosByKeywords, + listProjectConversations, + listConvoSummary, + listConvoFullTranscript, + grepConvoSnippets, + sendProgressUpdate, + ] + configured_llm = llm or _build_llm() + llm_with_tools = configured_llm.bind_tools(tools) + + def should_continue(state: dict) -> str: + messages = state.get("messages", []) + if not messages: + return END + last_message = messages[-1] + if hasattr(last_message, "tool_calls") and last_message.tool_calls: + return "tools" + return END + + async def call_model(state: dict) -> dict: + messages = state.get("messages", []) + # Build invocation list with system prompt, but don't persist duplicates + if not messages or not isinstance(messages[0], SystemMessage): + invocation_messages = [SystemMessage(content=SYSTEM_PROMPT)] + messages + else: + invocation_messages = list(messages) + + automatic_nudge = _build_automatic_nudge(messages) + nudge_milestone: int | None = None + if automatic_nudge: + nudge_content, nudge_milestone = automatic_nudge + invocation_messages.append(HumanMessage(content=nudge_content)) + + response = await llm_with_tools.ainvoke(invocation_messages) + + should_retry_after_nudge = ( + nudge_milestone is not None + and nudge_milestone not in nudge_retry_milestones + and not _message_has_tool_calls(response) + ) + if should_retry_after_nudge: + nudge_retry_milestones.add(nudge_milestone) + retry_messages = list(invocation_messages) + retry_messages.append(response) + retry_messages.append(SystemMessage(content=POST_NUDGE_CONTINUATION_SYSTEM_PROMPT)) + response = await llm_with_tools.ainvoke(retry_messages) + + # Return only the new response; LangGraph's reducer appends it to state + return {"messages": [response]} + + def _handle_tool_error(error: Exception) -> str: + return ( + "Tool error: " + f"{error.__class__.__name__}: {error}. " + "Continue with available evidence, avoid repeating failing calls, and summarize constraints." + ) + + workflow = StateGraph(CopilotKitState) + workflow.add_node("agent", call_model) + workflow.add_node("tools", ToolNode(tools, handle_tool_errors=_handle_tool_error)) + + workflow.set_entry_point("agent") + workflow.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END}) + workflow.add_edge("tools", "agent") + compiled_graph = workflow.compile(checkpointer=MemorySaver()) + recursion_limit = max(10, int(get_settings().agent_graph_recursion_limit)) + return compiled_graph.with_config({"recursion_limit": recursion_limit}) diff --git a/echo/agent/auth.py b/echo/agent/auth.py new file mode 100644 index 00000000..3ab6075b --- /dev/null +++ b/echo/agent/auth.py @@ -0,0 +1,27 @@ +from fastapi import HTTPException, Request, status + + +def parse_authorization_header(value: str | None) -> str | None: + if not value: + return None + + parts = value.strip().split(" ", 1) + if len(parts) != 2: + return None + + scheme, token = parts + token = token.strip() + if scheme.lower() != "bearer" or not token: + return None + + return token + + +def extract_bearer_token(request: Request) -> str: + token = parse_authorization_header(request.headers.get("authorization")) + if token is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Missing or invalid Authorization header", + ) + return token diff --git a/echo/agent/echo_client.py b/echo/agent/echo_client.py new file mode 100644 index 00000000..20b8f23a --- /dev/null +++ b/echo/agent/echo_client.py @@ -0,0 +1,111 @@ +from typing import Any, Optional, TypedDict, cast + +import httpx + +from settings import get_settings + + +class SearchConversationResult(TypedDict, total=False): + id: str + projectId: str + projectName: Optional[str] + displayLabel: str + status: str + startedAt: Optional[str] + lastChunkAt: Optional[str] + summary: Optional[str] + + +class SearchTranscriptResult(TypedDict, total=False): + id: str + conversationId: Optional[str] + conversationLabel: Optional[str] + excerpt: Optional[str] + timestamp: Optional[str] + + +class HomeSearchResponse(TypedDict, total=False): + projects: list[dict[str, Any]] + conversations: list[SearchConversationResult] + transcripts: list[SearchTranscriptResult] + chats: list[dict[str, Any]] + + +class AgentProjectConversation(TypedDict, total=False): + conversation_id: str + participant_name: Optional[str] + status: str + summary: Optional[str] + started_at: Optional[str] + last_chunk_at: Optional[str] + + +class AgentProjectConversationsResponse(TypedDict, total=False): + project_id: str + count: int + conversations: list[AgentProjectConversation] + + +class EchoClient: + def __init__(self, bearer_token: Optional[str] = None) -> None: + settings = get_settings() + headers: dict[str, str] = {} + if bearer_token: + headers["Authorization"] = f"Bearer {bearer_token}" + self._client = httpx.AsyncClient( + base_url=settings.echo_api_url, + headers=headers, + timeout=30.0, + ) + + async def close(self) -> None: + await self._client.aclose() + + async def get(self, path: str) -> Any: + response = await self._client.get(path) + response.raise_for_status() + return response.json() + + async def search_home(self, query: str, limit: int = 5) -> HomeSearchResponse: + response = await self._client.get( + "/home/search", + params={"query": query, "limit": limit}, + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + raise ValueError("Unexpected search response shape") + return cast(HomeSearchResponse, payload) + + async def get_conversation_transcript(self, conversation_id: str) -> str: + response = await self._client.get(f"/conversations/{conversation_id}/transcript") + response.raise_for_status() + payload = response.json() + if isinstance(payload, str): + return payload + if isinstance(payload, dict) and isinstance(payload.get("transcript"), str): + return payload["transcript"] + return str(payload) + + async def list_project_conversations( + self, + project_id: str, + limit: int = 20, + conversation_id: str | None = None, + transcript_query: str | None = None, + ) -> AgentProjectConversationsResponse: + params: dict[str, object] = {"limit": limit} + if conversation_id: + params["conversation_id"] = conversation_id + if transcript_query: + params["transcript_query"] = transcript_query + + response = await self._client.get( + f"/agentic/projects/{project_id}/conversations", + params=params, + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + raise ValueError("Unexpected list project conversations response shape") + return cast(AgentProjectConversationsResponse, payload) diff --git a/echo/agent/main.py b/echo/agent/main.py new file mode 100644 index 00000000..acde5c33 --- /dev/null +++ b/echo/agent/main.py @@ -0,0 +1,74 @@ +from logging import basicConfig, getLogger +from typing import Any, Optional + +from dotenv import load_dotenv +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware + +from copilotkit import CopilotKitRemoteEndpoint, LangGraphAgent +from copilotkit.integrations.fastapi import handler as copilotkit_handler + +from agent import create_agent_graph +from auth import extract_bearer_token +from settings import get_settings + +load_dotenv() +basicConfig(level="INFO") +logger = getLogger("echo-agent") +settings = get_settings() + +app = FastAPI( + title="Echo Agent Service", + description="Isolated CopilotKit runtime for Agentic Chat", + version="0.1.0", +) + +cors_origins = [origin.strip() for origin in settings.agent_cors_origins.split(",") if origin.strip()] +app.add_middleware( + CORSMiddleware, + allow_origins=cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/health") +async def health() -> dict[str, str]: + return {"status": "ok", "service": "echo-agent"} + + +@app.api_route("/copilotkit/{project_id}/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) +@app.api_route("/copilotkit/{project_id}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) +async def copilotkit_endpoint(request: Request, project_id: str, path: Optional[str] = None) -> Any: + logger.info( + "copilotkit request: method=%s project_id=%s path=%s", + request.method, + project_id, + path, + ) + bearer_token = extract_bearer_token(request) + + # CopilotKit fastapi handler routes by request.scope["path_params"]["path"]. + # Rewrite root chat posts to default agent execution path. + if request.method == "POST" and (path is None or path == ""): + path = "agent/default" + + request.scope.setdefault("path_params", {}) + request.scope["path_params"]["path"] = path or "" + + agent = LangGraphAgent( + name="default", + description="Echo Agentic Chat default agent", + graph=create_agent_graph(project_id=project_id, bearer_token=bearer_token), + ) + # CopilotKit currently rejects LangGraphAgent only for literal list inputs. + # Supplying a callable preserves expected runtime behavior in this SDK version. + endpoint = CopilotKitRemoteEndpoint(agents=lambda _context: [agent]) + return await copilotkit_handler(request, endpoint) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=True) diff --git a/echo/agent/pyproject.toml b/echo/agent/pyproject.toml new file mode 100644 index 00000000..7ed31478 --- /dev/null +++ b/echo/agent/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "echo-agent" +version = "0.1.0" +description = "Isolated CopilotKit runtime for Echo Agentic Chat" +requires-python = ">=3.11" +dependencies = [ + "copilotkit>=0.1.77", + "langgraph>=0.2", + "langchain-google-genai>=2.0", + "fastapi>=0.115", + "uvicorn[standard]>=0.30", + "httpx>=0.27", + "python-dotenv>=1.0", + "pydantic>=2.0", + "pydantic-settings>=2.0", + "pytest>=8.3", + "pytest-asyncio>=0.23", +] + +[tool.uv] +package = false diff --git a/echo/agent/settings.py b/echo/agent/settings.py new file mode 100644 index 00000000..392ec0b0 --- /dev/null +++ b/echo/agent/settings.py @@ -0,0 +1,27 @@ +from functools import lru_cache + +from pydantic import Field +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + echo_api_url: str = Field(default="http://localhost:8000/api", alias="ECHO_API_URL") + gemini_api_key: str = Field(default="", alias="GEMINI_API_KEY") + llm_model: str = Field(default="gemini-3-pro-preview", alias="LLM_MODEL") + agent_graph_recursion_limit: int = Field( + default=80, + alias="AGENT_GRAPH_RECURSION_LIMIT", + ) + agent_cors_origins: str = Field( + default="http://localhost:5173,http://localhost:5174", + alias="AGENT_CORS_ORIGINS", + ) + + class Config: + env_file = ".env" + extra = "ignore" + + +@lru_cache(maxsize=1) +def get_settings() -> Settings: + return Settings() diff --git a/echo/agent/stream_utils.py b/echo/agent/stream_utils.py new file mode 100644 index 00000000..ff3c5b71 --- /dev/null +++ b/echo/agent/stream_utils.py @@ -0,0 +1,35 @@ +import json +from collections.abc import Iterable +from typing import Any + + +def parse_json_event_stream(chunks: Iterable[str]) -> list[dict[str, Any]]: + events: list[dict[str, Any]] = [] + buffer = "" + + for chunk in chunks: + buffer += chunk + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.strip() + if not line: + continue + try: + payload = json.loads(line) + except json.JSONDecodeError: + continue + + if isinstance(payload, dict): + events.append(payload) + + trailing = buffer.strip() + if trailing: + try: + payload = json.loads(trailing) + except json.JSONDecodeError: + payload = None + + if isinstance(payload, dict): + events.append(payload) + + return events diff --git a/echo/agent/tests/conftest.py b/echo/agent/tests/conftest.py new file mode 100644 index 00000000..86a1a5ac --- /dev/null +++ b/echo/agent/tests/conftest.py @@ -0,0 +1,7 @@ +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) diff --git a/echo/agent/tests/test_agent_graph.py b/echo/agent/tests/test_agent_graph.py new file mode 100644 index 00000000..f987e4bd --- /dev/null +++ b/echo/agent/tests/test_agent_graph.py @@ -0,0 +1,190 @@ +import pytest +from langchain_core.messages import AIMessage, HumanMessage + +from agent import POST_NUDGE_CONTINUATION_SYSTEM_PROMPT, create_agent_graph + + +class FakeLLM: + def bind_tools(self, _tools): + return self + + async def ainvoke(self, _messages): + return AIMessage(content="mocked-response") + + +class SequenceLLM: + def __init__(self, responses: list[AIMessage]) -> None: + self.responses = responses + self.invocations: list[list[object]] = [] + self.bound_tools: list[object] = [] + + def bind_tools(self, tools): + self.bound_tools = tools + return self + + async def ainvoke(self, messages): + self.invocations.append(list(messages)) + if not self.responses: + raise AssertionError("Unexpected model invocation with no prepared response") + return self.responses.pop(0) + + +def _tool_call_response( + call_id: int, + *, + tool_name: str = "get_project_scope", + args: dict[str, object] | None = None, + content: str = "", +) -> AIMessage: + return AIMessage( + content=content, + tool_calls=[ + { + "id": f"call-{call_id}", + "name": tool_name, + "args": args or {}, + } + ], + ) + + +def _extract_automatic_nudges(invocations: list[list[object]]) -> list[str]: + nudges: list[str] = [] + for invocation in invocations: + for message in invocation: + if getattr(message, "type", None) != "human": + continue + content = getattr(message, "content", None) + if isinstance(content, str) and content.startswith(""): + nudges.append(content) + return nudges + + +def _count_corrective_retry_invocations(invocations: list[list[object]]) -> int: + count = 0 + for invocation in invocations: + if any( + getattr(message, "type", None) == "system" + and getattr(message, "content", None) == POST_NUDGE_CONTINUATION_SYSTEM_PROMPT + for message in invocation + ): + count += 1 + return count + + +@pytest.mark.asyncio +async def test_create_agent_graph_uses_mocked_llm_deterministically(): + graph = create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=FakeLLM(), + ) + + result = await graph.ainvoke( + {"messages": [HumanMessage(content="hello")]}, + config={"configurable": {"thread_id": "thread-test-1"}}, + ) + + # System message is used for LLM invocation but not persisted in state to avoid duplication + assert result["messages"][-1].content == "mocked-response" + assert any(msg.content == "mocked-response" for msg in result["messages"]) + + +def test_create_agent_graph_requires_bearer_token(): + with pytest.raises(ValueError): + create_agent_graph(project_id="project-1", bearer_token="", llm=FakeLLM()) + + +@pytest.mark.asyncio +async def test_create_agent_graph_binds_progress_tool_and_tool_is_callable(): + llm = SequenceLLM(responses=[AIMessage(content="done")]) + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + ) + tool_map = {tool.name: tool for tool in llm.bound_tools} + + assert "sendProgressUpdate" in tool_map + payload = await tool_map["sendProgressUpdate"].ainvoke( + { + "update": "I have a rough picture now.", + "next_steps": "I will verify two more conversations.", + } + ) + assert payload == { + "kind": "progress_update", + "update": "I have a rough picture now.", + "next_steps": "I will verify two more conversations.", + "visible_to_user": True, + } + + +@pytest.mark.asyncio +async def test_create_agent_graph_nudge_flow_can_continue_via_progress_tool_call(): + llm = SequenceLLM( + responses=[ + _tool_call_response(1), + _tool_call_response(2), + _tool_call_response(3), + _tool_call_response(4), + _tool_call_response( + 5, + tool_name="sendProgressUpdate", + args={ + "update": "I have a rough picture now.", + "next_steps": "I will verify two more conversations.", + }, + ), + AIMessage(content="done"), + ] + ) + graph = create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + ) + + result = await graph.ainvoke( + {"messages": [HumanMessage(content="hello")]}, + config={"configurable": {"thread_id": "thread-progress-tool-flow"}}, + ) + + nudges = _extract_automatic_nudges(llm.invocations) + assert len(nudges) == 1 + assert "4 tool calls" in nudges[0] + assert result["messages"][-1].content == "done" + assert not any( + isinstance(getattr(message, "content", None), str) + and message.content.startswith("") + for message in result["messages"] + ) + + +@pytest.mark.asyncio +async def test_create_agent_graph_retries_once_after_nudge_when_model_returns_text_only(): + llm = SequenceLLM( + responses=[ + _tool_call_response(1), + _tool_call_response(2), + _tool_call_response(3), + _tool_call_response(4), + AIMessage(content="Progress update but no tool call."), + AIMessage(content="Still text-only after retry."), + ] + ) + graph = create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + ) + + await graph.ainvoke( + {"messages": [HumanMessage(content="hello")]}, + config={"configurable": {"thread_id": "thread-single-retry-after-nudge"}}, + ) + + nudges = _extract_automatic_nudges(llm.invocations) + assert len(nudges) >= 1 + assert all("4 tool calls" in nudge for nudge in nudges) + assert _count_corrective_retry_invocations(llm.invocations) == 1 diff --git a/echo/agent/tests/test_agent_tools.py b/echo/agent/tests/test_agent_tools.py new file mode 100644 index 00000000..0e5188b6 --- /dev/null +++ b/echo/agent/tests/test_agent_tools.py @@ -0,0 +1,619 @@ +import pytest +from langchain_core.messages import AIMessage + +from agent import SYSTEM_PROMPT, create_agent_graph + + +class _CaptureLLM: + def __init__(self): + self.bound_tools = [] + + def bind_tools(self, tools): + self.bound_tools = tools + return self + + async def ainvoke(self, _messages): + return AIMessage(content="unused") + + +class _FakeEchoClient: + def __init__( + self, + *, + bearer_token: str, + search_payload: dict | None, + search_payload_by_query: dict[str, dict] | None, + transcripts: dict[str, str], + project_conversations_payload: dict | None, + project_conversations_payload_by_id: dict[str, dict] | None, + project_conversations_payload_by_transcript_query: dict[str, dict] | None, + ): + self.bearer_token = bearer_token + self.search_payload = search_payload or {} + self.search_payload_by_query = search_payload_by_query or {} + self.transcripts = transcripts + self.project_conversations_payload = project_conversations_payload or {} + self.project_conversations_payload_by_id = project_conversations_payload_by_id or {} + self.project_conversations_payload_by_transcript_query = ( + project_conversations_payload_by_transcript_query or {} + ) + self.search_calls: list[dict[str, object]] = [] + self.transcript_calls: list[str] = [] + self.project_conversations_calls: list[dict[str, object]] = [] + self.closed = False + + async def search_home(self, query: str, limit: int = 5) -> dict: + self.search_calls.append({"query": query, "limit": limit}) + return self.search_payload_by_query.get(query, self.search_payload) + + async def get_conversation_transcript(self, conversation_id: str) -> str: + self.transcript_calls.append(conversation_id) + return self.transcripts[conversation_id] + + async def list_project_conversations( + self, + project_id: str, + limit: int = 20, + conversation_id: str | None = None, + transcript_query: str | None = None, + ) -> dict: + self.project_conversations_calls.append( + { + "project_id": project_id, + "limit": limit, + "conversation_id": conversation_id, + "transcript_query": transcript_query, + } + ) + if conversation_id and conversation_id in self.project_conversations_payload_by_id: + return self.project_conversations_payload_by_id[conversation_id] + if ( + transcript_query + and transcript_query in self.project_conversations_payload_by_transcript_query + ): + return self.project_conversations_payload_by_transcript_query[transcript_query] + return self.project_conversations_payload + + async def close(self) -> None: + self.closed = True + + +class _FakeEchoClientFactory: + def __init__( + self, + *, + search_payload: dict | None, + search_payload_by_query: dict[str, dict] | None = None, + transcripts: dict[str, str], + project_conversations_payload: dict | None = None, + project_conversations_payload_by_id: dict[str, dict] | None = None, + project_conversations_payload_by_transcript_query: dict[str, dict] | None = None, + ): + self.search_payload = search_payload + self.search_payload_by_query = search_payload_by_query + self.transcripts = transcripts + self.project_conversations_payload = project_conversations_payload + self.project_conversations_payload_by_id = project_conversations_payload_by_id + self.project_conversations_payload_by_transcript_query = ( + project_conversations_payload_by_transcript_query + ) + self.instances: list[_FakeEchoClient] = [] + + def __call__(self, bearer_token: str) -> _FakeEchoClient: + client = _FakeEchoClient( + bearer_token=bearer_token, + search_payload=self.search_payload, + search_payload_by_query=self.search_payload_by_query, + transcripts=self.transcripts, + project_conversations_payload=self.project_conversations_payload, + project_conversations_payload_by_id=self.project_conversations_payload_by_id, + project_conversations_payload_by_transcript_query=self.project_conversations_payload_by_transcript_query, + ) + self.instances.append(client) + return client + + +def _tool_map(tools) -> dict[str, object]: # noqa: ANN001 + return {tool.name: tool for tool in tools} + + +def test_system_prompt_contains_conversational_and_research_directives(): + prompt = SYSTEM_PROMPT.lower() + # Conversational-first behavior + assert "conversational" in prompt + assert "greetings" in prompt + assert "do not use tools for greetings" in prompt + # Writing/analysis guidance should be present + assert "writing style" in prompt + assert "analysis guidelines" in prompt + # Citation policy still anchors output quality + assert '"[participant name]: quoted text"' in prompt + assert "[conversation_id:]" in SYSTEM_PROMPT + assert "working from summaries only" in prompt + assert "retrieve the full transcript" in prompt + assert "never fabricate quotes" in prompt + # Project context awareness + assert "project context" in prompt + assert "background info" in prompt + + +@pytest.mark.asyncio +async def test_find_convos_by_keywords_filters_to_current_project(): + llm = _CaptureLLM() + factory = _FakeEchoClientFactory( + search_payload={"conversations": []}, + project_conversations_payload_by_transcript_query={ + "policy": { + "project_id": "project-1", + "count": 1, + "conversations": [ + { + "conversation_id": "conv-1", + "participant_name": "Alice", + "status": "done", + "summary": "summary one", + "started_at": "2026-01-01T00:00:00Z", + "last_chunk_at": "2026-01-01T01:00:00Z", + } + ], + } + }, + transcripts={}, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + result = await tools["findConvosByKeywords"].ainvoke({"keywords": "policy", "limit": 7}) + + assert result["project_id"] == "project-1" + assert result["count"] == 1 + assert result["conversations"][0]["conversation_id"] == "conv-1" + assert factory.instances[0].search_calls == [] + assert factory.instances[0].project_conversations_calls == [ + { + "project_id": "project-1", + "limit": 7, + "conversation_id": None, + "transcript_query": "policy", + } + ] + assert factory.instances[0].closed is True + + +@pytest.mark.asyncio +async def test_find_convos_by_keywords_uses_single_transcript_query_call_for_long_input(): + llm = _CaptureLLM() + long_query = "Bad Bunny Super Bowl halftime show Dembrane TPUSA Turning Point USA" + factory = _FakeEchoClientFactory( + search_payload={"conversations": []}, + project_conversations_payload_by_transcript_query={ + long_query: { + "project_id": "project-1", + "count": 2, + "conversations": [ + { + "conversation_id": "conv-1", + "participant_name": "Alice", + "status": "done", + "summary": "talked about budget", + "started_at": "2026-01-01T00:00:00Z", + "last_chunk_at": "2026-01-01T01:00:00Z", + }, + { + "conversation_id": "conv-2", + "participant_name": "Bob", + "status": "done", + "summary": "other conversation", + "started_at": "2026-01-02T00:00:00Z", + "last_chunk_at": "2026-01-02T01:00:00Z", + }, + ], + }, + }, + transcripts={}, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + result = await tools["findConvosByKeywords"].ainvoke({"keywords": long_query, "limit": 5}) + + assert result["project_id"] == "project-1" + assert result["count"] == 2 + assert [conversation["conversation_id"] for conversation in result["conversations"]] == [ + "conv-1", + "conv-2", + ] + assert factory.instances[0].search_calls == [] + assert factory.instances[0].project_conversations_calls == [ + { + "project_id": "project-1", + "limit": 5, + "conversation_id": None, + "transcript_query": long_query, + } + ] + assert factory.instances[0].closed is True + + +@pytest.mark.asyncio +async def test_find_convos_by_keywords_rejects_low_signal_query(): + llm = _CaptureLLM() + factory = _FakeEchoClientFactory( + search_payload={"conversations": []}, + transcripts={}, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + result = await tools["findConvosByKeywords"].ainvoke({"keywords": "ok", "limit": 5}) + + assert result["count"] == 0 + assert result["conversations"] == [] + assert result["guardrail"]["code"] == "LOW_SIGNAL_QUERY" + assert result["guardrail"]["stop_search"] is False + + +@pytest.mark.asyncio +async def test_find_convos_by_keywords_stops_after_repeated_empty_results(): + llm = _CaptureLLM() + factory = _FakeEchoClientFactory( + search_payload={"conversations": []}, + transcripts={}, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + await tools["findConvosByKeywords"].ainvoke({"keywords": "representation", "limit": 5}) + await tools["findConvosByKeywords"].ainvoke({"keywords": "minority", "limit": 5}) + + result = await tools["findConvosByKeywords"].ainvoke({"keywords": "media", "limit": 5}) + + assert result["count"] == 0 + assert result["guardrail"]["code"] == "NO_MATCHES_AFTER_RETRIES" + assert result["guardrail"]["stop_search"] is True + assert result["guardrail"]["attempts"] == 3 + + +@pytest.mark.asyncio +async def test_find_convos_by_keywords_resets_empty_counter_after_success(): + llm = _CaptureLLM() + factory = _FakeEchoClientFactory( + search_payload={"conversations": []}, + project_conversations_payload_by_transcript_query={ + "success-topic": { + "project_id": "project-1", + "count": 1, + "conversations": [ + { + "conversation_id": "conv-1", + "participant_name": "Alice", + "status": "done", + } + ], + } + }, + transcripts={}, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + await tools["findConvosByKeywords"].ainvoke({"keywords": "representation", "limit": 5}) + success_result = await tools["findConvosByKeywords"].ainvoke( + {"keywords": "success-topic", "limit": 5} + ) + first_empty_after_success = await tools["findConvosByKeywords"].ainvoke( + {"keywords": "minority", "limit": 5} + ) + second_empty_after_success = await tools["findConvosByKeywords"].ainvoke( + {"keywords": "media", "limit": 5} + ) + third_empty_after_success = await tools["findConvosByKeywords"].ainvoke( + {"keywords": "narratives", "limit": 5} + ) + + assert success_result["count"] == 1 + assert first_empty_after_success["count"] == 0 + assert "guardrail" not in first_empty_after_success + assert second_empty_after_success["count"] == 0 + assert "guardrail" not in second_empty_after_success + assert third_empty_after_success["guardrail"]["code"] == "NO_MATCHES_AFTER_RETRIES" + assert third_empty_after_success["guardrail"]["attempts"] == 3 + + +@pytest.mark.asyncio +async def test_list_convo_summary_returns_nullable_summary_and_exact_match(): + llm = _CaptureLLM() + factory = _FakeEchoClientFactory( + search_payload={ + "conversations": [ + { + "id": "conv-12", + "projectId": "project-1", + "displayLabel": "Partial Match", + "status": "done", + "summary": "wrong match", + }, + { + "id": "conv-1", + "projectId": "project-1", + "displayLabel": "Exact Match", + "status": "done", + "summary": None, + }, + ] + }, + transcripts={}, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + result = await tools["listConvoSummary"].ainvoke({"conversation_id": "conv-1"}) + + assert result["conversation"]["conversation_id"] == "conv-1" + assert result["conversation"]["summary"] is None + assert factory.instances[0].closed is True + + +@pytest.mark.asyncio +async def test_list_convo_full_transcript_returns_text_for_project_conversation(): + llm = _CaptureLLM() + factory = _FakeEchoClientFactory( + search_payload={ + "conversations": [ + { + "id": "conv-1", + "projectId": "project-1", + "displayLabel": "Alice", + "status": "done", + "summary": "summary", + } + ] + }, + transcripts={"conv-1": "line one\nline two"}, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + result = await tools["listConvoFullTranscript"].ainvoke({"conversation_id": "conv-1"}) + + assert result["project_id"] == "project-1" + assert result["conversation_id"] == "conv-1" + assert result["transcript"] == "line one\nline two" + assert all(instance.closed for instance in factory.instances) + + +@pytest.mark.asyncio +async def test_list_project_conversations_returns_project_scoped_cards(): + llm = _CaptureLLM() + factory = _FakeEchoClientFactory( + search_payload={"conversations": []}, + transcripts={}, + project_conversations_payload={ + "project_id": "project-1", + "count": 2, + "conversations": [ + {"conversation_id": "conv-1", "participant_name": "Alice", "status": "done"}, + {"conversation_id": "conv-2", "participant_name": "Bob", "status": "live"}, + ], + }, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + result = await tools["listProjectConversations"].ainvoke({"limit": 9}) + + assert result["project_id"] == "project-1" + assert result["count"] == 2 + assert result["conversations"][0]["conversation_id"] == "conv-1" + assert factory.instances[0].project_conversations_calls == [ + { + "project_id": "project-1", + "limit": 9, + "conversation_id": None, + "transcript_query": None, + } + ] + assert factory.instances[0].closed is True + + +@pytest.mark.asyncio +async def test_list_convo_full_transcript_uses_scoped_lookup_for_exact_id(): + llm = _CaptureLLM() + factory = _FakeEchoClientFactory( + search_payload={"conversations": []}, + transcripts={"conv-1": "line one"}, + project_conversations_payload_by_id={ + "conv-1": { + "project_id": "project-1", + "count": 1, + "conversations": [ + { + "conversation_id": "conv-1", + "participant_name": "Alice", + "status": "done", + } + ], + } + }, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + result = await tools["listConvoFullTranscript"].ainvoke({"conversation_id": "conv-1"}) + + assert result["conversation_id"] == "conv-1" + assert result["transcript"] == "line one" + assert factory.instances[0].project_conversations_calls == [ + { + "project_id": "project-1", + "limit": 1, + "conversation_id": "conv-1", + "transcript_query": None, + } + ] + assert factory.instances[0].search_calls == [] + + +@pytest.mark.asyncio +async def test_grep_convo_snippets_returns_matches_for_in_scope_conversation(): + llm = _CaptureLLM() + factory = _FakeEchoClientFactory( + search_payload={ + "conversations": [ + { + "id": "conv-1", + "projectId": "project-1", + "displayLabel": "Alice", + "status": "done", + } + ] + }, + transcripts={ + "conv-1": "Minority representation matters for trust.\n" + "Some participants discussed representation gaps in media.\n" + "Other topics were unrelated.", + }, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + result = await tools["grepConvoSnippets"].ainvoke( + {"conversation_id": "conv-1", "query": "representation", "limit": 5} + ) + + assert result["project_id"] == "project-1" + assert result["conversation_id"] == "conv-1" + assert result["count"] == 2 + assert result["matches"][0]["snippet"] + assert factory.instances[-1].closed is True + + +@pytest.mark.asyncio +async def test_grep_convo_snippets_returns_empty_matches_when_no_hits(): + llm = _CaptureLLM() + factory = _FakeEchoClientFactory( + search_payload={ + "conversations": [ + { + "id": "conv-1", + "projectId": "project-1", + "displayLabel": "Alice", + "status": "done", + } + ] + }, + transcripts={"conv-1": "No relevant term in this transcript."}, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + result = await tools["grepConvoSnippets"].ainvoke( + {"conversation_id": "conv-1", "query": "representation", "limit": 5} + ) + + assert result["count"] == 0 + assert result["matches"] == [] + + +@pytest.mark.asyncio +async def test_list_convo_summary_raises_for_out_of_scope_or_missing_conversation(): + llm = _CaptureLLM() + factory = _FakeEchoClientFactory( + search_payload={ + "conversations": [ + { + "id": "conv-9", + "projectId": "other-project", + "displayLabel": "Other", + "status": "done", + "summary": "other", + } + ] + }, + transcripts={}, + ) + + create_agent_graph( + project_id="project-1", + bearer_token="token-1", + llm=llm, + echo_client_factory=factory, + ) + tools = _tool_map(llm.bound_tools) + + with pytest.raises(ValueError, match="Conversation not found in current project scope"): + await tools["listConvoSummary"].ainvoke({"conversation_id": "conv-9"}) + + with pytest.raises(ValueError, match="Conversation not found in current project scope"): + await tools["listConvoFullTranscript"].ainvoke({"conversation_id": "conv-9"}) + + with pytest.raises(ValueError, match="Conversation not found in current project scope"): + await tools["grepConvoSnippets"].ainvoke( + {"conversation_id": "conv-9", "query": "representation", "limit": 3} + ) diff --git a/echo/agent/tests/test_auth.py b/echo/agent/tests/test_auth.py new file mode 100644 index 00000000..234dae8c --- /dev/null +++ b/echo/agent/tests/test_auth.py @@ -0,0 +1,40 @@ +import pytest +from fastapi import HTTPException +from starlette.requests import Request + +from auth import extract_bearer_token, parse_authorization_header + + +def _request_with_header(header_value: str | None) -> Request: + headers = [] + if header_value is not None: + headers.append((b"authorization", header_value.encode("utf-8"))) + + scope = { + "type": "http", + "method": "POST", + "path": "/copilotkit/project-id", + "headers": headers, + } + return Request(scope) + + +def test_parse_authorization_header_accepts_bearer_token(): + assert parse_authorization_header("Bearer token-123") == "token-123" + + +def test_parse_authorization_header_rejects_invalid_scheme(): + assert parse_authorization_header("Basic abc") is None + + +def test_extract_bearer_token_reads_authorization_header(): + request = _request_with_header("Bearer token-xyz") + assert extract_bearer_token(request) == "token-xyz" + + +def test_extract_bearer_token_raises_when_missing(): + request = _request_with_header(None) + with pytest.raises(HTTPException) as exc: + extract_bearer_token(request) + + assert exc.value.status_code == 401 diff --git a/echo/agent/tests/test_echo_client.py b/echo/agent/tests/test_echo_client.py new file mode 100644 index 00000000..0e27325a --- /dev/null +++ b/echo/agent/tests/test_echo_client.py @@ -0,0 +1,135 @@ +import httpx +import pytest + +from echo_client import EchoClient + + +def test_echo_client_sets_authorization_header(): + client = EchoClient(bearer_token="abc123") + try: + assert client._client.headers.get("Authorization") == "Bearer abc123" + finally: + # Async close is tested in integration; this test only checks header wiring. + pass + + +def test_echo_client_without_token_has_no_authorization_header(): + client = EchoClient(bearer_token=None) + try: + assert client._client.headers.get("Authorization") is None + finally: + pass + + +class _FakeAsyncClient: + def __init__(self, *, base_url, headers, timeout): + self.base_url = base_url + self.headers = headers + self.timeout = timeout + self.calls: list[dict[str, object]] = [] + + async def aclose(self) -> None: + return None + + async def get(self, path: str, params=None): # noqa: ANN001 + self.calls.append({"path": path, "params": params}) + request = httpx.Request("GET", f"{self.base_url}{path}", params=params) + if path == "/home/search": + return httpx.Response( + status_code=200, + request=request, + json={"conversations": [], "projects": [], "transcripts": [], "chats": []}, + ) + if path.startswith("/agentic/projects/") and path.endswith("/conversations"): + return httpx.Response( + status_code=200, + request=request, + json={ + "project_id": "project-1", + "count": 1, + "conversations": [ + { + "conversation_id": "conv-1", + "participant_name": "Alice", + "status": "done", + "summary": "summary", + "started_at": "2026-02-01T12:00:00Z", + "last_chunk_at": "2026-02-01T12:10:00Z", + } + ], + }, + ) + if path.startswith("/conversations/") and path.endswith("/transcript"): + return httpx.Response(status_code=200, request=request, json="transcript text") + return httpx.Response(status_code=200, request=request, json={"ok": True}) + + +@pytest.mark.asyncio +async def test_search_home_uses_expected_path_and_query_params(monkeypatch): + monkeypatch.setattr("echo_client.httpx.AsyncClient", _FakeAsyncClient) + + client = EchoClient(bearer_token="token-1") + payload = await client.search_home(query="climate", limit=7) + + assert payload["conversations"] == [] + assert client._client.calls[0]["path"] == "/home/search" + assert client._client.calls[0]["params"] == {"query": "climate", "limit": 7} + assert client._client.headers.get("Authorization") == "Bearer token-1" + + +@pytest.mark.asyncio +async def test_get_conversation_transcript_uses_expected_path(monkeypatch): + monkeypatch.setattr("echo_client.httpx.AsyncClient", _FakeAsyncClient) + + client = EchoClient(bearer_token="token-1") + transcript = await client.get_conversation_transcript("conversation-123") + + assert transcript == "transcript text" + assert client._client.calls[0]["path"] == "/conversations/conversation-123/transcript" + assert client._client.headers.get("Authorization") == "Bearer token-1" + + +@pytest.mark.asyncio +async def test_list_project_conversations_uses_expected_path(monkeypatch): + monkeypatch.setattr("echo_client.httpx.AsyncClient", _FakeAsyncClient) + + client = EchoClient(bearer_token="token-1") + payload = await client.list_project_conversations("project-1", limit=9) + + assert payload["project_id"] == "project-1" + assert payload["count"] == 1 + assert client._client.calls[0]["path"] == "/agentic/projects/project-1/conversations" + assert client._client.calls[0]["params"] == {"limit": 9} + assert client._client.headers.get("Authorization") == "Bearer token-1" + + +@pytest.mark.asyncio +async def test_list_project_conversations_accepts_conversation_id_filter(monkeypatch): + monkeypatch.setattr("echo_client.httpx.AsyncClient", _FakeAsyncClient) + + client = EchoClient(bearer_token="token-1") + payload = await client.list_project_conversations( + "project-1", + limit=1, + conversation_id="conv-1", + ) + + assert payload["project_id"] == "project-1" + assert client._client.calls[0]["path"] == "/agentic/projects/project-1/conversations" + assert client._client.calls[0]["params"] == {"limit": 1, "conversation_id": "conv-1"} + + +@pytest.mark.asyncio +async def test_list_project_conversations_accepts_transcript_query_filter(monkeypatch): + monkeypatch.setattr("echo_client.httpx.AsyncClient", _FakeAsyncClient) + + client = EchoClient(bearer_token="token-1") + payload = await client.list_project_conversations( + "project-1", + limit=5, + transcript_query="Bad Bunny TPUSA", + ) + + assert payload["project_id"] == "project-1" + assert client._client.calls[0]["path"] == "/agentic/projects/project-1/conversations" + assert client._client.calls[0]["params"] == {"limit": 5, "transcript_query": "Bad Bunny TPUSA"} diff --git a/echo/agent/tests/test_main_routes.py b/echo/agent/tests/test_main_routes.py new file mode 100644 index 00000000..c771882f --- /dev/null +++ b/echo/agent/tests/test_main_routes.py @@ -0,0 +1,62 @@ +from fastapi.responses import JSONResponse +from fastapi.testclient import TestClient + +import main + + +class _DummyLangGraphAgent: + def __init__(self, **_kwargs): + pass + + +class _DummyEndpoint: + def __init__(self, agents): + self.agents = agents + + +async def _fake_handler(request, _endpoint): + return JSONResponse( + { + "path": request.scope.get("path_params", {}).get("path"), + "authorization": request.headers.get("authorization"), + } + ) + + +def test_copilotkit_root_post_rewrites_to_default_path(monkeypatch): + monkeypatch.setattr(main, "copilotkit_handler", _fake_handler) + monkeypatch.setattr(main, "LangGraphAgent", _DummyLangGraphAgent) + monkeypatch.setattr(main, "CopilotKitRemoteEndpoint", _DummyEndpoint) + + client = TestClient(main.app) + response = client.post( + "/copilotkit/project-1", + headers={"Authorization": "Bearer token-1"}, + json={}, + ) + + assert response.status_code == 200 + assert response.json()["path"] == "agent/default" + assert response.json()["authorization"] == "Bearer token-1" + + +def test_copilotkit_nested_path_is_preserved(monkeypatch): + monkeypatch.setattr(main, "copilotkit_handler", _fake_handler) + monkeypatch.setattr(main, "LangGraphAgent", _DummyLangGraphAgent) + monkeypatch.setattr(main, "CopilotKitRemoteEndpoint", _DummyEndpoint) + + client = TestClient(main.app) + response = client.post( + "/copilotkit/project-1/agent/custom", + headers={"Authorization": "Bearer token-1"}, + json={}, + ) + + assert response.status_code == 200 + assert response.json()["path"] == "agent/custom" + + +def test_copilotkit_requires_auth_header(): + client = TestClient(main.app) + response = client.post("/copilotkit/project-1", json={}) + assert response.status_code == 401 diff --git a/echo/agent/tests/test_settings.py b/echo/agent/tests/test_settings.py new file mode 100644 index 00000000..85383df1 --- /dev/null +++ b/echo/agent/tests/test_settings.py @@ -0,0 +1,20 @@ +from settings import get_settings + + +def test_settings_reads_env(monkeypatch): + get_settings.cache_clear() + monkeypatch.setenv("ECHO_API_URL", "http://example.test/api") + monkeypatch.setenv("GEMINI_API_KEY", "test-key") + monkeypatch.setenv("LLM_MODEL", "gemini-test") + monkeypatch.setenv("AGENT_GRAPH_RECURSION_LIMIT", "64") + monkeypatch.setenv("AGENT_CORS_ORIGINS", "http://localhost:1111,http://localhost:2222") + + settings = get_settings() + + assert settings.echo_api_url == "http://example.test/api" + assert settings.gemini_api_key == "test-key" + assert settings.llm_model == "gemini-test" + assert settings.agent_graph_recursion_limit == 64 + assert settings.agent_cors_origins == "http://localhost:1111,http://localhost:2222" + + get_settings.cache_clear() diff --git a/echo/agent/tests/test_stream_utils.py b/echo/agent/tests/test_stream_utils.py new file mode 100644 index 00000000..893da6ae --- /dev/null +++ b/echo/agent/tests/test_stream_utils.py @@ -0,0 +1,19 @@ +from stream_utils import parse_json_event_stream + + +def test_parse_json_event_stream_parses_complete_lines(): + chunks = ['{"type":"A"}\n{"type":"B"}\n'] + events = parse_json_event_stream(chunks) + assert events == [{"type": "A"}, {"type": "B"}] + + +def test_parse_json_event_stream_handles_partial_chunks(): + chunks = ['{"type":"A"', '}\n{"type":"B"}\n'] + events = parse_json_event_stream(chunks) + assert events == [{"type": "A"}, {"type": "B"}] + + +def test_parse_json_event_stream_skips_invalid_lines(): + chunks = ['{"type":"A"}\nnot-json\n{"type":"B"}\n'] + events = parse_json_event_stream(chunks) + assert events == [{"type": "A"}, {"type": "B"}] diff --git a/echo/agent/uv.lock b/echo/agent/uv.lock new file mode 100644 index 00000000..88377b72 --- /dev/null +++ b/echo/agent/uv.lock @@ -0,0 +1,1554 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "ag-ui-langgraph" +version = "0.0.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ag-ui-protocol" }, + { name = "langchain" }, + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/c6/48bf48fb20eb128ca87058ba7cc22785c272ca5d162236f534a8fdb4b8cb/ag_ui_langgraph-0.0.25.tar.gz", hash = "sha256:ee100631fe57026d331f695c939826d470f3f9564e0956ff46be391f87d9498c", size = 13198, upload-time = "2026-02-10T16:07:11.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/d5/041803505ec2258790b82d45090a3e38aa5f32a60026f97718822c9bc86e/ag_ui_langgraph-0.0.25-py3-none-any.whl", hash = "sha256:a48cde3723578c32a6610e5f5e2bcf1f31ddb711ec1dd1b2c6486a7a64abe1cd", size = 14943, upload-time = "2026-02-10T16:07:10.561Z" }, +] + +[package.optional-dependencies] +fastapi = [ + { name = "fastapi" }, +] + +[[package]] +name = "ag-ui-protocol" +version = "0.1.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/c1/33ab11dc829c6c28d0d346988b2f394aa632d3ad63d1d2eb5f16eccd769b/ag_ui_protocol-0.1.11.tar.gz", hash = "sha256:b336dfebb5751e9cc2c676a3008a4bce4819004e6f6f8cba73169823564472ae", size = 6249, upload-time = "2026-02-11T12:41:36.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/83/5c6f4cb24d27d9cbe0c31ba2f3b4d1ff42bc6f87ba9facfa9e9d44046c6b/ag_ui_protocol-0.1.11-py3-none-any.whl", hash = "sha256:b0cc25570462a8eba8e57a098e0a2d6892a1f571a7bea7da2d4b60efd5d66789", size = 8392, upload-time = "2026-02-11T12:41:35.303Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "copilotkit" +version = "0.1.78" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ag-ui-langgraph", extra = ["fastapi"] }, + { name = "fastapi" }, + { name = "langchain" }, + { name = "langgraph" }, + { name = "partialjson" }, + { name = "toml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/33/4ff1f1d732f89f8a08f08a20c6288af8d407438221b44748967f10df88f8/copilotkit-0.1.78.tar.gz", hash = "sha256:d27c303d61539eab3dc168ada6ec0a0ecb02f770da8aa4f1f9bbd9488235c556", size = 37578, upload-time = "2026-02-06T11:56:04.523Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/5c/81bcea99f0da7d6b43f5bbdce13943da260ea94cd5d61c0d8bb1d047e50c/copilotkit-0.1.78-py3-none-any.whl", hash = "sha256:d4230094c96de708a58c9d5da82258a4bfda6ce85846b2fb154a1837ed0a92f5", size = 46919, upload-time = "2026-02-06T11:56:03.318Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "echo-agent" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "copilotkit" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "langchain-google-genai" }, + { name = "langgraph" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "python-dotenv" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.metadata] +requires-dist = [ + { name = "copilotkit", specifier = ">=0.1.77" }, + { name = "fastapi", specifier = ">=0.115" }, + { name = "httpx", specifier = ">=0.27" }, + { name = "langchain-google-genai", specifier = ">=2.0" }, + { name = "langgraph", specifier = ">=0.2" }, + { name = "pydantic", specifier = ">=2.0" }, + { name = "pydantic-settings", specifier = ">=2.0" }, + { name = "pytest", specifier = ">=8.3" }, + { name = "pytest-asyncio", specifier = ">=0.23" }, + { name = "python-dotenv", specifier = ">=1.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.30" }, +] + +[[package]] +name = "fastapi" +version = "0.115.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" }, +] + +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + +[[package]] +name = "google-auth" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "1.62.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/4c/71b32b5c8db420cf2fd0d5ef8a672adbde97d85e5d44a0b4fca712264ef1/google_genai-1.62.0.tar.gz", hash = "sha256:709468a14c739a080bc240a4f3191df597bf64485b1ca3728e0fb67517774c18", size = 490888, upload-time = "2026-02-04T22:48:41.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/5f/4645d8a28c6e431d0dd6011003a852563f3da7037d36af53154925b099fd/google_genai-1.62.0-py3-none-any.whl", hash = "sha256:4c3daeff3d05fafee4b9a1a31f9c07f01bc22051081aa58b4d61f58d16d1bcc0", size = 724166, upload-time = "2026-02-04T22:48:39.956Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "langchain" +version = "1.2.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/22/a4d4ac98fc2e393537130bbfba0d71a8113e6f884d96f935923e247397fe/langchain-1.2.10.tar.gz", hash = "sha256:bdcd7218d9c79a413cf15e106e4eb94408ac0963df9333ccd095b9ed43bf3be7", size = 570071, upload-time = "2026-02-10T14:56:49.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/06/c3394327f815fade875724c0f6cff529777c96a1e17fea066deb997f8cf5/langchain-1.2.10-py3-none-any.whl", hash = "sha256:e07a377204451fffaed88276b8193e894893b1003e25c5bca6539288ccca3698", size = 111738, upload-time = "2026-02-10T14:56:47.985Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.2.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/17/1943cedfc118e04b8128e4c3e1dbf0fa0ea58eefddbb6198cfd699d19f01/langchain_core-1.2.11.tar.gz", hash = "sha256:f164bb36602dd74a3a50c1334fca75309ad5ed95767acdfdbb9fa95ce28a1e01", size = 831211, upload-time = "2026-02-10T20:35:28.35Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/30/1f80e3fc674353cad975ed5294353d42512535d2094ef032c06454c2c873/langchain_core-1.2.11-py3-none-any.whl", hash = "sha256:ae11ceb8dda60d0b9d09e763116e592f1683327c17be5b715f350fd29aee65d3", size = 500062, upload-time = "2026-02-10T20:35:26.698Z" }, +] + +[[package]] +name = "langchain-google-genai" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filetype" }, + { name = "google-genai" }, + { name = "langchain-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/0b/eae2305e207574dc633983a8a82a745e0ede1bce1f3a9daff24d2341fadc/langchain_google_genai-4.2.0.tar.gz", hash = "sha256:9a8d9bfc35354983ed29079cefff53c3e7c9c2a44b6ba75cc8f13a0cf8b55c33", size = 277361, upload-time = "2026-01-13T20:41:17.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/51/39942c0083139652494bb354dddf0ed397703a4882302f7b48aeca531c96/langchain_google_genai-4.2.0-py3-none-any.whl", hash = "sha256:856041aaafceff65a4ef0d5acf5731f2db95229ff041132af011aec51e8279d9", size = 66452, upload-time = "2026-01-13T20:41:16.296Z" }, +] + +[[package]] +name = "langgraph" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/49/e9551965d8a44dd9afdc55cbcdc5a9bd18bee6918cc2395b225d40adb77c/langgraph-1.0.8.tar.gz", hash = "sha256:2630fc578846995114fd659f8b14df9eff5a4e78c49413f67718725e88ceb544", size = 498708, upload-time = "2026-02-06T12:31:13.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/72/b0d7fc1007821a08dfc03ce232f39f209aa4aa46414ea3d125b24e35093a/langgraph-1.0.8-py3-none-any.whl", hash = "sha256:da737177c024caad7e5262642bece4f54edf4cba2c905a1d1338963f41cf0904", size = 158144, upload-time = "2026-02-06T12:31:12.489Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/76/55a18c59dedf39688d72c4b06af73a5e3ea0d1a01bc867b88fbf0659f203/langgraph_checkpoint-4.0.0.tar.gz", hash = "sha256:814d1bd050fac029476558d8e68d87bce9009a0262d04a2c14b918255954a624", size = 137320, upload-time = "2026-01-12T20:30:26.38Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/de/ddd53b7032e623f3c7bcdab2b44e8bf635e468f62e10e5ff1946f62c9356/langgraph_checkpoint-4.0.0-py3-none-any.whl", hash = "sha256:3fa9b2635a7c5ac28b338f631abf6a030c3b508b7b9ce17c22611513b589c784", size = 46329, upload-time = "2026-01-12T20:30:25.2Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/59/711aecd1a50999456850dc328f3cad72b4372d8218838d8d5326f80cb76f/langgraph_prebuilt-1.0.7.tar.gz", hash = "sha256:38e097e06de810de4d0e028ffc0e432bb56d1fb417620fb1dfdc76c5e03e4bf9", size = 163692, upload-time = "2026-01-22T16:45:22.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/49/5e37abb3f38a17a3487634abc2a5da87c208cc1d14577eb8d7184b25c886/langgraph_prebuilt-1.0.7-py3-none-any.whl", hash = "sha256:e14923516504405bb5edc3977085bc9622c35476b50c1808544490e13871fe7c", size = 35324, upload-time = "2026-01-22T16:45:21.784Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/2b/2dae368ac76e315197f07ab58077aadf20833c226fbfd450d71745850314/langgraph_sdk-0.3.5.tar.gz", hash = "sha256:64669e9885a908578eed921ef9a8e52b8d0cd38db1e3e5d6d299d4e6f8830ac0", size = 177470, upload-time = "2026-02-10T16:56:09.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d5/a14d957c515ba7a9713bf0f03f2b9277979c403bc50f829bdfd54ae7dc9e/langgraph_sdk-0.3.5-py3-none-any.whl", hash = "sha256:bcfa1dcbddadb604076ce46f5e08969538735e5ac47fa863d4fac5a512dab5c9", size = 70851, upload-time = "2026-02-10T16:56:07.983Z" }, +] + +[[package]] +name = "langsmith" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "uuid-utils" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/48/3151de6df96e0977b8d319b03905e29db0df6929a85df1d922a030b7e68d/langsmith-0.7.1.tar.gz", hash = "sha256:e3fec2f97f7c5192f192f4873d6a076b8c6469768022323dded07087d8cb70a4", size = 984367, upload-time = "2026-02-10T01:55:24.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/87/6f2b008a456b4f5fd0fb1509bb7e1e9368c1a0c9641a535f224a9ddc10f3/langsmith-0.7.1-py3-none-any.whl", hash = "sha256:92cfa54253d35417184c297ad25bfd921d95f15d60a1ca75f14d4e7acd152a29", size = 322515, upload-time = "2026-02-10T01:55:22.531Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" }, + { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, + { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, + { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "partialjson" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/b2/59669fdc3ecbc724a077c598c1c9b4068549af0cd8c3b5add9337bd4d93a/partialjson-0.0.8.tar.gz", hash = "sha256:91217e19a15049332df534477f56420065ad1729cedee7d8c7433e1d2acc7dca", size = 4142, upload-time = "2024-08-03T18:03:15.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/fb/453af21468774dbd0954853735a4fc7841544c3022ff86e5d93252d7ea72/partialjson-0.0.8-py3-none-any.whl", hash = "sha256:22c6c60944137f931a7033fa0eeee2d74b49114f3d45c25a560b07a6ebf22b76", size = 4549, upload-time = "2024-08-03T18:03:14.447Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/7c/3a926e847516e67bc6838634f2e54e24381105b4e80f9338dc35cca0086b/uuid_utils-0.14.0.tar.gz", hash = "sha256:fc5bac21e9933ea6c590433c11aa54aaca599f690c08069e364eb13a12f670b4", size = 22072, upload-time = "2026-01-20T20:37:15.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/42/42d003f4a99ddc901eef2fd41acb3694163835e037fb6dde79ad68a72342/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f6695c0bed8b18a904321e115afe73b34444bc8451d0ce3244a1ec3b84deb0e5", size = 601786, upload-time = "2026-01-20T20:37:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/96/e6/775dfb91f74b18f7207e3201eb31ee666d286579990dc69dd50db2d92813/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4f0a730bbf2d8bb2c11b93e1005e91769f2f533fa1125ed1f00fd15b6fcc732b", size = 303943, upload-time = "2026-01-20T20:37:18.767Z" }, + { url = "https://files.pythonhosted.org/packages/17/82/ea5f5e85560b08a1f30cdc65f75e76494dc7aba9773f679e7eaa27370229/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ce3fd1a4fdedae618fc3edc8faf91897012469169d600133470f49fd699ed3", size = 340467, upload-time = "2026-01-20T20:37:11.794Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/54b06415767f4569882e99b6470c6c8eeb97422686a6d432464f9967fd91/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ae4a98416a440e78f7d9543d11b11cae4bab538b7ed94ec5da5221481748f2", size = 346333, upload-time = "2026-01-20T20:37:12.818Z" }, + { url = "https://files.pythonhosted.org/packages/cb/10/a6bce636b8f95e65dc84bf4a58ce8205b8e0a2a300a38cdbc83a3f763d27/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:971e8c26b90d8ae727e7f2ac3ee23e265971d448b3672882f2eb44828b2b8c3e", size = 470859, upload-time = "2026-01-20T20:37:01.512Z" }, + { url = "https://files.pythonhosted.org/packages/8a/27/84121c51ea72f013f0e03d0886bcdfa96b31c9b83c98300a7bd5cc4fa191/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5cde1fa82804a8f9d2907b7aec2009d440062c63f04abbdb825fce717a5e860", size = 341988, upload-time = "2026-01-20T20:37:22.881Z" }, + { url = "https://files.pythonhosted.org/packages/90/a4/01c1c7af5e6a44f20b40183e8dac37d6ed83e7dc9e8df85370a15959b804/uuid_utils-0.14.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7343862a2359e0bd48a7f3dfb5105877a1728677818bb694d9f40703264a2db", size = 365784, upload-time = "2026-01-20T20:37:10.808Z" }, + { url = "https://files.pythonhosted.org/packages/04/f0/65ee43ec617b8b6b1bf2a5aecd56a069a08cca3d9340c1de86024331bde3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c51e4818fdb08ccec12dc7083a01f49507b4608770a0ab22368001685d59381b", size = 523750, upload-time = "2026-01-20T20:37:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/95/d3/6bf503e3f135a5dfe705a65e6f89f19bccd55ac3fb16cb5d3ec5ba5388b8/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:181bbcccb6f93d80a8504b5bd47b311a1c31395139596edbc47b154b0685b533", size = 615818, upload-time = "2026-01-20T20:37:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/df/6c/99937dd78d07f73bba831c8dc9469dfe4696539eba2fc269ae1b92752f9e/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c8ae96101c3524ba8dbf762b6f05e9e9d896544786c503a727c5bf5cb9af1a7", size = 580831, upload-time = "2026-01-20T20:37:19.691Z" }, + { url = "https://files.pythonhosted.org/packages/44/fa/bbc9e2c25abd09a293b9b097a0d8fc16acd6a92854f0ec080f1ea7ad8bb3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00ac3c6edfdaff7e1eed041f4800ae09a3361287be780d7610a90fdcde9befdc", size = 546333, upload-time = "2026-01-20T20:37:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9b/e5e99b324b1b5f0c62882230455786df0bc66f67eff3b452447e703f45d2/uuid_utils-0.14.0-cp39-abi3-win32.whl", hash = "sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151", size = 177319, upload-time = "2026-01-20T20:37:04.208Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/2c7d417ea483b6ff7820c948678fdf2ac98899dc7e43bb15852faa95acaf/uuid_utils-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1", size = 182566, upload-time = "2026-01-20T20:37:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/49e4bdda28e962fbd7266684171ee29b3d92019116971d58783e51770745/uuid_utils-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080", size = 182809, upload-time = "2026-01-20T20:37:05.139Z" }, + { url = "https://files.pythonhosted.org/packages/f1/03/1f1146e32e94d1f260dfabc81e1649102083303fb4ad549775c943425d9a/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:762e8d67992ac4d2454e24a141a1c82142b5bde10409818c62adbe9924ebc86d", size = 587430, upload-time = "2026-01-20T20:37:24.998Z" }, + { url = "https://files.pythonhosted.org/packages/87/ba/d5a7469362594d885fd9219fe9e851efbe65101d3ef1ef25ea321d7ce841/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:40be5bf0b13aa849d9062abc86c198be6a25ff35316ce0b89fc25f3bac6d525e", size = 298106, upload-time = "2026-01-20T20:37:23.896Z" }, + { url = "https://files.pythonhosted.org/packages/8a/11/3dafb2a5502586f59fd49e93f5802cd5face82921b3a0f3abb5f357cb879/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:191a90a6f3940d1b7322b6e6cceff4dd533c943659e0a15f788674407856a515", size = 333423, upload-time = "2026-01-20T20:37:17.828Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f2/c8987663f0cdcf4d717a36d85b5db2a5589df0a4e129aa10f16f4380ef48/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa4525f4ad82f9d9c842f9a3703f1539c1808affbaec07bb1b842f6b8b96aa5", size = 338659, upload-time = "2026-01-20T20:37:14.286Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c8/929d81665d83f0b2ffaecb8e66c3091a50f62c7cb5b65e678bd75a96684e/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdbd82ff20147461caefc375551595ecf77ebb384e46267f128aca45a0f2cdfc", size = 467029, upload-time = "2026-01-20T20:37:08.277Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a0/27d7daa1bfed7163f4ccaf52d7d2f4ad7bb1002a85b45077938b91ee584f/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff57e8a5d540006ce73cf0841a643d445afe78ba12e75ac53a95ca2924a56be", size = 333298, upload-time = "2026-01-20T20:37:07.271Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/acad86ce012b42ce18a12f31ee2aa3cbeeb98664f865f05f68c882945913/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fd9112ca96978361201e669729784f26c71fecc9c13a7f8a07162c31bd4d1e2", size = 359217, upload-time = "2026-01-20T20:36:59.687Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +] diff --git a/echo/cypress/TEST_DOCUMENTATION.md b/echo/cypress/TEST_DOCUMENTATION.md new file mode 100644 index 00000000..980e2805 --- /dev/null +++ b/echo/cypress/TEST_DOCUMENTATION.md @@ -0,0 +1,324 @@ +# Echo Cypress Test Suite Documentation + +## Overview + +This document describes all automated end-to-end test flows implemented in the Echo Cypress test suite. Each test ensures proper functionality across the application's core features. + +--- + +## Test Suites + +### 01 - Login & Logout Flow +**File:** `01-login-logout.cy.js` + +**Purpose:** Verifies basic authentication functionality. + +**Steps:** +1. Navigate to the application +2. Enter credentials and login +3. Open the settings menu +4. Click logout button +5. Verify redirect to login page + +--- + +### 02 - Multilingual Support Flow +**File:** `02-multilingual.cy.js` + +**Purpose:** Verifies the application's language switching capability. + +**Steps:** +1. Login to the application +2. Open settings menu +3. Change language to Spanish (es-ES) +4. Verify URL contains `/es-ES/` +5. Verify "Projects" header shows "Proyectos" +6. Verify logout button shows "Cerrar sesión" +7. Switch back to English (en-US) +8. Verify content reverts to English +9. Logout + +--- + +### 03 - Create & Delete Project Flow +**File:** `03-create-delete-project.cy.js` + +**Purpose:** Tests basic project creation and immediate deletion. + +**Steps:** +1. Login to the application +2. Click "Create" button to create new project +3. Wait for automatic navigation to project overview +4. Capture project ID from URL +5. Verify project page loads with default name "New Project" +6. Navigate to Project Settings tab +7. Click "Delete Project" button +8. Confirm deletion in modal +9. Verify redirect to projects list +10. Verify project no longer appears in list +11. Logout + +--- + +### 04 - Create, Edit & Delete Project Flow +**File:** `04-create-edit-delete-project.cy.js` + +**Purpose:** Tests comprehensive project lifecycle including editing. + +**Steps:** +1. Login and create new project +2. Update project name (with unique ID) +3. Open Portal Editor +4. Configure portal settings: + - Select tutorial type (Basic) + - Add custom tag + - Update portal title and content + - Change portal language to Italian +5. Navigate back to home +6. Verify updated project name in list +7. Re-enter project and verify: + - Name displays correctly in breadcrumb + - Portal settings persisted (tag, title, language) +8. Delete project +9. Logout + +--- + +### 05 - QR Code Language Change +**File:** `05-qr-code-language.cy.js` + +**Purpose:** Verifies QR code/portal link updates when language changes. + +**Steps:** +1. Login and create new project +2. Click "Copy link" to capture initial portal URL +3. Verify URL contains `/en-US/` (default language) +4. Open Portal Editor +5. Change portal language to Italian (it) +6. Click "Copy link" again +7. Verify new URL contains `/it-IT/` +8. Confirm URLs are different +9. Delete project +10. Logout + +--- + +### 06 - Announcements Feature +**File:** `06-announcements.cy.js` + +**Purpose:** Tests the announcements sidebar functionality. + +**Steps:** +1. Login to the application +2. Click the megaphone icon (Announcements button) +3. Verify announcements sidebar/drawer opens +4. Verify title shows "Announcements" +5. Verify content area exists +6. Click close button +7. Verify sidebar closes +8. Logout + +--- + +### 07 - Upload Conversation Flow +**File:** `07-upload-conversation.cy.js` + +**Purpose:** Tests uploading and processing audio files as conversations. + +**Steps:** +1. Login and create new project +2. Click "Upload" button to open modal +3. Select audio file (`videoplayback.mp3`) +4. Click "Upload Files" button +5. Wait 15 seconds for processing +6. Close upload modal +7. Click on uploaded conversation in list +8. Verify conversation name matches filename +9. Wait 25 seconds for transcript processing +10. Click "Transcript" tab +11. Verify transcript contains at least 100 characters +12. Navigate to project overview +13. Delete project +14. Logout + +--- + +### 08 - Participant Recording Flow +**File:** `08-participant-recording.cy.js` + +**Purpose:** Tests the complete participant portal text response flow, split into single-origin tests for WebKit compatibility. + +**Steps:** +1. Login and create new project +2. Construct portal URL with project ID +3. Open participant portal in a dedicated portal-origin test +4. Accept privacy policy checkbox +5. Click "I understand" button +6. Skip microphone check +7. Enter session name ("Cypress Test Recording") +8. Click "Next" +9. Handle microphone access denied modal (if present) +10. Click Text Response icon +11. Type 150-character test response +12. Click "Submit" +13. Click "Finish" +14. Confirm finish in modal +15. Return to dashboard in a dedicated dashboard-origin test +16. Verify conversation appears with correct name +17. Verify transcript matches submitted text +18. Delete project +19. Logout + +--- + +### 09 - Create Report Flow +**File:** `09-create-report.cy.js` + +**Purpose:** Tests AI report generation from conversations. + +**Steps:** +1. Login and create new project +2. Upload audio file (same as Suite 07) +3. Wait for processing +4. Click "Report" button +5. Click "Create Report" in modal +6. Wait 20 seconds for AI processing +7. Click "Report" button again +8. Verify report elements: + - Dembrane logo visible + - "Dembrane" heading + - "Report" text +9. Navigate to project overview +10. Delete project +11. Logout + +--- + +### 10 - Publish Report Flow +**File:** `10-publish-report.cy.js` + +**Purpose:** Tests publishing reports for public access. + +**Steps:** +1. Login and create new project +2. Upload audio file +3. Create report (same as Suite 09) +4. Open report view +5. Toggle "Publish" switch ON +6. Construct public URL from project ID +7. Visit public URL (cross-origin) +8. Verify public page shows: + - Dembrane logo + - "Dembrane" heading + - "Report" text +9. Return to dashboard +10. Delete project +11. Logout + +--- + +### 11 - Edit Report Flow +**File:** `11-edit-report.cy.js` + +**Purpose:** Tests in-place report editing functionality. + +**Steps:** +1. Login and create new project +2. Upload audio file +3. Create report +4. Open report view +5. Toggle "Editing mode" ON +6. Clear existing content in MDX editor +7. Type new content: + - Heading: "Automated Edit Verification" + - Paragraph: "This is a test edit from Cypress." +8. Toggle "Editing mode" OFF +9. Verify new content persists: + - H1 heading visible + - Paragraph text visible +10. Navigate to project +11. Delete project +12. Logout + +--- + +### 12 - Ask Feature (With Context) +**File:** `12-ask-feature.cy.js` + +**Purpose:** Tests the AI Ask feature with conversation context selected. + +**Steps:** +1. Login and create new project +2. Upload audio file +3. Wait for processing +4. Click "Ask" button +5. Select uploaded conversation as context (checkbox) +6. Type query "hello" +7. Submit and wait for AI response +8. Verify response appears +9. Navigate to project overview +10. Delete project +11. Logout + +--- + +### 13 - Ask Feature (No Context) +**File:** `13-ask-no-context.cy.js` + +**Purpose:** Tests the AI Ask feature without manually selecting context. + +**Steps:** +1. Login and create new project +2. Upload audio file +3. Wait for processing +4. Click "Ask" button +5. Type query "hello" (without selecting conversations) +6. Submit and wait for AI response +7. Verify response appears +8. Navigate to project overview +9. Delete project +10. Logout + +--- + +## Running Tests + +### Single Test +```powershell +npx cypress run --spec "e2e/suites/01-login-logout.cy.js" --env version=staging --browser chrome +``` + +### All Tests with HTML Report +```powershell +.\run-viewport-tests.ps1 # Mobile, Tablet, Desktop viewports +.\run-browser-tests.ps1 # Chrome, Firefox, Edge, WebKit browsers +``` + +### Safari (WebKit Experimental) +```powershell +npx cypress run --spec "e2e/suites/01-login-logout.cy.js" --env version=staging --browser webkit +``` + +Notes: +- WebKit support in Cypress is experimental. +- `cy.origin()` is not supported in WebKit; cross-origin flows will fail. +- On Linux, install WebKit system dependencies with `npx playwright install-deps webkit`. + +### Reports +HTML reports are generated at: `cypress/reports/test-report.html` + +--- + +## Helper Functions + +| Module | Functions | +|--------|-----------| +| `login` | `loginToApp()`, `logout()` | +| `settings` | `openSettingsMenu()`, `changeLanguage()`, `verifyLanguage()` | +| `project` | `createProject()`, `deleteProject()`, `updateProjectName()`, `navigateToHome()` | +| `portal` | `openPortalEditor()`, `selectTutorial()`, `addTag()`, `updatePortalContent()`, `changePortalLanguage()` | +| `conversation` | `openUploadModal()`, `uploadAudioFile()`, `selectConversation()`, `clickTranscriptTab()` | +| `chat` | `askWithContext()`, `askWithoutContext()` | + +--- + diff --git a/echo/cypress/assets/sampleaudio.mp3 b/echo/cypress/assets/sampleaudio.mp3 new file mode 100644 index 00000000..4829e826 Binary files /dev/null and b/echo/cypress/assets/sampleaudio.mp3 differ diff --git a/echo/cypress/assets/videoplayback.mp3 b/echo/cypress/assets/videoplayback.mp3 new file mode 100644 index 00000000..4829e826 Binary files /dev/null and b/echo/cypress/assets/videoplayback.mp3 differ diff --git a/echo/cypress/cypress.config.js b/echo/cypress/cypress.config.js new file mode 100644 index 00000000..c4c5ca05 --- /dev/null +++ b/echo/cypress/cypress.config.js @@ -0,0 +1,155 @@ +const { defineConfig } = require("cypress"); +const fs = require("fs"); +const path = require("path"); + +module.exports = defineConfig({ + e2e: { + defaultCommandTimeout: 10000, + // Enable experimental features for cross-origin testing + experimentalModifyObstructiveThirdPartyCode: true, + fixturesFolder: "fixtures", + // Mochawesome reporter for HTML test reports + reporter: "mochawesome", + reporterOptions: { + html: false, + json: true, + overwrite: false, + reportDir: process.env.CYPRESS_MOCHAWESOME_REPORT_DIR || "reports", + timestamp: "mmddyyyy_HHMMss", + }, + setupNodeEvents(on, config) { + on("task", { + deleteFile(filePath) { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + return true; + } + return null; + }, + findFile({ dir, ext }) { + if (!fs.existsSync(dir)) return null; + const files = fs.readdirSync(dir); + const foundFiles = files.filter((file) => file.endsWith(ext)); + if (foundFiles.length === 0) return null; + + // Return the most recently modified file + const recentFile = foundFiles + .map((file) => { + const filePath = path.join(dir, file); + return { file, mtime: fs.statSync(filePath).mtime }; + }) + .sort((a, b) => b.mtime - a.mtime)[0].file; + + return path.join(dir, recentFile); + }, + findFileByPattern({ dir, startsWith = "", endsWith = "" }) { + if (!fs.existsSync(dir)) return null; + const files = fs.readdirSync(dir); + const foundFiles = files.filter( + (file) => file.startsWith(startsWith) && file.endsWith(endsWith), + ); + if (foundFiles.length === 0) return null; + + const recentFile = foundFiles + .map((file) => { + const filePath = path.join(dir, file); + return { file, mtime: fs.statSync(filePath).mtime }; + }) + .sort((a, b) => b.mtime - a.mtime)[0].file; + + return path.join(dir, recentFile); + }, + log(message) { + console.log(message); + return null; + }, + }); + + // Add browser launch arguments for fake media devices (cross-browser support) + on("before:browser:launch", (browser = {}, launchOptions) => { + // Chromium-based browsers (Chrome, Edge, Electron) + if (browser.family === "chromium" || browser.name === "chrome") { + // Use fake media devices for microphone/camera testing + launchOptions.args.push("--use-fake-device-for-media-stream"); + launchOptions.args.push("--use-fake-ui-for-media-stream"); + // Auto-accept permission prompts + launchOptions.args.push( + "--disable-features=WebRtcHideLocalIpsWithMdns", + ); + + // Use a specific file for fake audio capture (fake microphone input) + // Note: This requires a .wav file. + launchOptions.args.push( + "--use-file-for-fake-audio-capture=c:/Users/charu/OneDrive/Desktop/echo/echo/cypress/fixtures/test-audio.wav", + ); + + // Grant clipboard permissions + // Ensure preferences object exists + if (!launchOptions.preferences) { + launchOptions.preferences = {}; + } + + launchOptions.preferences.default = { + profile: { + content_settings: { + exceptions: { + clipboard: { + "*": { setting: 1 }, + }, + }, + }, + }, + }; + } + + // Firefox + if (browser.family === "firefox") { + // Firefox uses preferences instead of command line args + launchOptions.preferences["media.navigator.permission.disabled"] = + true; + launchOptions.preferences["media.navigator.streams.fake"] = true; + launchOptions.preferences["dom.events.asyncClipboard.readText"] = + true; + launchOptions.preferences["dom.events.testing.asyncClipboard"] = true; + } + + return launchOptions; + }); + + // Cypress automatically loads cypress.env.json into config.env + // We expect config.env to look like { staging: { ... }, prod: { ... } } + + const version = config.env.version || "staging"; + const envConfig = config.env[version]; + + if (!envConfig) { + throw new Error( + `Unknown environment version: ${version}. Check cypress.env.json.`, + ); + } + + // Set baseUrl to the dashboardUrl by default + config.baseUrl = envConfig.dashboardUrl; + + // Merge the specific environment config to the top level of config.env + // So in tests we can do Cypress.env('auth') or Cypress.env('portalUrl') directly + config.env = { + ...config.env, + ...envConfig, + }; + + return config; + }, + specPattern: "e2e/suites/**/*.cy.{js,jsx,ts,tsx}", + supportFile: "support/e2e.js", + viewportHeight: process.env.CYPRESS_viewportHeight + ? Number.parseInt(process.env.CYPRESS_viewportHeight) + : 720, + // viewportWidth and viewportHeight are set via CLI --config flag + // Default fallbacks if not provided via CLI + viewportWidth: process.env.CYPRESS_viewportWidth + ? Number.parseInt(process.env.CYPRESS_viewportWidth) + : 1280, + }, + experimentalWebKitSupport: true, +}); diff --git a/echo/cypress/cypress.env.json b/echo/cypress/cypress.env.json new file mode 100644 index 00000000..e2b5ffe7 --- /dev/null +++ b/echo/cypress/cypress.env.json @@ -0,0 +1,40 @@ +{ + "viewports": { + "mobile": { + "width": 375, + "height": 667 + }, + "tablet": { + "width": 768, + "height": 1024 + }, + "desktop": { + "width": 1440, + "height": 900 + } + }, + "staging": { + "dashboardUrl": "https://dashboard.echo-next.dembrane.com/", + "portalUrl": "https://portal.echo-next.dembrane.com/", + "auth": { + "email": "charugundla.vipul6009@gmail.com", + "password": "test@1234" + } + }, + "prod": { + "dashboardUrl": "https://dashboard.echo.dembrane.com/", + "portalUrl": "https://portal.echo.dembrane.com/", + "auth": { + "email": "charugundla.vipul6009@gmail.com", + "password": "test@1234" + } + }, + "testing": { + "dashboardUrl": "https://test.echo.dembrane.com/", + "portalUrl": "https://test.portal.echo.dembrane.com/", + "auth": { + "email": "charugundla.vipul6009@gmail.com", + "password": "test@1234" + } + } +} \ No newline at end of file diff --git a/echo/cypress/cypress/downloads/merged-c73ccb37-8d3b-42e0-a51e-3edf8e20469b-eaa71516-0fcf-49e5-b9a7-3d45f4b5c4a6.mp3 b/echo/cypress/cypress/downloads/merged-c73ccb37-8d3b-42e0-a51e-3edf8e20469b-eaa71516-0fcf-49e5-b9a7-3d45f4b5c4a6.mp3 new file mode 100644 index 00000000..0723154d Binary files /dev/null and b/echo/cypress/cypress/downloads/merged-c73ccb37-8d3b-42e0-a51e-3edf8e20469b-eaa71516-0fcf-49e5-b9a7-3d45f4b5c4a6.mp3 differ diff --git a/echo/cypress/cypress/downloads/transcript-1771916490192 b/echo/cypress/cypress/downloads/transcript-1771916490192 new file mode 100644 index 00000000..462f87a0 --- /dev/null +++ b/echo/cypress/cypress/downloads/transcript-1771916490192 @@ -0,0 +1 @@ +Hey, everybody. My name is Chris Nash. It's a new year, which means it's time for a new podcast. Now, unlike all of your other favorite podcasts, this one will only take one minute of your time. Every time, all the time. Sometimes it'll be just me. Other times it'll be just you. Seriously, submit stuff, your opinions, videos. I want to know what you have to say. I want to share what you have to say. Other times, I'll be joined by excellent guests like Cameron Hart of the Tournamental Podcast. Hey, Nash, that one minute podcast idea. Don't do it. It's a terrible idea. Swell. Some episodes will be really funny. Other episodes will be really serious. But I guarantee that every episode will be the best minute of your day. Warning. Depending on your preferences, one minute podcast may be the worst minute of your day. \ No newline at end of file diff --git a/echo/cypress/cypress/screenshots/04-create-edit-delete-project.cy.js/Project Create, Edit, and Delete Flow -- should create a project, edit its name and portal settings, verify changes, and delete it (failed).png b/echo/cypress/cypress/screenshots/04-create-edit-delete-project.cy.js/Project Create, Edit, and Delete Flow -- should create a project, edit its name and portal settings, verify changes, and delete it (failed).png new file mode 100644 index 00000000..d6c065b8 Binary files /dev/null and b/echo/cypress/cypress/screenshots/04-create-edit-delete-project.cy.js/Project Create, Edit, and Delete Flow -- should create a project, edit its name and portal settings, verify changes, and delete it (failed).png differ diff --git a/echo/cypress/cypress/screenshots/30-report-lifecycle.cy.js/Report Lifecycle Flow -- creates a project and generates a report draft (failed).png b/echo/cypress/cypress/screenshots/30-report-lifecycle.cy.js/Report Lifecycle Flow -- creates a project and generates a report draft (failed).png new file mode 100644 index 00000000..9f51dd2e Binary files /dev/null and b/echo/cypress/cypress/screenshots/30-report-lifecycle.cy.js/Report Lifecycle Flow -- creates a project and generates a report draft (failed).png differ diff --git a/echo/cypress/e2e/suites/01-login-logout.cy.js b/echo/cypress/e2e/suites/01-login-logout.cy.js new file mode 100644 index 00000000..bbb61c24 --- /dev/null +++ b/echo/cypress/e2e/suites/01-login-logout.cy.js @@ -0,0 +1,18 @@ +import { loginToApp, logout } from '../../support/functions/login'; +import { openSettingsMenu } from '../../support/functions/settings'; + +describe('Login & Logout Flow', () => { + + it('should successfully login and logout', () => { + // 1. Perform Login + loginToApp(); + + // 2. Open Settings Menu (to access logout) + openSettingsMenu(); + + // 3. Perform Logout + logout(); + }); + +}); + diff --git a/echo/cypress/e2e/suites/02-multilingual.cy.js b/echo/cypress/e2e/suites/02-multilingual.cy.js new file mode 100644 index 00000000..c33aa182 --- /dev/null +++ b/echo/cypress/e2e/suites/02-multilingual.cy.js @@ -0,0 +1,51 @@ +import { loginToApp, logout } from "../../support/functions/login"; +import { + changeLanguage, + openSettingsMenu, + verifyLanguage, +} from "../../support/functions/settings"; + +describe("Multilingual Support Flow", () => { + beforeEach(() => { + // dynamic viewport + // login before each test is fine, or preserve cookies. + // For this flow, a fresh login ensures clean state. + loginToApp(); + }); + + it("should successfully switch languages and translate content", () => { + // 1. Open Settings Menu + openSettingsMenu(); + + // 2. Switch to Spanish (Español) + // Value identified from browser inspection: 'es-ES' + changeLanguage("es-ES"); + + // 3. Verify Changes + // URL should contain /es-ES/ + // Logout button should say "Cerrar sesión" + verifyLanguage("Cerrar sesión", "es-ES"); + + // 4. Verification Check: Page Header + // Heading level can vary by UI version (h2/h3), so assert by visible heading text. + cy.contains("h1, h2, h3", /^Proyectos$/).should("be.visible"); + + // 5. Switch back to English (Cleanup) + // Ensure menu is open (verifyLanguage ensures it's open, but let's be safe) + cy.get("body").then(($body) => { + if ($body.find('[data-testid="header-language-picker"]').length === 0) { + openSettingsMenu(); + } + }); + + changeLanguage("en-US"); + + // 6. Verify back to English + verifyLanguage("Logout", "en-US"); + cy.contains("h1, h2, h3", /^Projects$/).should("be.visible"); + + // 7. Logout + // The menu should be open from the previous step (verifyLanguage ensures it). + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/03-create-delete-project.cy.js b/echo/cypress/e2e/suites/03-create-delete-project.cy.js new file mode 100644 index 00000000..613301e9 --- /dev/null +++ b/echo/cypress/e2e/suites/03-create-delete-project.cy.js @@ -0,0 +1,40 @@ +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, verifyProjectPage, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; + +describe('Project Creation and Deletion Flow', () => { + beforeEach(() => { + loginToApp(); + }); + + it('should create a project and then immediately delete it', () => { + let createdProjectId; + + // 1. Create Project + createProject(); + + // Capture the ID from the current URL to pass to delete function + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + createdProjectId = parts[projectIndex + 1]; + cy.log(`Captured ID for deletion: ${createdProjectId}`); + + // 2. Verify Project Page (Optional here, but good practice) + verifyProjectPage('New Project'); + + // 3. Delete Project + // This function handles navigation to settings, deletion, and verification + deleteProject(createdProjectId); + } else { + throw new Error('Could not capture Project ID from URL'); + } + }); + + // 4. Logout (from the Projects Dashboard) + // Ensure settings menu is open first + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/04-create-edit-delete-project.cy.js b/echo/cypress/e2e/suites/04-create-edit-delete-project.cy.js new file mode 100644 index 00000000..8addd7f2 --- /dev/null +++ b/echo/cypress/e2e/suites/04-create-edit-delete-project.cy.js @@ -0,0 +1,80 @@ +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, verifyProjectPage, deleteProject, updateProjectName, navigateToHome } from '../../support/functions/project'; +import { openPortalEditor, selectTutorial, addTag, updatePortalContent, changePortalLanguage, toggleAskForName, toggleAskForEmail } from '../../support/functions/portal'; +import { openSettingsMenu } from '../../support/functions/settings'; + +describe('Project Create, Edit, and Delete Flow', () => { + beforeEach(() => { + loginToApp(); + }); + + it('should create a project, edit its name and portal settings, verify changes, and delete it', () => { + const uniqueId = Cypress._.random(0, 10000); + const newProjectName = `New Project_${uniqueId}`; + const portalTitle = `Title_${uniqueId}`; + const portalContent = `Content_${uniqueId}`; + const thankYouContent = `ThankYou_${uniqueId}`; + const tagName = `Tag_${uniqueId}`; + const portalLanguage = 'it'; // Italian + + // 1. Create Project + createProject(); + + let createdProjectId; + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + createdProjectId = parts[projectIndex + 1]; + cy.log(`Working with Project ID: ${createdProjectId}`); + + // 2. Edit Project Name + updateProjectName(newProjectName); + + // 3. Edit Portal Settings + openPortalEditor(); + toggleAskForName(true); + toggleAskForEmail(true); + selectTutorial('Advanced'); + addTag(tagName); + updatePortalContent(portalTitle, portalContent, thankYouContent); + changePortalLanguage(portalLanguage); + + // 4. Return to Home and Verify Name in List + navigateToHome(); + cy.wait(2000); // Wait for list reload + + // Check if the project list contains the new name + // Target the main content area (not the mobile sidebar) using the visible desktop sidebar + cy.get('main').within(() => { + cy.get(`a[href*="${createdProjectId}"]`).first().should('contain.text', newProjectName); + }); + + // 5. Enter Project and Verify Changes + cy.get('main').within(() => { + cy.get(`a[href*="${createdProjectId}"]`).first().click(); + }); + cy.wait(3000); // Wait for dashboard load + + // Check Name on Dashboard - verify in the breadcrumb title + cy.get('[data-testid="project-breadcrumb-name"]').should('contain.text', newProjectName); + + // Check Portal Settings Persistence + openPortalEditor(); + // Verify Tag - inside mantine-Badge-label span + cy.get('.mantine-Badge-label').contains(tagName).should('be.visible'); + // Verify Title Input Value + cy.get('[data-testid="portal-editor-page-title-input"]').should('have.value', portalTitle); + // Verify Language is set to Italian + cy.get('[data-testid="portal-editor-language-select"]').should('have.value', portalLanguage); + + // 6. Delete Project + deleteProject(createdProjectId); + } + }); + + // 7. Logout + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/05-qr-code-language.cy.js b/echo/cypress/e2e/suites/05-qr-code-language.cy.js new file mode 100644 index 00000000..32666a69 --- /dev/null +++ b/echo/cypress/e2e/suites/05-qr-code-language.cy.js @@ -0,0 +1,124 @@ +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openPortalEditor, changePortalLanguage } from '../../support/functions/portal'; +import { openSettingsMenu } from '../../support/functions/settings'; + +/** + * Helper to click a button that may have duplicate elements (mobile/desktop) + * Iterates through matching elements to find the first one that's actually visible + */ + +/** + * Helper to click the copy link button handling potential multiple elements (mobile/desktop) + */ +const clickVisibleCopyLinkButton = () => { + cy.get('[data-testid="project-copy-link-button"]').then($buttons => { + // Find the first button that is visible (not hidden by CSS) + const $visibleButton = $buttons.filter((index, el) => { + return Cypress.$(el).is(':visible'); + }); + + if ($visibleButton.length > 0) { + cy.wrap($visibleButton.first()).click(); + } else { + // Fallback: click the first button if none are visible + cy.wrap($buttons.first()).click({ force: true }); + } + }); +}; + +describe('QR Code Language Change Test', () => { + beforeEach(() => { + loginToApp(); + }); + + it('should verify QR code link changes when portal language is changed', () => { + let createdProjectId; + let initialLink; + let updatedLink; + + // 1. Create Project + createProject(); + + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + createdProjectId = parts[projectIndex + 1]; + cy.log(`Working with Project ID: ${createdProjectId}`); + + // 2. Copy the initial QR code link + clickVisibleCopyLinkButton(); + + // Wait for copy action + cy.wait(1000); + + // Store the current URL pattern (language should be default/English) + cy.window().then((win) => { + // Try to read from clipboard + return win.navigator.clipboard.readText().then((text) => { + initialLink = text; + cy.log(`Initial Link: ${initialLink}`); + }).catch(() => { + // Fallback: construct the expected URL pattern + const baseUrl = Cypress.env('portalUrl') || 'https://portal.echo-next.dembrane.com'; + initialLink = `${baseUrl}/en-US/${createdProjectId}/start`; + cy.log(`Constructed Initial Link: ${initialLink}`); + }); + }); + + // 3. Open Portal Editor and change language to Italian + openPortalEditor(); + changePortalLanguage('it'); + + // 4. The QR code is always visible at the top of the page + // After language change, just wait for auto-save and copy the updated link + cy.wait(2000); + + // 5. Copy the updated QR code link + clickVisibleCopyLinkButton(); + + cy.wait(1000); + + cy.window().then((win) => { + return win.navigator.clipboard.readText().then((text) => { + updatedLink = text; + cy.log(`Updated Link: ${updatedLink}`); + }).catch(() => { + // Fallback: construct with Italian language (it-IT format) + const baseUrl = Cypress.env('portalUrl') || 'https://portal.echo-next.dembrane.com'; + updatedLink = `${baseUrl}/it-IT/${createdProjectId}/start`; + cy.log(`Constructed Updated Link: ${updatedLink}`); + }); + }).then(() => { + // 6. Verify the links are different + cy.log(`Comparing links:`); + cy.log(`Initial: ${initialLink}`); + cy.log(`Updated: ${updatedLink}`); + + // Assert links are different + expect(updatedLink).to.not.equal(initialLink, + 'Portal link should change when language is changed to Italian'); + + // Additional check: Italian link should contain 'it-IT' language code + expect(updatedLink).to.include('/it-IT/', + 'Italian portal link should contain /it-IT/ in the URL'); + }); + + // 7. Click Project Settings tab first (scrollIntoView + force to handle clipped content) + cy.get('[data-testid="project-overview-tab-overview"]') + .first() + .scrollIntoView() + .click({ force: true }); + cy.wait(2000); + + // 8. Delete Project + deleteProject(createdProjectId); + } + }); + + // 8. Logout + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/06-announcements.cy.js b/echo/cypress/e2e/suites/06-announcements.cy.js new file mode 100644 index 00000000..5f071d4b --- /dev/null +++ b/echo/cypress/e2e/suites/06-announcements.cy.js @@ -0,0 +1,48 @@ +import { loginToApp, logout } from '../../support/functions/login'; +import { openSettingsMenu } from '../../support/functions/settings'; + +describe('Announcements Feature Test', () => { + beforeEach(() => { + loginToApp(); + }); + + it('should open and close the announcements sidebar', () => { + // 1. Click on the announcements icon button + cy.log('Clicking Announcements button'); + cy.wait(2000); // Wait for header controls to stabilize + cy.get('[data-testid="announcement-icon-button"]') + .filter(':visible') + .first() + .should('be.visible') + .click(); + + // 2. Verify the Announcements sidebar/drawer opens + cy.log('Verifying Announcements sidebar is open'); + cy.get('[data-testid="announcement-drawer"]').should('be.visible'); + + // 3. Verify the title is "Announcements" + cy.xpath('//h2[contains(@class, "mantine-Drawer-title")]') + .should('be.visible') + .and('contain.text', 'Announcements'); + + // 4. Verify the content area exists (may show "No announcements available" if empty) + cy.xpath('//section[@role="dialog"]//p') + .should('exist'); + + // 5. Close the sidebar by clicking the close button + cy.log('Closing Announcements sidebar'); + cy.get('[data-testid="announcement-close-drawer-button"]') + .should('be.visible') + .click(); + + + + cy.log('Announcements sidebar test completed successfully'); + }); + + afterEach(() => { + // Logout + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/07-upload-conversation.cy.js b/echo/cypress/e2e/suites/07-upload-conversation.cy.js new file mode 100644 index 00000000..112940bd --- /dev/null +++ b/echo/cypress/e2e/suites/07-upload-conversation.cy.js @@ -0,0 +1,107 @@ +/** + * Upload Conversation Flow Test Suite + * + * This test verifies the complete flow of: + * 1. Login and create a new project + * 2. Upload an audio file via the upload conversation modal + * 3. Wait for processing and close the modal + * 4. Click on the uploaded conversation and verify its name + * 5. Verify transcript text + * 6. Navigate to project overview and delete project + * 7. Logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + selectConversation, + verifyConversationName, + clickTranscriptTab, + verifyTranscriptText, + navigateToProjectOverview +} from '../../support/functions/conversation'; + +describe('Upload Conversation Flow', () => { + let projectId; + + beforeEach(() => { + loginToApp(); + }); + + it('should upload audio file, verify conversation, delete project, and logout', () => { + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + // 2. Open Upload Conversation modal + cy.log('Step 2: Opening upload modal'); + openUploadModal(); + + // 3. Upload the audio file from cypress assets + cy.log('Step 3: Uploading audio file'); + uploadAudioFile('assets/videoplayback.mp3'); + + // 4. Click Upload Files button to start the upload + cy.log('Step 4: Clicking Upload Files button'); + clickUploadFilesButton(); + + // 5. Wait 15 seconds for processing + cy.log('Step 5: Waiting 15 seconds for file processing'); + cy.wait(15000); + + // 6. Close the upload modal + cy.log('Step 6: Closing upload modal'); + closeUploadModal(); + + // 7. Click on the uploaded conversation in the list + cy.log('Step 7: Selecting uploaded conversation'); + selectConversation('videoplayback.mp3'); + + // 8. Verify the conversation name in Edit Conversation section + cy.log('Step 8: Verifying conversation name'); + verifyConversationName('videoplayback.mp3'); + + // 9. Wait 25 seconds for transcript processing + cy.log('Step 9: Waiting 25 seconds for transcript processing'); + cy.wait(25000); + + // 10. Click on Transcript tab + cy.log('Step 10: Clicking Transcript tab'); + clickTranscriptTab(); + + // 11. Verify transcript text has at least 100 characters + cy.log('Step 11: Verifying transcript text'); + verifyTranscriptText(400); + + // 12. Navigate back to project overview via breadcrumb + cy.log('Step 12: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 13. Delete the project (includes clicking Project Settings tab) + cy.log('Step 13: Deleting project'); + cy.then(() => { + deleteProject(projectId); + }); + + // 14. Open Settings menu and Logout + cy.log('Step 14: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); + diff --git a/echo/cypress/e2e/suites/08-participant-recording.cy.js b/echo/cypress/e2e/suites/08-participant-recording.cy.js new file mode 100644 index 00000000..ac694166 --- /dev/null +++ b/echo/cypress/e2e/suites/08-participant-recording.cy.js @@ -0,0 +1,188 @@ +/** + * Participant Recording Flow Test Suite + * + * Split into single-origin tests so it runs in Chromium/Firefox/WebKit + * without relying on cy.origin(). + */ + +import { + clickTranscriptTab, + navigateToProjectOverview, + selectConversation, + verifyConversationName, +} from "../../support/functions/conversation"; +import { loginToApp, logout } from "../../support/functions/login"; +import { + agreeToPrivacyPolicy, + confirmFinishText, + enterSessionName, + finishTextMode, + handleMicrophoneAccessDenied, + skipMicrophoneCheck, + submitText, + switchToTextMode, + typePortalText, +} from "../../support/functions/participant"; +import { createProject, deleteProject } from "../../support/functions/project"; +import { openSettingsMenu } from "../../support/functions/settings"; + +describe("Participant Recording Flow", () => { + let projectId; + const portalLocale = "en-US"; + const sessionName = "Cypress Test Recording"; + const textResponse = + "This is a 150 character automated response generated by Cypress to test the text submission flow. " + .repeat(2) + .substring(0, 150); + const portalBaseUrl = ( + Cypress.env("portalUrl") || "https://portal.echo-next.dembrane.com" + ).replace(/\/$/, ""); + const dashboardBaseUrl = ( + Cypress.env("dashboardUrl") || "https://dashboard.echo-next.dembrane.com" + ).replace(/\/$/, ""); + + const registerExceptionHandling = () => { + cy.on("uncaught:exception", (err) => { + if ( + err.message.includes("Syntax error, unrecognized expression") || + err.message.includes("BODY[style=") || + err.message.includes("ResizeObserver loop limit exceeded") + ) { + return false; + } + return true; + }); + }; + + const resolveProjectId = () => { + return cy + .then(() => { + if (!projectId) { + projectId = Cypress.env("participantRecordingProjectId"); + } + + if (projectId) { + return projectId; + } + + return cy + .readFile("fixtures/createdProjects.json", { log: false }) + .then((projects) => { + const lastProject = Array.isArray(projects) + ? projects[projects.length - 1] + : null; + if (!lastProject || !lastProject.id) { + throw new Error( + "projectId not found. Ensure the create step completed successfully.", + ); + } + projectId = lastProject.id; + Cypress.env("participantRecordingProjectId", projectId); + return projectId; + }); + }) + .then((id) => { + expect(id, "projectId").to.be.a("string").and.not.be.empty; + return id; + }); + }; + + const captureProjectIdFromUrl = () => { + cy.url().then((url) => { + const parts = url.split("/"); + const projectIndex = parts.indexOf("projects"); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + Cypress.env("participantRecordingProjectId", projectId); + cy.log("Captured Project ID:", projectId); + } + }); + }; + + const getPortalUrl = () => + `${portalBaseUrl}/${portalLocale}/${projectId}/start`; + + it("Step 1: creates a project for participant recording", () => { + registerExceptionHandling(); + loginToApp(); + + cy.log("Step 1: Creating new project"); + createProject(); + captureProjectIdFromUrl(); + resolveProjectId(); + }); + + it("Step 2: submits a participant text response in the portal", () => { + registerExceptionHandling(); + + resolveProjectId().then(() => { + cy.log("Step 2: Opening participant portal"); + cy.visit(getPortalUrl()); + }); + + cy.log("Step 3: Agreeing to privacy policy"); + agreeToPrivacyPolicy(); + + cy.log("Step 4: Skipping microphone check"); + skipMicrophoneCheck(); + + cy.log("Step 5: Entering session name"); + enterSessionName(sessionName); + + cy.log("Step 6: Handling microphone access modal if present"); + handleMicrophoneAccessDenied(); + + cy.log("Step 7: Switching to text response"); + switchToTextMode(); + cy.wait(1000); + + cy.log("Step 8: Typing and submitting text response"); + typePortalText(textResponse); + submitText(); + cy.wait(2000); + + cy.log("Step 9: Finishing conversation"); + finishTextMode(); + confirmFinishText(); + cy.wait(2000); + }); + + it("should complete participant recording flow and verify conversation", () => { + registerExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + cy.log("Step 10: Returning to dashboard"); + cy.visit(`${dashboardBaseUrl}/${portalLocale}/projects/${id}/overview`); + }); + + cy.log("Step 11: Verifying conversation and transcript"); + cy.wait(5000); + selectConversation(sessionName); + verifyConversationName(sessionName); + + cy.log("Waiting 10 seconds for transcript processing"); + cy.wait(10000); + clickTranscriptTab(); + + cy.log("Verifying transcript text matches exact response"); + cy.xpath( + '//div[contains(@class, "mantine-Paper-root")]//div[contains(@style, "flex")]//div/p[contains(@class, "mantine-Text-root")]', + ).each(($el) => { + if ($el.text().length > 50) { + expect($el.text().trim()).to.equal(textResponse.trim()); + } + }); + + cy.log("Step 12: Navigating back and Deleting project"); + navigateToProjectOverview(); + resolveProjectId().then((id) => { + deleteProject(id); + Cypress.env("participantRecordingProjectId", null); + }); + + cy.log("Step 13: Logging out"); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/09-create-report.cy.js b/echo/cypress/e2e/suites/09-create-report.cy.js new file mode 100644 index 00000000..8c4ac8bb --- /dev/null +++ b/echo/cypress/e2e/suites/09-create-report.cy.js @@ -0,0 +1,118 @@ +/** + * Report Creation Flow Test Suite + * + * This test verifies the flow of creating a report from an uploaded conversation: + * 1. Login and create a new project + * 2. Upload an audio file (replicating Suite 08 flow) + * 3. Click Report button and Create Report in the modal + * 4. Wait for processing (40s) + * 5. Re-open Report to verify generation + * 6. Cleanup (delete project and logout) + */ + +import { + clickUploadFilesButton, + closeUploadModal, + navigateToProjectOverview, + openUploadModal, + uploadAudioFile, +} from "../../support/functions/conversation"; +import { loginToApp, logout } from "../../support/functions/login"; +import { createProject, deleteProject } from "../../support/functions/project"; +import { verifyReportRendered } from "../../support/functions/report"; +import { openSettingsMenu } from "../../support/functions/settings"; + +describe("Report Creation Flow", () => { + let projectId; + + beforeEach(() => { + loginToApp(); + }); + + it("should upload audio, create report, and verify report existence", () => { + // 1. Create new project + cy.log("Step 1: Creating new project"); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split("/"); + const projectIndex = parts.indexOf("projects"); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log("Captured Project ID:", projectId); + } + }); + + // 2. Open Upload Conversation modal + cy.log("Step 2: Opening upload modal"); + openUploadModal(); + + // 3. Upload the audio file from cypress assets + cy.log("Step 3: Uploading audio file"); + uploadAudioFile("assets/videoplayback.mp3"); + + // 4. Click Upload Files button to start the upload + cy.log("Step 4: Clicking Upload Files button"); + clickUploadFilesButton(); + + // 5. Wait 15 seconds for processing + cy.log("Step 5: Waiting 20 seconds for file processing"); + cy.wait(20000); + + // 6. Close the upload modal + cy.log("Step 6: Closing upload modal"); + closeUploadModal(); + + // 7. Click on the Report button + cy.log("Step 7: Clicking Report button"); + cy.get('[data-testid="sidebar-report-button"]') + .filter(":visible") + .first() + .should("be.visible") + .click(); + + // 8. Click Create Report in the modal + cy.log("Step 8: Clicking Create Report in modal"); + // Wait for modal and click the "Create Report" button (filled variant) + cy.get('section[role="dialog"]').should("be.visible"); + cy.get('[data-testid="report-create-button"]') + .filter(":visible") + .first() + .should("be.visible") + .click(); + + // 9. Wait 40 seconds for processing + cy.log("Step 9: Waiting 40 seconds for report processing"); + cy.wait(40000); + + // 10. Click on the Report button again to view report + cy.log("Step 10: Clicking Report button again"); + cy.get('[data-testid="sidebar-report-button"]') + .filter(":visible") + .first() + .should("be.visible") + .click(); + cy.wait(5000); // Wait for report content to load + + // 11. Verify report existence + cy.log("Step 11: Verifying report existence"); + verifyReportRendered(); + cy.log("Report successfully verified"); + + // 12. Navigate back to Project Overview + cy.log("Step 12: Navigating to Project Overview"); + navigateToProjectOverview(); + + // 13. Delete the project + cy.log("Step 13: Deleting project"); + cy.then(() => { + deleteProject(projectId); + }); + + // 14. Open Settings menu and Logout + cy.log("Step 14: Opening settings and logging out"); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/10-publish-report.cy.js b/echo/cypress/e2e/suites/10-publish-report.cy.js new file mode 100644 index 00000000..caaebeb0 --- /dev/null +++ b/echo/cypress/e2e/suites/10-publish-report.cy.js @@ -0,0 +1,157 @@ +/** + * Publish Report Flow Test Suite + * + * Split into single-origin tests so it runs in Chromium/Firefox/WebKit + * without relying on cy.origin(). + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal +} from '../../support/functions/conversation'; +import { + registerReportFlowExceptionHandling, + setReportPublishState, + waitForPublicReportPublished +} from '../../support/functions/report'; + +describe('Publish Report Flow', () => { + let projectId; + let locale = 'en-US'; + + const portalBaseUrl = (Cypress.env('portalUrl') || 'https://portal.echo-next.dembrane.com').replace(/\/$/, ''); + const dashboardBaseUrl = (Cypress.env('dashboardUrl') || '').replace(/\/$/, ''); + + const resolveProjectId = () => { + return cy.then(() => { + if (!projectId) { + projectId = Cypress.env('publishReportProjectId'); + } + + if (projectId) { + return projectId; + } + + return cy.readFile('fixtures/createdProjects.json', { log: false }).then((projects) => { + const lastProject = Array.isArray(projects) ? projects[projects.length - 1] : null; + if (!lastProject || !lastProject.id) { + throw new Error('projectId not found. Ensure report setup test completed.'); + } + projectId = lastProject.id; + Cypress.env('publishReportProjectId', projectId); + return projectId; + }); + }).then((id) => { + expect(id, 'projectId').to.be.a('string').and.not.be.empty; + return id; + }); + }; + + const openDashboardReportPage = (id) => { + if (dashboardBaseUrl) { + cy.visit(`${dashboardBaseUrl}/projects/${id}/report`); + return; + } + + cy.visit(`/${locale}/projects/${id}/report`); + }; + + const openPublicReportPage = (id) => { + cy.visit(`${portalBaseUrl}/${locale}/${id}/report`); + }; + + it('creates a project and generates a report draft', () => { + registerReportFlowExceptionHandling(); + loginToApp(); + + cy.log('Step 1: Creating new project'); + createProject(); + + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + if (parts[projectIndex - 1]) { + locale = parts[projectIndex - 1]; + } + Cypress.env('publishReportProjectId', projectId); + cy.log('Captured Project ID:', projectId); + } + }); + + cy.log('Step 2: Uploading audio'); + openUploadModal(); + uploadAudioFile('assets/videoplayback.mp3'); + clickUploadFilesButton(); + cy.wait(20000); + closeUploadModal(); + + cy.log('Step 3: Creating report'); + cy.get('[data-testid="sidebar-report-button"]').filter(':visible').first().click(); + cy.get('section[role="dialog"]').should('be.visible'); + cy.get('[data-testid="report-create-button"]').filter(':visible').first().click(); + cy.wait(30000); + + cy.get('[data-testid="sidebar-report-button"]').filter(':visible').first().click(); + cy.get('[data-testid="report-renderer-container"]', { timeout: 20000 }).should('be.visible'); + }); + + it('shows report as unavailable on public URL before publish', () => { + registerReportFlowExceptionHandling(); + + resolveProjectId().then((id) => { + openPublicReportPage(id); + }); + + cy.get('[data-testid="public-report-not-available"]', { timeout: 20000 }).should('be.visible'); + cy.get('[data-testid="public-report-view"]').should('not.exist'); + }); + + it('publishes the report from dashboard', () => { + registerReportFlowExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + openDashboardReportPage(id); + }); + + setReportPublishState(true); + }); + + it('shows published report on public URL after publish', () => { + registerReportFlowExceptionHandling(); + + resolveProjectId().then((id) => { + openPublicReportPage(id); + }); + + // waitForPublicReportPublished(); + + cy.get('[data-testid="public-report-not-available"]').should('not.exist'); + cy.get('[data-testid="public-report-view"]', { timeout: 20000 }).should('be.visible'); + }); + + it('deletes the project and logs out', () => { + registerReportFlowExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + if (dashboardBaseUrl) { + cy.visit(`${dashboardBaseUrl}/projects/${id}/overview`); + } else { + cy.visit(`/${locale}/projects/${id}/overview`); + } + deleteProject(id); + Cypress.env('publishReportProjectId', null); + }); + + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/11-edit-report.cy.js b/echo/cypress/e2e/suites/11-edit-report.cy.js new file mode 100644 index 00000000..d562276d --- /dev/null +++ b/echo/cypress/e2e/suites/11-edit-report.cy.js @@ -0,0 +1,173 @@ +/** + * Edit Report Flow Test Suite + * + * This test verifies the flow of creating a report and editing its content: + * 1. Login and create a new project + * 2. Upload an audio file + * 3. Create a report + * 4. Toggle "Editing mode" + * 5. Modify the report content + * 6. Toggle "Editing mode" OFF + * 7. Verify the modifications are visible + * 8. Cleanup + */ + +import { + clickUploadFilesButton, + closeUploadModal, + navigateToProjectOverview, + openUploadModal, + uploadAudioFile, +} from "../../support/functions/conversation"; +import { loginToApp, logout } from "../../support/functions/login"; +import { createProject, deleteProject } from "../../support/functions/project"; +import { openSettingsMenu } from "../../support/functions/settings"; + +describe("Edit Report Flow", () => { + let projectId; + + beforeEach(() => { + // Handle uncaught exceptions + cy.on("uncaught:exception", (err, runnable) => { + return false; + }); + loginToApp(); + }); + + it("should upload audio, create report, edit content, and verify changes", () => { + // 1. Create new project + cy.log("Step 1: Creating new project"); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split("/"); + const projectIndex = parts.indexOf("projects"); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log("Captured Project ID:", projectId); + } + }); + + // 2. Open Upload Conversation modal + cy.log("Step 2: Opening upload modal"); + openUploadModal(); + + // 3. Upload the audio file from cypress assets + cy.log("Step 3: Uploading audio file"); + uploadAudioFile("assets/videoplayback.mp3"); + + // 4. Click Upload Files button to start the upload + cy.log("Step 4: Clicking Upload Files button"); + clickUploadFilesButton(); + + // 5. Wait 15 seconds for processing + cy.log("Step 5: Waiting 15 seconds for file processing"); + cy.wait(15000); + + // 6. Close the upload modal + cy.log("Step 6: Closing upload modal"); + closeUploadModal(); + + // 7. Click on the Report button + cy.log("Step 7: Clicking Report button"); + cy.get('[data-testid="sidebar-report-button"]') + .filter(":visible") + .first() + .click(); + + // 8. Click Create Report in the modal + cy.log("Step 8: Clicking Create Report in modal"); + cy.get('section[role="dialog"]').should("be.visible"); + cy.get('[data-testid="report-create-button"]') + .filter(":visible") + .first() + .click(); + + // 9. Wait 20 seconds for processing + cy.log("Step 9: Waiting 20 seconds for report processing"); + cy.wait(20000); + + // 10. Click on the Report button again to view report + cy.log("Step 10: Clicking Report button again"); + cy.get('[data-testid="sidebar-report-button"]') + .filter(":visible") + .first() + .click(); + cy.wait(5000); // Wait for report content to load + + // 11. Toggle Editing Mode ON + cy.log("Step 11: Toggling Editing Mode ON"); + // Use robust xpath for the switch containing 'Editing mode' + cy.get('[data-testid="report-editing-mode-toggle"]').click({ force: true }); + cy.wait(1000); // Wait for editor to initialize + + // 12. Modify Report Content + cy.log("Step 12: Modifying report content"); + + // Locate the editor textbox inside report container. Do not use visibility assertion: + // the editor can be inside a scrollable/overflow parent in some layouts. + cy.get( + '[data-testid="report-renderer-container"] [role="textbox"][contenteditable="true"]', + { timeout: 20000 }, + ) + .should("exist") + .first() + .scrollIntoView() + .then(($editor) => { + // Clear existing content and type new content + // Using {selectall}{backspace} to clear ensuring we don't break the editor state + // processing: { force: true } added to bypass "element hidden" errors + cy.wrap($editor).type("{selectall}{backspace}", { force: true }); + cy.wait(500); + + // Type new markdown content + // We use '# ' for Heading 1 and then a paragraph + cy.wrap($editor).type( + "# Automated Edit Verification{enter}This is a test edit from Cypress.", + { force: true }, + ); + }); + + cy.wait(1000); // Wait for auto-save or state update + + // 13. Toggle Editing Mode OFF + cy.log("Step 13: Toggling Editing Mode OFF"); + cy.get('[data-testid="report-editing-mode-toggle"]').click({ force: true }); + cy.wait(1000); // Wait for read-only view validation + + // 14. Verify New Content Persists + cy.log("Step 14: Verifying edited content"); + + // Check for the H1 heading + cy.contains("h1", "Automated Edit Verification").should("be.visible"); + + // Check for the paragraph text + cy.contains("p", "This is a test edit from Cypress.").should("be.visible"); + + // 15. Navigate back via Project Overview + cy.log("Step 15: Navigating to Project Overview"); + // Ensure manual return to project page to reliably use cleanup + const dashboardUrl = Cypress.env("dashboardUrl"); + if (projectId && dashboardUrl) { + const dashboardProjectUrl = `${dashboardUrl}/projects/${projectId}`; + cy.visit(dashboardProjectUrl); + } else { + navigateToProjectOverview(); + } + cy.wait(3000); + + // 16. Delete the project + cy.log("Step 16: Deleting project"); + cy.then(() => { + if (projectId) { + deleteProject(projectId); + } + }); + + // 17. Open Settings menu and Logout + cy.log("Step 17: Opening settings and logging out"); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/12-chat-ask-feature.cy.js b/echo/cypress/e2e/suites/12-chat-ask-feature.cy.js new file mode 100644 index 00000000..6fbb07a6 --- /dev/null +++ b/echo/cypress/e2e/suites/12-chat-ask-feature.cy.js @@ -0,0 +1,87 @@ +/** + * Ask Feature Flow Test Suite + * + * This test verifies the complete flow of using the Ask feature: + * 1. Login and create a new project + * 2. Upload an audio file (replicating Suite 08/10 flow) + * 3. Use Ask feature with context selection + * 4. Verify AI response + * 5. Navigate to Home, delete project, and logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + navigateToProjectOverview +} from '../../support/functions/conversation'; +import { askWithContext } from '../../support/functions/chat'; + +describe('Ask Feature Flow', () => { + let projectId; + + beforeEach(() => { + loginToApp(); + }); + + it('should upload audio, use Ask feature with Specific Details, verify response, delete project and logout', () => { + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + // 2. Open Upload Conversation modal + cy.log('Step 2: Opening upload modal'); + openUploadModal(); + + cy.wait(5000); + + // 3. Upload the audio file from cypress assets + cy.log('Step 3: Uploading audio file'); + uploadAudioFile('assets/videoplayback.mp3'); + + // 4. Click Upload Files button to start the upload + cy.log('Step 4: Clicking Upload Files button'); + clickUploadFilesButton(); + + // 5. Wait 15 seconds for processing + cy.log('Step 5: Waiting 15 seconds for file processing'); + cy.wait(15000); + + // 6. Close the upload modal + cy.log('Step 6: Closing upload modal'); + closeUploadModal(); + + // 7. Use Ask feature with context selection + cy.log('Step 7: Using Ask feature with context'); + askWithContext('hello'); + + // 8. Navigate to Project Overview + cy.log('Step 8: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 9. Delete the project + cy.log('Step 9: Deleting project'); + cy.then(() => { + deleteProject(projectId); + }); + + // 10. Open Settings menu and Logout + cy.log('Step 10: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/13-chat-ask-no-context.cy.js b/echo/cypress/e2e/suites/13-chat-ask-no-context.cy.js new file mode 100644 index 00000000..44888b17 --- /dev/null +++ b/echo/cypress/e2e/suites/13-chat-ask-no-context.cy.js @@ -0,0 +1,85 @@ +/** + * Ask Feature Flow (No Context Selection) Test Suite + * + * This test verifies the Ask feature without manually selecting conversations: + * 1. Login and create a new project + * 2. Upload an audio file (replicating Suite 08/10 flow) + * 3. Use Ask feature without context selection + * 4. Verify AI response + * 5. Navigate to Home, delete project, and logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + navigateToProjectOverview +} from '../../support/functions/conversation'; +import { askWithoutContext } from '../../support/functions/chat'; + +describe('Ask Feature Flow (No Context Selection)', () => { + let projectId; + + beforeEach(() => { + loginToApp(); + }); + + it('should upload audio, use Ask feature without selecting context, verify response, delete project and logout', () => { + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + // 2. Open Upload Conversation modal + cy.log('Step 2: Opening upload modal'); + openUploadModal(); + + // 3. Upload the audio file from cypress assets + cy.log('Step 3: Uploading audio file'); + uploadAudioFile('assets/videoplayback.mp3'); + + // 4. Click Upload Files button to start the upload + cy.log('Step 4: Clicking Upload Files button'); + clickUploadFilesButton(); + + // 5. Wait 15 seconds for processing + cy.log('Step 5: Waiting 15 seconds for file processing'); + cy.wait(15000); + + // 6. Close the upload modal + cy.log('Step 6: Closing upload modal'); + closeUploadModal(); + + // 7. Use Ask feature without context selection + cy.log('Step 7: Using Ask feature without context'); + askWithoutContext('hello'); + + // 8. Navigate to Project Overview + cy.log('Step 8: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 9. Delete the project + cy.log('Step 9: Deleting project'); + cy.then(() => { + deleteProject(projectId); + }); + + // 10. Open Settings menu and Logout + cy.log('Step 10: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/14-participant-audio-flow.cy.js b/echo/cypress/e2e/suites/14-participant-audio-flow.cy.js new file mode 100644 index 00000000..8afba649 --- /dev/null +++ b/echo/cypress/e2e/suites/14-participant-audio-flow.cy.js @@ -0,0 +1,229 @@ +/** + * Participant Audio Recording Flow Test Suite + * + * This flow is split across single-origin tests so it can run in Chromium, + * Firefox, and WebKit without relying on cy.origin(). + */ + +import { + clickTranscriptTab, + navigateToProjectOverview, + selectConversation, +} from "../../support/functions/conversation"; +import { loginToApp, logout } from "../../support/functions/login"; +import { + agreeToPrivacyPolicy, + confirmFinishConversation, + continueMicrophoneCheck, + enterSessionName, + finishRecordingFromModal, + handleMicrophoneAccessDenied, + handleRecordingInterruption, + installParticipantAudioStubs, + prepareForRecording, + primeMicrophoneAccess, + reapplyParticipantAudioStubs, + retryRecordingIfAccessDenied, + startRecording, + stopRecording, +} from "../../support/functions/participant"; +import { createProject, deleteProject } from "../../support/functions/project"; +import { openSettingsMenu } from "../../support/functions/settings"; + +describe("Participant Audio Recording Flow", () => { + let projectId; + + const portalBaseUrl = + Cypress.env("portalUrl") || "https://portal.echo-next.dembrane.com"; + const dashboardBaseUrl = + Cypress.env("dashboardUrl") || "https://dashboard.echo-next.dembrane.com"; + const portalLocale = "en-US"; + const sessionName = "Audio Test Session"; + + const registerExceptionHandling = () => { + cy.on("uncaught:exception", (err) => { + if ( + err.message.includes("Syntax error, unrecognized expression") || + err.message.includes("BODY[style=") || + err.message.includes("ResizeObserver loop limit exceeded") + ) { + return false; + } + return true; + }); + }; + + const resolveProjectId = () => { + return cy + .then(() => { + if (!projectId) { + projectId = Cypress.env("participantAudioProjectId"); + } + + if (projectId) { + return projectId; + } + + return cy + .readFile("fixtures/createdProjects.json", { log: false }) + .then((projects) => { + const lastProject = Array.isArray(projects) + ? projects[projects.length - 1] + : null; + if (!lastProject || !lastProject.id) { + throw new Error( + "projectId not found. Ensure the create step completed successfully.", + ); + } + projectId = lastProject.id; + Cypress.env("participantAudioProjectId", projectId); + return projectId; + }); + }) + .then((id) => { + expect(id, "projectId").to.be.a("string").and.not.be.empty; + }); + }; + + const getPortalUrl = () => + `${portalBaseUrl}/${portalLocale}/${projectId}/start`; + + it("creates a project for participant audio recording", () => { + registerExceptionHandling(); + loginToApp(); + + cy.log("Step 1: Creating new project"); + createProject(); + + cy.url().then((url) => { + const parts = url.split("/"); + const projectIndex = parts.indexOf("projects"); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + Cypress.env("participantAudioProjectId", projectId); + cy.log("Captured Project ID:", projectId); + } + }); + + resolveProjectId(); + + // Keep the session intact for the remaining flow parts. + }); + + it("records audio in the participant portal", () => { + registerExceptionHandling(); + resolveProjectId().then(() => { + cy.log("Step 2: Opening participant portal"); + // Use WAV for stable chunk sizing across browsers + cy.readFile("fixtures/test-audio.wav", "base64").then((audioBase64) => { + installParticipantAudioStubs({ + audioBase64, + audioMimeType: "audio/wav", + }); + cy.visit(getPortalUrl()); + }); + }); + + agreeToPrivacyPolicy(); + reapplyParticipantAudioStubs(); + primeMicrophoneAccess(); + + cy.log("Step 4: Microphone check"); + cy.wait(3000); + handleMicrophoneAccessDenied(); + + cy.get('[data-testid="portal-onboarding-mic-skip-button"]').should( + "be.visible", + ); + + cy.get("body").then(($body) => { + const selector = $body.find('[role="combobox"], select'); + if (selector.length > 0) { + cy.log("Microphone selector present"); + } else { + cy.log("No microphone selector found - using default device"); + } + }); + + cy.wait(2000); + reapplyParticipantAudioStubs(); + const allowSkip = Cypress.browser && Cypress.browser.name === "webkit"; + continueMicrophoneCheck({ allowSkip }); + + enterSessionName(sessionName); + reapplyParticipantAudioStubs(); + + cy.log("Step 6: Start Recording"); + handleMicrophoneAccessDenied(); + prepareForRecording(); + reapplyParticipantAudioStubs(); + startRecording(); + retryRecordingIfAccessDenied(); + + // Wait for Stop button or handle interruption + cy.get("body", { timeout: 30000 }).then(($body) => { + if ( + $body.find('[data-testid="portal-audio-stop-button"]:visible').length > + 0 + ) { + cy.log("Stop button visible - recording started successfully"); + } else if ( + $body.find( + '[data-testid="portal-audio-interruption-reconnect-button"]:visible', + ).length > 0 + ) { + cy.log("Recording interrupted - reconnecting"); + handleRecordingInterruption(); + } + }); + + cy.log("Recording for 60 seconds..."); + cy.wait(60000); + + // Check for interruption after waiting + handleRecordingInterruption(); + + stopRecording(); + + finishRecordingFromModal(); + confirmFinishConversation(); + cy.wait(2000); + }); + + it("verifies transcription and cleans up", () => { + registerExceptionHandling(); + loginToApp(); + + resolveProjectId().then(() => { + cy.visit( + `${dashboardBaseUrl}/${portalLocale}/projects/${projectId}/overview`, + ); + }); + + cy.wait(5000); + selectConversation(sessionName); + + cy.log("Waiting for transcript processing..."); + cy.wait(15000); + + clickTranscriptTab(); + + cy.xpath( + '//div[contains(@class, "mantine-Paper-root")]//div[contains(@style, "flex")]//div/p[contains(@class, "mantine-Text-root")]', + ) + .should("have.length.gt", 0) + .then(($els) => { + const text = $els.text(); + cy.log("Transcribed text:", text); + expect(text).to.not.be.empty; + }); + + navigateToProjectOverview(); + cy.then(() => { + deleteProject(projectId); + }); + + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/15-change-conversation-name.cy.js b/echo/cypress/e2e/suites/15-change-conversation-name.cy.js new file mode 100644 index 00000000..deb17581 --- /dev/null +++ b/echo/cypress/e2e/suites/15-change-conversation-name.cy.js @@ -0,0 +1,108 @@ +/** + * Change Conversation Name Flow Test Suite + * + * This test verifies the flow of: + * 1. Login and create a new project + * 2. Upload an audio file via the upload conversation modal + * 3. Select the conversation + * 4. Update the conversation name + * 5. Verify the name update in the list + * 6. DELETE project and Logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + selectConversation, + verifyConversationName, + updateConversationName, + navigateToProjectOverview, + verifyConversationInList +} from '../../support/functions/conversation'; + +describe('Change Conversation Name Flow', () => { + let projectId; + const randomName = `Updated Name ${Math.floor(Math.random() * 10000)}`; + + beforeEach(() => { + loginToApp(); + }); + + it('should upload audio, change conversation name, verify in list, delete project, and logout', () => { + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + // 2. Open Upload Conversation modal + cy.log('Step 2: Opening upload modal'); + openUploadModal(); + + // 3. Upload the audio file from cypress assets + cy.log('Step 3: Uploading audio file'); + uploadAudioFile('assets/videoplayback.mp3'); + + // 4. Click Upload Files button to start the upload + cy.log('Step 4: Clicking Upload Files button'); + clickUploadFilesButton(); + + // 5. Wait 15 seconds for processing + cy.log('Step 5: Waiting 15 seconds for file processing'); + cy.wait(15000); + + // 6. Close the upload modal + cy.log('Step 6: Closing upload modal'); + closeUploadModal(); + + // 7. Click on the uploaded conversation in the list + cy.log('Step 7: Selecting uploaded conversation'); + selectConversation('videoplayback.mp3'); + + // 8. Update conversation name using the random name + cy.log('Step 8: Updating conversation name'); + updateConversationName(randomName); + + // 9. Wait 10 seconds for auto-save + cy.log('Step 9: Waiting 10 seconds for auto-save'); + cy.wait(10000); + + // 10. Verify name in the input field + cy.log('Step 10: Verifying updated name in input'); + verifyConversationName(randomName); + + // 11. Navigate back to project overview via breadcrumb + cy.log('Step 11: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 12. Verify the updated name appears in the list + cy.log('Step 12: Verifying updated name in conversation list'); + verifyConversationInList(randomName); + + // 13. Delete the project (includes clicking Project Settings tab) + cy.log('Step 13: Deleting project'); + cy.then(() => { + if (projectId) { + deleteProject(projectId); + } + }); + + // 14. Open Settings menu and Logout + cy.log('Step 14: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/16-project-tags-conversation-flow.cy.js b/echo/cypress/e2e/suites/16-project-tags-conversation-flow.cy.js new file mode 100644 index 00000000..b053c2f1 --- /dev/null +++ b/echo/cypress/e2e/suites/16-project-tags-conversation-flow.cy.js @@ -0,0 +1,99 @@ +/** + * Project Tags & Conversation Flow + * + * This test verifies the flow of: + * 1. Login and create a new project + * 2. Add tags to the project in Portal Editor + * 3. Upload an audio file via the upload conversation modal (Dashboard) + * 4. Verify tags are selectable in the conversation overview + * 5. Verify selected tags are visible + * 6. DELETE project and Logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { openPortalEditor, addTag } from '../../support/functions/portal'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + selectConversation, + selectConversationTags, + verifySelectedTags, + navigateToProjectOverview +} from '../../support/functions/conversation'; + +describe('Project Tags & Conversation Flow', () => { + let projectId; + const tag1 = 'TagOne'; + const tag2 = 'TagTwo'; + + beforeEach(() => { + loginToApp(); + }); + + it('should create project with tags, upload audio, and verify tags in conversation', () => { + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + // 2. Add Tags in Portal Editor + cy.log('Step 2: Adding tags in Portal Editor'); + openPortalEditor(); + addTag(tag1); + addTag(tag2); + + // Return to Project Overview to upload file + cy.log('Step 3: Returning to Project Overview'); + navigateToProjectOverview(); + + // 3. Upload Conversation (Manual Flow) + cy.log('Step 4: Uploading audio file'); + openUploadModal(); + uploadAudioFile('assets/videoplayback.mp3'); + clickUploadFilesButton(); + + // Wait for processing + cy.log('Step 5: Waiting 15 seconds for file processing'); + cy.wait(15000); + closeUploadModal(); + + // 4. Select Conversation & Verify Tags + cy.log('Step 6: Selecting uploaded conversation'); + selectConversation('videoplayback.mp3'); + + // Verify tags input and select tags + cy.log('Step 7: Selecting and verifying tags'); + selectConversationTags([tag1, tag2]); + + // Verify they are shown as selected + cy.log('Step 8: Verifying selected tags visibility'); + verifySelectedTags([tag1, tag2]); + + // 5. Cleanup + cy.log('Step 9: Cleanup - Deleting Project'); + navigateToProjectOverview(); + cy.then(() => { + if (projectId) { + deleteProject(projectId); + } + }); + + // Logout + cy.log('Step 10: Logging out'); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/17-make-it-concrete-flow.cy.js b/echo/cypress/e2e/suites/17-make-it-concrete-flow.cy.js new file mode 100644 index 00000000..ccac11e5 --- /dev/null +++ b/echo/cypress/e2e/suites/17-make-it-concrete-flow.cy.js @@ -0,0 +1,273 @@ +/** + * Make it Concrete Flow + * + * This test verifies the "Make it concrete" participant flow. + * It is split into multiple tests to handle cross-origin navigation boundaries (Dashboard -> Portal -> Dashboard) + * ensuring stability across WebKit and other browsers. + */ + +import { + navigateToProjectOverview, + verifySelectedTags, +} from "../../support/functions/conversation"; +import { loginToApp, logout } from "../../support/functions/login"; +import { + agreeToPrivacyPolicy, + approveArtefact, + clickEchoButton, + confirmFinishConversation, + continueMicrophoneCheck, + enterSessionName, + finishRecordingFromModal, + handleMicrophoneAccessDenied, + handleRecordingInterruption, + installParticipantAudioStubs, + prepareForRecording, + primeMicrophoneAccess, + proceedFromInstructions, + proceedFromTopicSelection, + reapplyParticipantAudioStubs, + retryRecordingIfAccessDenied, + selectMakeItConcrete, + selectVerifyTopic, + startRecording, + stopRecording, +} from "../../support/functions/participant"; +import { + openPortalEditor, + toggleMakeItConcrete, + toggleOpenForParticipation, +} from "../../support/functions/portal"; +import { createProject, deleteProject } from "../../support/functions/project"; +import { openSettingsMenu } from "../../support/functions/settings"; + +describe("Make it Concrete Flow", () => { + let projectId; + const concreteTopic = "What we actually agreed on"; + const portalBaseUrl = + Cypress.env("portalUrl") || "https://portal.echo-next.dembrane.com"; + const dashboardBaseUrl = + Cypress.env("dashboardUrl") || "https://dashboard.echo-next.dembrane.com"; + const portalLocale = "en-US"; + const sessionName = "Concrete Test Session"; // Name for the participant session + + // Same exception handling as Test 14 + const registerExceptionHandling = () => { + cy.on("uncaught:exception", (err) => { + if ( + err.message.includes("Syntax error, unrecognized expression") || + err.message.includes("BODY[style=") || + err.message.includes("ResizeObserver loop limit exceeded") || + err.message.includes("Can't find variable: MediaRecorder") + ) { + return false; + } + return true; + }); + }; + + // Helper to persist/retrieve Project ID across tests + const resolveProjectId = () => { + return cy + .then(() => { + if (!projectId) { + projectId = Cypress.env("participantConcreteProjectId"); + } + + if (projectId) { + return projectId; + } + + return cy + .readFile("fixtures/createdProjects.json", { log: false }) + .then((projects) => { + const lastProject = Array.isArray(projects) + ? projects[projects.length - 1] + : null; + if (!lastProject || !lastProject.id) { + throw new Error( + "projectId not found. Ensure the create step completed successfully.", + ); + } + projectId = lastProject.id; + Cypress.env("participantConcreteProjectId", projectId); + return projectId; + }); + }) + .then((id) => { + expect(id, "projectId").to.be.a("string").and.not.be.empty; + }); + }; + + const getPortalUrl = () => + `${portalBaseUrl}/${portalLocale}/${projectId}/start`; + + it("Step 1: Creates a project and enables Make it Concrete", () => { + registerExceptionHandling(); + loginToApp(); + + cy.log("Creating new project"); + createProject(); + + cy.url().then((url) => { + const parts = url.split("/"); + const projectIndex = parts.indexOf("projects"); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + Cypress.env("participantConcreteProjectId", projectId); + cy.log("Captured Project ID:", projectId); + } + }); + + resolveProjectId(); + + cy.log("Enabling Make it concrete in Portal Editor"); + openPortalEditor(); + toggleMakeItConcrete(true); + // toggleOpenForParticipation(true); + }); + + it("Step 2: Participant records audio and uses Make it Concrete", () => { + registerExceptionHandling(); + + resolveProjectId().then(() => { + cy.log("Opening participant portal"); + // Use local fixtures path pattern or 'fixtures/...' depending on setup. + // Test 14 uses 'fixtures/test-audio.wav'. + cy.readFile("fixtures/test-audio.wav", "base64").then((audioBase64) => { + installParticipantAudioStubs({ + audioBase64, + audioMimeType: "audio/wav", + }); + cy.visit(getPortalUrl()); + }); + }); + + // Exact flow from Test 14 + agreeToPrivacyPolicy(); + reapplyParticipantAudioStubs(); + primeMicrophoneAccess(); + + cy.log("Microphone check"); + cy.wait(3000); + handleMicrophoneAccessDenied(); + + // Check for 'Skip' availability logic from Test 14 (simplified for this flow, but good to have) + cy.get('[data-testid="portal-onboarding-mic-skip-button"]').should( + "be.visible", + ); + + cy.wait(2000); + reapplyParticipantAudioStubs(); + const allowSkip = Cypress.browser && Cypress.browser.name === "webkit"; + continueMicrophoneCheck({ allowSkip }); + + enterSessionName(sessionName); + reapplyParticipantAudioStubs(); + + cy.log("Start Recording Flow"); + handleMicrophoneAccessDenied(); + prepareForRecording(); + reapplyParticipantAudioStubs(); + startRecording(); + retryRecordingIfAccessDenied(); + + // Wait for Stop button or handle interruption logic from Test 14 + cy.get("body", { timeout: 30000 }).then(($body) => { + if ( + $body.find('[data-testid="portal-audio-stop-button"]:visible').length > + 0 + ) { + cy.log("Stop button visible - recording started successfully"); + } else if ( + $body.find( + '[data-testid="portal-audio-interruption-reconnect-button"]:visible', + ).length > 0 + ) { + cy.log("Recording interrupted - reconnecting"); + handleRecordingInterruption(); + } + }); + + // Record for 60+ seconds as required for Refine button + cy.log("Recording for 65 seconds to enable Refine..."); + cy.wait(67000); + + // Note: We do NOT finishRecordingFromModal() here because we want to use Refine -> Make it Concrete + // Verify Refine/Echo button is visible + cy.log("Refine -> Make it concrete"); + // We might need to wait a moment for the post-recording options to appear + cy.wait(2000); + + clickEchoButton(); + cy.wait(2000); + selectMakeItConcrete(); + + // Select Topic & Next + cy.log("Selecting Topic"); + selectVerifyTopic("agreements"); // "What we actually agreed on" + proceedFromTopicSelection(); + + // Wait for submission/processing + cy.wait(40000); + proceedFromInstructions(); + cy.wait(20000); + approveArtefact(); + cy.wait(1000); + + // Verify the concrete object is created and visible + cy.get('[data-testid="portal-verified-artefact-item-0"]') + .should("be.visible") + .and("contain", "What we actually agreed on"); + + stopRecording(); + cy.wait(1000); + finishRecordingFromModal(); + confirmFinishConversation(); + cy.wait(2000); + }); + + it("Step 3: Dashboard verification and cleanup", () => { + registerExceptionHandling(); + loginToApp(); + + resolveProjectId().then(() => { + cy.visit(`${dashboardBaseUrl}/projects/${projectId}/overview`); + }); + + // Select the conversation (it should be the only one, or most recent) + // Ensure we click the visible one (desktop view) to avoid clicking hidden mobile elements + cy.get('[data-testid^="conversation-item-"]') + .filter(":visible") + .first() + .click(); + + // Verify Conversation Overview sections + cy.log("Verifying Conversation Overview"); + // cy.contains('h2', 'Summary').should('be.visible'); + // cy.contains('h2', 'Artefacts').should('be.visible'); + // cy.contains('h2', 'Edit Conversation').should('be.visible'); + + // Verify Concrete Artefact + cy.log("Verifying concrete artefact presence"); + cy.get('[data-testid="conversation-artefacts-accordion"]') + .scrollIntoView() + .should("be.visible") + .within(() => { + cy.contains("What we actually agreed on").should("be.visible"); + }); + + // Cleanup + cy.log("Cleanup - Deleting Project"); + navigateToProjectOverview(); + cy.then(() => { + if (projectId) { + deleteProject(projectId); + } + }); + + // Logout + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/18-go-deeper-flow.cy.js b/echo/cypress/e2e/suites/18-go-deeper-flow.cy.js new file mode 100644 index 00000000..b76f2401 --- /dev/null +++ b/echo/cypress/e2e/suites/18-go-deeper-flow.cy.js @@ -0,0 +1,251 @@ +/** + * Make it Concrete Flow + * + * This test verifies the "Make it concrete" participant flow. + * It is split into multiple tests to handle cross-origin navigation boundaries (Dashboard -> Portal -> Dashboard) + * ensuring stability across WebKit and other browsers. + */ + +import { + navigateToProjectOverview, + selectConversation, + verifyConversationName, + verifySelectedTags, +} from "../../support/functions/conversation"; +import { loginToApp, logout } from "../../support/functions/login"; +import { + agreeToPrivacyPolicy, + clickEchoButton, + confirmFinishConversation, + continueMicrophoneCheck, + enterSessionName, + finishRecordingFromModal, + handleMicrophoneAccessDenied, + handleRecordingInterruption, + installParticipantAudioStubs, + prepareForRecording, + primeMicrophoneAccess, + reapplyParticipantAudioStubs, + retryRecordingIfAccessDenied, + selectMakeItDeeper, + startRecording, + stopRecording, +} from "../../support/functions/participant"; +import { + openPortalEditor, + toggleGoDeeper, + toggleMakeItConcrete, + toggleOpenForParticipation, +} from "../../support/functions/portal"; +import { createProject, deleteProject } from "../../support/functions/project"; +import { openSettingsMenu } from "../../support/functions/settings"; + +describe("Go Deeper Checking Flow", () => { + let projectId; + const concreteTopic = "What we actually agreed on"; + const portalBaseUrl = + Cypress.env("portalUrl") || "https://portal.echo-next.dembrane.com"; + const dashboardBaseUrl = + Cypress.env("dashboardUrl") || "https://dashboard.echo-next.dembrane.com"; + const portalLocale = "en-US"; + const sessionName = "Concrete Test Session"; // Name for the participant session + + // Same exception handling as Test 14 + const registerExceptionHandling = () => { + cy.on("uncaught:exception", (err) => { + if ( + err.message.includes("Syntax error, unrecognized expression") || + err.message.includes("BODY[style=") || + err.message.includes("ResizeObserver loop limit exceeded") || + err.message.includes("Can't find variable: MediaRecorder") + ) { + return false; + } + return true; + }); + }; + + // Helper to persist/retrieve Project ID across tests + const resolveProjectId = () => { + return cy + .then(() => { + if (!projectId) { + projectId = Cypress.env("participantConcreteProjectId"); + } + + if (projectId) { + return projectId; + } + + return cy + .readFile("fixtures/createdProjects.json", { log: false }) + .then((projects) => { + const lastProject = Array.isArray(projects) + ? projects[projects.length - 1] + : null; + if (!lastProject || !lastProject.id) { + throw new Error( + "projectId not found. Ensure the create step completed successfully.", + ); + } + projectId = lastProject.id; + Cypress.env("participantConcreteProjectId", projectId); + return projectId; + }); + }) + .then((id) => { + expect(id, "projectId").to.be.a("string").and.not.be.empty; + }); + }; + + const getPortalUrl = () => + `${portalBaseUrl}/${portalLocale}/${projectId}/start`; + + it("Step 1: Creates a project and enables Make it Concrete", () => { + registerExceptionHandling(); + loginToApp(); + + cy.log("Creating new project"); + createProject(); + + cy.url().then((url) => { + const parts = url.split("/"); + const projectIndex = parts.indexOf("projects"); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + Cypress.env("participantConcreteProjectId", projectId); + cy.log("Captured Project ID:", projectId); + } + }); + + resolveProjectId(); + + cy.log("Enabling Make it concrete in Portal Editor"); + openPortalEditor(); + toggleGoDeeper(true); + toggleMakeItConcrete(true); + // toggleOpenForParticipation(true); + }); + + it("Step 2: Participant records audio and uses Make it Concrete", () => { + registerExceptionHandling(); + + resolveProjectId().then(() => { + cy.log("Opening participant portal"); + // Use local fixtures path pattern or 'fixtures/...' depending on setup. + // Test 14 uses 'fixtures/test-audio.wav'. + cy.readFile("fixtures/test-audio.wav", "base64").then((audioBase64) => { + installParticipantAudioStubs({ + audioBase64, + audioMimeType: "audio/wav", + }); + cy.visit(getPortalUrl()); + }); + }); + + // Exact flow from Test 14 + agreeToPrivacyPolicy(); + reapplyParticipantAudioStubs(); + primeMicrophoneAccess(); + + cy.log("Microphone check"); + cy.wait(3000); + handleMicrophoneAccessDenied(); + + // Check for 'Skip' availability logic from Test 14 (simplified for this flow, but good to have) + cy.get('[data-testid="portal-onboarding-mic-skip-button"]').should( + "be.visible", + ); + + cy.wait(2000); + reapplyParticipantAudioStubs(); + const allowSkip = Cypress.browser && Cypress.browser.name === "webkit"; + continueMicrophoneCheck({ allowSkip }); + + enterSessionName(sessionName); + reapplyParticipantAudioStubs(); + + cy.log("Start Recording Flow"); + handleMicrophoneAccessDenied(); + prepareForRecording(); + reapplyParticipantAudioStubs(); + startRecording(); + retryRecordingIfAccessDenied(); + + // Wait for Stop button or handle interruption logic from Test 14 + cy.get("body", { timeout: 30000 }).then(($body) => { + if ( + $body.find('[data-testid="portal-audio-stop-button"]:visible').length > + 0 + ) { + cy.log("Stop button visible - recording started successfully"); + } else if ( + $body.find( + '[data-testid="portal-audio-interruption-reconnect-button"]:visible', + ).length > 0 + ) { + cy.log("Recording interrupted - reconnecting"); + handleRecordingInterruption(); + } + }); + + // Record for 60+ seconds as required for Refine button + cy.log("Recording for 65 seconds to enable Refine..."); + cy.wait(65000); + + // Note: We do NOT finishRecordingFromModal() here because we want to use Refine -> Make it Concrete + // Verify Refine/Echo button is visible + cy.log("Refine -> Make it concrete"); + // We might need to wait a moment for the post-recording options to appear + cy.wait(2000); + + clickEchoButton(); + selectMakeItDeeper(); + cy.wait(45000); + + // Check for Go Deeper message container and messages + cy.get('[data-testid="portal-explore-messages-container"]') + .should("exist") + .should("be.visible") + .within(() => { + // Check for either message 0 or 1 + cy.get('[data-testid^="portal-explore-message-"]').should("exist"); + }); + + stopRecording(); + cy.wait(1000); + finishRecordingFromModal(); + confirmFinishConversation(); + cy.wait(2000); + }); + + it("Step 3: Dashboard verification and cleanup", () => { + registerExceptionHandling(); + loginToApp(); + + resolveProjectId().then(() => { + cy.visit(`${dashboardBaseUrl}/projects/${projectId}/overview`); + }); + + // 7. Click on the uploaded conversation in the list + cy.log("Step 7: Selecting uploaded conversation"); + selectConversation(sessionName); + + // 8. Verify the conversation name in Edit Conversation section + cy.log("Step 8: Verifying conversation name"); + verifyConversationName(sessionName); + + // Cleanup + cy.log("Cleanup - Deleting Project"); + navigateToProjectOverview(); + cy.then(() => { + if (projectId) { + deleteProject(projectId); + } + }); + + // Logout + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/19-project-clone.cy.js b/echo/cypress/e2e/suites/19-project-clone.cy.js new file mode 100644 index 00000000..cf16c534 --- /dev/null +++ b/echo/cypress/e2e/suites/19-project-clone.cy.js @@ -0,0 +1,78 @@ +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, verifyProjectPage, deleteProject, updateProjectName, navigateToHome } from '../../support/functions/project'; +import { openPortalEditor, selectTutorial, addTag, updatePortalContent, changePortalLanguage } from '../../support/functions/portal'; +import { openSettingsMenu } from '../../support/functions/settings'; + +describe('Project Clone Flow', () => { + beforeEach(() => { + loginToApp(); + }); + + it('project clone test', () => { + const uniqueId = Cypress._.random(0, 10000); + const projectName = 'Project To Clone'; + const clonedProjectName = `Clone Test_${uniqueId}`; + + // 1. Create project + createProject(); + updateProjectName(projectName); + + let projectId; + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log(`Project ID: ${projectId}`); + } + }).then(() => { + // 2. Go to project settings + // We ensure we are on the Overview tab where the actions are derived + cy.get('[data-testid="project-overview-tab-overview"]').click(); + + // 3. Instead of clicking delete, click on clone button + cy.get('[data-testid="project-actions-clone-button"]').click(); + + // 4. Modal interaction + // Handle project-clone-name-input + cy.get('[data-testid="project-clone-name-input"]').should('be.visible').clear().type(clonedProjectName); + + // Click project-clone-confirm-button + cy.get('[data-testid="project-clone-confirm-button"]').click(); + + // 5. Wait for 10 seconds + cy.wait(10000); + + // 6. Check both the link whether the part between projects and portal editor changed + cy.url().then((currentUrl) => { + const parts = currentUrl.split('/'); + const projectIndex = parts.indexOf('projects'); + const newProjectId = parts[projectIndex + 1]; + + cy.log(`New Project ID: ${newProjectId}`); + + // Assert ID has changed + expect(newProjectId).to.not.equal(projectId); + + return cy.wrap(newProjectId); + }).then((newProjectId) => { + // 7. Check if the project-breadcrumb-name span text is updated to the newly given name + cy.get('[data-testid="project-breadcrumb-name"]').should('contain.text', clonedProjectName); + + // 8. Delete the cloned project + deleteProject(newProjectId); + + // 9. Search/Open the original project + // We find the project in the list by looking for its link + cy.get('main').find(`a[href*="${projectId}"]`).first().click(); + + // 11. Delete the original project + deleteProject(projectId); + + // 12. Logout + openSettingsMenu(); + logout(); + }); + }); + }); +}); diff --git a/echo/cypress/e2e/suites/20-download-transcription.cy.js b/echo/cypress/e2e/suites/20-download-transcription.cy.js new file mode 100644 index 00000000..2e946cad --- /dev/null +++ b/echo/cypress/e2e/suites/20-download-transcription.cy.js @@ -0,0 +1,149 @@ +/** + * Upload Conversation Flow Test Suite + * + * This test verifies the complete flow of: + * 1. Login and create a new project + * 2. Upload an audio file via the upload conversation modal + * 3. Wait for processing and close the modal + * 4. Click on the uploaded conversation and verify its name + * 5. Verify transcript text + * 6. Navigate to project overview and delete project + * 7. Logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProjectInsideProjectSettings, openProjectSettings, exportProjectTranscripts } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + selectConversation, + verifyConversationName, + clickTranscriptTab, + verifyTranscriptText, + navigateToProjectOverview +} from '../../support/functions/conversation'; + +describe('Upload Conversation Flow', () => { + let projectId; + + beforeEach(() => { + loginToApp(); + }); + + it('should upload audio file and download single transcript', () => { + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID for deletion and next test + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + // 2. Open Upload Conversation modal + cy.log('Step 2: Opening upload modal'); + openUploadModal(); + + // 3. Upload the audio file from cypress assets + cy.log('Step 3: Uploading audio file'); + uploadAudioFile('assets/videoplayback.mp3'); + + // 4. Click Upload Files button to start the upload + cy.log('Step 4: Clicking Upload Files button'); + clickUploadFilesButton(); + + // 5. Wait 15 seconds for processing + cy.log('Step 5: Waiting 15 seconds for file processing'); + cy.wait(15000); + + // 6. Close the upload modal + cy.log('Step 6: Closing upload modal'); + closeUploadModal(); + + // 7. Click on the uploaded conversation in the list + cy.log('Step 7: Selecting uploaded conversation'); + selectConversation('videoplayback.mp3'); + + // 8. Verify the conversation name in Edit Conversation section + cy.log('Step 8: Verifying conversation name'); + verifyConversationName('videoplayback.mp3'); + + // 9. Wait 25 seconds for transcript processing + cy.log('Step 9: Waiting 25 seconds for transcript processing'); + cy.wait(25000); + + // 10. Click on Transcript tab + cy.log('Step 10: Clicking Transcript tab'); + clickTranscriptTab(); + + // 11. Verify transcript text has at least 100 characters + cy.log('Step 11: Verifying transcript text'); + verifyTranscriptText(100); + + // New Step: Download Single Transcript + cy.log('Step 11b: Downloading single transcript'); + cy.get('[data-testid="transcript-download-button"]').should('be.visible').click(); + const singleTranscriptFile = `transcript-${Date.now()}`; + cy.get('[data-testid="transcript-download-filename-input"]').should('be.visible').clear().type(singleTranscriptFile); + cy.get('[data-testid="transcript-download-confirm-button"]').should('be.visible').click(); + + // Wait and Verify Single Download + cy.wait(5000); + cy.task('findFile', { dir: 'cypress/downloads', ext: '.md' }).then((filePath) => { // Assuming MD or similar + // Robust check: ensure it matches our random name if possible, or just latest + cy.log('Found downloaded transcript:', filePath); + if (filePath) cy.task('deleteFile', filePath); + }); + }); + + it('should download all transcripts (export project) and clean up', () => { + // Ensure we have a project ID from the previous test + expect(projectId).to.not.be.undefined; + + // Navigate to the project overview + cy.log('Navigating to Project Overview for Export'); + // Simple navigation assuming finding the link works, or direct visit + // Using verifyLogin-style navigation or direct URL + cy.visit(`/en-US/projects/${projectId}/overview`); + + // Export Transcripts and Verify Zip + cy.log('Step 12: Exporting project transcripts'); + // Ensure we are on the Project Settings tab where the export button is located + openProjectSettings(); + + exportProjectTranscripts(); + + // Wait for download to complete (arbitrary wait or until file exists) + cy.wait(5000); + + // Find and verify the downloaded zip file + cy.task('findFile', { dir: 'cypress/downloads', ext: '.zip' }).then((filePath) => { + expect(filePath).to.not.be.null; + cy.log('Found downloaded file:', filePath); + + // Cleanup: Delete the downloaded zip file + cy.task('deleteFile', filePath); + }); + + // 13. Delete the project (includes clicking Project Settings tab) + cy.log('Step 13: Deleting project'); + cy.then(() => { + // We are already on settings tab mostly, but helper handles scrolling + deleteProjectInsideProjectSettings(projectId); + }); + + // 14. Open Settings menu and Logout + cy.log('Step 14: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); + diff --git a/echo/cypress/e2e/suites/21-generate-and-regenerate-summary.cy.js b/echo/cypress/e2e/suites/21-generate-and-regenerate-summary.cy.js new file mode 100644 index 00000000..3667606e --- /dev/null +++ b/echo/cypress/e2e/suites/21-generate-and-regenerate-summary.cy.js @@ -0,0 +1,144 @@ +/** + * Upload Conversation Flow Test Suite + * + * This test verifies the complete flow of: + * 1. Login and create a new project + * 2. Upload an audio file via the upload conversation modal + * 3. Wait for processing and close the modal + * 4. Click on the uploaded conversation and verify its name + * 5. Verify transcript text + * 6. Navigate to project overview and delete project + * 7. Logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject, openProjectSettings, exportProjectTranscripts } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + selectConversation, + verifyConversationName, + clickOverviewTab, + verifyTranscriptText, + navigateToProjectOverview +} from '../../support/functions/conversation'; + +describe('Upload Conversation Flow', () => { + let projectId; + + beforeEach(() => { + loginToApp(); + }); + + it('should upload audio file, verify conversation, delete project, and logout', () => { + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + // 2. Open Upload Conversation modal + cy.log('Step 2: Opening upload modal'); + openUploadModal(); + + // 3. Upload the audio file from cypress assets + cy.log('Step 3: Uploading audio file'); + uploadAudioFile('assets/videoplayback.mp3'); + + // 4. Click Upload Files button to start the upload + cy.log('Step 4: Clicking Upload Files button'); + clickUploadFilesButton(); + + // 5. Wait 15 seconds for processing + cy.log('Step 5: Waiting 15 seconds for file processing'); + cy.wait(15000); + + // 6. Close the upload modal + cy.log('Step 6: Closing upload modal'); + closeUploadModal(); + + // 7. Click on the uploaded conversation in the list + cy.log('Step 7: Selecting uploaded conversation'); + selectConversation('videoplayback.mp3'); + + // 8. Verify the conversation name in Edit Conversation section + cy.log('Step 8: Verifying conversation name'); + verifyConversationName('videoplayback.mp3'); + + // 9. Wait 25 seconds for transcript processing + cy.log('Step 9: Waiting 25 seconds for transcript processing'); + cy.wait(25000); + + // 10. Click on Transcript tab + cy.log('Step 10: Clicking Transcript tab'); + clickOverviewTab(); + + // 11. Generate/Check Summary + cy.log('Step 11: Checking/Generating Summary'); + cy.wait(5000); // Initial wait as requested + + cy.get('body').then(($body) => { + const generateBtnSelector = '[data-testid="conversation-overview-generate-summary-button"]'; + if ($body.find(generateBtnSelector).length > 0 && $body.find(generateBtnSelector).is(':visible')) { + cy.log('Generate button found, clicking...'); + cy.get(generateBtnSelector).click(); + cy.wait(40000); // Wait 1 min for generation + } else { + cy.log('Generate button not found or not visible, waiting...'); + cy.wait(40000); // Wait 1 min + } + }); + + // Check summary length and copy + let initialSummary = ''; + // Using .prose p selector as per HTML structure + cy.get('.prose p', { timeout: 10000 }).should('exist').invoke('text').then((text) => { + expect(text.length).to.be.gt(200); + initialSummary = text; + cy.log('Initial Summary Length:', text.length); + cy.log('Initial Summary:', text); + }); + + // 12. Regenerate Summary + cy.log('Step 12: Regenerating Summary'); + cy.get('[data-testid="conversation-overview-regenerate-summary-button"]').should('be.visible').click(); + + cy.log('Waiting 40 seconds for regeneration...'); + cy.wait(40000); + + // Check new summary + cy.get('.prose p').invoke('text').then((newText) => { + expect(newText.length).to.be.gt(200); + cy.log('New Summary Length:', newText.length); + cy.log('New Summary:', newText); + expect(newText).to.not.equal(initialSummary); + cy.log('Regeneration Successful: Summaries are different.'); + }); + + // 13. Navigate to Project Overview + cy.log('Step 13: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 14. Delete the project + cy.log('Step 14: Deleting project'); + cy.then(() => { + deleteProject(projectId); + }); + + // 15. Open Settings menu and Logout + cy.log('Step 15: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/22-rename-chat.cy.js b/echo/cypress/e2e/suites/22-rename-chat.cy.js new file mode 100644 index 00000000..d02f394e --- /dev/null +++ b/echo/cypress/e2e/suites/22-rename-chat.cy.js @@ -0,0 +1,117 @@ +/** + * Ask Feature Flow (No Context Selection) Test Suite + * + * This test verifies the Ask feature without manually selecting conversations: + * 1. Login and create a new project + * 2. Upload an audio file (replicating Suite 08/10 flow) + * 3. Use Ask feature without context selection + * 4. Verify AI response + * 5. Navigate to Home, delete project, and logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + navigateToProjectOverview +} from '../../support/functions/conversation'; +import { askWithoutContext } from '../../support/functions/chat'; + +describe('Ask Feature Flow (No Context Selection)', () => { + let projectId; + + beforeEach(() => { + // Suppress known Minified React error #185 + cy.on('uncaught:exception', (err, runnable) => { + if (err.message.includes('Minified React error #185')) { + return false; + } + }); + loginToApp(); + }); + + it('should upload audio, use Ask feature without selecting context, verify response, delete project and logout', () => { + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + + // 7. Use Ask feature without context selection + cy.log('Step 7: Using Ask feature without context'); + askWithoutContext('hello'); + + // New Step: Rename Flow + cy.log('Step 7b: Renaming the first chat'); + const newChatName = `Renamed Chat ${Date.now()}`; + + // Ensure Chats accordion is expanded (avoid toggling it closed by accident) + cy.get('[data-testid="chat-accordion"] [data-accordion-control="true"]') + .filter(':visible') + .first() + .then(($control) => { + if ($control.attr('aria-expanded') !== 'true') { + cy.wrap($control).click(); + } + }); + cy.wait(1500); + + // Verify chats exist or fail gracefully with info + cy.get('body').then($body => { + if ($body.find('[data-testid="chat-accordion-empty-text"]').length > 0) { + cy.log('No chats found in the list!'); + throw new Error('No chats found to rename'); + } + }); + + cy.window().then((win) => { + cy.stub(win, 'prompt').returns(newChatName); + }); + + // Click first chat menu button + cy.get('[data-testid="chat-item-menu"]', { timeout: 10000 }) + .filter(':visible') + .first() + .click({ force: true }); + + // Click Rename option + cy.get('[data-testid="chat-item-menu-rename"]').should('be.visible').click(); + + // Wait for server update + cy.log('Waiting 5 seconds for rename to persist'); + cy.wait(5000); + + // Verify rename text appears in the chats sidebar list + cy.log('Verifying chat rename'); + cy.get('[data-testid="chat-accordion"]', { timeout: 10000 }) + .should('contain.text', newChatName); + + // 8. Navigate to Project Overview + cy.log('Step 8: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 9. Delete the project + cy.log('Step 9: Deleting project'); + cy.then(() => { + deleteProject(projectId); + }); + + // 10. Open Settings menu and Logout + cy.log('Step 10: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/23-delete-chat.cy.js b/echo/cypress/e2e/suites/23-delete-chat.cy.js new file mode 100644 index 00000000..c83e665b --- /dev/null +++ b/echo/cypress/e2e/suites/23-delete-chat.cy.js @@ -0,0 +1,115 @@ +/** + * Ask Feature Flow (Delete Chat) Test Suite + * + * This test verifies: + * 1. Login and create a new project + * 2. Use Ask feature without context + * 3. Delete the created chat and accept browser confirm popup + * 4. Verify chats empty state + * 5. Navigate to Home, delete project, and logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { navigateToProjectOverview } from '../../support/functions/conversation'; +import { askWithoutContext } from '../../support/functions/chat'; + +describe('Ask Feature Flow (Delete Chat)', () => { + let projectId; + + beforeEach(() => { + // Suppress known Minified React error #185 + cy.on('uncaught:exception', (err, runnable) => { + if (err.message.includes('Minified React error #185')) { + return false; + } + }); + loginToApp(); + }); + + it('should upload audio, use Ask feature, delete chat, verify empty chats state, delete project and logout', () => { + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + + // 7. Use Ask feature without context selection + cy.log('Step 7: Using Ask feature without context'); + askWithoutContext('hello'); + + // New Step: Delete Flow + cy.log('Step 7b: Deleting the first chat'); + + // Ensure Chats accordion is expanded (avoid toggling it closed by accident) + cy.get('[data-testid="chat-accordion"] [data-accordion-control="true"]') + .filter(':visible') + .first() + .then(($control) => { + if ($control.attr('aria-expanded') !== 'true') { + cy.wrap($control).click(); + } + }); + cy.wait(1500); + + // Verify chats exist or fail gracefully with info + cy.get('body').then($body => { + if ($body.find('[data-testid="chat-accordion-empty-text"]').length > 0) { + cy.log('No chats found in the list!'); + throw new Error('No chats found to delete'); + } + }); + + // Accept browser confirmation popup when deleting + cy.on('window:confirm', () => { + return true; + }); + + // Click first chat menu button + cy.get('[data-testid="chat-item-menu"]', { timeout: 10000 }) + .filter(':visible') + .first() + .click({ force: true }); + + // Click Delete option and proceed with browser confirm + cy.get('[data-testid="chat-item-menu-delete"]') + .filter(':visible') + .first() + .click({ force: true }); + + // Wait for server update + cy.log('Waiting 5 seconds for delete to persist'); + cy.wait(5000); + + // Verify chats empty state + cy.log('Verifying chats empty state after delete'); + cy.get('[data-testid="chat-accordion-empty-text"]', { timeout: 10000 }) + .should('be.visible') + .and('contain.text', 'No chats found. Start a chat using the "Ask" button.'); + + // 8. Navigate to Project Overview + cy.log('Step 8: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 9. Delete the project + cy.log('Step 9: Deleting project'); + cy.then(() => { + deleteProject(projectId); + }); + + // 10. Open Settings menu and Logout + cy.log('Step 10: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/24-dynamic-suggestions.cy.js b/echo/cypress/e2e/suites/24-dynamic-suggestions.cy.js new file mode 100644 index 00000000..447da623 --- /dev/null +++ b/echo/cypress/e2e/suites/24-dynamic-suggestions.cy.js @@ -0,0 +1,103 @@ +/** + * Ask Feature Flow (Dynamic Suggestions) Test Suite + * + * This test verifies: + * 1. Login and create a new project + * 2. Open Ask (Specific Details) without sending a message + * 3. Verify initial suggestions state (only static suggestions + more button) + * 4. Send one message and verify 3 dynamic suggestion chips appear + * 5. Navigate to Home, delete project, and logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { navigateToProjectOverview } from '../../support/functions/conversation'; +import { + clickSendButton, + getDynamicSuggestionIds, + openAskSpecificDetailsWithoutSending, + typeMessage, + verifyDynamicSuggestionsAfterMessage, + verifyInitialSuggestionState, + waitForAITyping +} from '../../support/functions/chat'; + +describe('Ask Feature Flow (Dynamic Suggestions)', () => { + let projectId; + + beforeEach(() => { + cy.on('uncaught:exception', (err) => { + if (err.message.includes('Minified React error #185')) { + return false; + } + }); + loginToApp(); + }); + + it('should verify suggestions update dynamically after sending first chat message', () => { + let beforeDynamicSuggestionIds = []; + + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + // 2. Open Ask in Specific Details mode, but do not send yet + cy.log('Step 2: Opening Ask in Specific Details mode'); + openAskSpecificDetailsWithoutSending(); + + // 3. Verify initial suggestion state before first message + cy.log('Step 3: Verifying initial suggestions before sending message'); + verifyInitialSuggestionState(); + cy.get('[data-testid="chat-templates-more-button"]').should('be.visible'); + getDynamicSuggestionIds().then((ids) => { + beforeDynamicSuggestionIds = ids; + cy.log(`Dynamic suggestions before first message: ${ids.length}`); + expect(ids.length, 'dynamic suggestions before first message').to.equal(0); + }); + + // 4. Send first message + cy.log('Step 4: Sending first message'); + typeMessage('hello'); + clickSendButton(); + waitForAITyping(90000); + + cy.wait(20000); + + // 5. Verify 3 dynamic suggestions appear after message + cy.log('Step 5: Verifying dynamic suggestions after first message'); + verifyDynamicSuggestionsAfterMessage(beforeDynamicSuggestionIds, 3, 120000, 3); + cy.get('[data-testid="chat-template-static-summarize"]').should('be.visible'); + cy.get('[data-testid="chat-template-static-compare-&-contrast"]').should('be.visible'); + cy.get('[data-testid="chat-templates-more-button"]').should('be.visible'); + getDynamicSuggestionIds().then((afterIds) => { + const uniqueAfterIds = [...new Set(afterIds)]; + expect(uniqueAfterIds.length, 'dynamic suggestions after first message').to.equal(3); + }); + + // 6. Navigate to Project Overview + cy.log('Step 6: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 7. Delete the project + cy.log('Step 7: Deleting project'); + cy.then(() => { + deleteProject(projectId); + }); + + // 8. Open Settings menu and Logout + cy.log('Step 8: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/25-delete-conversation.cy.js b/echo/cypress/e2e/suites/25-delete-conversation.cy.js new file mode 100644 index 00000000..39b9c2f2 --- /dev/null +++ b/echo/cypress/e2e/suites/25-delete-conversation.cy.js @@ -0,0 +1,102 @@ +/** + * Upload Conversation Flow Test Suite + * + * This test verifies the complete flow of: + * 1. Login and create a new project + * 2. Upload an audio file via the upload conversation modal + * 3. Wait for processing and close the modal + * 4. Click on the uploaded conversation and verify its name + * 5. Delete the conversation and verify empty state + * 6. Navigate to project overview and delete project + * 7. Logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + selectConversation, + verifyConversationName, + deleteConversation, + navigateToProjectOverview +} from '../../support/functions/conversation'; + +describe('Upload Conversation Flow', () => { + let projectId; + + beforeEach(() => { + loginToApp(); + }); + + it('should upload audio file, verify conversation, delete conversation, delete project, and logout', () => { + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + // 2. Open Upload Conversation modal + cy.log('Step 2: Opening upload modal'); + openUploadModal(); + + // 3. Upload the audio file from cypress assets + cy.log('Step 3: Uploading audio file'); + uploadAudioFile('assets/videoplayback.mp3'); + + // 4. Click Upload Files button to start the upload + cy.log('Step 4: Clicking Upload Files button'); + clickUploadFilesButton(); + + // 5. Wait 15 seconds for processing + cy.log('Step 5: Waiting 15 seconds for file processing'); + cy.wait(15000); + + // 6. Close the upload modal + cy.log('Step 6: Closing upload modal'); + closeUploadModal(); + + // 7. Click on the uploaded conversation in the list + cy.log('Step 7: Selecting uploaded conversation'); + selectConversation('videoplayback.mp3'); + + // 8. Verify the conversation name in Edit Conversation section + cy.log('Step 8: Verifying conversation name'); + verifyConversationName('videoplayback.mp3'); + + // 9. Scroll to delete button and delete conversation (accept browser popup) + cy.log('Step 9: Deleting conversation'); + deleteConversation(true); + + // 10. Verify no conversations state after deletion + cy.log('Step 10: Verifying no conversations state'); + cy.contains('p:visible', /^No conversations found\./).should('be.visible'); + cy.log('No conversations found'); + + // 11. Navigate back to project overview via breadcrumb + cy.log('Step 11: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 12. Delete the project (includes clicking Project Settings tab) + cy.log('Step 12: Deleting project'); + cy.then(() => { + deleteProject(projectId); + }); + + // 13. Open Settings menu and Logout + cy.log('Step 13: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/26-download-conversation.cy.js b/echo/cypress/e2e/suites/26-download-conversation.cy.js new file mode 100644 index 00000000..b595a7cb --- /dev/null +++ b/echo/cypress/e2e/suites/26-download-conversation.cy.js @@ -0,0 +1,123 @@ +/** + * Upload Conversation Flow Test Suite + * + * This test verifies the complete flow of: + * 1. Login and create a new project + * 2. Upload an audio file via the upload conversation modal + * 3. Wait for processing and close the modal + * 4. Click on the uploaded conversation and verify its name + * 5. Download merged conversation audio and verify file exists + * 6. Delete the downloaded file from cypress downloads + * 7. Navigate to project overview and delete project + * 8. Logout + */ + +import { + clickUploadFilesButton, + closeUploadModal, + downloadAudio, + navigateToProjectOverview, + openUploadModal, + selectConversation, + uploadAudioFile, + verifyConversationName, +} from "../../support/functions/conversation"; +import { loginToApp, logout } from "../../support/functions/login"; +import { createProject, deleteProject } from "../../support/functions/project"; +import { openSettingsMenu } from "../../support/functions/settings"; + +describe("Download Conversation Flow", () => { + let projectId; + let audioDownloadHref; + + beforeEach(() => { + loginToApp(); + }); + + it("should upload audio file, download conversation audio, delete project, and logout", () => { + // 1. Create new project + cy.log("Step 1: Creating new project"); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split("/"); + const projectIndex = parts.indexOf("projects"); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log("Captured Project ID:", projectId); + } + }); + + // 2. Open Upload Conversation modal + cy.log("Step 2: Opening upload modal"); + openUploadModal(); + + // 3. Upload the audio file from cypress assets + cy.log("Step 3: Uploading audio file"); + uploadAudioFile("assets/videoplayback.mp3"); + + // 4. Click Upload Files button to start the upload + cy.log("Step 4: Clicking Upload Files button"); + clickUploadFilesButton(); + + // 5. Wait 15 seconds for processing + cy.log("Step 5: Waiting 15 seconds for file processing"); + cy.wait(15000); + + // 6. Close the upload modal + cy.log("Step 6: Closing upload modal"); + closeUploadModal(); + + // 7. Click on the uploaded conversation in the list + cy.log("Step 7: Selecting uploaded conversation"); + selectConversation("videoplayback.mp3"); + + // 8. Verify the conversation name in Edit Conversation section + cy.log("Step 8: Verifying conversation name"); + verifyConversationName("videoplayback.mp3"); + + // 9. Download conversation audio + cy.log("Step 9: Downloading conversation audio"); + cy.get('[data-testid="conversation-download-audio-button"]') + .should("have.attr", "href") + .then((href) => { + expect(href, "conversation audio href").to.be.a("string").and.not.be + .empty; + audioDownloadHref = href; + }); + downloadAudio(); + + // 10. Verify download endpoint returns a valid signed URL + // The button opens an anchor target, which may not produce a local file in all runners. + cy.log("Step 10: Verifying conversation audio download endpoint"); + cy.then(() => { + const separator = audioDownloadHref.includes("?") ? "&" : "?"; + const signedUrlEndpoint = `${audioDownloadHref}${separator}return_url=true`; + cy.request(signedUrlEndpoint).then((response) => { + expect(response.status, "download endpoint status").to.eq(200); + expect(response.body, "signed merged mp3 url").to.be.a("string").and.not + .be.empty; + expect(response.body, "signed merged mp3 url format").to.match( + /^https?:\/\//, + ); + expect(response.body, "signed merged mp3 filename").to.include(".mp3"); + }); + }); + + // 11. Navigate back to project overview via breadcrumb + cy.log("Step 11: Navigating to Project Overview"); + navigateToProjectOverview(); + + // 12. Delete the project (includes clicking Project Settings tab) + cy.log("Step 12: Deleting project"); + cy.then(() => { + deleteProject(projectId); + }); + + // 13. Open Settings menu and Logout + cy.log("Step 13: Opening settings and logging out"); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/27-retranscribe-conversation.cy.js b/echo/cypress/e2e/suites/27-retranscribe-conversation.cy.js new file mode 100644 index 00000000..24fea5d0 --- /dev/null +++ b/echo/cypress/e2e/suites/27-retranscribe-conversation.cy.js @@ -0,0 +1,137 @@ +/** + * Upload Conversation Flow Test Suite + * + * This test verifies the complete flow of: + * 1. Login and create a new project + * 2. Upload an audio file via the upload conversation modal + * 3. Wait for processing and close the modal + * 4. Click on the uploaded conversation and verify its name + * 5. Verify transcript text + * 6. Navigate to project overview and delete project + * 7. Logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + selectConversation, + verifyConversationInList, + verifyConversationName, + clickTranscriptTab, + verifyTranscriptText, + retranscribeConversation, + navigateToProjectOverview +} from '../../support/functions/conversation'; + +describe('Upload Conversation Flow', () => { + let projectId; + + beforeEach(() => { + loginToApp(); + }); + + it('should upload audio file, verify conversation, delete project, and logout', () => { + const retranscribedConversationName = `Retranscribed Conversation ${Date.now()}`; + + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID for deletion + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + // 2. Open Upload Conversation modal + cy.log('Step 2: Opening upload modal'); + openUploadModal(); + + // 3. Upload the audio file from cypress assets + cy.log('Step 3: Uploading audio file'); + uploadAudioFile('assets/videoplayback.mp3'); + + // 4. Click Upload Files button to start the upload + cy.log('Step 4: Clicking Upload Files button'); + clickUploadFilesButton(); + + // 5. Wait 15 seconds for processing + cy.log('Step 5: Waiting 15 seconds for file processing'); + cy.wait(15000); + + // 6. Close the upload modal + cy.log('Step 6: Closing upload modal'); + closeUploadModal(); + + // 7. Click on the uploaded conversation in the list + cy.log('Step 7: Selecting uploaded conversation'); + selectConversation('videoplayback.mp3'); + + // 8. Verify the conversation name in Edit Conversation section + cy.log('Step 8: Verifying conversation name'); + verifyConversationName('videoplayback.mp3'); + + // 9. Wait 25 seconds for transcript processing + cy.log('Step 9: Waiting 25 seconds for transcript processing'); + cy.wait(25000); + + // 10. Click on Transcript tab + cy.log('Step 10: Clicking Transcript tab'); + clickTranscriptTab(); + + // 11. Verify transcript text has at least 100 characters + cy.log('Step 11: Verifying transcript text'); + verifyTranscriptText(400); + + // 12. Retranscribe with a random conversation name + cy.log('Step 12: Retranscribing conversation with a random name'); + retranscribeConversation(retranscribedConversationName); + + // 13. Wait for retranscription processing + cy.log('Step 13: Waiting 40 seconds for retranscription processing'); + cy.wait(40000); + + // 14. Verify conversation name with the new retranscribed name + cy.log('Step 14: Verifying retranscribed conversation name'); + verifyConversationInList(retranscribedConversationName); + selectConversation(retranscribedConversationName); + verifyConversationName(retranscribedConversationName); + + // 15. Wait for transcript regeneration + cy.log('Step 15: Waiting 25 seconds for transcript processing'); + cy.wait(25000); + + // 16. Click on Transcript tab again + cy.log('Step 16: Clicking Transcript tab after retranscribe'); + clickTranscriptTab(); + + // 17. Verify transcript text again + cy.log('Step 17: Verifying transcript text after retranscribe'); + verifyTranscriptText(400); + + // 18. Navigate back to project overview via breadcrumb + cy.log('Step 18: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 19. Delete the project (includes clicking Project Settings tab) + cy.log('Step 19: Deleting project'); + cy.then(() => { + deleteProject(projectId); + }); + + // 20. Open Settings menu and Logout + cy.log('Step 20: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); + diff --git a/echo/cypress/e2e/suites/28-move-conversation-between-projects.cy.js b/echo/cypress/e2e/suites/28-move-conversation-between-projects.cy.js new file mode 100644 index 00000000..48e1f2f5 --- /dev/null +++ b/echo/cypress/e2e/suites/28-move-conversation-between-projects.cy.js @@ -0,0 +1,158 @@ +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject, updateProjectName, navigateToHome } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + selectConversation, + verifyConversationName, + verifyConversationInList, + moveConversationToProjectById, + navigateToProjectOverview +} from '../../support/functions/conversation'; + +describe('Move Conversation Between Projects Flow', () => { + let firstProjectId; + let secondProjectId; + let movedConversationId; + + const firstProjectName = `Move Target Project ${Date.now()}`; + + beforeEach(() => { + loginToApp(); + }); + + it('should move conversation from second project to first project, verify, delete both projects, and logout', () => { + // 1. Create first project + cy.log('Step 1: Creating first project'); + createProject(); + + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + firstProjectId = parts[projectIndex + 1]; + cy.log(`Captured first project ID: ${firstProjectId}`); + } + }); + + // 2. Rename first project with random name (edit flow style) + cy.log('Step 2: Renaming first project'); + updateProjectName(firstProjectName); + cy.get('[data-testid="project-breadcrumb-name"]').filter(':visible').first().should('contain.text', firstProjectName); + + // 3. Go to projects home + cy.log('Step 3: Navigating to projects home'); + navigateToHome(); + + // 4. Create second project (source project for upload) + cy.log('Step 4: Creating second project'); + createProject(); + + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + secondProjectId = parts[projectIndex + 1]; + cy.log(`Captured second project ID: ${secondProjectId}`); + } + }); + + cy.then(() => { + expect(firstProjectId, 'first project ID').to.be.a('string').and.not.be.empty; + expect(secondProjectId, 'second project ID').to.be.a('string').and.not.be.empty; + expect(secondProjectId, 'second project should be different').to.not.equal(firstProjectId); + }); + + // 5. Upload a conversation in second project + cy.log('Step 5: Opening upload modal'); + openUploadModal(); + + cy.log('Step 6: Uploading audio file'); + uploadAudioFile('assets/videoplayback.mp3'); + + cy.log('Step 7: Starting upload'); + clickUploadFilesButton(); + + cy.log('Step 8: Waiting for upload processing'); + cy.wait(15000); + + cy.log('Step 9: Closing upload modal'); + closeUploadModal(); + + // 6. Open uploaded conversation and verify + cy.log('Step 10: Selecting uploaded conversation'); + selectConversation('videoplayback.mp3'); + + cy.log('Step 11: Verifying conversation name before move'); + verifyConversationName('videoplayback.mp3'); + + cy.url().then((url) => { + const parts = url.split('/'); + const conversationIndex = parts.indexOf('conversation'); + if (conversationIndex !== -1 && parts[conversationIndex + 1]) { + movedConversationId = parts[conversationIndex + 1]; + cy.log(`Captured conversation ID to move: ${movedConversationId}`); + } + }); + + // 7. Move conversation to first project via modal search + exact project ID radio + cy.log('Step 12: Moving conversation to first project'); + cy.then(() => { + expect(firstProjectId, 'first project ID before move').to.be.a('string').and.not.be.empty; + moveConversationToProjectById(firstProjectName, firstProjectId); + }); + + cy.log('Step 13: Waiting for transfer'); + cy.wait(20000); + + // 8. Verify URL switched to first project + cy.log('Step 14: Verifying URL changed to first project'); + cy.then(() => { + expect(firstProjectId, 'first project ID for URL check').to.be.a('string').and.not.be.empty; + expect(secondProjectId, 'second project ID for URL check').to.be.a('string').and.not.be.empty; + cy.url().should('include', `/projects/${firstProjectId}/`); + cy.url().then((currentUrl) => { + expect(currentUrl).to.not.include(`/projects/${secondProjectId}/`); + if (movedConversationId) { + expect(currentUrl).to.include(`/conversation/${movedConversationId}/`); + } + cy.log('Conversation transfer successful: URL now points to first project'); + }); + }); + + // 9. Verify conversation is selectable and name is correct in first project + cy.log('Step 15: Verifying moved conversation in first project list'); + verifyConversationInList('videoplayback.mp3'); + selectConversation('videoplayback.mp3'); + verifyConversationName('videoplayback.mp3'); + + cy.log('Step 16: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 10. Delete first project + cy.log('Step 17: Deleting first project'); + cy.then(() => { + deleteProject(firstProjectId); + }); + + // 11. Open and delete second project + cy.log('Step 18: Opening and deleting second project'); + cy.then(() => { + cy.get('main').within(() => { + cy.get(`a[href*="${secondProjectId}"]`, { timeout: 10000 }).filter(':visible').first().should('be.visible').click(); + }); + }); + cy.wait(3000); + cy.then(() => { + deleteProject(secondProjectId); + }); + + // 12. Logout + cy.log('Step 19: Opening settings and logging out'); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/29-search-with-tags.cy.js b/echo/cypress/e2e/suites/29-search-with-tags.cy.js new file mode 100644 index 00000000..b6da8dbe --- /dev/null +++ b/echo/cypress/e2e/suites/29-search-with-tags.cy.js @@ -0,0 +1,156 @@ +/** + * Project Tags & Conversation Flow + * + * This test verifies the flow of: + * 1. Login and create a new project + * 2. Add tags to the project in Portal Editor + * 3. Upload an audio file via the upload conversation modal (Dashboard) + * 4. Verify tags are selectable in the conversation overview + * 5. Verify selected tags are visible + * 6. DELETE project and Logout + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { openPortalEditor, addTag } from '../../support/functions/portal'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal, + selectConversation, + selectConversationTags, + verifySelectedTags, + navigateToProjectOverview, + searchConversation, + toggleFilterOptions +} from '../../support/functions/conversation'; + +describe('Project Tags & Conversation Flow', () => { + let projectId; + const tag1 = 'TagOne'; + const tag2 = 'TagTwo'; + + beforeEach(() => { + loginToApp(); + }); + + it('should create project with tags, upload audio, and verify tags in conversation', () => { + // 1. Create new project + cy.log('Step 1: Creating new project'); + createProject(); + + // Capture project ID + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log('Captured Project ID:', projectId); + } + }); + + // 2. Add Tags in Portal Editor + cy.log('Step 2: Adding tags in Portal Editor'); + openPortalEditor(); + addTag(tag1); + addTag(tag2); + + // Return to Project Overview to upload file + cy.log('Step 3: Returning to Project Overview'); + navigateToProjectOverview(); + + // 3. Upload Conversation (Manual Flow) + cy.log('Step 4: Uploading audio file'); + openUploadModal(); + uploadAudioFile('assets/videoplayback.mp3'); + clickUploadFilesButton(); + + // Wait for processing + cy.log('Step 5: Waiting 15 seconds for file processing'); + cy.wait(15000); + closeUploadModal(); + + // 4. Select Conversation & Verify Tags + cy.log('Step 6: Selecting uploaded conversation'); + selectConversation('videoplayback.mp3'); + + // Verify tags input and select tags + cy.log('Step 7: Selecting and verifying tags'); + selectConversationTags([tag1, tag2]); + + // Verify they are shown as selected + cy.log('Step 8: Verifying selected tags visibility'); + verifySelectedTags([tag1, tag2]); + navigateToProjectOverview(); + + // 3. Upload Conversation (Manual Flow) + cy.log('Step 9: Uploading audio file'); + openUploadModal(); + uploadAudioFile('assets/sampleaudio.mp3'); + clickUploadFilesButton(); + + // Wait for processing + cy.log('Step 10: Waiting 15 seconds for file processing'); + cy.wait(15000); + closeUploadModal(); + + cy.log('Step 11: Navigating to Project Overview'); + navigateToProjectOverview(); + + // 5. Search by auto-formatted conversation name and verify exactly one result + cy.log('Step 12: Searching for auto-formatted "- videoplayback.mp3"'); + searchConversation('- videoplayback.mp3'); + + cy.log('Step 13: Verifying only one search result is returned'); + cy.get('[data-testid^="conversation-item-"]').filter(':visible').should('have.length', 1); + cy.get('[data-testid^="conversation-item-"]').filter(':visible').first() + .should('contain.text', '- videoplayback.mp3'); + cy.log('Search test successful: exactly one conversation found for "- videoplayback.mp3"'); + + // Clear search so tag-filter validation is independent + cy.get('[data-testid="conversation-search-input"]').filter(':visible').first().clear(); + + // 6. Open tags filter, select both created tags, then close menu + cy.log('Step 14: Opening filter options and tags filter'); + cy.get('body').then(($body) => { + if ($body.find('[data-testid="conversation-filter-tags-button"]:visible').length === 0) { + toggleFilterOptions(); + } + }); + cy.get('[data-testid="conversation-filter-tags-button"]').filter(':visible').first().click(); + + cy.log('Step 15: Selecting both created tags in dropdown'); + cy.get('[data-menu-dropdown="true"]').filter(':visible').last().within(() => { + cy.contains('label', tag1).click(); + cy.contains('label', tag2).click(); + }); + + // Re-click tags button to close dropdown + cy.get('[data-testid="conversation-filter-tags-button"]').filter(':visible').first().click(); + + // 7. Verify tag filter returns exactly one conversation: - videoplayback.mp3 + cy.log('Step 16: Verifying tag-filtered result count and conversation name'); + cy.get('[data-testid^="conversation-item-"]').filter(':visible').should('have.length', 1); + cy.get('[data-testid^="conversation-item-"]').filter(':visible').first() + .should('contain.text', '- videoplayback.mp3'); + cy.log('Tag filter test successful: only "- videoplayback.mp3" is shown'); + + + + // 5. Cleanup + cy.log('Step 17: Cleanup - Deleting Project'); + navigateToProjectOverview(); + cy.then(() => { + if (projectId) { + deleteProject(projectId); + } + }); + + // Logout + cy.log('Step 18: Logging out'); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/30-report-lifecycle.cy.js b/echo/cypress/e2e/suites/30-report-lifecycle.cy.js new file mode 100644 index 00000000..2b569531 --- /dev/null +++ b/echo/cypress/e2e/suites/30-report-lifecycle.cy.js @@ -0,0 +1,183 @@ +/** + * Report Lifecycle Flow Test Suite + * + * Verifies report lifecycle states with single-origin tests: + * - Draft report generation + * - Published report with portal link disabled + * - Published report with portal link enabled + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal +} from '../../support/functions/conversation'; +import { + registerReportFlowExceptionHandling, + setReportPublishState, + setReportPortalLinkState, + waitForPublicReportPublished +} from '../../support/functions/report'; + +describe('Report Lifecycle Flow', () => { + let projectId; + let locale = 'en-US'; + + const portalBaseUrl = (Cypress.env('portalUrl') || 'https://portal.echo-next.dembrane.com').replace(/\/$/, ''); + const dashboardBaseUrl = (Cypress.env('dashboardUrl') || '').replace(/\/$/, ''); + + const resolveProjectId = () => { + return cy.then(() => { + if (!projectId) { + projectId = Cypress.env('reportLifecycleProjectId'); + } + + if (projectId) { + return projectId; + } + + return cy.readFile('fixtures/createdProjects.json', { log: false }).then((projects) => { + const lastProject = Array.isArray(projects) ? projects[projects.length - 1] : null; + if (!lastProject || !lastProject.id) { + throw new Error('projectId not found. Ensure report setup test completed.'); + } + projectId = lastProject.id; + Cypress.env('reportLifecycleProjectId', projectId); + return projectId; + }); + }).then((id) => { + expect(id, 'projectId').to.be.a('string').and.not.be.empty; + return id; + }); + }; + + const openDashboardReportPage = (id) => { + if (dashboardBaseUrl) { + cy.visit(`${dashboardBaseUrl}/projects/${id}/report`); + return; + } + + cy.visit(`/${locale}/projects/${id}/report`); + }; + + const openPublicReportPage = (id) => { + cy.visit(`${portalBaseUrl}/${locale}/${id}/report`); + }; + + it('creates a project and generates a report draft', () => { + registerReportFlowExceptionHandling(); + loginToApp(); + + cy.log('Step 1: Creating new project'); + createProject(); + + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + if (parts[projectIndex - 1]) { + locale = parts[projectIndex - 1]; + } + Cypress.env('reportLifecycleProjectId', projectId); + cy.log('Captured Project ID:', projectId); + } + }); + + cy.log('Step 2: Uploading audio'); + openUploadModal(); + uploadAudioFile('assets/videoplayback.mp3'); + clickUploadFilesButton(); + cy.wait(20000); + closeUploadModal(); + + cy.log('Step 3: Creating report'); + cy.get('[data-testid="sidebar-report-button"]').filter(':visible').first().click(); + cy.get('section[role="dialog"]').should('be.visible'); + cy.get('[data-testid="report-create-button"]').filter(':visible').first().click(); + cy.wait(30000); + + cy.get('[data-testid="sidebar-report-button"]').filter(':visible').first().click(); + cy.get('[data-testid="report-renderer-container"]', { timeout: 20000 }).should('be.visible'); + }); + + it('publishes report and disables portal link in settings', () => { + registerReportFlowExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + openDashboardReportPage(id); + }); + + setReportPublishState(true); + setReportPortalLinkState(false); + }); + + it('shows published report without portal CTA when portal link is disabled', () => { + registerReportFlowExceptionHandling(); + + resolveProjectId().then((id) => { + openPublicReportPage(id); + }); + + // waitForPublicReportPublished(); + cy.get('[data-testid="report-renderer-container"]').should('be.visible'); + cy.get('[data-testid="public-report-not-available"]').should('not.exist'); + cy.contains('Do you want to contribute to this project?').should('not.exist'); + cy.contains('a', 'Share your voice').should('not.exist'); + }); + + it('enables portal link in report settings', () => { + registerReportFlowExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + openDashboardReportPage(id); + }); + + setReportPublishState(true); + setReportPortalLinkState(true); + }); + + it('shows published report with portal CTA when portal link is enabled', () => { + registerReportFlowExceptionHandling(); + + resolveProjectId().then((id) => { + openPublicReportPage(id); + }); + + // waitForPublicReportPublished(); + resolveProjectId().then((id) => { + cy.get('[data-testid="report-renderer-container"]').should('be.visible'); + cy.get('[data-testid="public-report-not-available"]').should('not.exist'); + // Use text content since data-testid attributes are missing on these components + cy.contains('Do you want to contribute to this project?').should('be.visible'); + cy.contains('a', 'Share your voice') + .should('be.visible') + .and('have.attr', 'href') + .and('include', `/${locale}/${id}/start`); + }); + }); + + it('deletes the project and logs out', () => { + registerReportFlowExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + if (dashboardBaseUrl) { + cy.visit(`${dashboardBaseUrl}/projects/${id}/overview`); + } else { + cy.visit(`/${locale}/projects/${id}/overview`); + } + deleteProject(id); + Cypress.env('reportLifecycleProjectId', null); + }); + + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/31-print-report.cy.js b/echo/cypress/e2e/suites/31-print-report.cy.js new file mode 100644 index 00000000..ce1f0f34 --- /dev/null +++ b/echo/cypress/e2e/suites/31-print-report.cy.js @@ -0,0 +1,168 @@ +/** + * Publish Report Flow Test Suite + * + * Split into single-origin tests so it runs in Chromium/Firefox/WebKit + * without relying on cy.origin(). + */ + +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { + openUploadModal, + uploadAudioFile, + clickUploadFilesButton, + closeUploadModal +} from '../../support/functions/conversation'; +import { + registerReportFlowExceptionHandling, + setReportPublishState +} from '../../support/functions/report'; + +describe('Publish Report Flow', () => { + let projectId; + let locale = 'en-US'; + const localeSegmentPattern = /^[a-z]{2}-[A-Z]{2}$/; + + const portalBaseUrl = (Cypress.env('portalUrl') || 'https://portal.echo-next.dembrane.com').replace(/\/$/, ''); + const dashboardBaseUrl = (Cypress.env('dashboardUrl') || '').replace(/\/$/, ''); + const buildDashboardProjectUrl = (id, page) => { + if (!dashboardBaseUrl) { + return `/${locale}/projects/${id}/${page}`; + } + + const baseUrl = new URL(`${dashboardBaseUrl}/`); + const basePathSegments = baseUrl.pathname.split('/').filter(Boolean); + const baseHasLocale = basePathSegments.some((segment) => localeSegmentPattern.test(segment)); + const relativePath = baseHasLocale + ? `projects/${id}/${page}` + : `${locale}/projects/${id}/${page}`; + + return new URL(relativePath, baseUrl).toString(); + }; + + const resolveProjectId = () => { + return cy.then(() => { + if (!projectId) { + projectId = Cypress.env('publishReportProjectId'); + } + + if (projectId) { + return projectId; + } + + return cy.readFile('fixtures/createdProjects.json', { log: false }).then((projects) => { + const lastProject = Array.isArray(projects) ? projects[projects.length - 1] : null; + if (!lastProject || !lastProject.id) { + throw new Error('projectId not found. Ensure report setup test completed.'); + } + projectId = lastProject.id; + Cypress.env('publishReportProjectId', projectId); + return projectId; + }); + }).then((id) => { + expect(id, 'projectId').to.be.a('string').and.not.be.empty; + return id; + }); + }; + + const openDashboardReportPage = (id) => { + cy.visit(buildDashboardProjectUrl(id, 'report')); + }; + + const openPublicReportPage = (id) => { + cy.visit(`${portalBaseUrl}/${locale}/${id}/report`); + }; + + it('creates a project and generates a report draft', () => { + registerReportFlowExceptionHandling(); + loginToApp(); + + cy.log('Step 1: Creating new project'); + createProject(); + + cy.location('pathname').then((pathname) => { + const pathSegments = pathname.split('/').filter(Boolean); + const projectIndex = pathSegments.indexOf('projects'); + if (projectIndex !== -1 && pathSegments[projectIndex + 1]) { + projectId = pathSegments[projectIndex + 1]; + const localeSegment = pathSegments[projectIndex - 1]; + if (localeSegment && localeSegmentPattern.test(localeSegment)) { + locale = localeSegment; + } + Cypress.env('publishReportProjectId', projectId); + cy.log('Captured Project ID:', projectId); + } + }); + + cy.log('Step 2: Uploading audio'); + openUploadModal(); + uploadAudioFile('assets/videoplayback.mp3'); + clickUploadFilesButton(); + cy.wait(20000); + closeUploadModal(); + + cy.log('Step 3: Creating report'); + cy.get('[data-testid="sidebar-report-button"]').filter(':visible').first().click(); + cy.get('section[role="dialog"]').should('be.visible'); + cy.get('[data-testid="report-create-button"]').filter(':visible').first().click(); + cy.wait(30000); + + cy.get('[data-testid="sidebar-report-button"]').filter(':visible').first().click(); + cy.get('[data-testid="report-renderer-container"]', { timeout: 20000 }).should('be.visible'); + }); + + + + it('print report', () => { + registerReportFlowExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + openDashboardReportPage(id); + setReportPublishState(true); + cy.get('[data-testid="report-renderer-container"]', { timeout: 20000 }).should('be.visible'); + let expectedPrintUrl; + cy.location('pathname').then((pathname) => { + const localeSegment = pathname + .split('/') + .filter(Boolean) + .find((segment) => localeSegmentPattern.test(segment)); + if (localeSegment) { + locale = localeSegment; + } + expectedPrintUrl = `${portalBaseUrl}/${locale}/${id}/report?print=true`; + }); + + cy.window().then((win) => { + cy.stub(win, 'open').as('windowOpen'); + }); + + cy.get('[data-testid="report-print-button"]').filter(':visible').first().click(); + + cy.get('@windowOpen').should('have.been.calledOnce'); + cy.get('@windowOpen').then((openStub) => { + const [openedUrl, target] = openStub.getCall(0).args; + expect(target, 'window.open target').to.equal('_blank'); + expect(openedUrl, 'print URL').to.equal(expectedPrintUrl); + }); + }); + + }); + + + + it('deletes the project and logs out', () => { + registerReportFlowExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + cy.visit(buildDashboardProjectUrl(id, 'overview')); + deleteProject(id); + Cypress.env('publishReportProjectId', null); + }); + + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/32-portal-participation-availability.cy.js b/echo/cypress/e2e/suites/32-portal-participation-availability.cy.js new file mode 100644 index 00000000..fde80fc4 --- /dev/null +++ b/echo/cypress/e2e/suites/32-portal-participation-availability.cy.js @@ -0,0 +1,252 @@ +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, deleteProject } from '../../support/functions/project'; +import { openSettingsMenu } from '../../support/functions/settings'; +import { toggleOpenForParticipation } from '../../support/functions/portal'; +import { openUploadModal, uploadAudioFile, clickUploadFilesButton, closeUploadModal } from '../../support/functions/conversation'; + +describe('Portal Participation Availability Flow', () => { + let projectId; + let locale = 'en-US'; + let participantLink; + + const localeSegmentPattern = /^[a-z]{2}-[A-Z]{2}$/; + const portalBaseUrl = (Cypress.env('portalUrl') || 'https://portal.echo-next.dembrane.com').replace(/\/$/, ''); + const dashboardBaseUrl = (Cypress.env('dashboardUrl') || '').replace(/\/$/, ''); + + const projectIdEnvKey = 'portalAvailabilityProjectId'; + const participantLinkEnvKey = 'portalAvailabilityParticipantLink'; + const localeEnvKey = 'portalAvailabilityLocale'; + + const registerExceptionHandling = () => { + cy.on('uncaught:exception', (err) => { + if (err.message.includes('Syntax error, unrecognized expression') || + err.message.includes('BODY[style=') || + err.message.includes('ResizeObserver loop limit exceeded') || + err.message.includes('Request failed with status code')) { + return false; + } + return true; + }); + }; + + const buildDashboardProjectUrl = (id, page) => { + if (!dashboardBaseUrl) { + return `/${locale}/projects/${id}/${page}`; + } + + const baseUrl = new URL(`${dashboardBaseUrl}/`); + const basePathSegments = baseUrl.pathname.split('/').filter(Boolean); + const baseHasLocale = basePathSegments.some((segment) => localeSegmentPattern.test(segment)); + const relativePath = baseHasLocale + ? `projects/${id}/${page}` + : `${locale}/projects/${id}/${page}`; + + return new URL(relativePath, baseUrl).toString(); + }; + + const clickVisibleCopyLinkButton = () => { + cy.get('[data-testid="project-copy-link-button"]').then(($buttons) => { + const $visibleButtons = $buttons.filter((index, button) => Cypress.$(button).is(':visible')); + if ($visibleButtons.length > 0) { + cy.wrap($visibleButtons.first()).click(); + return; + } + cy.wrap($buttons.first()).click({ force: true }); + }); + }; + + const assertOpenForParticipationState = (expectedState) => { + cy.get('[data-testid="dashboard-open-for-participation-toggle"]', { timeout: 20000 }) + .should('have.length.greaterThan', 0) + .then(($inputs) => { + const $visibleInput = Cypress.$($inputs).filter((_, el) => { + return Cypress.$(el).closest('.mantine-Switch-root').is(':visible'); + }).first(); + + const $target = $visibleInput.length > 0 ? $visibleInput : Cypress.$($inputs.first()); + expect($target.prop('checked'), 'open for participation toggle').to.equal(expectedState); + }); + }; + + const resolveProjectId = () => { + return cy.then(() => { + if (!projectId) { + projectId = Cypress.env(projectIdEnvKey); + } + if (Cypress.env(localeEnvKey)) { + locale = Cypress.env(localeEnvKey); + } + + if (projectId) { + return projectId; + } + + return cy.readFile('fixtures/createdProjects.json', { log: false }).then((projects) => { + const lastProject = Array.isArray(projects) ? projects[projects.length - 1] : null; + if (!lastProject || !lastProject.id) { + throw new Error('projectId not found. Ensure portal availability setup test completed.'); + } + projectId = lastProject.id; + Cypress.env(projectIdEnvKey, projectId); + return projectId; + }); + }).then((id) => { + expect(id, 'projectId').to.be.a('string').and.not.be.empty; + return id; + }); + }; + + const resolveParticipantLink = () => { + return cy.then(() => { + if (!participantLink) { + participantLink = Cypress.env(participantLinkEnvKey); + } + + if (!locale) { + locale = Cypress.env(localeEnvKey) || 'en-US'; + } + + if (participantLink) { + return participantLink; + } + + return resolveProjectId().then((id) => { + participantLink = `${portalBaseUrl}/${locale}/${id}/start`; + Cypress.env(participantLinkEnvKey, participantLink); + return participantLink; + }); + }).then((link) => { + expect(link, 'participantLink').to.be.a('string').and.not.be.empty; + return link; + }); + }; + + it('creates project and copies participant link', () => { + registerExceptionHandling(); + loginToApp(); + + createProject(); + + cy.location('pathname').then((pathname) => { + const pathSegments = pathname.split('/').filter(Boolean); + const projectIndex = pathSegments.indexOf('projects'); + if (projectIndex !== -1 && pathSegments[projectIndex + 1]) { + projectId = pathSegments[projectIndex + 1]; + const localeSegment = pathSegments[projectIndex - 1]; + if (localeSegment && localeSegmentPattern.test(localeSegment)) { + locale = localeSegment; + } + } + }).then(() => { + Cypress.env(projectIdEnvKey, projectId); + Cypress.env(localeEnvKey, locale); + expect(projectId, 'captured projectId').to.be.a('string').and.not.be.empty; + }); + + resolveProjectId().then((id) => { + cy.visit(buildDashboardProjectUrl(id, 'overview')); + }); + + // toggleOpenForParticipation(true); + + + + clickVisibleCopyLinkButton(); + cy.wait(1000); + + cy.window().then((win) => { + if (win.navigator.clipboard && typeof win.navigator.clipboard.readText === 'function') { + return win.navigator.clipboard.readText().catch(() => ''); + } + return ''; + }).then((copiedText) => { + const fallbackLink = `${portalBaseUrl}/${locale}/${projectId}/start`; + participantLink = copiedText && copiedText.trim().length > 0 ? copiedText.trim() : fallbackLink; + Cypress.env(participantLinkEnvKey, participantLink); + + expect(participantLink, 'participant link shape').to.include(`/${projectId}/start`); + expect(participantLink, 'participant link should not include undefined').to.not.include('/undefined/'); + }); + + }); + + it('opens participant link and verifies onboarding next button', () => { + registerExceptionHandling(); + + resolveParticipantLink().then((link) => { + cy.visit(link); + }); + + cy.get('[data-testid="portal-onboarding-next-button"]', { timeout: 30000 }).should('be.visible'); + }); + + it('closes participation from dashboard', () => { + registerExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + cy.visit(buildDashboardProjectUrl(id, 'overview')); + }); + + toggleOpenForParticipation(false); + cy.wait(20000); + assertOpenForParticipationState(false); + }); + + it('shows portal error alert after 60 seconds when participation is closed', () => { + registerExceptionHandling(); + + resolveParticipantLink().then((link) => { + cy.visit(link); + }); + + cy.wait(60000); + cy.reload(); + cy.get('[data-testid="portal-error-alert"]', { timeout: 20000 }).should('be.visible'); + }); + + it('checks upload conversation is not working', () => { + registerExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + cy.visit(buildDashboardProjectUrl(id, 'overview')); + }); + + openUploadModal(); + uploadAudioFile('assets/videoplayback.mp3'); + clickUploadFilesButton(); + cy.wait(15000); + closeUploadModal(); + + // Verify that no conversation was created since participation is closed + cy.wait(3000); + cy.get('body').then(($body) => { + const conversationItems = $body.find('[data-testid^="conversation-item-"]'); + if (conversationItems.length === 0) { + cy.log('SUCCESS: No conversations found — upload correctly blocked when participation is closed'); + } else { + cy.log(`FAILED: Found ${conversationItems.length} conversation(s) — upload should have been blocked`); + throw new Error( + `Expected no conversations when participation is closed, but found ${conversationItems.length} conversation item(s)` + ); + } + }); + }); + + it('deletes the project and logs out', () => { + registerExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + cy.visit(buildDashboardProjectUrl(id, 'overview')); + deleteProject(id); + Cypress.env(projectIdEnvKey, null); + Cypress.env(participantLinkEnvKey, null); + Cypress.env(localeEnvKey, null); + }); + + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/33-test-participant-portal-changes.cy.js b/echo/cypress/e2e/suites/33-test-participant-portal-changes.cy.js new file mode 100644 index 00000000..83a597e1 --- /dev/null +++ b/echo/cypress/e2e/suites/33-test-participant-portal-changes.cy.js @@ -0,0 +1,355 @@ +import { loginToApp, logout } from "../../support/functions/login"; +import { + agreeToPrivacyPolicy, + clickThroughOnboardingUntilCheckbox, + clickThroughOnboardingUntilMicrophone, + confirmFinishText, + enterNotificationEmail, + finishTextMode, + submitEmailNotification, + submitSessionForm, + submitText, + switchToTextMode, + typePortalText, + typeSessionName, + verifyAndSelectTag, +} from "../../support/functions/participant"; +import { + addTag, + openPortalEditor, + selectTutorial, + toggleAskForEmail, + toggleAskForName, + updatePortalContent, +} from "../../support/functions/portal"; +import { + createProject, + deleteProject, + navigateToHome, + updateProjectName, +} from "../../support/functions/project"; +import { openSettingsMenu } from "../../support/functions/settings"; + +describe("Project Create, Edit, and Delete Flow", () => { + let projectId; + let locale = "en-US"; + + const localeSegmentPattern = /^[a-z]{2}-[A-Z]{2}$/; + const portalBaseUrl = ( + Cypress.env("portalUrl") || "https://portal.echo-next.dembrane.com" + ).replace(/\/$/, ""); + const dashboardBaseUrl = (Cypress.env("dashboardUrl") || "").replace( + /\/$/, + "", + ); + + const projectIdEnvKey = "participantAudioProjectId"; + const participantLinkEnvKey = "participantUrl"; + const localeEnvKey = "participantLocale"; + const tagEnvKey = "participantTagName"; + const thankYouEnvKey = "participantThankYouContent"; + + const registerExceptionHandling = () => { + cy.on("uncaught:exception", (err) => { + if ( + err.message.includes("Syntax error, unrecognized expression") || + err.message.includes("BODY[style=") || + err.message.includes("ResizeObserver loop limit exceeded") || + err.message.includes("Request failed with status code") + ) { + return false; + } + return true; + }); + }; + + const buildDashboardProjectUrl = (id, page) => { + if (!dashboardBaseUrl) { + return `/${locale}/projects/${id}/${page}`; + } + + const baseUrl = new URL(`${dashboardBaseUrl}/`); + const basePathSegments = baseUrl.pathname.split("/").filter(Boolean); + const baseHasLocale = basePathSegments.some((segment) => + localeSegmentPattern.test(segment), + ); + const relativePath = baseHasLocale + ? `projects/${id}/${page}` + : `${locale}/projects/${id}/${page}`; + + return new URL(relativePath, baseUrl).toString(); + }; + + const clickVisibleCopyLinkButton = () => { + cy.get('[data-testid="project-copy-link-button"]').then(($buttons) => { + const $visibleButtons = $buttons.filter((index, button) => + Cypress.$(button).is(":visible"), + ); + if ($visibleButtons.length > 0) { + cy.wrap($visibleButtons.first()).click(); + return; + } + cy.wrap($buttons.first()).click({ force: true }); + }); + }; + + const resolveProjectId = () => { + return cy + .then(() => { + if (!projectId) { + projectId = Cypress.env(projectIdEnvKey); + } + if (Cypress.env(localeEnvKey)) { + locale = Cypress.env(localeEnvKey); + } + + if (projectId) { + return projectId; + } + + return cy + .readFile("fixtures/createdProjects.json", { log: false }) + .then((projects) => { + const lastProject = Array.isArray(projects) + ? projects[projects.length - 1] + : null; + if (!lastProject || !lastProject.id) { + throw new Error( + "projectId not found. Ensure the setup test completed successfully.", + ); + } + projectId = lastProject.id; + Cypress.env(projectIdEnvKey, projectId); + return projectId; + }); + }) + .then((id) => { + expect(id, "projectId").to.be.a("string").and.not.be.empty; + return id; + }); + }; + + const resolveParticipantUrl = () => { + return cy + .then(() => { + const existingLink = Cypress.env(participantLinkEnvKey); + if (existingLink && existingLink.trim().length > 0) { + return existingLink.trim(); + } + + return resolveProjectId().then((id) => { + const fallbackLink = `${portalBaseUrl}/${locale}/${id}/start`; + Cypress.env(participantLinkEnvKey, fallbackLink); + return fallbackLink; + }); + }) + .then((link) => { + expect(link, "participant link").to.be.a("string").and.not.be.empty; + return link; + }); + }; + + // beforeEach(() => { + // loginToApp(); + // }); + + // Shared variables are stored in Cypress.env to persist across tests in the same suite + + it("Part 1: Admin Setup - Create Project and Configure Portal", () => { + registerExceptionHandling(); + loginToApp(); + const uniqueId = Cypress._.random(0, 10000); + const newProjectName = `New Project_${uniqueId}`; + const portalTitle = `Title_${uniqueId}`; + const portalContent = `Content_${uniqueId}`; + const thankYouContent = `ThankYou_${uniqueId}`; + const tagName = `Tag_${uniqueId}`; + const portalLanguage = "it"; // Italian + + // 1. Create Project + createProject(); + + cy.location("pathname").then((pathname) => { + const pathSegments = pathname.split("/").filter(Boolean); + const projectIndex = pathSegments.indexOf("projects"); + if (projectIndex !== -1 && pathSegments[projectIndex + 1]) { + const createdProjectId = pathSegments[projectIndex + 1]; + const localeSegment = pathSegments[projectIndex - 1]; + if (localeSegment && localeSegmentPattern.test(localeSegment)) { + locale = localeSegment; + } + cy.log(`Working with Project ID: ${createdProjectId}`); + + // Save to env for subsequent tests + projectId = createdProjectId; + Cypress.env(projectIdEnvKey, createdProjectId); + Cypress.env(localeEnvKey, locale); + Cypress.env(tagEnvKey, tagName); + Cypress.env(thankYouEnvKey, thankYouContent); + Cypress.env("portalLanguage", portalLanguage); // Save language too + + // 2. Edit Project Name + updateProjectName(newProjectName); + + // 3. Edit Portal Settings + openPortalEditor(); + toggleAskForName(true); + toggleAskForEmail(true); + selectTutorial("Basic"); + addTag(tagName); + updatePortalContent(portalTitle, portalContent, thankYouContent); + + // 4. Return to Home and Verify Name in List + navigateToHome(); + cy.wait(2000); // Wait for list reload + + // Check if the project list contains the new name + cy.get("main").within(() => { + cy.get(`a[href*="${createdProjectId}"]`) + .first() + .should("contain.text", newProjectName); + }); + + // 5. Enter Project and Verify Changes + cy.get("main").within(() => { + cy.get(`a[href*="${createdProjectId}"]`).first().click(); + }); + cy.wait(3000); // Wait for dashboard load + + // Check Name on Dashboard + cy.get('[data-testid="project-breadcrumb-name"]').should( + "contain.text", + newProjectName, + ); + + clickVisibleCopyLinkButton(); + + // Wait for copy action + cy.wait(1000); + + // Store participant URL from clipboard with fallback. + cy.window() + .then((win) => { + if ( + win.navigator.clipboard && + typeof win.navigator.clipboard.readText === "function" + ) { + return win.navigator.clipboard.readText().catch(() => ""); + } + return ""; + }) + .then((copiedText) => { + const fallbackLink = `${portalBaseUrl}/${locale}/${createdProjectId}/start`; + const participantUrl = + copiedText && copiedText.trim().length > 0 + ? copiedText.trim() + : fallbackLink; + cy.log(`Participant URL: ${participantUrl}`); + Cypress.env(participantLinkEnvKey, participantUrl); + }); + } + }); + + // Logout to ensure clean state for Part 3 + openSettingsMenu(); + logout(); + }); + + it("Part 2: Participant Flow - Text Typing (Public URL)", () => { + // Retrieve env vars + const tagName = Cypress.env(tagEnvKey); + const thankYouContent = Cypress.env(thankYouEnvKey); + const sessionName = `Session_${Cypress._.random(0, 10000)}`; + registerExceptionHandling(); + + resolveParticipantUrl().then((link) => { + cy.log(`Opening participant portal: ${link}`); + cy.visit(link); + }); + + clickThroughOnboardingUntilCheckbox(); + + agreeToPrivacyPolicy(); + clickThroughOnboardingUntilMicrophone(); + + cy.log("Step 4: Microphone check (skipping for text mode)"); + // Skip microphone check if present + cy.get("body").then(($body) => { + if ( + $body.find('[data-testid="portal-onboarding-mic-skip-button"]').length > + 0 + ) { + cy.get('[data-testid="portal-onboarding-mic-skip-button"]') + .first() + .should("be.visible") + .click({ force: true }); + } + }); + + // Wait for name input + typeSessionName(sessionName); + + // Tag selection + if (tagName) { + verifyAndSelectTag(tagName); + } else { + cy.log("Tag name not found in env, selecting first available tag"); + verifyAndSelectTag(); + } + + submitSessionForm(); + + cy.log("Step 6: Switch to Text Mode"); + switchToTextMode(); + + cy.log("Step 7: Type and Submit Text"); + const textContent = + "This is a test response typed into the participant portal."; + typePortalText(textContent); + submitText(); + + cy.log("Step 8: Finish Conversation"); + finishTextMode(); + confirmFinishText(); + + cy.wait(2000); + + cy.log("Step 9: Verify Thank You Content"); + if (thankYouContent) { + cy.get('[data-testid="portal-finish-custom-message"]') + .contains(thankYouContent) + .should("be.visible"); + } else { + cy.log("Thank you content not found in env, skipping verification"); + } + + cy.log("Step 10: Email Notification"); + cy.get("body").then(($body) => { + if ($body.find('[data-testid="portal-finish-email-input"]').length > 0) { + enterNotificationEmail("test@example.com"); + submitEmailNotification(); + cy.wait(2000); + } else { + cy.log("Email input not found, skipping email notification"); + } + }); + }); + + it("deletes the project and logs out", () => { + registerExceptionHandling(); + loginToApp(); + + resolveProjectId().then((id) => { + cy.visit(buildDashboardProjectUrl(id, "overview")); + deleteProject(id); + Cypress.env(projectIdEnvKey, null); + Cypress.env(localeEnvKey, null); + Cypress.env(participantLinkEnvKey, null); + Cypress.env(tagEnvKey, null); + Cypress.env(thankYouEnvKey, null); + }); + + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/34-search-project-feature.cy.js b/echo/cypress/e2e/suites/34-search-project-feature.cy.js new file mode 100644 index 00000000..b0198150 --- /dev/null +++ b/echo/cypress/e2e/suites/34-search-project-feature.cy.js @@ -0,0 +1,72 @@ +import { loginToApp, logout } from '../../support/functions/login'; +import { createProject, verifyProjectPage, deleteProject, updateProjectName, navigateToHome } from '../../support/functions/project'; +import { openPortalEditor, selectTutorial, addTag, updatePortalContent, changePortalLanguage, toggleAskForName, toggleAskForEmail, searchProject } from '../../support/functions/portal'; +import { openSettingsMenu } from '../../support/functions/settings'; + +describe('Project Create, Edit, and Delete Flow', () => { + beforeEach(() => { + loginToApp(); + }); + + it('should create a project, edit its name and portal settings, verify changes, and delete it', () => { + const uniqueId = Cypress._.random(0, 10000); + const newProjectName = `New Project_${uniqueId}`; + const portalTitle = `Title_${uniqueId}`; + const portalContent = `Content_${uniqueId}`; + const thankYouContent = `ThankYou_${uniqueId}`; + const tagName = `Tag_${uniqueId}`; + const portalLanguage = 'it'; // Italian + + // 1. Create Project + createProject(); + + let createdProjectId; + cy.url().then((url) => { + const parts = url.split('/'); + const projectIndex = parts.indexOf('projects'); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + createdProjectId = parts[projectIndex + 1]; + cy.log(`Working with Project ID: ${createdProjectId}`); + + // 2. Edit Project Name + updateProjectName(newProjectName); + + // 3. Edit Portal Settings + openPortalEditor(); + + + // 4. Return to Home and Verify Name in List + navigateToHome(); + cy.wait(2000); // Wait for list reload + + // Search from the home search input and verify the filtered project result + searchProject(newProjectName); + cy.get('main').within(() => { + cy.contains('a[href]', newProjectName, { timeout: 10000 }) + .filter(':visible') + .first() + .as('projectResult') + .should('have.attr', 'href') + .and('include', createdProjectId); + }); + + // 5. Enter Project and Verify Changes + cy.get('@projectResult').click(); + cy.wait(3000); // Wait for dashboard load + + // Check Name on Dashboard - verify in the breadcrumb title + cy.get('[data-testid="project-breadcrumb-name"]').should('contain.text', newProjectName); + + // Check Portal Settings Persistence + openPortalEditor(); + + // 6. Delete Project + deleteProject(createdProjectId); + } + }); + + // 7. Logout + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/e2e/suites/35-register-flow.cy.js b/echo/cypress/e2e/suites/35-register-flow.cy.js new file mode 100644 index 00000000..5a47d5da --- /dev/null +++ b/echo/cypress/e2e/suites/35-register-flow.cy.js @@ -0,0 +1,96 @@ +describe('Register Flow', () => { + const registerExceptionHandling = () => { + cy.on('uncaught:exception', (err) => { + if ( + err.message.includes('ResizeObserver loop limit exceeded') || + err.message.includes('Request failed with status code') + ) { + return false; + } + return true; + }); + }; + + const openRegisterForm = () => { + cy.visit('/'); + cy.get('[data-testid="auth-login-email-input"]').should('be.visible'); + cy.get('[data-testid="auth-login-register-button"]').filter(':visible').first().click(); + cy.get('[data-testid="auth-register-first-name-input"]').should('be.visible'); + }; + + const buildUserData = () => { + const uniqueId = `${Date.now()}_${Cypress._.random(1000, 9999)}`; + return { + firstName: `Auto${uniqueId}`, + lastName: `User${uniqueId}`, + email: `autotest.${uniqueId}@gmail.com`, + password: `EchoTest@${Cypress._.random(100000, 999999)}` + }; + }; + + const fillRegisterForm = ({ + firstName, + lastName, + email, + password, + confirmPassword + }) => { + cy.get('[data-testid="auth-register-first-name-input"]').clear().type(firstName); + cy.get('[data-testid="auth-register-last-name-input"]').clear().type(lastName); + cy.get('[data-testid="auth-register-email-input"]').clear().type(email); + cy.get('[data-testid="auth-register-password-input"]').clear().type(password); + cy.get('[data-testid="auth-register-confirm-password-input"]').clear().type(confirmPassword); + }; + + beforeEach(() => { + registerExceptionHandling(); + openRegisterForm(); + }); + + it('opens register screen from login link and shows all required fields', () => { + cy.contains('h1', 'Create an Account').should('be.visible'); + cy.get('[data-testid="auth-register-first-name-input"]').should('be.visible'); + cy.get('[data-testid="auth-register-last-name-input"]').should('be.visible'); + cy.get('[data-testid="auth-register-email-input"]').should('be.visible'); + cy.get('[data-testid="auth-register-password-input"]').should('be.visible'); + cy.get('[data-testid="auth-register-confirm-password-input"]').should('be.visible'); + cy.get('[data-testid="auth-register-submit-button"]').should('be.visible'); + }); + + it('shows validation error when passwords do not match', () => { + const user = buildUserData(); + + fillRegisterForm({ + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + password: user.password, + confirmPassword: `${user.password}_mismatch` + }); + + cy.get('[data-testid="auth-register-submit-button"]').click(); + cy.contains('Passwords do not match').should('be.visible'); + }); + + it('submits registration with matching passwords and shows check email screen', () => { + const user = buildUserData(); + + fillRegisterForm({ + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + password: user.password, + confirmPassword: user.password + }); + + cy.get('[data-testid="auth-register-submit-button"]').click(); + + cy.get('[data-testid="auth-check-email-title"]', { timeout: 20000 }) + .should('be.visible') + .and('contain.text', 'Check your email'); + + cy.get('[data-testid="auth-check-email-text"]') + .should('be.visible') + .and('contain.text', 'We have sent you an email with next steps.'); + }); +}); diff --git a/echo/cypress/e2e/suites/participant-audio-flow.original.cy.js b/echo/cypress/e2e/suites/participant-audio-flow.original.cy.js new file mode 100644 index 00000000..211c2d7d --- /dev/null +++ b/echo/cypress/e2e/suites/participant-audio-flow.original.cy.js @@ -0,0 +1,467 @@ +/** + * Participant Audio Recording Flow Test Suite + * + * This test verifies the participant recording flow with REAL (injected) audio: + * 1. Login and create a new project + * 2. Navigate to participant portal + * 3. Select the "fake" microphone (injected via Chrome flags) + * 4. Record audio (which plays the injected wav file) + * 5. Verify transcription matches the audio content + */ + +import { + clickTranscriptTab, + navigateToProjectOverview, + selectConversation, + verifyConversationName, +} from "../../support/functions/conversation"; +import { loginToApp, logout } from "../../support/functions/login"; +import { createProject, deleteProject } from "../../support/functions/project"; +import { openSettingsMenu } from "../../support/functions/settings"; + +describe("Participant Audio Recording Flow", () => { + let projectId; + + beforeEach(() => { + // Ignore benign application errors + cy.on("uncaught:exception", (err, runnable) => { + if ( + err.message.includes("Syntax error, unrecognized expression") || + err.message.includes("BODY[style=") || + err.message.includes("ResizeObserver loop limit exceeded") + ) { + return false; + } + return true; + }); + loginToApp(); + }); + + it("should record audio using fake device and verify transcription", () => { + // 1. Create project + cy.log("Step 1: Creating new project"); + createProject(); + + // Capture project ID + cy.url().then((url) => { + const parts = url.split("/"); + const projectIndex = parts.indexOf("projects"); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + projectId = parts[projectIndex + 1]; + cy.log("Captured Project ID:", projectId); + } + }); + + // 2. Navigate to participant portal + cy.log("Step 2: Opening participant portal"); + cy.then(() => { + const portalBaseUrl = + Cypress.env("portalUrl") || "https://portal.echo-next.dembrane.com"; + const portalUrl = `${portalBaseUrl}/en-US/${projectId}/start`; + + // Explicitly grant microphone permission + cy.wrap(null).then(() => { + const dashboardUrl = + Cypress.env("dashboardUrl") || + "https://dashboard.echo-next.dembrane.com"; + + const grantPortal = Cypress.automation("remote:debugger:protocol", { + command: "Browser.grantPermissions", + params: { + origin: portalBaseUrl, + permissions: ["audioCapture"], + }, + }); + + const grantDashboard = Cypress.automation("remote:debugger:protocol", { + command: "Browser.grantPermissions", + params: { + origin: dashboardUrl, + permissions: ["audioCapture"], + }, + }); + + return Promise.all([grantPortal, grantDashboard]); + }); + + cy.readFile("assets/videoplayback.mp3", "base64").then((mp3Base64) => { + cy.origin( + portalBaseUrl, + { args: { mp3Base64, portalUrl, projectId } }, + ({ portalUrl, projectId, mp3Base64 }) => { + const installMediaStubs = (win) => { + const safeDefine = (obj, key, value) => { + if (!obj) { + return; + } + try { + Object.defineProperty(obj, key, { + configurable: true, + value, + writable: true, + }); + } catch (error) { + try { + obj[key] = value; + } catch (_error) {} + } + }; + + const patchAudioContext = (AudioContextCtor) => { + if (!AudioContextCtor || AudioContextCtor.__cypressPatched) { + return; + } + + AudioContextCtor.__cypressPatched = true; + const originalCreateAnalyser = + AudioContextCtor.prototype.createAnalyser; + if (typeof originalCreateAnalyser !== "function") { + return; + } + + AudioContextCtor.prototype.createAnalyser = function () { + const analyser = originalCreateAnalyser.call(this); + const originalGetByteTimeDomainData = + typeof analyser.getByteTimeDomainData === "function" + ? analyser.getByteTimeDomainData.bind(analyser) + : null; + + analyser.getByteTimeDomainData = (array) => { + if (originalGetByteTimeDomainData) { + originalGetByteTimeDomainData(array); + } + + // Force a strong signal so the UI marks mic test as successful. + for (let i = 0; i < array.length; i++) { + array[i] = 200; + } + }; + + return analyser; + }; + }; + + patchAudioContext(win.AudioContext); + patchAudioContext(win.webkitAudioContext); + + const mp3DataUrl = mp3Base64 + ? `data:audio/mpeg;base64,${mp3Base64}` + : null; + + const buildMp3Stream = () => { + if (win.__cypressMp3Stream) { + return win.__cypressMp3Stream; + } + + try { + const AudioContextCtor = + win.AudioContext || win.webkitAudioContext; + if (!AudioContextCtor) { + win.__cypressMp3Stream = new win.MediaStream(); + return win.__cypressMp3Stream; + } + + const audioCtx = new AudioContextCtor(); + const destination = audioCtx.createMediaStreamDestination(); + + if (mp3DataUrl) { + const audioEl = new win.Audio(); + audioEl.src = mp3DataUrl; + audioEl.loop = true; + audioEl.preload = "auto"; + audioEl.crossOrigin = "anonymous"; + + const source = audioCtx.createMediaElementSource(audioEl); + source.connect(destination); + + const startPlayback = () => { + if (audioCtx.state === "suspended") { + audioCtx.resume().catch(() => {}); + } + audioEl.play().catch(() => {}); + }; + + audioEl.addEventListener("canplay", startPlayback); + startPlayback(); + + win.__cypressMp3AudioElement = audioEl; + } else { + const oscillator = audioCtx.createOscillator(); + oscillator.connect(destination); + oscillator.start(); + } + + if (audioCtx.state === "suspended") { + audioCtx.resume().catch(() => {}); + } + + win.__cypressMp3AudioContext = audioCtx; + win.__cypressMp3Stream = destination.stream; + return win.__cypressMp3Stream; + } catch (error) { + win.__cypressMp3Stream = new win.MediaStream(); + return win.__cypressMp3Stream; + } + }; + + const ensureMp3Playback = () => { + if (win.__cypressMp3AudioElement) { + try { + win.__cypressMp3AudioElement.play().catch(() => {}); + } catch (_error) {} + } + }; + + const applyMediaStubs = () => { + if (!win.navigator.permissions) { + safeDefine(win.navigator, "permissions", {}); + } + + if (win.navigator.permissions) { + safeDefine(win.navigator.permissions, "query", () => + Promise.resolve({ + onchange: null, + state: "granted", + }), + ); + } + + if (!win.navigator.mediaDevices) { + safeDefine(win.navigator, "mediaDevices", {}); + } + + if (!win.navigator.mediaDevices) { + return; + } + + const fallbackDevices = [ + { + deviceId: "default", + groupId: "default_group_id", + kind: "audioinput", + label: "Default Microphone", + }, + { + deviceId: "communications", + groupId: "communications_group_id", + kind: "audioinput", + label: "Communications Microphone", + }, + ]; + + safeDefine(win.navigator.mediaDevices, "enumerateDevices", () => + Promise.resolve(fallbackDevices), + ); + + safeDefine(win.navigator.mediaDevices, "getUserMedia", () => + Promise.resolve(buildMp3Stream()), + ); + }; + + win.__cypressBuildMp3Stream = buildMp3Stream; + win.__cypressApplyMediaStubs = applyMediaStubs; + win.__cypressEnsureMp3Playback = ensureMp3Playback; + + applyMediaStubs(); + ensureMp3Playback(); + }; + + cy.on("window:before:load", (win) => { + installMediaStubs(win); + }); + + cy.visit(portalUrl); + + cy.window().then((win) => { + if (win.__cypressApplyMediaStubs) { + win.__cypressApplyMediaStubs(); + } + if (win.__cypressEnsureMp3Playback) { + win.__cypressEnsureMp3Playback(); + } + }); + + // 3. Agree to privacy policy + cy.get("#checkbox-0", { timeout: 10000 }).check({ force: true }); + cy.wait(500); + cy.get("button") + .contains("I understand") + .should("not.be.disabled") + .click(); + + // 4. Microphone check + cy.log("Step 4: Microphone check"); + cy.wait(3000); + + // Wait for the "Check microphone access" or "Microphone" dropdown + // Use a generous timeout for devices to enumerate + cy.get("body").then(($body) => { + if ($body.text().includes("microphone access was denied")) { + cy.contains("button", "Check microphone access").click({ + force: true, + }); + } + }); + + // Wait for the dropdown or device list + // Assuming the UI has a select element or a list of devices + // We'll try to find the dropdown and assert it has options + + // If "Skip" is visible, it means we are on the check page + cy.get('[data-testid="portal-onboarding-mic-skip-button"]').should( + "be.visible", + ); + + // Device selector is optional here; default device should already be selected. + // Avoid failing the test if a combobox/select isn't rendered in this UI state. + cy.get("body").then(($body) => { + const selector = $body.find('[role="combobox"], select'); + if (selector.length > 0) { + cy.log("Microphone selector present"); + } else { + cy.log("No microphone selector found - using default device"); + } + }); + + // Click "Check" or "Record" button on this step if it exists to verify audio + // Or just click Continue/Skip if we've selected it. + // Usually there is a visual indicator of audio level. + + // If the "Continue" button is disabled, we might need to make some noise. + // But since we injected a file, it should be playing constantly (looping). + + cy.wait(2000); // Wait for audio level to register + + // Click Continue (it replaces Skip when audio is detected usually, or just click Next) + // If "Continue" is not there, we might have to click "Skip" if the excessive test fails, + // but we want to fail if audio isn't detected. + cy.contains("button", "Continue", { timeout: 20000 }) + .should("be.visible") + .should("not.be.disabled") + .click({ force: true }); + + // 5. Enter session name + cy.get('input[placeholder="Group 1, John Doe, etc."]').type( + "Audio Test Session", + ); + cy.get("button").contains("Next").click(); + cy.wait(2000); + + // 6. Start Recording + cy.log("Step 6: Start Recording"); + + // Ensure media APIs are still patched before starting the recorder + // (route changes can drop our earlier stubs if the app reloads). + cy.window().then((win) => { + if (win.__cypressApplyMediaStubs) { + win.__cypressApplyMediaStubs(); + } + if (win.__cypressEnsureMp3Playback) { + win.__cypressEnsureMp3Playback(); + } + }); + + // Click the Record button (label-based, since aria-label may be missing) + cy.contains("button", "Record", { timeout: 15000 }) + .should("be.visible") + .click({ force: true }); + + // If the permission modal appears anyway, re-apply stubs and retry. + cy.wait(1000); + cy.get("body").then(($body) => { + if ($body.text().includes("microphone access was denied")) { + cy.contains("button", "Check microphone access").click({ + force: true, + }); + cy.wait(2000); + cy.window().then((win) => { + if (win.__cypressApplyMediaStubs) { + win.__cypressApplyMediaStubs(); + } + if (win.__cypressEnsureMp3Playback) { + win.__cypressEnsureMp3Playback(); + } + }); + cy.contains("button", "Record", { timeout: 15000 }) + .should("be.visible") + .click({ force: true }); + } + }); + + // Ensure recording UI is active before waiting the full duration + cy.contains("button", "Stop", { timeout: 20000 }).should( + "be.visible", + ); + + cy.log("Recording for 60 seconds..."); + cy.wait(60000); + + // 7. Stop Recording + cy.contains("button", "Stop", { timeout: 15000 }) + .should("be.visible") + .click({ force: true }); + cy.wait(1000); + + // 8. Finish from the "Recording Paused" modal + cy.get('[role="dialog"]', { timeout: 15000 }) + .should("be.visible") + .within(() => { + cy.contains("button", "Finish") + .should("be.visible") + .click({ force: true }); + }); + + // Optional confirmation modal (if shown) + cy.get("body").then(($body) => { + if ($body.text().includes("Finish Conversation")) { + cy.contains("button", "Yes").click({ force: true }); + } + }); + + cy.wait(2000); + }, + ); + }); + }); + + // 9. Return to dashboard + cy.then(() => { + const dashboardBaseUrl = + Cypress.env("dashboardUrl") || + "https://dashboard.echo-next.dembrane.com"; + cy.visit(`${dashboardBaseUrl}/en-US/projects/${projectId}/overview`); + }); + + // 10. Verify and Transcription + cy.wait(5000); + selectConversation("Audio Test Session"); + + cy.log("Waiting for transcript processing..."); + cy.wait(15000); // Give it enough time for backend to transcribe + + clickTranscriptTab(); + + // Verify some content we expect from the wav file + // The server/tests/data/audio/wav.wav usually contains "Hello this is a test" or similar? + // We'll just check if there is ANY text for now, or log it. + // If we don't know the exact content, we can just assert the transcript is not empty. + + cy.xpath( + '//div[contains(@class, "mantine-Paper-root")]//div[contains(@style, "flex")]//div/p[contains(@class, "mantine-Text-root")]', + ) + .should("have.length.gt", 0) + .then(($els) => { + const text = $els.text(); + cy.log("Transcribed text:", text); + expect(text).to.not.be.empty; + }); + + // Cleanup + navigateToProjectOverview(); + cy.then(() => { + deleteProject(projectId); + }); + openSettingsMenu(); + logout(); + }); +}); diff --git a/echo/cypress/fixtures/createdProjects.json b/echo/cypress/fixtures/createdProjects.json new file mode 100644 index 00000000..3d4641e1 --- /dev/null +++ b/echo/cypress/fixtures/createdProjects.json @@ -0,0 +1,2512 @@ +[ + { + "createdAt": "2026-01-17T18:34:40.761Z", + "id": "9fc167ec-1b1c-4e72-a72c-317eda800dfd", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T18:37:29.267Z", + "id": "4907acfe-2338-4ec3-9f6f-138b9b46d617", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T18:38:32.809Z", + "id": "95634a21-b059-4ca8-8fcd-f241888f2e21", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T18:40:24.993Z", + "id": "c382fd01-cbb0-4396-bab0-b03ba71b1683", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T18:41:14.992Z", + "id": "07466ffa-7d87-4ada-b5e0-66a8d1ec5130", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T18:47:21.991Z", + "id": "797c4f76-120f-474d-8843-89b43f0630ef", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T18:55:24.215Z", + "id": "f223a09d-b37b-47d7-b5f2-0e725956d2a1", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T19:03:26.078Z", + "id": "8904ed2e-9839-4744-9a83-e50c3668a0b4", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T19:43:59.645Z", + "id": "11ded7cc-9b1e-4fbc-8371-284c9fb995a6", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T19:47:12.501Z", + "id": "7e66c589-3152-40a3-b17b-8ff9e0b64a21", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T19:48:14.067Z", + "id": "b5232c25-6420-4fcf-abc6-d514dae36583", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T19:49:42.659Z", + "id": "6f9f3e6e-6d49-4594-b662-02289fe29878", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T19:51:44.738Z", + "id": "67eae638-5fd8-4566-9603-84ace8f6202b", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T19:55:42.731Z", + "id": "d1d35822-e1b3-4e78-98e6-62d544dbab78", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T19:57:03.643Z", + "id": "0966a16c-63dd-4581-add4-c7147ff854a7", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T19:57:51.846Z", + "id": "dc837318-7ae1-4828-b5c7-a08b2c115b96", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:01:15.299Z", + "id": "f4a9b7de-3882-458c-953d-95aafe8f329f", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:03:20.026Z", + "id": "6f363e24-becb-4800-af31-910c9388ec19", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:06:02.429Z", + "id": "4f3ec526-6247-4149-af2a-593b0c87297b", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:10:40.713Z", + "id": "b5c20df7-b781-4fb9-8fc7-d35e317e4817", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:13:13.798Z", + "id": "d4ef43ba-d027-45eb-86dc-03519ac6e303", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:16:45.167Z", + "id": "3a973b29-4044-4d69-b752-b01e6a7d5f4d", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:19:39.053Z", + "id": "496cb7f4-6aeb-40ee-ad9c-43eed34fd17d", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:20:35.570Z", + "id": "9ced5aee-9ec3-4ccb-a8a9-556021ecf528", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:24:11.786Z", + "id": "b53d2882-4984-4b8b-a587-ce9a0cdeb6e6", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:25:22.001Z", + "id": "4ddc725c-720a-4739-876d-7704a15245c2", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:52:37.723Z", + "id": "6b84c9fb-b6ca-4814-92c9-ad5222b671b6", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T20:58:07.972Z", + "id": "a53ce731-ded7-4c77-bbcc-cbeb95d97eb0", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T21:00:10.792Z", + "id": "46094b80-f860-44f4-81e4-1cd6c270428a", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T21:06:22.008Z", + "id": "3101ff3c-6b7c-4a63-a6a1-63704617c889", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T21:10:34.758Z", + "id": "231e08d9-d0ef-4b28-b7f1-9a0d4da0f18e", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T21:26:50.860Z", + "id": "7cf0b160-0ad1-4573-9764-c8c293a701ab", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T21:32:41.084Z", + "id": "37045956-f4dd-4626-b3b8-d14ca7fc27a8", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T21:35:51.415Z", + "id": "8ec0d5a4-c665-46c6-8601-5763895ed1b4", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T21:54:14.734Z", + "id": "42809564-d4df-4599-9c8e-ec23da63a73c", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T21:56:37.181Z", + "id": "96c94c4c-3838-4541-8dd1-ac6cf3c6a92d", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T22:00:48.417Z", + "id": "ed281715-d0ef-4429-a923-1d6bf29fdaf3", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T22:23:23.084Z", + "id": "4d02f5d3-43ff-430c-8ff4-ad7645dd0adb", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T22:27:32.336Z", + "id": "1f053574-71e8-4fad-85f0-86a2357abb67", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T22:32:02.138Z", + "id": "611afae5-0dfa-4d04-97f8-ed5746b6af88", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T22:35:54.888Z", + "id": "e59db99c-5d7b-4a4f-bea0-de25c0ae5e44", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T22:39:24.094Z", + "id": "ab647a6e-f1e7-42d5-8efc-5a86b5dd878c", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T22:42:29.045Z", + "id": "7567d763-e0cb-49af-98ab-b031172b7655", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T22:48:44.581Z", + "id": "f8da57fd-0701-42f8-8084-5e74e935d6f2", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T22:52:11.239Z", + "id": "4a07931d-00c8-4fd2-b698-41f994e1f35d", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T22:56:03.225Z", + "id": "f869857a-d0b0-4e96-a1c0-a66850d96664", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T22:58:40.541Z", + "id": "ddf5290e-73cb-443e-9e85-ecd0af03df92", + "name": "New Project" + }, + { + "createdAt": "2026-01-17T23:17:13.903Z", + "id": "ca46da92-0625-42ec-b9e9-7d5713f0a9d6", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T02:41:33.708Z", + "id": "a03a02a2-4982-4a0f-950e-f37555cc85ce", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T03:16:24.265Z", + "id": "1dbd26a0-6a41-403e-983f-f0bcee44cdb3", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T03:30:48.628Z", + "id": "d47c187a-2f6f-4481-922a-07b8fdb16a6e", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T03:44:23.552Z", + "id": "cacf0914-32a1-44cf-a2a4-88cb0b0fcc97", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T04:00:26.887Z", + "id": "5e7e8c44-518c-4693-acaa-05b2a3a068f1", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T04:13:16.880Z", + "id": "3b6ae2a2-361c-4d1b-b37c-7387425272f0", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T04:22:10.274Z", + "id": "ddbc93d2-ac92-4fea-8edc-dff4b47f3077", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T04:29:09.282Z", + "id": "beff5820-b6ee-4386-a780-ddce604e66d3", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T04:44:11.456Z", + "id": "c6bdb772-0a89-45c3-88b7-2903b64a51b6", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T04:45:10.211Z", + "id": "6e07b18d-b0e9-4e48-8c4e-cd6406339a3b", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T04:56:18.238Z", + "id": "f4c901bf-7d10-421e-af95-c612c2ed618e", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T04:59:24.772Z", + "id": "0d7f4882-a32d-444b-a277-d9476a2d3203", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T05:02:21.586Z", + "id": "48ccf6e7-471c-4013-83cb-3fcff0cb9de4", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T05:08:49.189Z", + "id": "f8b2d61e-c4a7-4e63-a5c4-d00752346101", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T05:14:24.932Z", + "id": "db623cf0-2054-42ac-a7a5-ebd1ac3341a7", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T05:32:28.018Z", + "id": "5773ff4d-8954-4248-b25d-1a7cbb9fd783", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T05:36:07.276Z", + "id": "4758c714-88cc-4f6e-82ad-14992b52147e", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T05:48:05.330Z", + "id": "0fb6777a-621e-4d61-aebf-d70612462ec8", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T05:53:10.315Z", + "id": "63d2bee1-502a-4018-aeb3-fa064ac11c8f", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T05:55:20.529Z", + "id": "682f3849-2907-401b-81ff-0f46370aeae2", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T06:10:25.077Z", + "id": "0294eeea-7112-46ab-ac75-50574cf064c3", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T06:12:04.081Z", + "id": "da3c18cd-0ebf-4cfb-a620-5647250ff85b", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T06:17:37.824Z", + "id": "3dea293b-a48d-4f41-8725-0fad1d0256a7", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T06:52:25.622Z", + "id": "f165cd9c-a834-495a-a8b9-e32aedfc5d61", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T06:54:20.522Z", + "id": "d598f0ad-e83a-4814-8906-ecc1a5d74e0b", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T07:05:36.738Z", + "id": "4c54e551-67ef-4f58-9db1-2900358402e9", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T07:08:53.564Z", + "id": "c269eb16-8a1f-42e5-8239-3b24582a4173", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T07:23:21.571Z", + "id": "e83cc451-41c9-44d5-a061-4b521b036481", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T15:21:03.678Z", + "id": "86ed0ce7-1b53-49f3-9a50-5dfa5f63bb20", + "name": "New Project" + }, + { + "createdAt": "2026-01-19T15:31:23.489Z", + "id": "bcc27672-2137-4f6f-aa48-bf5bf04fa9a0", + "name": "New Project" + }, + { + "createdAt": "2026-01-22T01:38:18.903Z", + "id": "842fe737-760d-4752-8ed5-b73bbe2a1735", + "name": "New Project" + }, + { + "createdAt": "2026-01-22T01:39:25.471Z", + "id": "f2fd210c-69bf-4b4f-b4ea-1da05c64af73", + "name": "New Project" + }, + { + "createdAt": "2026-01-22T01:44:27.115Z", + "id": "397a9776-4998-4f7b-a958-5bbe5efcfa44", + "name": "New Project" + }, + { + "createdAt": "2026-01-22T01:49:24.455Z", + "id": "3415e6f2-75a8-4e65-bd5c-bd7e71450945", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T20:03:53.350Z", + "id": "97189494-40d0-41c4-9b3c-e80e16d77a74", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T20:06:55.677Z", + "id": "9c60f47b-a404-4cef-823a-c920906c0a4a", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T20:16:04.852Z", + "id": "79155828-0738-4909-a4b5-75d0a53c06f5", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T20:22:16.031Z", + "id": "06ec1b0e-4854-42cf-9e56-d94a14eb73dc", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T20:27:30.755Z", + "id": "396e90fe-b40d-4462-923b-30fafa39a897", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T20:36:15.925Z", + "id": "d5796225-04de-4f45-8f60-b3e60e60c6b7", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T20:40:26.467Z", + "id": "46c1b6fb-cf66-4b31-9c2e-c877593a12eb", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T22:14:05.056Z", + "id": "7d71ca6e-2166-45df-afba-0b23e1146a95", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T22:15:55.761Z", + "id": "1453e4c2-7621-4efe-9d6e-2f8bd3dabe7f", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T22:19:18.081Z", + "id": "02b06003-db6b-4c8e-9d9e-ba1532f5cfdc", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T22:26:00.308Z", + "id": "29ffda17-4119-4ad1-956a-9e65b8e6172b", + "name": "New Project" + }, + { + "createdAt": "2026-01-23T22:43:03.961Z", + "id": "51b3c0c2-c20e-473b-b26f-16e9a366a8bb", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T18:09:55.163Z", + "id": "2d8dc4a9-d0c2-46f9-b0f8-6b83cc2a46a7", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T18:50:11.063Z", + "id": "91a81919-00d1-4140-a446-18c026cd2657", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T19:01:27.900Z", + "id": "f90a5b52-05cf-4ea2-bffd-6c2f9838e356", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T19:03:35.118Z", + "id": "2476be62-8417-406d-afcc-f3cba5e9a380", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T19:06:03.531Z", + "id": "919ddc44-8f42-49cf-835e-35322694e5e7", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T19:10:00.475Z", + "id": "c00a6676-e28a-4565-82cf-08e33e09bb98", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T19:23:44.730Z", + "id": "4b85fb6e-5be1-4609-94fd-db5b10902bf8", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T19:32:03.498Z", + "id": "336a2038-18a9-4712-8785-3fe330e7a529", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T19:36:39.466Z", + "id": "550d88ef-e195-40cc-acb7-e87b986e34d2", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T19:39:54.247Z", + "id": "fc23a73a-f6c2-43f5-9078-e27bfb070d69", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T19:42:24.221Z", + "id": "9b279635-ca36-498b-ac82-065dc61d59b8", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T19:45:15.722Z", + "id": "83bc1f3f-08d7-4c65-a4bf-ca4f45b94f64", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T19:55:38.654Z", + "id": "64639ecf-f70f-4c06-b8df-75ce9dd5ff38", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T20:00:21.201Z", + "id": "1b474e28-ec3a-42ec-970e-1577c04dfe58", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T20:01:54.829Z", + "id": "5f344074-4ede-493a-bd93-c5ad185b4210", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T20:30:19.729Z", + "id": "a05b5702-7177-4837-a63a-6201f101a5ea", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T20:33:20.239Z", + "id": "50dd520e-ddef-4a96-b896-25344cb30f99", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T20:35:38.231Z", + "id": "016fbdf6-78f9-4064-aa38-b37af5ed1f8e", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T20:36:22.693Z", + "id": "133ece00-22ee-4d7c-bb22-9e6468327b38", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T20:48:22.847Z", + "id": "0f8a2c73-dd75-430f-8584-d5ba887dd0f2", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T20:58:21.302Z", + "id": "ab24c9cc-a115-4f04-8e92-582435e2cbde", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T21:01:38.647Z", + "id": "0b7dbbf5-bc13-4bfe-9a0c-991530e58f43", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T21:03:54.347Z", + "id": "f69c44b3-7f5d-445d-ab75-d573d200f701", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T21:10:40.725Z", + "id": "199cbebc-1428-4c5b-bcc3-178e201cdad7", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T21:18:32.688Z", + "id": "b71fd7e6-69c0-4650-8abd-9e61a1fa41d5", + "name": "New Project" + }, + { + "createdAt": "2026-01-27T21:57:33.975Z", + "id": "16596bc3-2108-4b75-b127-b032db6278e5", + "name": "New Project" + }, + { + "createdAt": "2026-01-28T00:03:51.859Z", + "id": "0cae0aad-ab98-461f-bed3-0af58b7fd4b7", + "name": "New Project" + }, + { + "createdAt": "2026-01-28T00:05:47.050Z", + "id": "7603fba7-b57a-46de-9461-bd3b5d7e2ed8", + "name": "New Project" + }, + { + "createdAt": "2026-01-28T00:07:08.744Z", + "id": "76aa8da5-9370-4ab2-8ed1-6d73ebaf2d71", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T00:28:37.236Z", + "id": "744cb6e6-58d9-4b84-8e52-c5308ef32bc9", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T00:30:03.400Z", + "id": "a129bbff-2212-45b1-b6d7-06829489da05", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T00:32:58.607Z", + "id": "158dd15c-5dfd-4901-9335-59021f56f0be", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T00:45:03.412Z", + "id": "ab2904d7-373b-4eb3-92ec-d095666eb605", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T01:05:25.905Z", + "id": "976630e3-5290-42ea-988d-ef9f38b8284e", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T01:25:21.143Z", + "id": "202038d2-43ef-48ab-925b-8f78998f34ed", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T01:29:48.153Z", + "id": "9e4f2ae6-5680-438d-8ad4-3f322c673f3e", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T01:30:28.517Z", + "id": "b2898147-1da7-4f15-9138-b27f1b86a28b", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T01:34:38.139Z", + "id": "2e7c203a-4996-4b7d-bfdf-095374f232ca", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T01:45:59.738Z", + "id": "8b749c42-652e-4c06-9118-b52984088cd7", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T01:47:03.591Z", + "id": "703c30e6-6edb-4c2d-b47b-f5fb6faa44f1", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T02:58:43.616Z", + "id": "7fbbc5ca-fc87-4fa6-86ee-c4652a4f0688", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:01:58.354Z", + "id": "766a28de-fefa-4edc-b54e-5a947436b84d", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:03:14.869Z", + "id": "3fbd0d0f-86af-4b45-8b0b-354a1d097a1f", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:05:13.768Z", + "id": "76f2053c-8b55-4bd6-9bc5-a84c8fcc58bc", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:07:47.030Z", + "id": "c7238e77-920d-46ab-8a85-1d9602e7c575", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:10:42.535Z", + "id": "d10608b6-2a54-4cb3-852a-770ea7ebab04", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:12:52.466Z", + "id": "0d8c5546-b386-494f-9366-40192ca8671f", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:23:05.553Z", + "id": "16dbba2d-5cd6-47d7-bda3-73ba5f7b1870", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:24:01.520Z", + "id": "b7c8434a-fb1d-4572-ad4e-2639d848110b", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:24:35.295Z", + "id": "38399d72-94ee-4b40-996c-eaf50c8a2bd1", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:33:11.781Z", + "id": "c34683aa-b51c-446a-8fe1-338bae11810b", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:37:41.916Z", + "id": "593ed72b-25b0-490f-9e36-e088192e55ef", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:42:53.092Z", + "id": "09c56c6c-b6b5-44f6-be98-89f25cac6524", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T03:47:20.626Z", + "id": "d628e32a-f0f9-4c94-9a4a-d9f8e4b65056", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T04:16:03.632Z", + "id": "a7db3d25-d9a3-4698-a815-6e9d3ca27466", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T04:28:10.363Z", + "id": "17a34d3f-e725-4fdb-b320-084179a9de56", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T04:34:56.487Z", + "id": "8ddbde6f-c025-4109-b7f4-bc2b249250b4", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T04:45:39.476Z", + "id": "246e163e-4047-4056-b673-60898aba5dff", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T04:51:35.859Z", + "id": "25a4b932-062f-45ba-be1a-31c5b31a551f", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T04:54:53.696Z", + "id": "c5a8c39a-cbe9-4346-94c4-0c48d963a0ad", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T04:59:34.749Z", + "id": "b13de539-309a-4032-8695-d1f5eb39f65b", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T05:03:34.350Z", + "id": "1fb31d9d-3227-4835-a6d1-1818902d0261", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T05:12:21.752Z", + "id": "aa1fbb17-057d-46eb-9774-1122a0a7808e", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T05:14:57.873Z", + "id": "8573df1f-0db0-4f39-9adf-bf4476740404", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T05:43:50.423Z", + "id": "debe3db6-22d9-4251-9fff-63d9a25cb773", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T05:50:22.581Z", + "id": "f51d9d61-f1bd-46b8-b299-800eb7f1335c", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T20:12:24.072Z", + "id": "5dc88d4d-760a-46eb-96bb-b9284c2349c3", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T20:38:22.557Z", + "id": "733f7634-81b8-492f-b6d9-a5a77d309242", + "name": "New Project" + }, + { + "createdAt": "2026-02-01T22:03:11.175Z", + "id": "107b6b97-2bf6-49b6-9576-e2a38f093abd", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T20:34:03.294Z", + "id": "e33373cb-5676-4e64-b237-0ac025661fff", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T20:37:47.949Z", + "id": "1551161c-f0df-491a-a2d2-4eb8a3aa67ff", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:11:31.664Z", + "id": "be5e980b-4256-4f6c-927a-a324e61d84d8", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:13:05.962Z", + "id": "af4da355-fe4c-4403-82d9-7fcde1ba3884", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:14:29.511Z", + "id": "ba6f3bb2-badd-46fe-b1c5-289cf3e8bbeb", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:21:02.143Z", + "id": "bce43249-87da-4318-850a-bf1b5738863c", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:26:41.004Z", + "id": "46a9c4ad-28d7-42e7-b352-497d7aca4b29", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:29:27.645Z", + "id": "98fe09a6-1c99-4e6a-8c5c-1d9aa933849f", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:33:43.610Z", + "id": "9cba2921-4b3f-45fe-a0be-430a5a780b64", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:38:57.402Z", + "id": "b079b9c5-3b1b-4578-a626-d0f260d87798", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:43:03.161Z", + "id": "4a9cf62c-0bc0-4e85-afe0-3caa8ac9fab5", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:45:01.763Z", + "id": "ea385158-2ad7-4a86-879f-ed304c8d47fc", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:47:40.095Z", + "id": "3d3a1ba4-86cc-43fa-903a-24d3f03f61fd", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:49:13.110Z", + "id": "954cebb2-9b18-478b-bdd2-6a971255c96d", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T21:57:38.963Z", + "id": "0993f22b-8e6c-40c9-b673-f2867ab7c433", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T22:03:14.800Z", + "id": "22452f67-cad1-40c9-a6f2-c1a188b76996", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T22:21:52.832Z", + "id": "9d0e05e8-f738-4077-869e-5133ec2d362c", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T22:24:19.096Z", + "id": "d6a42e12-c6af-46f2-9e50-4ddc491c4988", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T23:09:47.937Z", + "id": "4c9557e9-5256-4404-ac2e-7d9d2e30f002", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T23:17:47.745Z", + "id": "36b881bf-4180-4602-a76d-c7ca036d91eb", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T23:21:08.910Z", + "id": "f0dee18b-2d8a-456c-9bc2-8e77ac0dbe29", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T23:24:47.836Z", + "id": "d29a9e86-47cc-4701-a59b-aa5b5bbea5a0", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T23:50:34.197Z", + "id": "d488e898-b6b5-4a2f-9bb3-45db9f6a7759", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T23:50:54.252Z", + "id": "35aac6ed-aa5c-46ac-b298-0bfa5753a82a", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T23:54:36.712Z", + "id": "8c7e3b20-324b-476f-bf1f-6ee05f304d0d", + "name": "New Project" + }, + { + "createdAt": "2026-02-02T23:57:27.862Z", + "id": "d9a530db-0166-4ae1-b52b-eda4168c116e", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T00:02:30.428Z", + "id": "382846a0-a865-4041-b003-34c93fe07ffa", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T00:05:34.469Z", + "id": "e698c5dc-7996-4d17-8435-ef43df4576f6", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T00:24:12.420Z", + "id": "450becbc-62de-4114-81b2-942506d94880", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T01:15:28.951Z", + "id": "944cb44d-8e67-4c88-9d8f-4f5183a1fa63", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T01:21:55.040Z", + "id": "da8c37c6-4055-4a21-8b32-2f81b04be984", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T01:25:07.406Z", + "id": "2942975b-08e7-4362-a03e-d16fceeff621", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T01:34:24.263Z", + "id": "f4995eaa-aeb9-4831-8bf7-9001bb330392", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T01:42:28.763Z", + "id": "ce867043-f6bc-482c-b09b-87f8337a1af7", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T01:45:51.403Z", + "id": "e4e5163c-8093-4352-b54e-b2a639dc8350", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T19:27:16.149Z", + "id": "c4a8f840-7532-4276-956b-9976c23159a5", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T21:26:49.854Z", + "id": "4fcdc4c8-79a4-4448-bfed-143f9878527a", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T21:40:22.291Z", + "id": "46fa70ba-1369-4209-b24a-eef6a22442bb", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T21:43:19.162Z", + "id": "5416b6ad-7e54-47ae-8d3b-125fd94e8cd7", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T21:48:44.154Z", + "id": "52a04098-5bde-42c3-a4c4-3e7ecd1d8c37", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T21:52:06.720Z", + "id": "63e01e9b-6ce5-4285-a97a-c70c8555ff92", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T21:56:09.949Z", + "id": "977fe9db-f702-4af2-b7c4-f4e1b6b099ba", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T21:59:57.639Z", + "id": "10a1e310-eed0-4622-b8f8-6127c539b996", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T22:28:38.898Z", + "id": "66c5aeb7-5306-48f6-aa89-e7b48e448160", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T22:32:43.548Z", + "id": "7db327f9-2400-4c7c-b389-7aa04cfed49e", + "name": "New Project" + }, + { + "createdAt": "2026-02-03T22:47:40.036Z", + "id": "9fca1841-da39-4057-ba67-ae4f95c50b5f", + "name": "New Project" + }, + { + "createdAt": "2026-02-07T20:50:11.121Z", + "id": "c6da9952-86c9-4be2-8509-cb3725b53a45", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T00:56:06.159Z", + "id": "60a175df-b361-40a6-a0b7-14e110bc9694", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T00:57:27.604Z", + "id": "3564ae4e-4449-45ac-9eab-e28d861aebdd", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T01:00:12.276Z", + "id": "00d0720c-6109-431e-a2f3-545134a1ecb3", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T01:01:08.682Z", + "id": "da232318-c12e-4998-9564-af143cb64762", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T01:07:28.703Z", + "id": "662ac0ec-4285-40b9-b83c-25f89a2be447", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T01:07:56.761Z", + "id": "bfda2929-6e67-43bf-b08b-fa96c82dc762", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T01:12:23.471Z", + "id": "0a18718e-052e-4756-a33e-542b60317e5b", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T01:14:12.549Z", + "id": "3ba55829-a49b-49cf-b56b-8b99939599bf", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T01:14:29.221Z", + "id": "24b8ac8a-c44c-4478-8f06-772ed79a3ccd", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T01:27:52.588Z", + "id": "c617c505-f91a-4168-942a-355c2639eb67", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T02:29:46.366Z", + "id": "c501d6fa-b96a-4e88-b8fd-a92cf875acca", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T02:34:32.113Z", + "id": "910fe44d-7eca-42f8-a11a-e4dca5878e48", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T02:42:42.393Z", + "id": "585aa947-eac3-4d84-a000-dd0b898344c5", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T03:02:56.910Z", + "id": "d0fa18df-9a8e-4b63-a5cc-98b6e84f96b7", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T04:32:38.924Z", + "id": "4269aab1-13b5-48e5-be6b-bf4da66fcc61", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T05:34:39.101Z", + "id": "3eca58b3-cb59-4420-8037-353654a14dc5", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T18:10:15.269Z", + "id": "f5743c27-03e9-44c6-a848-4e227900819d", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T18:41:49.958Z", + "id": "32c3ac0c-120b-4eee-9f16-a377753d14d9", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T18:42:07.618Z", + "id": "9df75b05-1051-463a-8da6-f0f9c8c8d52c", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T18:45:47.752Z", + "id": "59633fde-80dd-4f5b-bed6-08aec677f0b4", + "name": "New Project" + }, + { + "createdAt": "2026-02-08T18:46:04.664Z", + "id": "31d732e9-3df9-485d-8485-963d7e578cef", + "name": "New Project" + }, + { + "createdAt": "2026-02-09T01:06:31.267Z", + "id": "7362122d-553a-41fc-8468-73da62e768f1", + "name": "New Project" + }, + { + "createdAt": "2026-02-09T01:42:03.916Z", + "id": "dabdb64b-d8b3-4e33-bf26-03f31674db75", + "name": "New Project" + }, + { + "createdAt": "2026-02-09T03:30:00.411Z", + "id": "790637f4-dcdd-498c-8070-ffab93db025d", + "name": "New Project" + }, + { + "createdAt": "2026-02-09T03:39:27.354Z", + "id": "e4aeeda6-1e52-4c51-8d88-a0e16c83a36d", + "name": "New Project" + }, + { + "createdAt": "2026-02-09T19:43:16.869Z", + "id": "cebddfa8-d029-4327-bd57-22cd625fb765", + "name": "New Project" + }, + { + "createdAt": "2026-02-09T19:51:07.953Z", + "id": "55ed6368-6a54-4352-ae44-cded9a5911c2", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T19:24:54.254Z", + "id": "654749df-3d96-4aea-b2c2-ddf3707d1834", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T19:26:54.451Z", + "id": "be7c145c-898d-4000-a540-44b1e1beb45b", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T21:55:54.286Z", + "id": "0c50e8b1-86db-45bc-ac7a-8cb267cfcd81", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T22:30:53.054Z", + "id": "996d81b1-475c-4760-8db3-f26eed5fc09b", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T22:38:31.096Z", + "id": "e2c5e4df-7e5c-4c7d-8cfd-e11e8bce6bb9", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T22:43:21.454Z", + "id": "6c876141-60d9-4b5b-b722-e697d28d41c9", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T22:48:36.545Z", + "id": "113bd01b-146b-4a49-8edc-16ba380179fa", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T22:50:50.812Z", + "id": "59e5aff3-aabd-455c-9889-81c79f31ada4", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T22:52:53.037Z", + "id": "4df58ecc-5289-4233-a6c5-9db5238ce88c", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T22:53:51.158Z", + "id": "0fb8c1e8-4c5b-4a48-a307-015da5da86ea", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T22:59:00.114Z", + "id": "2b0b5517-2533-4a19-8ddc-69d38971c115", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T23:04:24.365Z", + "id": "8937591d-7522-4664-9df2-4929844d47bd", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T23:28:29.824Z", + "id": "e170560e-c0cf-4aa2-9955-61c34e4c4c49", + "name": "New Project" + }, + { + "createdAt": "2026-02-13T23:33:51.356Z", + "id": "d092b27a-a789-4367-9485-829002cd6ee8", + "name": "New Project" + }, + { + "createdAt": "2026-02-15T21:48:24.001Z", + "id": "ea26520b-70c4-4488-915d-84252ac37cac", + "name": "New Project" + }, + { + "createdAt": "2026-02-15T22:09:24.109Z", + "id": "e3064ce5-abb4-4600-b53e-cc1ce3b763a3", + "name": "New Project" + }, + { + "createdAt": "2026-02-15T22:36:41.230Z", + "id": "bc529541-b482-4826-9a79-7f4fff17b2fc", + "name": "New Project" + }, + { + "createdAt": "2026-02-17T03:57:31.606Z", + "id": "3b39e6a6-3d05-4113-8e25-b321b2bc79dd", + "name": "New Project" + }, + { + "createdAt": "2026-02-17T06:02:46.302Z", + "id": "e452ff85-dd96-4371-b2e2-37e009922173", + "name": "New Project" + }, + { + "createdAt": "2026-02-17T06:08:03.961Z", + "id": "4c8cfc62-6da8-4dff-a537-5827905e50ce", + "name": "New Project" + }, + { + "createdAt": "2026-02-17T06:15:35.201Z", + "id": "e2494d75-ca50-4007-adea-8b6b836849af", + "name": "New Project" + }, + { + "createdAt": "2026-02-17T06:24:55.479Z", + "id": "292193d5-2564-472c-b4f3-fa46d5b57260", + "name": "New Project" + }, + { + "createdAt": "2026-02-17T06:32:23.430Z", + "id": "c872740a-4b80-4d30-a0f7-fa26bc3a5ab1", + "name": "New Project" + }, + { + "createdAt": "2026-02-17T06:34:19.264Z", + "id": "30a08bf0-79ea-49ad-8757-a4ff18a8a66f", + "name": "New Project" + }, + { + "createdAt": "2026-02-17T06:42:46.987Z", + "id": "9a274ce0-f8b0-40c0-9101-868861989e9d", + "name": "New Project" + }, + { + "createdAt": "2026-02-17T06:53:22.437Z", + "id": "799dc084-8e68-409c-8dca-732c8da20c3d", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T01:52:24.671Z", + "id": "37ac69b6-04fb-4be0-92c0-3b84314586b3", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T02:21:05.670Z", + "id": "5dc935d7-be97-4a05-8bca-d4fc0bd6aaf7", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:16:36.756Z", + "id": "5df53e1f-0d49-43eb-91ed-0a4d3d461b32", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:20:56.644Z", + "id": "125f62ac-8e41-4258-9fdb-430b60ac20f3", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:21:53.614Z", + "id": "3e3222a2-3a72-45a3-9611-991c49d11120", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:22:01.599Z", + "id": "e6690710-0adf-48b0-b852-e3b3b697581b", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:23:52.767Z", + "id": "065df6c7-bb8d-416a-8fd2-788af960cee7", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:24:28.190Z", + "id": "395b3ee1-f086-4d6e-95fb-7f7135f14d04", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:27:59.034Z", + "id": "27944e4f-6c39-428f-8b20-5e94aea24d20", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:32:29.296Z", + "id": "30a341c4-98c8-468d-92c0-79faeeed7a7b", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:34:01.958Z", + "id": "9280ad5c-f267-486f-b691-9eebb405c79b", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:39:18.801Z", + "id": "f5d4fba5-14b0-4dcb-9865-c5a1785008ea", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:39:43.697Z", + "id": "339e7eaf-e3ac-4977-8d48-bedc083eeef4", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:44:38.959Z", + "id": "d73f0d03-120a-4ddd-a6ff-3d7710e161aa", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:46:16.206Z", + "id": "e824f039-58ef-4f44-8b2f-8ee69f79b2d3", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T19:52:31.490Z", + "id": "d03b7d81-ba7a-4c7a-93f0-31bacdbe7f69", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:07:47.748Z", + "id": "07c13f51-a428-4a14-b3b5-6274721292ce", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:19:10.089Z", + "id": "101c8c27-4c39-497b-9f85-98652472d5ac", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:25:21.010Z", + "id": "b81ac424-db43-4780-b97f-dc6c7e14713d", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:26:13.494Z", + "id": "e6d1b36a-ee59-48c6-a949-65901383a537", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:29:03.369Z", + "id": "25461d0d-ab77-4e24-b3c5-1f21188f50df", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:32:22.551Z", + "id": "c813e923-ec21-4ea2-acc8-83875d1fe92a", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:38:58.151Z", + "id": "8cf2bac3-2de3-4286-8007-0185597d400f", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:47:32.013Z", + "id": "cb824d26-4d68-47ea-b2cb-c80718ec8364", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:48:13.039Z", + "id": "4eb0a012-eaa0-4094-b4e2-6698e195d6bf", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:49:27.470Z", + "id": "0e06c3b4-d5e4-4195-910f-25f770f49c12", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:53:22.292Z", + "id": "db0d7d59-cc11-459f-ba91-1111df0148e0", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:57:36.524Z", + "id": "886c7574-c966-4eff-83c3-d1e8a9c526c4", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T20:59:01.217Z", + "id": "e06b3433-90b2-43f7-b6b0-65e620c97b4b", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:05:58.863Z", + "id": "e1e8bee9-8e20-46a4-934f-410bf5a57ccb", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:14:19.454Z", + "id": "537ad776-fa2f-4a9e-b300-dbe3a12ee0e7", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:25:35.172Z", + "id": "8c6494d9-f13f-4c48-8042-77c83425f9cd", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:26:28.604Z", + "id": "ec69b461-2435-4d4a-bf64-e035c8e702c2", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:28:36.941Z", + "id": "54b80c19-02aa-4ba3-ae20-587b7b03ff25", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:29:02.776Z", + "id": "6b901e05-d96e-44c1-b6d0-e63e67fc721a", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:30:37.522Z", + "id": "d8e4c7b3-62e9-4e6d-b31b-ee12dc8b5fef", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:33:52.966Z", + "id": "c3489686-b0f8-45d9-b579-4c72c1485dce", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:34:12.485Z", + "id": "bca5405b-0aa2-4d0b-a855-24b120e69151", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:38:55.817Z", + "id": "804e4152-6def-49d6-b92a-acd98343945e", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:46:46.525Z", + "id": "5cfe02a1-0ebf-4908-9780-8036b97f1775", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T21:52:08.574Z", + "id": "1d565bcc-a533-4ff3-83b8-133ad128b23e", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T22:06:50.317Z", + "id": "88039f17-32d8-45e7-a327-2982e7a1be77", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T22:07:02.289Z", + "id": "83dc2929-7d86-4169-98ef-fbb845a19ad0", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T22:08:44.863Z", + "id": "80293135-3f33-4246-9335-ddd1a648df81", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T22:11:36.391Z", + "id": "8e918c7d-3de2-4743-9be4-e534f58d28e8", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T22:16:55.010Z", + "id": "d03e6e42-24bc-407f-bc21-5806fd168296", + "name": "New Project" + }, + { + "createdAt": "2026-02-21T22:18:19.936Z", + "id": "74a88a41-7623-4f91-839f-932fbf0a13ba", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T00:55:21.642Z", + "id": "84e7fc98-9145-48c6-b3d4-9526a46c364c", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T01:03:10.046Z", + "id": "010cdd23-b8f2-4dcf-9e13-1dd91108f533", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T01:08:32.135Z", + "id": "8435af9c-350c-4db8-a183-3e3558714ef8", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T01:13:42.502Z", + "id": "08cf52dd-60eb-4785-93f7-1ab12864c721", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T01:14:03.151Z", + "id": "ec9652e5-a89a-450e-991c-c330938d977f", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T01:31:08.757Z", + "id": "63ddb1e2-c062-4437-aeea-d134ad3793d2", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T02:44:41.484Z", + "id": "a318e4ab-6ba7-4b43-830e-07d045542566", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T02:50:23.008Z", + "id": "80f8ad97-1845-48ec-a480-42a168169c5f", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T02:51:14.576Z", + "id": "b011b810-c2c4-4c16-800d-a21e0c0bb9ba", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T02:52:23.779Z", + "id": "cbea2105-9478-490d-ad10-e2795cc0b312", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T02:53:09.136Z", + "id": "ed6fb580-95b4-46e1-9417-752886e20153", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T02:54:16.597Z", + "id": "950eb7c8-0b0a-4e0b-9cb9-e0df0a37ce88", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T02:54:30.976Z", + "id": "de92c129-0740-4618-b69a-ffcaa55e140e", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T02:58:09.551Z", + "id": "b2a31df1-8ec6-4120-9bc0-2795860fd49a", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T02:59:15.743Z", + "id": "247080ed-0344-4d50-9010-00bf44f07e48", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T02:59:33.332Z", + "id": "d8882857-793c-4439-8bde-f0dfd6a0e966", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:04:27.129Z", + "id": "8c4db9b1-d38b-4659-bf95-032c0b1b5260", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:04:29.130Z", + "id": "8bbc7d69-8b71-434e-94c3-57f3d4ca211b", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:05:53.538Z", + "id": "69591cc6-fa3d-42f3-a458-b656b5cab634", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:06:55.398Z", + "id": "12c53c5f-ad3b-49a3-a15a-422808caf872", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:12:33.503Z", + "id": "0f54fb2b-60c1-4e2a-a06f-5ac09d503bc5", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:13:24.607Z", + "id": "1a9a6a46-3179-4d34-83e3-5caa3f7ddd60", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:17:55.786Z", + "id": "89bfae7a-7b76-4725-bdcd-a436034bb6c9", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:24:21.336Z", + "id": "4847507e-eb40-4009-862b-bba05426ee88", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:25:40.540Z", + "id": "1bd5a2c2-1722-4271-be7d-851ced60c1bf", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:34:12.314Z", + "id": "e3448c89-648b-499e-9e93-e5524bf65d4e", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:37:24.632Z", + "id": "21fbd9f5-7c78-4904-b661-9019e0ff6378", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:48:22.150Z", + "id": "926b0d86-ea50-4c36-a774-2de62ec4811b", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:52:45.386Z", + "id": "c12fa6e8-fcec-41ae-bcb0-956061c0b5f6", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:54:08.442Z", + "id": "307c7d9c-fa24-44fd-8715-68fb709f1922", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:55:45.906Z", + "id": "75c9964a-6af3-48f0-a5c9-3a4d83c4fb2d", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T03:56:39.220Z", + "id": "83fb3805-7011-42b3-9845-cb7972afd506", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:01:59.520Z", + "id": "ac29204c-af73-47df-ab61-c195c9aa7f8c", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:15:58.303Z", + "id": "faa1a867-69bf-4fb6-ae8e-ab167c18e258", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:16:38.283Z", + "id": "8e954ba7-d3a9-41e7-9a17-8e8866447808", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:22:23.327Z", + "id": "c3b4b88d-7672-4c86-b058-7bf73da28238", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:26:26.236Z", + "id": "d9ea3a95-52ea-4ec8-9ff3-8176c8179de0", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:27:54.809Z", + "id": "ea4f12d6-216e-4d19-8fe6-020919accd74", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:30:01.531Z", + "id": "b962e379-f817-456d-bd60-653a2eb27e01", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:33:10.543Z", + "id": "f49ef77a-1166-4c07-86b0-0f4a8486076c", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:34:56.417Z", + "id": "66cccbf6-47a3-4fad-8188-a9222241bde0", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:35:08.682Z", + "id": "22a27a77-412f-4558-8161-cda92aaf2f51", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:44:54.670Z", + "id": "4d206d1a-b653-4c79-8ab4-679d890613fb", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:48:46.901Z", + "id": "12900b2e-cb52-4d1a-b09d-3d5f97f96682", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:49:37.521Z", + "id": "701a4387-74db-4ea9-8053-0c3c1d7a1404", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:52:14.524Z", + "id": "5d5a4c57-141c-4c1b-a6bd-84398b03190d", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T04:55:36.465Z", + "id": "9261cc68-3788-48cc-ba06-8367c6f20d84", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T05:01:38.020Z", + "id": "c24cf2d9-f3ad-4ada-b37b-f9ea11f191b6", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T05:02:54.723Z", + "id": "7eba3e87-1199-4049-8770-1f5c46a2f06f", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T05:05:37.942Z", + "id": "c0cf80e4-9208-46e6-aff6-ead6d085e580", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T05:12:28.221Z", + "id": "eea8271c-4485-4b75-80ca-a5b9a05ba0d1", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T05:17:47.017Z", + "id": "a59291dc-fd79-487b-8d6c-3bc9b9bb9b8e", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T05:30:36.330Z", + "id": "ed3b06c3-9b12-49a1-b0fe-1944c78cce32", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T05:31:16.417Z", + "id": "ca02b965-519a-4ea8-8d3c-dfc584d057b6", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T05:32:28.399Z", + "id": "a548d55d-1321-4a8a-9101-8488fd190340", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T05:36:25.982Z", + "id": "d90695c8-3140-40d4-8321-be1efd1a02c2", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T05:38:11.564Z", + "id": "03e1f8da-85e7-4a76-bbc0-13007bc1d9a0", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T05:40:00.535Z", + "id": "93538e2f-9e21-453c-9632-d04d96a5d730", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T06:11:49.375Z", + "id": "6625e021-f954-41a7-8be1-c2337c2b7a4e", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T06:43:35.210Z", + "id": "8848539d-4d9b-4ae2-8e95-6105c4f7db73", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T06:47:54.444Z", + "id": "2b5c1e1c-d98a-41eb-8801-b426a5f21c4e", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T06:48:46.577Z", + "id": "4b63c161-a2fb-43c4-839b-1ab808f9df06", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T06:49:40.776Z", + "id": "14077cbe-677a-4f22-9c04-07d545b2e478", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T06:51:26.617Z", + "id": "0bcb292e-96ea-44ca-b90e-59d7e1017408", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T06:51:36.429Z", + "id": "5cdc6a8a-6934-4e40-8b0e-afec4d905007", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T06:55:46.219Z", + "id": "d6a91058-51d1-4349-8801-c9305d1921ea", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T06:56:02.029Z", + "id": "6d11c587-6cc2-49d8-b7c8-4f993fc5d749", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T06:57:59.532Z", + "id": "6210a942-3d93-47b7-97bf-0486535f3841", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T07:09:54.822Z", + "id": "b032b10f-1fb7-42be-9856-7aab920520e2", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T07:33:17.387Z", + "id": "3e41445e-fe23-435b-8ca0-85bd19a4e7b1", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T08:06:56.312Z", + "id": "ad410de3-6cd6-4d9c-854d-ee9e67ba8fc7", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T08:07:32.574Z", + "id": "d7b2b21a-ee70-48d8-88aa-a3240b740977", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T08:12:17.043Z", + "id": "4712b275-3272-4781-8241-72ac8b308b64", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T08:16:10.871Z", + "id": "d807734e-52ac-4f2a-947e-6951907a9f21", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T08:24:23.427Z", + "id": "d7cc316c-12fd-4cfb-b628-2beed05cce3a", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T09:19:59.334Z", + "id": "d73ea74a-e296-4155-b443-18d6dc3b655d", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T09:20:37.256Z", + "id": "116fd660-ec59-47a5-9a25-d59c15c42c52", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T09:28:16.018Z", + "id": "1841181a-f4c8-4009-84d6-61dc54afa7da", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T09:30:00.352Z", + "id": "a58dc33a-b7a0-4c97-9e31-0907c7c93d3b", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T09:31:58.215Z", + "id": "f43e804a-21b9-44b8-b67a-82a4e8871069", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T09:39:18.269Z", + "id": "75b7932d-9b55-48de-b2f1-f4b2c184316f", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T09:50:32.827Z", + "id": "72443bfc-9739-4176-8559-9d40a28da47f", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:02:57.942Z", + "id": "ca62e48b-020a-4796-9729-203d2ac17a58", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:04:41.897Z", + "id": "5f2e2023-2f1a-4f68-8afc-e06ba879af3f", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:04:58.033Z", + "id": "747679df-f5c9-487b-8a26-8406c46a8ad1", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:18:29.033Z", + "id": "42481824-87ad-479b-8f8b-750c9b461ff4", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:20:14.213Z", + "id": "2fe629fe-a1ba-416f-a43b-5ae6bea401be", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:21:14.298Z", + "id": "404dad0c-a761-463e-8d7f-8d84720e8e15", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:21:58.881Z", + "id": "a036c8d1-53a6-4033-a7c0-7e589dfda5d9", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:22:11.594Z", + "id": "a10261a3-3a0e-411c-a92f-42b4f854cbda", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:22:55.394Z", + "id": "18887d48-21d8-477b-94a6-dc4622dc0e2a", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:23:29.342Z", + "id": "c3f41c0a-f39e-49f5-8123-349062ae0236", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:26:01.756Z", + "id": "f045d48a-a549-44f0-8d10-651798eb12df", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:27:04.025Z", + "id": "fafdf5aa-55af-43de-9136-535c2b3194d8", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:29:38.153Z", + "id": "9092a9e6-c26f-428e-afdf-d673aa04945f", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:30:26.151Z", + "id": "82e5efde-93dc-48a7-9bb9-25daf83c0b6d", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:35:47.990Z", + "id": "283f0458-1c97-406e-92c2-51d01b0a1031", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:36:03.611Z", + "id": "1ed1df52-8d75-4d78-bc55-dba7d2c5de8f", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:37:45.832Z", + "id": "fc9708e1-a489-411f-958b-0391eca56834", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:38:33.170Z", + "id": "2186082d-e0bf-4aa6-8eb4-f8c5aaf59189", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:40:50.284Z", + "id": "f0f1626c-fcf9-4e25-bef1-074d742ea638", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:45:23.387Z", + "id": "1bca2ef1-79be-410e-af1e-56f0c0d42da4", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:46:20.329Z", + "id": "5506c28e-c997-414e-bb4f-8363664124f0", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:48:13.517Z", + "id": "bad201a8-fe42-4509-bbb7-3dd46df0fb76", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:48:25.886Z", + "id": "f59cdc79-fe5f-4030-ab2b-b8dd0dfa784b", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:50:32.041Z", + "id": "1ec2e386-071a-4183-828f-c96fe27d5aea", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:51:13.894Z", + "id": "cd73e928-bab0-4975-aa52-9a4cc9a3542d", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:52:28.572Z", + "id": "e63445fe-0ea9-488a-8b2a-03fd4545e316", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T15:56:19.192Z", + "id": "ed34b7fe-f0af-4a4e-87f1-538bb9d6fe36", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:03:41.468Z", + "id": "747ae125-ae9f-46b0-9e8a-1e1b637ae803", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:05:06.085Z", + "id": "2d23ba5d-d7c7-49e4-b534-06b8d0b70f8f", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:06:48.757Z", + "id": "6f1cb3ed-5098-4892-a145-fe24b24273f4", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:07:16.231Z", + "id": "a590a5f2-7ff1-44d0-bed0-b497e4dfa706", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:09:31.880Z", + "id": "782f8d24-8bc3-4edd-a994-2323e029d700", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:18:46.946Z", + "id": "763db833-c76e-48b3-9fbe-66e117a6c634", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:20:28.059Z", + "id": "2440ac54-6ef8-48d6-ba3e-2693c1251bea", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:24:51.463Z", + "id": "d4661fc2-6e51-4d34-8ecb-958130f67acf", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:25:11.961Z", + "id": "ec6ba18e-f90e-4ffb-a001-2d9d242fc149", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:29:52.580Z", + "id": "87911662-fde3-4b9d-8f25-3c0b65339f56", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:42:44.498Z", + "id": "5c150f4e-a6b6-4675-8e93-253abb858290", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:45:49.952Z", + "id": "99909d0c-a4ad-4234-a8b8-17c4e1ee7ae1", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:55:07.179Z", + "id": "6664516e-998d-45cc-abcb-7b9328ef8f0d", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T16:57:29.240Z", + "id": "3a16485c-4e2d-4b78-a8cb-b0a0d8fa7842", + "name": "New Project" + }, + { + "createdAt": "2026-02-22T17:00:42.035Z", + "id": "6a69e749-f98a-48bc-99c3-7537d598311d", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T02:04:56.739Z", + "id": "9b5972ea-76bd-4f0e-9fcc-97e586655c51", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:17:21.488Z", + "id": "29dc429e-77e5-48aa-bb6c-af9ac61d816e", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:18:37.270Z", + "id": "f542ac0b-c90a-4c46-9014-5d67cdb00ebf", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:19:35.451Z", + "id": "f825610c-a7d9-4cf1-a006-2e40090308a2", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:19:42.899Z", + "id": "328d085a-d36a-4161-b951-352d3ce8539d", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:20:34.579Z", + "id": "9f1d2741-e7fb-4f67-88fb-65cb6213d0e5", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:20:41.710Z", + "id": "e35edae3-84e1-4885-b9d1-28b31d74e61f", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:21:39.264Z", + "id": "6a3c08bd-0da7-4703-a013-d32521e55b53", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:21:42.798Z", + "id": "e6c546b1-85a0-4d8d-b5c2-3ad723698580", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:22:42.537Z", + "id": "f1266fde-611e-4fae-84f3-412489cd995c", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:23:13.574Z", + "id": "4fedfcf7-f220-4dc1-ad7c-ed956399d332", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:24:16.828Z", + "id": "306f1470-5009-42e4-a3db-eb415c8e70ed", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:24:48.379Z", + "id": "009820bc-9e23-4cec-a069-4fd9da75cf5a", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:25:53.550Z", + "id": "7e4703aa-8d2a-44b7-ae4b-30caac560a0d", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:26:23.436Z", + "id": "4e1a9a95-5e72-4abd-9b51-6d6869059205", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:32:37.162Z", + "id": "3458826a-58f7-4cf6-8eb2-9e5df378ac64", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:32:39.366Z", + "id": "c6a7789e-39ca-4ad7-b728-bc14c8e86f75", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:37:25.469Z", + "id": "455c0e33-2b7f-42dd-9409-6180007dce5d", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:42:03.981Z", + "id": "3cbdd5e3-dee4-49ab-957d-95a1dc013419", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:44:00.484Z", + "id": "64b9e94e-ab53-4ecf-8063-8e5f530ada08", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:44:13.789Z", + "id": "ced025b8-fa31-4c71-bf47-9885d688fc76", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:45:58.250Z", + "id": "3a410c6b-a1d8-4b06-908a-496869a69516", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:47:41.511Z", + "id": "ee53b084-5f69-4b97-90ad-7b20b5d679de", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:50:52.135Z", + "id": "1af033cb-1dee-46a0-99d4-667458669143", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:51:15.870Z", + "id": "401548be-cbe4-4b7b-979f-0bbf658d0e3f", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:52:36.649Z", + "id": "d1d5d2e0-892b-45da-8e5e-e13225a380ff", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:54:20.307Z", + "id": "e152b446-873e-4b36-9788-32979ee1ff7a", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:56:05.308Z", + "id": "5657fd74-d082-4143-b3d5-33bc4c1f7217", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:56:55.189Z", + "id": "2710dd99-cf22-491c-84eb-0c20461fda38", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:58:57.298Z", + "id": "8c0b0a30-8d07-4841-8498-2cfed0848c1d", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T06:59:39.869Z", + "id": "8bd70dde-01ac-4bd2-95f7-3570fb8306f5", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T07:01:00.127Z", + "id": "97d61f9b-04bd-4d24-8704-86eac4fce0d1", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T07:03:04.953Z", + "id": "116962b1-9c34-4684-ba32-4f4a4eda9a71", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T07:06:58.812Z", + "id": "68596876-b3e0-45c2-be50-c9ce94d5d5d6", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T07:09:42.642Z", + "id": "3799f703-db2d-4f0d-a9a6-4ffbc1d5f80f", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T07:11:46.113Z", + "id": "0b7bae9d-b831-45dd-bbe8-5da9cd1bcbd0", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T07:11:52.984Z", + "id": "c5340b67-4571-4065-8a3a-c22a1c7df43a", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T07:13:48.031Z", + "id": "81104201-e9bd-4564-a23b-9c01d4f5cf7e", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T07:15:50.666Z", + "id": "d8279f3a-382f-4e13-9f66-8409b43b88c6", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T19:53:51.082Z", + "id": "8e4d39c2-3d0d-4b7b-a2e6-400c9daa1d7b", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T19:59:46.639Z", + "id": "a164e852-89b3-4b62-b89a-fe66d3e9f70b", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:04:42.190Z", + "id": "3d097142-ae28-4e44-a9f2-551d124aedc6", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:09:01.626Z", + "id": "2ce840ee-6677-44f5-ac1c-d3e82426a5f6", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:18:50.299Z", + "id": "ee408592-4980-4db1-aad1-60e69f1cb6da", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:20:34.602Z", + "id": "4cdb96b6-6ce0-4c96-b713-b8ae64786467", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:20:40.525Z", + "id": "93e0fe7c-c30b-4083-be3c-8b6705fda313", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:22:18.247Z", + "id": "404be6cd-9da2-4510-8220-19d4860e926e", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:30:23.836Z", + "id": "2a19bd47-e7b4-435f-8039-72a4717685b1", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:30:48.672Z", + "id": "4eeda808-7d4c-4e6c-9eff-8b13c59dbd57", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:39:48.069Z", + "id": "7ffa5360-5cb9-49cd-9d49-66daeb52edee", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:40:20.583Z", + "id": "5b67ab46-ee1a-45c0-b4b6-b2b08eec3cbf", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:42:06.279Z", + "id": "0630c076-b4f7-444d-9434-165683e898a3", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:43:49.822Z", + "id": "bcb3a82a-695f-4c6f-8739-1110103f244c", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:44:34.311Z", + "id": "db803eba-596d-4be1-8760-c05be0f3c16e", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:49:30.427Z", + "id": "f33edfc2-31cf-4631-9318-7ccd8ab008c2", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:49:36.350Z", + "id": "138c985d-aaf2-4c97-9b50-9ed33c3d2bef", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:50:01.860Z", + "id": "2ce63763-a51f-4a00-a325-121fc9023466", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:52:17.921Z", + "id": "f8b6f689-6f3f-4db4-b4e2-044428f74db8", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:57:54.320Z", + "id": "a3e74f64-2dfc-4b29-b533-c2ffbe1f2c69", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T20:58:41.369Z", + "id": "27ed6c6c-66a9-49a6-91f7-fe61871b7060", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T21:00:18.512Z", + "id": "e0510aa3-8d53-4db6-a4d2-991c333cc562", + "name": "New Project" + }, + { + "createdAt": "2026-02-23T21:15:01.402Z", + "id": "677dc60a-a840-486d-b158-acd708f0725f", + "name": "New Project" + }, + { + "createdAt": "2026-02-24T05:10:11.997Z", + "id": "538f719f-d81a-462b-9f69-df44e307c967", + "name": "New Project" + }, + { + "createdAt": "2026-02-24T05:13:42.638Z", + "id": "458ccfa7-494e-4169-9589-b08526b23a0e", + "name": "New Project" + }, + { + "createdAt": "2026-02-24T05:21:06.994Z", + "id": "7d9e5b73-bbad-4bb2-bbfd-0f0257b43d00", + "name": "New Project" + }, + { + "createdAt": "2026-02-24T05:38:18.672Z", + "id": "4b19aa6f-a299-4b71-8d87-fa738c176c4b", + "name": "New Project" + }, + { + "createdAt": "2026-02-24T05:43:13.541Z", + "id": "e11dd8ae-2b07-4aa4-8083-f366961c4681", + "name": "New Project" + }, + { + "createdAt": "2026-02-24T06:12:24.413Z", + "id": "75ae4db2-2252-4684-a509-09ea3256cee1", + "name": "New Project" + } +] diff --git a/echo/cypress/fixtures/test-audio.wav b/echo/cypress/fixtures/test-audio.wav new file mode 100644 index 00000000..b560a5e3 Binary files /dev/null and b/echo/cypress/fixtures/test-audio.wav differ diff --git a/echo/cypress/package-lock.json b/echo/cypress/package-lock.json new file mode 100644 index 00000000..5247b771 --- /dev/null +++ b/echo/cypress/package-lock.json @@ -0,0 +1,3537 @@ +{ + "name": "cypress", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cypress", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "audio-recorder-polyfill": "^0.4.1", + "cypress": "^15.9.0", + "cypress-xpath": "^2.0.1", + "mochawesome": "^7.1.4", + "mochawesome-merge": "^4.4.1", + "mochawesome-report-generator": "^6.3.2", + "playwright-webkit": "^1.58.0" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", + "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.4", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "~6.14.1", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "25.0.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", + "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sizzle": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", + "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0", + "peer": true + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/audio-recorder-polyfill": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/audio-recorder-polyfill/-/audio-recorder-polyfill-0.4.1.tgz", + "integrity": "sha512-SS4qVOzuVwlS/tjQdd0uR+9cCKBTkx4jsAdjM+rMNqoTEWf6bMnBSTfv+FO4Zn9ngxviJOxhkgRWWXsAMqM96Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", + "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "colors": "1.4.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cypress": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.9.0.tgz", + "integrity": "sha512-Ks6Bdilz3TtkLZtTQyqYaqtL/WT3X3APKaSLhTV96TmTyudzSjc6EJsJCHmBb7DxO+3R12q3Jkbjgm/iPgmwfg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@cypress/request": "^3.0.10", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "@types/tmp": "^0.2.3", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "ci-info": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-table3": "0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "hasha": "5.2.2", + "is-installed-globally": "~0.4.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "supports-color": "^8.1.1", + "systeminformation": "^5.27.14", + "tmp": "~0.2.4", + "tree-kill": "1.2.2", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^20.1.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/cypress-xpath": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cypress-xpath/-/cypress-xpath-2.0.1.tgz", + "integrity": "sha512-qMagjvinBppNJdMAkucWESP9aP4rDTs7c96m0vwMuZTVx3NqP2E3z/hkoRf8Ea9soL8yTvUuuyF1cg/Sb1Yhbg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fsu/-/fsu-1.1.1.tgz", + "integrity": "sha512-xQVsnjJ/5pQtcKh+KjUoZGzVWn4uNkchxTF6Lwjr4Gf7nQr8fmUfhKJ62zE77+xQg9xnxi5KUps7XGs+VC986A==", + "dev": true, + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mochawesome": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/mochawesome/-/mochawesome-7.1.4.tgz", + "integrity": "sha512-fucGSh8643QkSvNRFOaJ3+kfjF0FhA/YtvDncnRAG0A4oCtAzHIFkt/+SgsWil1uwoeT+Nu5fsAnrKkFtnPcZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "diff": "^5.0.0", + "json-stringify-safe": "^5.0.1", + "lodash.isempty": "^4.4.0", + "lodash.isfunction": "^3.0.9", + "lodash.isobject": "^3.0.2", + "lodash.isstring": "^4.0.1", + "mochawesome-report-generator": "^6.3.0", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "mocha": ">=7" + } + }, + "node_modules/mochawesome-merge": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/mochawesome-merge/-/mochawesome-merge-4.4.1.tgz", + "integrity": "sha512-QCzsXrfH5ewf4coUGvrAOZSpRSl9Vg39eqL2SpKKGkUw390f18hx9C90BNWTA4f/teD2nA0Inb1yxYPpok2gvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^7.0.1", + "glob": "^7.1.6", + "yargs": "^15.3.1" + }, + "bin": { + "mochawesome-merge": "bin/mochawesome-merge.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mochawesome-merge/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/mochawesome-merge/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mochawesome-merge/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/mochawesome-merge/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mochawesome-merge/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mochawesome-merge/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/mochawesome-merge/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mochawesome-merge/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/mochawesome-merge/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mochawesome-merge/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mochawesome-merge/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mochawesome-merge/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mochawesome-merge/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/mochawesome-merge/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mochawesome-merge/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/mochawesome-merge/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mochawesome-merge/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mochawesome-report-generator": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/mochawesome-report-generator/-/mochawesome-report-generator-6.3.2.tgz", + "integrity": "sha512-iB6iyOUMyMr8XOUYTNfrqYuZQLZka3K/Gr2GPc6CHPe7t2ZhxxfcoVkpMLOtyDKnWbY1zgu1/7VNRsigrvKnOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "dateformat": "^4.5.1", + "escape-html": "^1.0.3", + "fs-extra": "^10.0.0", + "fsu": "^1.1.1", + "lodash.isfunction": "^3.0.9", + "opener": "^1.5.2", + "prop-types": "^15.7.2", + "tcomb": "^3.2.17", + "tcomb-validation": "^3.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "marge": "bin/cli.js" + } + }, + "node_modules/mochawesome-report-generator/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mochawesome/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/playwright-core": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz", + "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright-webkit": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright-webkit/-/playwright-webkit-1.58.0.tgz", + "integrity": "sha512-7AHmm62ZpjE4Mrts4Sh/Zp7xB9xrxrcbY+871YwysUwXkgTEs/UesuZe36xV6JgHpxHSw7k+v+WSt6oiWfBayA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/systeminformation": { + "version": "5.30.5", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.5.tgz", + "integrity": "sha512-DpWmpCckhwR3hG+6udb6/aQB7PpiqVnvSljrjbKxNSvTRsGsg7NVE3/vouoYf96xgwMxXFKcS4Ux+cnkFwYM7A==", + "dev": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/tcomb": { + "version": "3.2.29", + "resolved": "https://registry.npmjs.org/tcomb/-/tcomb-3.2.29.tgz", + "integrity": "sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tcomb-validation": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tcomb-validation/-/tcomb-validation-3.4.1.tgz", + "integrity": "sha512-urVVMQOma4RXwiVCa2nM2eqrAomHROHvWPuj6UkDGz/eb5kcy0x6P0dVt6kzpUZtYMNoAqJLWmz1BPtxrtjtrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tcomb": "^3.0.0" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/echo/cypress/package.json b/echo/cypress/package.json new file mode 100644 index 00000000..ffcf5cf0 --- /dev/null +++ b/echo/cypress/package.json @@ -0,0 +1,24 @@ +{ + "name": "cypress", + "version": "1.0.0", + "main": "index.js", + "devDependencies": { + "audio-recorder-polyfill": "^0.4.1", + "cypress": "^15.9.0", + "cypress-xpath": "^2.0.1", + "mochawesome": "^7.1.4", + "mochawesome-merge": "^4.4.1", + "mochawesome-report-generator": "^6.3.2", + "playwright-webkit": "^1.58.0" + }, + "scripts": { + "test": "powershell -ExecutionPolicy Bypass -File ./test-suites/run-test-suites.ps1", + "test:suite1": "powershell -ExecutionPolicy Bypass -File ./test-suites/run-browser-tests.ps1", + "test:suite2": "powershell -ExecutionPolicy Bypass -File ./test-suites/run-viewport-tests.ps1", + "test:all": "powershell -ExecutionPolicy Bypass -c \"npm run test:desktop; npm run test:mobile; npm run test:tablet\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "" +} diff --git a/echo/cypress/run-browser-tests.ps1 b/echo/cypress/run-browser-tests.ps1 new file mode 100644 index 00000000..c325a8e2 --- /dev/null +++ b/echo/cypress/run-browser-tests.ps1 @@ -0,0 +1,81 @@ +# Run All Tests - Multiple Browsers with HTML Reports +# Generates Mochawesome HTML reports for each browser +# Browsers: Chrome, Firefox, Edge, WebKit (Safari) + +$ErrorActionPreference = "Continue" + +# Configuration +$specPattern = "e2e/suites/[0-9]*.cy.js" +$envVersion = "staging" +$reportsDir = "reports" + +# Desktop viewport +$viewportWidth = 1440 +$viewportHeight = 900 + +# Browsers to test +$browsers = @("chrome", "firefox", "edge", "webkit") + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " CYPRESS MULTI-BROWSER TEST RUNNER" -ForegroundColor Cyan +Write-Host " (with Mochawesome HTML Reports)" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "Viewport: Desktop ($viewportWidth x $viewportHeight)" +Write-Host "" + +# Clean up old reports +if (Test-Path $reportsDir) { + Remove-Item -Recurse -Force $reportsDir +} +New-Item -ItemType Directory -Path $reportsDir -Force | Out-Null + +# Set viewport environment variables +$env:CYPRESS_viewportWidth = $viewportWidth +$env:CYPRESS_viewportHeight = $viewportHeight + +$exitCodes = @() + +foreach ($browser in $browsers) { + Write-Host "----------------------------------------" -ForegroundColor Yellow + Write-Host " Running tests: $browser" -ForegroundColor Yellow + Write-Host "----------------------------------------" -ForegroundColor Yellow + + # Run Cypress + npx cypress run --spec "$specPattern" --env version=$envVersion --browser $browser + $exitCodes += $LASTEXITCODE + + Write-Host "" +} + +# Clear environment variables +Remove-Item Env:CYPRESS_viewportWidth -ErrorAction SilentlyContinue +Remove-Item Env:CYPRESS_viewportHeight -ErrorAction SilentlyContinue + +Write-Host "----------------------------------------" -ForegroundColor Cyan +Write-Host " Generating Combined HTML Report..." -ForegroundColor Cyan +Write-Host "----------------------------------------" -ForegroundColor Cyan + +# Merge all JSON reports into one +npx mochawesome-merge "$reportsDir/*.json" -o "$reportsDir/combined-report.json" + +# Generate HTML report from merged JSON +npx marge "$reportsDir/combined-report.json" --reportDir "$reportsDir" --reportFilename "test-report" + +Write-Host "" +Write-Host "========================================" -ForegroundColor Green +Write-Host " REPORT GENERATED!" -ForegroundColor Green +Write-Host "========================================" -ForegroundColor Green +Write-Host "" +Write-Host " Open: $reportsDir\test-report.html" -ForegroundColor White +Write-Host "" + +# Open the report in browser +Start-Process "$reportsDir\test-report.html" + +# Exit with failure if any tests failed +if ($exitCodes -contains 1) { + exit 1 +} +else { + exit 0 +} diff --git a/echo/cypress/run-parallel-tests.ps1 b/echo/cypress/run-parallel-tests.ps1 new file mode 100644 index 00000000..6e141f68 --- /dev/null +++ b/echo/cypress/run-parallel-tests.ps1 @@ -0,0 +1,125 @@ +# Parallel Cypress Test Runner (Fixed) +# Runs all test suites in parallel using PowerShell background jobs + +param( + [int]$MaxParallel = 5, # Max concurrent tests (adjust based on CPU/RAM) + [string]$Browser = "chrome", + [switch]$Headed, + [string]$Version = "staging" +) + +$ErrorActionPreference = "Continue" + +# Get all test files (exclude .original files) +$testDir = "e2e/suites" +$testFiles = Get-ChildItem -Path $testDir -Filter "*.cy.js" | +Where-Object { $_.Name -notlike "*.original.*" } | +Sort-Object Name + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Parallel Cypress Test Runner" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "Tests found: $($testFiles.Count)" -ForegroundColor Yellow +Write-Host "Max parallel: $MaxParallel" -ForegroundColor Yellow +Write-Host "Browser: $Browser" -ForegroundColor Yellow +Write-Host "" + +# Create results directory +$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$resultsDir = "parallel-results-$timestamp" +New-Item -ItemType Directory -Path $resultsDir -Force | Out-Null + +Write-Host "Starting parallel execution..." -ForegroundColor Green +Write-Host "Results will be saved to: $resultsDir" -ForegroundColor Gray +Write-Host "" + +# Track jobs and results +$jobs = @() +$headedFlag = if ($Headed) { "--headed" } else { "--headless" } + +# Launch tests in batches +foreach ($testFile in $testFiles) { + $testName = $testFile.BaseName + $specPath = "$testDir/$($testFile.Name)" + $logFile = Join-Path (Get-Location) "$resultsDir\$testName.log" + + # Wait if we've hit max parallel jobs + while (($jobs | Where-Object { $_.State -eq "Running" }).Count -ge $MaxParallel) { + Start-Sleep -Seconds 3 + } + + Write-Host "Starting: $testName" -ForegroundColor Gray + + # Start background job - capture exit code properly + $job = Start-Job -Name $testName -ScriptBlock { + param($specPath, $version, $browser, $headedFlag, $logFile, $workDir) + + Set-Location $workDir + $env:CYPRESS_viewportWidth = 1440 + $env:CYPRESS_viewportHeight = 900 + + # Run cypress and capture output + exit code + $output = & npx cypress run --spec $specPath --env version=$version --browser $browser $headedFlag 2>&1 + $exitCode = $LASTEXITCODE + + # Save output to log file + $output | Out-File -FilePath $logFile -Encoding utf8 + + # Return exit code as the job result + return $exitCode + } -ArgumentList $specPath, $Version, $Browser, $headedFlag, $logFile, (Get-Location).Path + + $jobs += $job + Start-Sleep -Milliseconds 500 +} + +Write-Host "" +Write-Host "All $($jobs.Count) tests launched. Waiting for completion..." -ForegroundColor Yellow +Write-Host "" + +# Wait for all jobs to complete +$jobs | Wait-Job | Out-Null + +# Collect results +$results = @{} +foreach ($job in $jobs) { + $exitCode = Receive-Job -Job $job + + # Determine status from exit code + $status = if ($exitCode -eq 0) { "PASS" } else { "FAIL" } + $color = if ($exitCode -eq 0) { "Green" } else { "Red" } + + Write-Host "[$status] $($job.Name)" -ForegroundColor $color + $results[$job.Name] = @{ Status = $status; ExitCode = $exitCode } +} + +# Cleanup jobs +$jobs | Remove-Job -Force + +# Summary +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " SUMMARY" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan + +$passed = ($results.Values | Where-Object { $_.Status -eq "PASS" }).Count +$failed = ($results.Values | Where-Object { $_.Status -eq "FAIL" }).Count +$total = $results.Count + +Write-Host "Passed: $passed" -ForegroundColor Green +Write-Host "Failed: $failed" -ForegroundColor $(if ($failed -gt 0) { "Red" } else { "Green" }) +Write-Host "Total: $total" -ForegroundColor White +Write-Host "" +Write-Host "Logs saved to: $resultsDir" -ForegroundColor Gray + +# List failed tests +if ($failed -gt 0) { + Write-Host "" + Write-Host "Failed Tests:" -ForegroundColor Red + $results.GetEnumerator() | Where-Object { $_.Value.Status -eq "FAIL" } | ForEach-Object { + Write-Host " - $($_.Key)" -ForegroundColor Red + } +} + +# Exit with appropriate code +if ($failed -gt 0) { exit 1 } else { exit 0 } diff --git a/echo/cypress/run-viewport-tests.ps1 b/echo/cypress/run-viewport-tests.ps1 new file mode 100644 index 00000000..1ee177a2 --- /dev/null +++ b/echo/cypress/run-viewport-tests.ps1 @@ -0,0 +1,145 @@ +# Suite 2: Chrome-only run in 3 viewports in parallel +# - 3 parallel jobs (mobile/tablet/desktop) +# - Failed runs are retried up to 3 times per viewport job + +param( + [string]$SpecPattern = "e2e/suites/[0-9]*.cy.js", + [string]$Version = "staging", + [string]$Browser = "chrome", + [int]$MaxRunAttempts = 3 +) + +$ErrorActionPreference = "Continue" + +# Ensure Cypress launches Electron app mode, not Node mode. +if (Test-Path Env:ELECTRON_RUN_AS_NODE) { + Remove-Item Env:ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue +} + +# Use project-local Cypress cache for stability in this environment. +$env:CYPRESS_CACHE_FOLDER = "$PSScriptRoot\.cypress-cache" +$cypressExe = Get-ChildItem -Path $env:CYPRESS_CACHE_FOLDER -Recurse -Filter "Cypress.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 +if (-not $cypressExe) { + Write-Host "Cypress binary not found in local cache. Installing..." -ForegroundColor Yellow + npx cypress install +} + +$suiteRoot = "reports/suite-2-chrome-viewports" +$logsDir = "$suiteRoot/logs" +$viewports = @( + @{ name = "mobile"; width = 375; height = 667 }, + @{ name = "tablet"; width = 768; height = 1024 }, + @{ name = "desktop"; width = 1440; height = 900 } +) + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " SUITE 2: CHROME + 3 VIEWPORTS (PARALLEL=3)" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "Browser: $Browser" -ForegroundColor Yellow +Write-Host "Viewports: $($viewports.name -join ', ')" -ForegroundColor Yellow +Write-Host "Spec Pattern: $SpecPattern" -ForegroundColor Yellow +Write-Host "Run retries: max $MaxRunAttempts attempts per viewport job" -ForegroundColor Yellow +Write-Host "" + +if (Test-Path $suiteRoot) { + Remove-Item -Recurse -Force $suiteRoot +} +New-Item -ItemType Directory -Path $logsDir -Force | Out-Null + +$jobs = @() +$workDir = (Get-Location).Path + +foreach ($viewport in $viewports) { + $viewportName = $viewport.name + $width = $viewport.width + $height = $viewport.height + $runReportDir = "$suiteRoot/$viewportName" + $logFile = "$logsDir/$viewportName.log" + + $job = Start-Job -Name "suite2-$viewportName" -ScriptBlock { + param($workDir, $browser, $specPattern, $version, $viewportName, $width, $height, $runReportDir, $logFile, $maxRunAttempts) + + Set-Location $workDir + if (Test-Path Env:ELECTRON_RUN_AS_NODE) { + Remove-Item Env:ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue + } + $env:CYPRESS_CACHE_FOLDER = "$workDir\.cypress-cache" + $env:CYPRESS_viewportWidth = $width + $env:CYPRESS_viewportHeight = $height + + $attempt = 0 + $exitCode = 1 + $allOutput = @() + + while ($attempt -lt $maxRunAttempts -and $exitCode -ne 0) { + $attempt++ + + if (Test-Path $runReportDir) { + Remove-Item -Recurse -Force $runReportDir + } + New-Item -ItemType Directory -Path $runReportDir -Force | Out-Null + + $env:CYPRESS_MOCHAWESOME_REPORT_DIR = $runReportDir + $attemptOutput = & npx cypress run --config-file cypress.config.js --spec $specPattern --env "version=$version" --browser $browser --reporter mochawesome 2>&1 + $exitCode = $LASTEXITCODE + Remove-Item Env:CYPRESS_MOCHAWESOME_REPORT_DIR -ErrorAction SilentlyContinue + + $allOutput += "===== Attempt $attempt/$maxRunAttempts ($viewportName) =====" + $allOutput += $attemptOutput + $allOutput += "" + } + + $allOutput | Out-File -FilePath $logFile -Encoding utf8 + + Remove-Item Env:CYPRESS_viewportWidth -ErrorAction SilentlyContinue + Remove-Item Env:CYPRESS_viewportHeight -ErrorAction SilentlyContinue + + return @{ + Name = $viewportName + ExitCode = $exitCode + LogFile = $logFile + Attempts = $attempt + } + } -ArgumentList $workDir, $Browser, $SpecPattern, $Version, $viewportName, $width, $height, $runReportDir, $logFile, $MaxRunAttempts + + $jobs += $job +} + +Write-Host "Started $($jobs.Count) jobs in parallel. Waiting..." -ForegroundColor Green +Write-Host "" + +$jobs | Wait-Job | Out-Null + +$results = @() +foreach ($job in $jobs) { + $result = Receive-Job -Job $job + $results += $result + $status = if ($result.ExitCode -eq 0) { "PASS" } else { "FAIL" } + $color = if ($result.ExitCode -eq 0) { "Green" } else { "Red" } + Write-Host "[$status] $($result.Name) after $($result.Attempts) attempt(s) (log: $($result.LogFile))" -ForegroundColor $color +} + +$jobs | Remove-Job -Force + +$jsonFiles = Get-ChildItem -Path $suiteRoot -Recurse -Filter "mochawesome*.json" | Select-Object -ExpandProperty FullName +$combinedJson = "$suiteRoot/combined-report.json" +$htmlReportName = "suite-2-report" + +if ($jsonFiles.Count -gt 0) { + & npx mochawesome-merge @jsonFiles -o $combinedJson + & npx marge $combinedJson --reportDir $suiteRoot --reportFilename $htmlReportName + Write-Host "" + Write-Host "Suite 2 HTML report: $suiteRoot/$htmlReportName.html" -ForegroundColor Cyan +} else { + Write-Host "No mochawesome JSON files found for Suite 2." -ForegroundColor Red +} + +$failed = ($results | Where-Object { $_.ExitCode -ne 0 }).Count +Write-Host "" +Write-Host "Suite 2 Summary: Passed=$($results.Count - $failed), Failed=$failed, Total=$($results.Count)" -ForegroundColor White + +if ($failed -gt 0) { + exit 1 +} + +exit 0 diff --git a/echo/cypress/support/commands.js b/echo/cypress/support/commands.js new file mode 100644 index 00000000..a5f41446 --- /dev/null +++ b/echo/cypress/support/commands.js @@ -0,0 +1,12 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** + +// Example: +// Cypress.Commands.add('login', (email, password) => { ... }) diff --git a/echo/cypress/support/e2e.js b/echo/cypress/support/e2e.js new file mode 100644 index 00000000..98c57619 --- /dev/null +++ b/echo/cypress/support/e2e.js @@ -0,0 +1,65 @@ +// This file is processed and loaded automatically before your test files. +// This is a great place to put global configuration and behavior that modifies Cypress. + +import './commands' +require('cypress-xpath') + +const AudioRecorderPolyfill = require('audio-recorder-polyfill'); + +beforeEach(() => { + if (Cypress.browser && Cypress.browser.name === 'webkit') { + cy.on('window:before:load', (win) => { + if (win.MediaRecorder) { + return; + } + + class CypressMediaRecorder extends AudioRecorderPolyfill { + constructor(stream, options = {}) { + const normalizedOptions = { ...options }; + if ( + normalizedOptions.mimeType && + typeof AudioRecorderPolyfill.isTypeSupported === 'function' && + !AudioRecorderPolyfill.isTypeSupported(normalizedOptions.mimeType) + ) { + normalizedOptions.mimeType = 'audio/wav'; + } + super(stream, normalizedOptions); + } + } + + CypressMediaRecorder.isTypeSupported = (mimeType) => { + if (typeof AudioRecorderPolyfill.isTypeSupported === 'function') { + return AudioRecorderPolyfill.isTypeSupported(mimeType); + } + return true; + }; + + win.MediaRecorder = CypressMediaRecorder; + }); + } + + // Check for CLI viewport overrides first (--config viewportWidth=X,viewportHeight=Y) + const cliWidth = Cypress.config('viewportWidth'); + const cliHeight = Cypress.config('viewportHeight'); + + // If CLI overrides are not the default (1280x720), use them + const defaultWidth = 1280; + const defaultHeight = 720; + + if (cliWidth !== defaultWidth || cliHeight !== defaultHeight) { + cy.viewport(cliWidth, cliHeight); + cy.log(`Viewport set from CLI: ${cliWidth}x${cliHeight}`); + } else { + // Otherwise, use device-based viewport from env config + const device = Cypress.env('device') || 'desktop'; + const viewports = Cypress.env('viewports'); + + if (viewports && viewports[device]) { + const { width, height } = viewports[device]; + cy.viewport(width, height); + cy.log(`Viewport set to ${device} (${width}x${height})`); + } else { + cy.log(`Using default viewport: ${defaultWidth}x${defaultHeight}`); + } + } +}); diff --git a/echo/cypress/support/functions/chat/index.js b/echo/cypress/support/functions/chat/index.js new file mode 100644 index 00000000..36169ff4 --- /dev/null +++ b/echo/cypress/support/functions/chat/index.js @@ -0,0 +1,385 @@ +/** + * Chat/Ask Feature Functions + * Helper functions for the Ask/Chat feature in the Echo application. + * Updated to use data-testid selectors for robust testing. + */ + +// ============= Navigation Functions ============= + +/** + * Clicks the Ask button in the project sidebar + */ +export const clickAskButton = () => { + cy.log('Clicking Ask button'); + cy.get('[data-testid="sidebar-ask-button"]').filter(':visible').first().click(); +}; + +/** + * Clicks the Library button in the sidebar + */ +export const clickLibraryButton = () => { + cy.log('Clicking Library button'); + cy.get('[data-testid="sidebar-library-button"]').filter(':visible').first().click(); +}; + +/** + * Clicks the Report button in the sidebar + */ +export const clickReportButton = () => { + cy.log('Clicking Report button'); + cy.get('[data-testid="sidebar-report-button"]').filter(':visible').first().click(); +}; + +// ============= Mode Selection ============= + +/** + * Selects Specific Details (Deep Dive) mode + */ +export const clickSpecificDetails = () => { + cy.log('Clicking Specific Details mode'); + cy.get('[data-testid="chat-mode-card-deep_dive"]').filter(':visible').click(); +}; + +/** + * Selects Overview mode + */ +export const clickOverviewMode = () => { + cy.log('Clicking Overview mode'); + cy.get('[data-testid="chat-mode-card-overview"]').filter(':visible').click(); +}; + +/** + * Opens Ask and selects Overview mode without sending a message + */ +export const openAskWithoutSending = () => { + clickAskButton(); + cy.wait(4000); + + clickOverviewMode(); + cy.wait(10000); +}; + +/** + * Opens Ask and selects Specific Details mode without sending a message + */ +export const openAskSpecificDetailsWithoutSending = () => { + clickAskButton(); + cy.wait(4000); + + clickSpecificDetails(); + cy.wait(10000); +}; + +// ============= Conversation Context Selection ============= + +/** + * Selects a conversation for context by ID + */ +export const selectConversationContextById = (conversationId) => { + cy.log('Selecting conversation context:', conversationId); + cy.get(`[data-testid="conversation-chat-selection-checkbox-${conversationId}"]`) + .filter(':visible') + .first() + .click({ force: true }); + cy.wait(3000); +}; + +/** + * Selects a conversation from the sidebar checkbox for context (first available) + */ +export const selectConversationContext = () => { + cy.log('Selecting first conversation for context'); + cy.get('[data-testid^="conversation-chat-selection-checkbox-"]') + .filter(':visible') + .first() + .click({ force: true }); + cy.wait(3000); +}; + +/** + * Selects all conversations for context + */ +export const selectAllConversationsForContext = () => { + cy.log('Selecting all conversations for context'); + cy.get('[data-testid="conversation-select-all-button"]').filter(':visible').click(); + cy.get('[data-testid="select-all-confirmation-modal"]').should('be.visible'); + cy.get('[data-testid="select-all-proceed-button"]').click(); + cy.wait(5000); +}; + +// ============= Chat Interface ============= + +/** + * Types a message in the chat message box + * @param {string} message - The message to type + */ +export const typeMessage = (message) => { + cy.log('Typing message:', message); + cy.get('[data-testid="chat-input-textarea"]') + .should('be.visible') + .type(message); +}; + +/** + * Clicks the Send button to send the message + */ +export const clickSendButton = () => { + cy.log('Clicking Send button'); + cy.get('[data-testid="chat-send-button"]') + .filter(':visible') + .click(); +}; + +/** + * Stops the AI generation + */ +export const stopGeneration = () => { + cy.log('Stopping AI generation'); + cy.get('[data-testid="chat-stop-button"]').should('be.visible').click(); +}; + +/** + * Retries after an error + */ +export const retryChat = () => { + cy.log('Retrying chat'); + cy.get('[data-testid="chat-retry-button"]').should('be.visible').click(); +}; + +/** + * Waits for and verifies that an AI response has been received + */ +export const verifyAIResponse = () => { + cy.log('Waiting for AI response'); + // Wait for thinking text to disappear + cy.get('[data-testid="chat-thinking-text"]', { timeout: 60000 }).should('not.exist'); + + cy.log('Verifying AI response received'); + cy.get('[data-testid="chat-interface"]') + .should('be.visible') + .invoke('text') + .should('have.length.greaterThan', 50); +}; + +/** + * Waits for AI to finish typing + */ +export const waitForAITyping = (timeout = 60000) => { + cy.log('Waiting for AI to finish typing'); + cy.get('[data-testid="chat-thinking-text"]', { timeout }).should('not.exist'); +}; + +// ============= Chat Templates ============= + +/** + * Clicks the more templates button + */ +export const clickMoreTemplates = () => { + cy.log('Clicking more templates'); + cy.get('[data-testid="chat-templates-more-button"]').should('be.visible').click(); +}; + +/** + * Clicks a static template by name + */ +export const clickStaticTemplate = (templateName) => { + cy.log('Clicking static template:', templateName); + cy.get(`[data-testid="chat-template-static-${templateName}"]`).should('be.visible').click(); +}; + +/** + * Clicks an AI suggestion template + */ +export const clickSuggestionTemplate = (suggestionName) => { + cy.log('Clicking suggestion template:', suggestionName); + cy.get(`[data-testid="chat-template-suggestion-${suggestionName}"]`).should('be.visible').click(); +}; + +/** + * Verifies initial template state before first user message + * - Three static templates are visible + * - No dynamic AI suggestions are shown yet + */ +export const verifyInitialSuggestionState = () => { + cy.log('Verifying initial suggestion state'); + cy.get('[data-testid="chat-templates-menu"]').should('be.visible'); + + const expectedStaticTemplateIds = [ + 'chat-template-static-summarize', + 'chat-template-static-compare-&-contrast', + 'chat-template-static-meeting-notes' + ]; + + cy.get('[data-testid^="chat-template-static-"]') + .filter(':visible') + .then(($staticTemplates) => { + const staticIds = [...$staticTemplates] + .map((el) => el.getAttribute('data-testid')) + .filter(Boolean); + + expect(staticIds.length, 'visible static templates before first message').to.equal(3); + expect(staticIds, 'expected static templates before first message') + .to.have.members(expectedStaticTemplateIds); + }); + + cy.get('body').then(($body) => { + const dynamicSuggestionCount = $body.find( + '[data-testid="chat-templates-menu"] [data-testid^="chat-template-suggestion-"]' + ).length; + + expect(dynamicSuggestionCount, 'dynamic suggestions before first message').to.equal(0); + }); +}; + +/** + * Returns dynamic suggestion test IDs currently shown + */ +export const getDynamicSuggestionIds = () => { + return cy.get('body').then(($body) => { + const suggestions = $body.find( + '[data-testid="chat-templates-menu"] [data-testid^="chat-template-suggestion-"]' + ); + + return [...suggestions] + .map((el) => el.getAttribute('data-testid')) + .filter(Boolean); + }); +}; + +/** + * Verifies dynamic suggestions appear after sending a message + */ +export const verifyDynamicSuggestionsAfterMessage = ( + beforeIds = [], + minimumCount = 1, + timeoutMs = 90000, + minimumNewCount = 0 +) => { + cy.log('Verifying dynamic suggestions after message'); + cy.get('[data-testid="chat-templates-menu"] [data-testid^="chat-template-suggestion-"]', { timeout: timeoutMs }) + .should(($suggestions) => { + expect($suggestions.length).to.be.gte(minimumCount); + }) + .then(($suggestions) => { + const afterIds = [...$suggestions] + .map((el) => el.getAttribute('data-testid')) + .filter(Boolean); + const uniqueAfterIds = [...new Set(afterIds)]; + + expect(uniqueAfterIds.length, 'unique dynamic suggestion IDs').to.be.gte(minimumCount); + const newIds = uniqueAfterIds.filter((id) => !beforeIds.includes(id)); + + if (minimumNewCount > 0) { + expect(newIds.length, 'new dynamic suggestion IDs after first message').to.be.gte(minimumNewCount); + } else { + expect(uniqueAfterIds.length, 'dynamic suggestions should remain available after first message') + .to.be.gte(beforeIds.length); + } + }); +}; + +// ============= Chat Item Management ============= + +/** + * Clicks on a specific chat item by ID + */ +export const selectChatById = (chatId) => { + cy.log('Selecting chat:', chatId); + cy.get(`[data-testid="chat-item-${chatId}"]`).filter(':visible').click(); +}; + +/** + * Opens the chat item menu (3 dots) + */ +export const openChatItemMenu = () => { + cy.log('Opening chat item menu'); + cy.get('[data-testid="chat-item-menu"]').filter(':visible').first().click(); +}; + +/** + * Renames a chat + */ +export const renameChat = () => { + cy.log('Renaming chat'); + cy.get('[data-testid="chat-item-menu-rename"]').should('be.visible').click(); +}; + +/** + * Deletes a chat + */ +export const deleteChat = (acceptConfirm = true) => { + cy.log('Deleting chat'); + if (acceptConfirm) { + cy.on('window:confirm', () => { + return true; + }); + } + + cy.get('[data-testid="chat-item-menu-delete"]') + .filter(':visible') + .first() + .click({ force: true }); +}; + +// ============= Complete Ask Flows ============= + +/** + * Complete Ask flow with context selection + */ +export const askWithContext = (message = 'hello') => { + clickAskButton(); + cy.wait(4000); + + clickSpecificDetails(); + cy.wait(10000); + + selectConversationContext(); + // Deselect + selectConversationContext(); + cy.log('able to deslet a conversation'); + // Select again + selectConversationContext(); + + typeMessage(message); + clickSendButton(); + + cy.wait(30000); + verifyAIResponse(); +}; + +/** + * Complete Ask flow without context selection (Overview mode) + */ +export const askWithoutContext = (message = 'hello') => { + clickAskButton(); + cy.wait(4000); + + clickOverviewMode(); + cy.wait(10000); + + typeMessage(message); + clickSendButton(); + + cy.wait(50000); + verifyAIResponse(); +}; + +/** + * Ask with specific details but no conversation selected + */ +export const askSpecificNoContext = (message = 'hello') => { + clickAskButton(); + cy.wait(4000); + + clickSpecificDetails(); + cy.wait(10000); + + // Verify the no conversations alert shows + cy.get('[data-testid="chat-no-conversations-alert"]').should('be.visible'); + + typeMessage(message); + clickSendButton(); + + cy.wait(50000); + verifyAIResponse(); +}; diff --git a/echo/cypress/support/functions/conversation/index.js b/echo/cypress/support/functions/conversation/index.js new file mode 100644 index 00000000..665dee12 --- /dev/null +++ b/echo/cypress/support/functions/conversation/index.js @@ -0,0 +1,464 @@ +/** + * Conversation Functions + * Helper functions for managing conversations in the Echo application. + * Updated to use data-testid selectors for robust testing. + */ + +// ============= Upload Functions ============= + +/** + * Opens the upload conversation modal + */ +export const openUploadModal = () => { + cy.log('Opening Upload Conversation Modal'); + cy.get('[data-testid="conversation-upload-button"]') + .filter(':visible') + .first() + .should('be.visible') + .click(); + cy.wait(1000); +}; + +/** + * Uploads an audio file using Cypress selectFile + * @param {string} filePath - Path to the file relative to cypress folder + */ +export const uploadAudioFile = (filePath) => { + cy.log('Uploading Audio File:', filePath); + cy.get('[data-testid="conversation-upload-modal"]').should('be.visible'); + cy.get('[data-testid="conversation-upload-dropzone"]') + .find('input[type="file"]') + .selectFile(filePath, { force: true }); + cy.wait(1000); +}; + +/** + * Clicks the "Upload Files" button after file selection + */ +export const clickUploadFilesButton = () => { + cy.log('Clicking Upload Files Button'); + cy.get('[data-testid="conversation-upload-files-button"]') + .should('be.visible') + .click(); +}; + +/** + * Closes the upload modal + */ +export const closeUploadModal = () => { + cy.log('Closing Upload Modal'); + cy.get('[data-testid="conversation-upload-close-button"]').should('be.visible').click(); + cy.wait(500); +}; + +/** + * Cancels the upload before it starts + */ +export const cancelUpload = () => { + cy.log('Canceling Upload'); + cy.get('[data-testid="conversation-upload-cancel-button"]').should('be.visible').click(); +}; + +/** + * Retries a failed upload + */ +export const retryUpload = () => { + cy.log('Retrying Upload'); + cy.get('[data-testid="conversation-upload-retry-button"]').should('be.visible').click(); +}; + +/** + * Goes back to file selection + */ +export const backToFileSelection = () => { + cy.log('Going back to file selection'); + cy.get('[data-testid="conversation-upload-back-button"]').should('be.visible').click(); +}; + +// ============= Conversation Selection & Navigation ============= + +/** + * Clicks on a conversation by ID in the sidebar list + * @param {string} conversationId - ID of the conversation + */ +export const selectConversationById = (conversationId) => { + cy.log('Selecting Conversation by ID:', conversationId); + cy.get(`[data-testid="conversation-item-${conversationId}"]`) + .filter(':visible') + .first() + .should('be.visible') + .click(); + cy.wait(2000); +}; + +/** + * Clicks on a conversation by name in the sidebar list + * Handles cases where the name has a prefix like " - filename" + * @param {string} name - Name/filename of the conversation + */ +export const selectConversation = (name) => { + cy.log('Selecting Conversation:', name); + + // Try data-testid first, then fallback to XPath for robustness + cy.get('body').then(($body) => { + // Check if data-testid conversation items exist + const hasDataTestid = $body.find('[data-testid^="conversation-item-"]').length > 0; + + if (hasDataTestid) { + // Use data-testid - the name may have a prefix like " - " + cy.get('[data-testid^="conversation-item-"]') + .filter(':visible') + .contains(name) + .first() + .closest('[data-testid^="conversation-item-"]') + .click(); + } else { + // Fallback to XPath for links containing conversation href + cy.xpath(`//a[contains(@href, "/conversation/") and .//*[contains(text(), "${name}")]]`) + .filter(':visible') + .first() + .click(); + } + }); + + cy.wait(2000); +}; + +// ============= Conversation Overview Functions ============= + +/** + * Verifies the conversation name in the Edit Conversation section + */ +export const verifyConversationName = (expectedName) => { + cy.log('Verifying Conversation Name:', expectedName); + cy.get('[data-testid="conversation-edit-name-input"]') + .should('be.visible') + .invoke('val') + .then((value) => { + const expectedWithoutExt = expectedName.replace(/\.[^/.]+$/, ''); + expect(value).to.satisfy((v) => + v.includes(expectedName) || v.includes(expectedWithoutExt) + ); + }); +}; + +/** + * Updates the conversation name + */ +export const updateConversationName = (newName) => { + cy.log('Updating Conversation Name to:', newName); + cy.get('[data-testid="conversation-edit-name-input"]') + .should('be.visible') + .clear() + .type(newName); + cy.wait(2000); // Wait for auto-save +}; + +/** + * Selects tags for the conversation + */ +export const selectConversationTags = (tags) => { + cy.log('Selecting tags:', tags); + cy.get('[data-testid="conversation-edit-tags-select"]').should('be.visible').click(); + tags.forEach(tag => { + cy.contains(tag).click(); + }); +}; + +/** + * Verifies that the specified tags are selected and visible + * @param {string[]} tags - Array of tag names to verify + */ +export const verifySelectedTags = (tags) => { + cy.log('Verifying Selected Tags:', tags); + tags.forEach(tag => { + // Verify tag appears in the pill group within the multiselect + // Using a more robust selector strategy that looks for the pill label text + cy.contains('.mantine-Pill-label', tag).should('be.visible'); + }); +}; + + +/** + * Verifies that a conversation with the given name exists and is visible in the list + * @param {string} name - Name/filename of the conversation + */ +export const verifyConversationInList = (name) => { + cy.log('Verifying Conversation in List:', name); + cy.get('body').then(($body) => { + // Check if data-testid conversation items exist (Preferred) + if ($body.find('[data-testid^="conversation-item-"]').length > 0) { + cy.get('[data-testid^="conversation-item-"]') + .filter(':visible') + .contains(name) + .should('be.visible'); + } else { + // Fallback to XPath + cy.xpath(`//a[contains(@href, "/conversation/") and .//*[contains(text(), "${name}")]]`) + .filter(':visible') + .should('be.visible'); + } + }); +}; + + +// ============= Summary Functions ============= + +/** + * Generates a summary for the conversation + */ +export const generateSummary = () => { + cy.log('Generating Summary'); + cy.get('[data-testid="conversation-overview-generate-summary-button"]') + .should('be.visible') + .click(); + cy.wait(10000); // Wait for AI generation +}; + +/** + * Regenerates the summary + */ +export const regenerateSummary = () => { + cy.log('Regenerating Summary'); + cy.get('[data-testid="conversation-overview-regenerate-summary-button"]') + .should('be.visible') + .click(); + cy.wait(10000); +}; + +/** + * Copies the summary to clipboard + */ +export const copySummary = () => { + cy.log('Copying Summary'); + cy.get('[data-testid="conversation-overview-copy-summary-button"]') + .should('be.visible') + .click(); +}; + +// ============= Danger Zone Functions ============= + +/** + * Moves conversation to another project + */ +export const moveConversation = (projectSearchTerm) => { + cy.log('Moving conversation to:', projectSearchTerm); + cy.get('[data-testid="conversation-move-button"]').should('be.visible').click(); + cy.get('[data-testid="conversation-move-modal"]').should('be.visible'); + cy.get('[data-testid="conversation-move-search-input"]').type(projectSearchTerm); + cy.wait(1000); + // Click on first matching project + cy.get('[data-testid^="conversation-move-project-radio-"]').first().click(); + cy.get('[data-testid="conversation-move-submit-button"]').click(); + cy.wait(3000); +}; + +/** + * Moves conversation to a specific project by exact target project ID + */ +export const moveConversationToProjectById = (projectSearchTerm, targetProjectId) => { + cy.log(`Moving conversation to project ID: ${targetProjectId} (search: ${projectSearchTerm})`); + cy.get('[data-testid="conversation-move-button"]') + .scrollIntoView() + .should('be.visible') + .click(); + + cy.get('[data-testid="conversation-move-modal"]').should('be.visible'); + cy.get('[data-testid="conversation-move-search-input"]') + .should('be.visible') + .clear() + .type(projectSearchTerm); + + cy.get(`[data-testid="conversation-move-project-radio-${targetProjectId}"]`, { timeout: 10000 }) + .should('exist') + .check({ force: true }); + + cy.get('[data-testid="conversation-move-submit-button"]') + .should('not.be.disabled') + .click(); +}; + +/** + * Downloads the conversation audio + */ +export const downloadAudio = () => { + cy.log('Downloading Audio'); + cy.get('[data-testid="conversation-download-audio-button"]').should('be.visible').click(); +}; + +/** + * Deletes the conversation + */ +export const deleteConversation = (acceptConfirm = true) => { + cy.log('Deleting Conversation'); + if (acceptConfirm) { + cy.on('window:confirm', () => { + return true; + }); + } + + cy.get('[data-testid="conversation-delete-button"]') + .scrollIntoView() + .should('be.visible') + .click({ force: true }); + cy.wait(2000); +}; + +// ============= Transcript Functions ============= + +/** + * Clicks on the Transcript tab + */ +export const clickTranscriptTab = () => { + cy.log('Clicking Transcript Tab'); + cy.get('[data-testid="project-overview-tab-transcript"]').should('be.visible').click(); + cy.wait(2000); +}; + +export const clickOverviewTab = () => { + cy.log('Clicking Overview Tab'); + cy.get('[data-testid="project-overview-tab-overview"]').should('be.visible').click(); + cy.wait(2000); +}; + +/** + * Verifies transcript has content + */ +export const verifyTranscriptText = (minLength = 100) => { + cy.log(`Verifying Transcript has at least ${minLength} characters`); + cy.get('[data-testid^="transcript-chunk-"]') + .should('exist') + .then(($chunks) => { + let totalText = ''; + $chunks.each((i, el) => { + totalText += Cypress.$(el).text(); + }); + expect(totalText.length).to.be.at.least(minLength); + }); +}; + +/** + * Downloads the transcript + */ +export const downloadTranscript = (filename) => { + cy.log('Downloading Transcript'); + cy.get('[data-testid="transcript-download-button"]').should('be.visible').click(); + // cy.get('[data-testid="transcript-download-modal"]').should('be.visible'); // Removed as it might not affect the user provided snippet + if (filename) { + cy.get('[data-testid="transcript-download-filename-input"]') + .should('be.visible') + .clear() + .type(filename); + } + cy.get('[data-testid="transcript-download-confirm-button"]').click(); + cy.wait(2000); +}; + +/** + * Copies the transcript to clipboard + */ +export const copyTranscript = () => { + cy.log('Copying Transcript'); + cy.get('[data-testid="transcript-copy-button"]').should('be.visible').click(); +}; + +/** + * Retranscribes the conversation + */ +export const retranscribeConversation = (newName, enablePII = false) => { + cy.log('Retranscribing Conversation'); + cy.get('[data-testid="transcript-retranscribe-button"]').should('be.visible').click(); + cy.get('[data-testid="transcript-retranscribe-modal"]').should('be.visible'); + if (newName) { + cy.get('[data-testid="transcript-retranscribe-name-input"]').clear().type(newName); + } + if (enablePII) { + cy.get('[data-testid="transcript-retranscribe-pii-toggle"]').click(); + } + cy.get('[data-testid="transcript-retranscribe-confirm-button"]').click(); + cy.wait(5000); +}; + +/** + * Toggles the audio player visibility in transcript + */ +export const toggleTranscriptAudioPlayer = () => { + cy.log('Toggling Audio Player'); + cy.get('[data-testid="transcript-show-audio-player-toggle"]').click(); +}; + +// ============= Filter & Search Functions ============= + +/** + * Searches for a conversation + */ +export const searchConversation = (searchTerm) => { + cy.log('Searching for conversation:', searchTerm); + cy.get('[data-testid="conversation-search-input"]') + .filter(':visible') + .first() + .should('be.visible') + .clear() + .type(searchTerm); +}; + +/** + * Clears the conversation search + */ +export const clearConversationSearch = () => { + cy.log('Clearing conversation search'); + cy.get('[data-testid="conversation-search-clear-button"]').should('be.visible').click(); +}; + +/** + * Toggles filter options visibility + */ +export const toggleFilterOptions = () => { + cy.log('Toggling filter options'); + cy.get('[data-testid="conversation-filter-options-toggle"]') + .filter(':visible') + .first() + .click(); +}; + +/** + * Filters by verified only + */ +export const filterVerifiedOnly = () => { + cy.log('Filtering verified only'); + cy.get('[data-testid="conversation-filter-verified-button"]').click(); +}; + +/** + * Resets all filters + */ +export const resetFilters = () => { + cy.log('Resetting all filters'); + cy.get('[data-testid="conversation-filter-reset-button"]').should('be.visible').click(); +}; + +/** + * Selects all conversations + */ +export const selectAllConversations = () => { + cy.log('Selecting all conversations'); + cy.get('[data-testid="conversation-select-all-button"]').should('be.visible').click(); + cy.get('[data-testid="select-all-confirmation-modal"]').should('be.visible'); + cy.get('[data-testid="select-all-proceed-button"]').click(); + cy.wait(3000); +}; + +// ============= Legacy Functions (for backwards compatibility) ============= + +export const startConversation = () => { + cy.log('Starting conversation'); +}; + +export const navigateToProjectOverview = () => { + cy.log('Navigating to Project Overview via breadcrumb'); + cy.get('[data-testid="project-breadcrumb-name"]') + .filter(':visible') + .first() + .click(); + cy.wait(2000); +}; diff --git a/echo/cypress/support/functions/login/index.js b/echo/cypress/support/functions/login/index.js new file mode 100644 index 00000000..4f6f0fa9 --- /dev/null +++ b/echo/cypress/support/functions/login/index.js @@ -0,0 +1,55 @@ +export const loginToApp = () => { + cy.log('Logging in with data-testid selectors'); + const user = Cypress.env('auth'); + + if (!user || !user.email) { + throw new Error('User credentials not found in environment configuration.'); + } + + cy.visit('/'); + + // 1. Enter Email using data-testid + cy.get('[data-testid="auth-login-email-input"]').type(user.email); + + // 2. Enter Password using data-testid + cy.get('[data-testid="auth-login-password-input"]').type(user.password); + + // 3. Click Login Button using data-testid + cy.get('[data-testid="auth-login-submit-button"]').click(); + + // 4. Wait for URL change + cy.url().should('not.include', '/login'); +}; + +export const verifyLogin = (expectedEmail) => { + cy.log('Verifying login for', expectedEmail); + + // 1. Click Settings Icon using data-testid + // Wait for stability as the button might re-render (detached DOM issue) + cy.wait(2000); + cy.get('[data-testid="header-settings-gear-button"]').filter(':visible').first().click(); + + // 2. Verify Email OR Logout Button using data-testid + cy.get('body').then(($body) => { + // Try to find email + const emailVisible = $body.find(`p:contains("${expectedEmail}")`).length > 0; + + if (emailVisible) { + cy.contains('p', expectedEmail).should('be.visible'); + } else { + cy.log('Email not found directly, checking for Logout button as fallback verification'); + cy.get('[data-testid="header-logout-menu-item"]').filter(':visible').first().should('be.visible'); + } + }); +}; + +export const logout = () => { + cy.log('Logging out'); + + // 1. Click Logout Button using data-testid + // We assume the menu is already open when this function is called. + cy.get('[data-testid="header-logout-menu-item"]').filter(':visible').first().click(); + + // 2. Verify redirected to login using data-testid + cy.get('[data-testid="auth-login-email-input"]').should('be.visible'); +}; diff --git a/echo/cypress/support/functions/participant/index.js b/echo/cypress/support/functions/participant/index.js new file mode 100644 index 00000000..d609ab87 --- /dev/null +++ b/echo/cypress/support/functions/participant/index.js @@ -0,0 +1,1645 @@ +/** + * Participant Portal Functions + * Helper functions for the participant recording flow in the Echo portal. + * Updated to use data-testid selectors for robust testing. + */ + +// ============= Loading & Error States ============= + +/** + * Verifies portal is loading + */ +export const verifyPortalLoading = () => { + cy.log("Verifying Portal Loading"); + cy.get('[data-testid="portal-loading-spinner"]').should("be.visible"); +}; + +/** + * Verifies portal loading error + */ +export const verifyPortalError = () => { + cy.log("Verifying Portal Error"); + cy.get('[data-testid="portal-error-alert"]').should("be.visible"); +}; + +// ============= Onboarding Flow ============= + +/** + * Skips the onboarding entirely + */ +export const skipOnboarding = () => { + cy.log("Skipping Onboarding"); + cy.get('[data-testid="portal-onboarding-skip"]').should("be.visible").click(); +}; + +/** + * Clicks through onboarding slides until the privacy policy checkbox appears + * @param {number} maxAttempts - Maximum number of Next clicks before giving up + */ +export const clickThroughOnboardingUntilCheckbox = (maxAttempts = 10) => { + const clickNext = (attempt = 0) => { + if (attempt >= maxAttempts) { + cy.log("Max attempts reached waiting for privacy checkbox"); + return; + } + cy.get("body").then(($body) => { + if ($body.find('[data-testid="portal-onboarding-checkbox"]').length > 0) { + cy.log("Privacy policy checkbox found"); + } else { + cy.get('[data-testid="portal-onboarding-next-button"]') + .should("be.visible") + .click(); + cy.wait(1000); + clickNext(attempt + 1); + } + }); + }; + clickNext(); +}; + +export const clickThroughOnboardingUntilMicrophone = (maxAttempts = 10) => { + const clickNext = (attempt = 0) => { + if (attempt >= maxAttempts) { + cy.log("Max attempts reached waiting for microphone check"); + return; + } + cy.get("body").then(($body) => { + if ( + $body.find('[data-testid="portal-onboarding-mic-skip-button"]').length > + 0 + ) { + cy.log("Microphone check found"); + } else { + cy.get('[data-testid="portal-onboarding-next-button"]') + .should("be.visible") + .click(); + cy.wait(1000); + clickNext(attempt + 1); + } + }); + }; + clickNext(); +}; + +/** + * Agrees to the privacy policy by checking the checkbox and clicking I understand + */ +export const agreeToPrivacyPolicy = () => { + cy.log("Agreeing to Privacy Policy"); + // Check the checkbox + cy.get('[data-testid="portal-onboarding-checkbox"]').check({ force: true }); + cy.wait(500); + // Click the "I understand" / Next button + cy.get('[data-testid="portal-onboarding-next-button"]') + .should("be.visible") + .click(); + cy.wait(1000); +}; + +/** + * Clicks the Next button on onboarding slides + */ +export const clickOnboardingNext = () => { + cy.log("Clicking Onboarding Next"); + cy.get('[data-testid="portal-onboarding-next-button"]') + .should("be.visible") + .click(); + cy.wait(1000); +}; + +/** + * Clicks the Back button on onboarding slides + */ +export const clickOnboardingBack = () => { + cy.log("Clicking Onboarding Back"); + cy.get('[data-testid="portal-onboarding-back-button"]') + .should("be.visible") + .click(); +}; + +/** + * Skips the microphone check step + */ +export const skipMicrophoneCheck = () => { + cy.log("Skipping Microphone Check"); + cy.get('[data-testid="portal-onboarding-mic-skip-button"]') + .should("be.visible") + .click(); + cy.wait(1000); +}; + +/** + * Continues from microphone check + * @param {Object} options - Options object + * @param {boolean} options.allowSkip - If true, will skip if Continue button not found + */ +export const continueMicrophoneCheck = ({ allowSkip = false } = {}) => { + cy.log("Continuing from Microphone Check"); + // Wait for mic check page to stabilize + cy.wait(2000); + + // Click the Continue button directly using data-testid + cy.get('[data-testid="portal-onboarding-mic-continue-button"]', { + timeout: 10000, + }) + .should("be.visible") + .click(); + cy.wait(1000); +}; + +/** + * Goes back from microphone check + */ +export const backFromMicrophoneCheck = () => { + cy.log("Going back from Microphone Check"); + cy.get('[data-testid="portal-onboarding-mic-back-button"]') + .should("be.visible") + .click(); +}; + +// ============= Conversation Initiation ============= + +/** + * Enters session/conversation name and clicks Next + * @param {string} name - Session name to enter + */ +export const enterSessionName = (name) => { + cy.log("Entering Session Name:", name); + cy.get('[data-testid="portal-initiate-name-input"]', { timeout: 15000 }) + .should("be.visible") + .clear() + .type(name); + cy.get('[data-testid="portal-initiate-next-button"]') + .should("be.visible") + .should("not.be.disabled") + .click({ force: true }); + cy.wait(2000); +}; + +/** + * Types session name without clicking Next (for cases where you need to interact with tags first) + * @param {string} name - Session name to enter + */ +export const typeSessionName = (name) => { + cy.log("Typing Session Name:", name); + cy.get('[data-testid="portal-initiate-name-input"]', { timeout: 15000 }) + .should("be.visible") + .clear() + .type(name); +}; + +/** + * Verifies a tag exists in the tags dropdown and selects it + * @param {string} tagName - Optional tag name to verify and select + */ +export const verifyAndSelectTag = (tagName) => { + const normalizedTagName = typeof tagName === "string" ? tagName.trim() : ""; + cy.log( + normalizedTagName + ? `Verifying and selecting tag: ${normalizedTagName}` + : "Selecting first available tag", + ); + + // Open the tags dropdown + cy.get('[data-testid="portal-initiate-tags-select"]', { timeout: 15000 }) + .should("exist") + .click({ force: true }); + + // Wait for visible options to render + cy.get('[data-combobox-option="true"]:visible', { timeout: 10000 }) + .its("length") + .should("be.gt", 0); + + cy.get('[data-combobox-option="true"]:visible').then(($options) => { + const $matchingOptions = normalizedTagName + ? $options.filter( + (_, option) => Cypress.$(option).text().trim() === normalizedTagName, + ) + : Cypress.$(); + + const $targetOption = + $matchingOptions.length > 0 ? $matchingOptions.first() : $options.first(); + const selectedTagLabel = $targetOption.text().trim(); + + cy.wrap($targetOption).click({ force: true }); + cy.get('[data-testid="portal-initiate-tags-select"]') + .parent() + .should("contain.text", selectedTagLabel); + }); + + cy.wait(500); +}; + +/** + * Submits the session form by clicking the Next button + */ +export const submitSessionForm = () => { + cy.log("Submitting Session Form"); + cy.get('[data-testid="portal-initiate-next-button"]') + .should("be.visible") + .should("not.be.disabled") + .click({ force: true }); + cy.wait(2000); +}; + +/** + * Selects tags for the conversation + */ +export const selectTags = () => { + cy.log("Opening Tags Select"); + cy.get('[data-testid="portal-initiate-tags-select"]') + .should("exist") + .click({ force: true }); +}; + +/** + * Verifies initiation error message + */ +export const verifyInitiationError = () => { + cy.log("Verifying Initiation Error"); + cy.get('[data-testid="portal-initiate-error-alert"]').should("be.visible"); +}; + +// ============= Audio Recording Mode ============= + +/** + * Starts the recording by clicking the Record button + * Includes retry mechanism if microphone access is denied + */ +export const startRecording = () => { + cy.log("Starting Recording"); + + // Ensure media APIs are still patched before starting + cy.window().then((win) => { + if (win.__cypressApplyMediaStubs) { + win.__cypressApplyMediaStubs(); + } + if (win.__cypressEnsureMp3Playback) { + win.__cypressEnsureMp3Playback(); + } + }); + + // Click the Record button + cy.get('[data-testid="portal-audio-record-button"]', { timeout: 15000 }) + .should("be.visible") + .should("not.be.disabled") + .click({ force: true }); + + // If the permission modal appears, re-apply stubs and retry + cy.wait(1000); + cy.get("body").then(($body) => { + if ($body.text().includes("microphone access was denied")) { + cy.contains("button", "Check microphone access").click({ force: true }); + cy.wait(2000); + cy.window().then((win) => { + if (win.__cypressApplyMediaStubs) { + win.__cypressApplyMediaStubs(); + } + if (win.__cypressEnsureMp3Playback) { + win.__cypressEnsureMp3Playback(); + } + }); + // Retry clicking Record + cy.get('[data-testid="portal-audio-record-button"]', { timeout: 15000 }) + .should("be.visible") + .click({ force: true }); + } + }); + + // Ensure recording UI is active + cy.contains("button", "Stop", { timeout: 20000 }).should("be.visible"); +}; + +/** + * Stops the recording by clicking the Stop button + * Handles the case where recording is interrupted and needs reconnection + */ +export const stopRecording = () => { + cy.log("Stopping Recording"); + + // First check if Stop button appears within 30 seconds + cy.get("body", { timeout: 30000 }).then(($body) => { + // Check if Stop button is visible + if ( + $body.find('[data-testid="portal-audio-stop-button"]:visible').length > 0 + ) { + // Stop button found - click it + cy.get('[data-testid="portal-audio-stop-button"]') + .should("be.visible") + .click({ force: true }); + } else if ( + $body.find( + '[data-testid="portal-audio-interruption-reconnect-button"]:visible', + ).length > 0 + ) { + // Recording was interrupted - handle reconnection + cy.log("Recording interrupted - clicking Reconnect"); + handleRecordingInterruption(); + // After reconnecting, wait and try to stop again + cy.wait(60000); // Wait for recording again + cy.get('[data-testid="portal-audio-stop-button"]', { timeout: 30000 }) + .should("be.visible") + .click({ force: true }); + } else { + // Try waiting a bit longer for stop button + cy.get('[data-testid="portal-audio-stop-button"]', { timeout: 15000 }) + .should("be.visible") + .click({ force: true }); + } + }); + cy.wait(1000); +}; + +/** + * Handles the recording interruption modal by clicking Reconnect + * and waiting for recording to resume + */ +export const handleRecordingInterruption = () => { + cy.log("Handling Recording Interruption"); + + cy.get("body").then(($body) => { + if ( + $body.find('[data-testid="portal-audio-interruption-reconnect-button"]') + .length > 0 + ) { + cy.get('[data-testid="portal-audio-interruption-reconnect-button"]') + .should("be.visible") + .click(); + cy.wait(3000); // Wait for reconnection + + // Check if recording resumed (Record button should appear or recording continues) + cy.get("body").then(($bodyAfter) => { + if ( + $bodyAfter.find('[data-testid="portal-audio-record-button"]:visible') + .length > 0 + ) { + // Need to click Record again + cy.get('[data-testid="portal-audio-record-button"]') + .should("be.visible") + .click({ force: true }); + } + }); + } + }); +}; + +/** + * Resumes recording from pause + */ +export const resumeRecording = () => { + cy.log("Resuming Recording"); + cy.get('[data-testid="portal-audio-stop-resume-button"]') + .should("be.visible") + .click(); +}; + +/** + * Finishes the recording from pause modal + */ +export const finishFromPause = () => { + cy.log("Finishing from Pause"); + cy.get('[data-testid="portal-audio-stop-finish-button"]') + .should("be.visible") + .click(); + cy.wait(2000); +}; + +/** + * Finishes the recording session + */ +export const finishRecording = () => { + cy.log("Finishing Recording"); + cy.get('[data-testid="portal-audio-finish-button"]', { timeout: 15000 }) + .should("be.visible") + .click({ force: true }); + cy.wait(2000); +}; + +/** + * Clicks the Refine/Echo button + */ +export const clickEchoButton = () => { + cy.log("Clicking Echo/Refine Button"); + cy.get('[data-testid="portal-audio-echo-button"]') + .should("be.visible") + .click(); +}; + +/** + * Switches to text mode + */ +export const switchToTextMode = () => { + cy.log("Switching to Text Mode"); + cy.get('[data-testid="portal-audio-switch-to-text-button"]') + .should("be.visible") + .click(); +}; + +/** + * Closes the echo info modal + */ +export const closeEchoInfoModal = () => { + cy.log("Closing Echo Info Modal"); + cy.get('[data-testid="portal-audio-echo-info-close-button"]') + .should("be.visible") + .click(); +}; + +/** + * Reconnects after interruption + */ +export const reconnectAfterInterruption = () => { + cy.log("Reconnecting after Interruption"); + cy.get('[data-testid="portal-audio-interruption-reconnect-button"]') + .should("be.visible") + .click(); +}; + +/** + * Verifies recording timer is visible + */ +export const verifyRecordingTimer = () => { + cy.log("Verifying Recording Timer"); + cy.get('[data-testid="portal-audio-recording-timer"]').should("be.visible"); +}; + +// ============= Text Input Mode ============= + +/** + * Types text in the text mode textarea + * @param {string} text - Text to type + */ +export const typePortalText = (text) => { + cy.log("Typing Portal Text:", text); + cy.get('[data-testid="portal-text-input-textarea"]') + .should("be.visible") + .type(text); +}; + +/** + * Submits the text + */ +export const submitText = () => { + cy.log("Submitting Text"); + cy.get('[data-testid="portal-text-submit-button"]') + .should("be.visible") + .click(); +}; + +/** + * Switches to audio mode from text mode + */ +export const switchToAudioMode = () => { + cy.log("Switching to Audio Mode"); + cy.get('[data-testid="portal-text-switch-to-audio-button"]') + .should("be.visible") + .click(); +}; + +/** + * Finishes from text mode + */ +export const finishTextMode = () => { + cy.log("Finishing Text Mode"); + cy.get('[data-testid="portal-text-finish-button"]') + .should("be.visible") + .click(); +}; + +/** + * Confirms finishing from text mode modal + */ +export const confirmFinishText = () => { + cy.log("Confirming Finish Text"); + cy.get('[data-testid="portal-text-finish-confirm-button"]') + .should("be.visible") + .click(); +}; + +/** + * Cancels finishing from text mode modal + */ +export const cancelFinishText = () => { + cy.log("Canceling Finish Text"); + cy.get('[data-testid="portal-text-finish-cancel-button"]') + .should("be.visible") + .click(); +}; + +// ============= Refine Flow - Selection ============= + +/** + * Selects "Make it concrete" (verify) card + */ +export const selectMakeItConcrete = () => { + cy.log("Selecting Make it Concrete"); + cy.get('[data-testid="portal-echo-verify-card"]') + .should("be.visible") + .click(); +}; + +export const selectMakeItDeeper = () => { + cy.log("Selecting Make it Concrete"); + cy.get('[data-testid="portal-echo-explore-card"]') + .should("be.visible") + .click(); +}; + +// ============= Refine Flow - Go Deeper (Explore) ============= + +/** + * Waits for explore response + */ +export const waitForExploreResponse = (timeout = 30000) => { + cy.log("Waiting for Explore Response"); + cy.get('[data-testid="portal-explore-thinking"]', { timeout }).should( + "not.exist", + ); + cy.get('[data-testid^="portal-explore-message-"]').should("exist"); +}; + +// ============= Make it Concrete (Verify) - Topic Selection ============= + +/** + * Selects a topic for verification + * @param {string} topicKey - gems, actions, agreements, etc. + */ +export const selectVerifyTopic = (topicKey) => { + cy.log("Selecting Verify Topic:", topicKey); + cy.get(`[data-testid="portal-verify-topic-${topicKey}"]`) + .should("be.visible") + .click(); +}; + +/** + * Proceeds from topic selection + */ +export const proceedFromTopicSelection = () => { + cy.log("Proceeding from Topic Selection"); + cy.get('[data-testid="portal-verify-selection-next-button"]') + .should("be.visible") + .click(); +}; + +// ============= Make it Concrete (Verify) - Instructions ============= + +/** + * Proceeds from instructions + */ +export const proceedFromInstructions = () => { + cy.log("Proceeding from Instructions"); + cy.get('[data-testid="portal-verify-instructions-next-button"]') + .should("be.visible") + .click(); +}; + +// ============= Make it Concrete (Verify) - Artefact Review ============= + +/** + * Reads the artefact aloud + */ +export const readArtefactAloud = () => { + cy.log("Reading Artefact Aloud"); + cy.get('[data-testid="portal-verify-artefact-read-aloud-button"]') + .should("be.visible") + .click(); +}; + +/** + * Revises the artefact (regenerate from conversation) + */ +export const reviseArtefact = () => { + cy.log("Revising Artefact"); + cy.get('[data-testid="portal-verify-artefact-revise-button"]') + .should("be.visible") + .click(); +}; + +/** + * Enters edit mode for artefact + */ +export const editArtefact = () => { + cy.log("Editing Artefact"); + cy.get('[data-testid="portal-verify-artefact-edit-button"]') + .should("be.visible") + .click(); +}; + +/** + * Approves the artefact + */ +export const approveArtefact = () => { + cy.log("Approving Artefact"); + cy.get('[data-testid="portal-verify-artefact-approve-button"]') + .should("be.visible") + .click(); +}; + +/** + * Saves edited artefact content + */ +export const saveArtefactEdit = () => { + cy.log("Saving Artefact Edit"); + cy.get('[data-testid="portal-verify-artefact-save-edit-button"]') + .should("be.visible") + .click(); +}; + +/** + * Cancels artefact editing + */ +export const cancelArtefactEdit = () => { + cy.log("Canceling Artefact Edit"); + cy.get('[data-testid="portal-verify-artefact-cancel-edit-button"]') + .should("be.visible") + .click(); +}; + +// ============= View Your Responses ============= + +/** + * Clicks view your responses button + */ +export const viewResponses = () => { + cy.log("Viewing Responses"); + cy.get('[data-testid="portal-view-responses-button"]') + .should("be.visible") + .click(); +}; + +/** + * Verifies responses modal is visible + */ +export const verifyResponsesModal = () => { + cy.log("Verifying Responses Modal"); + cy.get('[data-testid="portal-view-responses-modal"]').should("be.visible"); +}; + +// ============= Header & Navigation ============= + +/** + * Clicks back button in portal header + */ +export const clickPortalBack = () => { + cy.log("Clicking Portal Back"); + cy.get('[data-testid="portal-header-back-button"]') + .should("be.visible") + .click(); +}; + +/** + * Clicks cancel button in portal header + */ +export const clickPortalCancel = () => { + cy.log("Clicking Portal Cancel"); + cy.get('[data-testid="portal-header-cancel-button"]') + .should("be.visible") + .click(); +}; + +/** + * Opens portal settings + */ +export const openPortalSettings = () => { + cy.log("Opening Portal Settings"); + cy.get('[data-testid="portal-header-settings-button"]') + .should("be.visible") + .click(); + cy.get('[data-testid="portal-settings-modal"]').should("be.visible"); +}; + +// ============= Portal Settings - Microphone Test ============= + +/** + * Selects a microphone from dropdown + */ +export const selectMicrophone = (micName) => { + cy.log("Selecting Microphone:", micName); + cy.get('[data-testid="portal-settings-mic-select"]') + .should("be.visible") + .select(micName); +}; + +/** + * Verifies microphone is working + */ +export const verifyMicrophoneWorking = () => { + cy.log("Verifying Microphone Working"); + cy.get('[data-testid="portal-settings-mic-success-alert"]').should( + "be.visible", + ); +}; + +/** + * Verifies microphone issue + */ +export const verifyMicrophoneIssue = () => { + cy.log("Verifying Microphone Issue"); + cy.get('[data-testid="portal-settings-mic-issue-alert"]').should( + "be.visible", + ); +}; + +/** + * Confirms microphone change + */ +export const confirmMicrophoneChange = () => { + cy.log("Confirming Microphone Change"); + cy.get('[data-testid="portal-settings-mic-change-confirm-button"]') + .should("be.visible") + .click(); +}; + +/** + * Cancels microphone change + */ +export const cancelMicrophoneChange = () => { + cy.log("Canceling Microphone Change"); + cy.get('[data-testid="portal-settings-mic-change-cancel-button"]') + .should("be.visible") + .click(); +}; + +// ============= Email Notification (Finish Screen) ============= + +/** + * Enters email for notifications + */ +export const enterNotificationEmail = (email) => { + cy.log("Entering Notification Email:", email); + cy.get('[data-testid="portal-finish-email-input"]') + .should("be.visible") + .type(email); + cy.get('[data-testid="portal-finish-email-add-button"]').click(); +}; + +/** + * Submits email notification subscription + */ +export const submitEmailNotification = () => { + cy.log("Submitting Email Notification"); + cy.get('[data-testid="portal-finish-email-submit-button"]') + .should("be.visible") + .click(); +}; + +/** + * Verifies email submission success + */ +export const verifyEmailSubmissionSuccess = () => { + cy.log("Verifying Email Submission Success"); + cy.get('[data-testid="portal-finish-email-success"]').should("be.visible"); +}; + +// ============= Audio Stubs (for testing) ============= + +/** + * Installs audio/mic stubs before loading the portal + * Uses REAL audio injection via MP3 file for proper server upload + * @param {Object} options - Options object + * @param {string} options.audioBase64 - Base64 encoded audio file + * @param {string} options.audioMimeType - MIME type of the audio (default: audio/mpeg) + */ +export const installParticipantAudioStubs = ({ + audioBase64, + audioMimeType = "audio/mpeg", +} = {}) => { + cy.on("window:before:load", (win) => { + const ensureMediaStreamCtor = () => { + if (!win.MediaStream) { + win.MediaStream = class MediaStreamPolyfill { + constructor(tracks = []) { + this._tracks = tracks; + } + + getTracks() { + return this._tracks; + } + + getAudioTracks() { + return this._tracks; + } + + addTrack(track) { + this._tracks.push(track); + } + + removeTrack(track) { + this._tracks = this._tracks.filter((t) => t !== track); + } + }; + } + }; + + const safeDefine = (obj, key, value) => { + if (!obj) { + return; + } + try { + Object.defineProperty(obj, key, { + configurable: true, + value, + writable: true, + }); + } catch (error) { + try { + obj[key] = value; + } catch (_error) {} + } + }; + + const safeDefineGetter = (obj, key, getter) => { + if (!obj) { + return; + } + try { + Object.defineProperty(obj, key, { + configurable: true, + get: getter, + }); + } catch (error) { + try { + obj[key] = getter(); + } catch (_error) {} + } + }; + + const createAnalyserStub = () => { + const analyser = { + _fftSize: 1024, + smoothingTimeConstant: 0.8, + }; + + Object.defineProperty(analyser, "fftSize", { + configurable: true, + get() { + return this._fftSize || 1024; + }, + set(value) { + const normalized = Number(value) || 1024; + this._fftSize = normalized; + this.frequencyBinCount = Math.max(1, Math.floor(normalized / 2)); + }, + }); + + analyser.frequencyBinCount = Math.max( + 1, + Math.floor(analyser._fftSize / 2), + ); + + analyser.getByteTimeDomainData = (array) => { + for (let i = 0; i < array.length; i++) { + array[i] = 200; + } + }; + + analyser.getFloatTimeDomainData = (array) => { + for (let i = 0; i < array.length; i++) { + array[i] = 0.8; + } + }; + + analyser.getByteFrequencyData = (array) => { + for (let i = 0; i < array.length; i++) { + array[i] = 180; + } + }; + + analyser.getFloatFrequencyData = (array) => { + for (let i = 0; i < array.length; i++) { + array[i] = 0.7; + } + }; + + return analyser; + }; + + const ensureAudioContextCtor = () => { + if (!win.AudioContext) { + win.AudioContext = win.webkitAudioContext; + } + if (!win.AudioContext) { + win.AudioContext = function AudioContextFallback() { + this.sampleRate = 44100; + this.state = "running"; + this.destination = {}; + }; + } + + if (typeof win.AudioContext.prototype.createAnalyser !== "function") { + win.AudioContext.prototype.createAnalyser = () => createAnalyserStub(); + } + + if ( + typeof win.AudioContext.prototype.createMediaStreamSource !== "function" + ) { + win.AudioContext.prototype.createMediaStreamSource = () => ({ + connect() {}, + disconnect() {}, + }); + } + + if ( + typeof win.AudioContext.prototype.createMediaStreamDestination !== + "function" + ) { + win.AudioContext.prototype.createMediaStreamDestination = () => { + ensureMediaStreamCtor(); + const track = { + enabled: true, + kind: "audio", + muted: false, + readyState: "live", + stop() {}, + }; + return { stream: new win.MediaStream([track]) }; + }; + } + + if ( + typeof win.AudioContext.prototype.createMediaElementSource !== + "function" + ) { + win.AudioContext.prototype.createMediaElementSource = () => ({ + connect() {}, + disconnect() {}, + }); + } + + if (typeof win.AudioContext.prototype.createOscillator !== "function") { + win.AudioContext.prototype.createOscillator = () => ({ + connect() {}, + disconnect() {}, + frequency: { value: 440 }, + start() {}, + stop() {}, + type: "sine", + }); + } + + if ( + typeof win.AudioContext.prototype.createScriptProcessor !== "function" + ) { + win.AudioContext.prototype.createScriptProcessor = () => ({ + connect() {}, + disconnect() {}, + onaudioprocess: null, + }); + } + + if (typeof win.AudioContext.prototype.close !== "function") { + win.AudioContext.prototype.close = () => Promise.resolve(); + } + + if (typeof win.AudioContext.prototype.resume !== "function") { + win.AudioContext.prototype.resume = () => Promise.resolve(); + } + + if (typeof win.AudioContext.prototype.suspend !== "function") { + win.AudioContext.prototype.suspend = () => Promise.resolve(); + } + }; + + const patchAudioContext = (AudioContextCtor) => { + if (!AudioContextCtor || AudioContextCtor.__cypressPatched) { + return; + } + + AudioContextCtor.__cypressPatched = true; + const originalCreateAnalyser = AudioContextCtor.prototype.createAnalyser; + if (typeof originalCreateAnalyser !== "function") { + AudioContextCtor.prototype.createAnalyser = () => createAnalyserStub(); + return; + } + + AudioContextCtor.prototype.createAnalyser = function () { + const analyser = originalCreateAnalyser.call(this); + const originalGetByteTimeDomainData = + typeof analyser.getByteTimeDomainData === "function" + ? analyser.getByteTimeDomainData.bind(analyser) + : null; + const originalGetFloatTimeDomainData = + typeof analyser.getFloatTimeDomainData === "function" + ? analyser.getFloatTimeDomainData.bind(analyser) + : null; + const originalGetByteFrequencyData = + typeof analyser.getByteFrequencyData === "function" + ? analyser.getByteFrequencyData.bind(analyser) + : null; + const originalGetFloatFrequencyData = + typeof analyser.getFloatFrequencyData === "function" + ? analyser.getFloatFrequencyData.bind(analyser) + : null; + + analyser.getByteTimeDomainData = (array) => { + if (originalGetByteTimeDomainData) { + originalGetByteTimeDomainData(array); + } + + for (let i = 0; i < array.length; i++) { + array[i] = 200; + } + }; + + analyser.getFloatTimeDomainData = (array) => { + if (originalGetFloatTimeDomainData) { + originalGetFloatTimeDomainData(array); + } + + for (let i = 0; i < array.length; i++) { + array[i] = 0.8; + } + }; + + analyser.getByteFrequencyData = (array) => { + if (originalGetByteFrequencyData) { + originalGetByteFrequencyData(array); + } + + for (let i = 0; i < array.length; i++) { + array[i] = 180; + } + }; + + analyser.getFloatFrequencyData = (array) => { + if (originalGetFloatFrequencyData) { + originalGetFloatFrequencyData(array); + } + + for (let i = 0; i < array.length; i++) { + array[i] = 0.7; + } + }; + + if ( + typeof analyser.frequencyBinCount !== "number" || + analyser.frequencyBinCount <= 0 + ) { + const fftSize = analyser.fftSize || 1024; + try { + analyser.frequencyBinCount = Math.max(1, Math.floor(fftSize / 2)); + } catch (_error) {} + } + + return analyser; + }; + + const originalCreateMediaStreamSource = + AudioContextCtor.prototype.createMediaStreamSource; + if (typeof originalCreateMediaStreamSource === "function") { + AudioContextCtor.prototype.createMediaStreamSource = function (stream) { + try { + return originalCreateMediaStreamSource.call(this, stream); + } catch (error) { + return { + connect() {}, + disconnect() {}, + }; + } + }; + } else { + AudioContextCtor.prototype.createMediaStreamSource = () => ({ + connect() {}, + disconnect() {}, + }); + } + + if ( + typeof AudioContextCtor.prototype.createMediaStreamDestination !== + "function" + ) { + AudioContextCtor.prototype.createMediaStreamDestination = () => { + ensureMediaStreamCtor(); + return { stream: new win.MediaStream() }; + }; + } + }; + + patchAudioContext(win.AudioContext); + patchAudioContext(win.webkitAudioContext); + ensureAudioContextCtor(); + + const audioDataUrl = audioBase64 + ? `data:${audioMimeType};base64,${audioBase64}` + : null; + win.__cypressForceMimeType = audioMimeType || "audio/mpeg"; + + const base64ToUint8Array = (base64) => { + const binary = win.atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; + }; + + const buildToneWavBlob = () => { + const sampleRate = 16000; + const durationSeconds = 2; + const numSamples = sampleRate * durationSeconds; + const buffer = new ArrayBuffer(44 + numSamples * 2); + const view = new DataView(buffer); + + const writeString = (offset, value) => { + for (let i = 0; i < value.length; i++) { + view.setUint8(offset + i, value.charCodeAt(i)); + } + }; + + writeString(0, "RIFF"); + view.setUint32(4, 36 + numSamples * 2, true); + writeString(8, "WAVE"); + writeString(12, "fmt "); + view.setUint32(16, 16, true); + view.setUint16(20, 1, true); + view.setUint16(22, 1, true); + view.setUint32(24, sampleRate, true); + view.setUint32(28, sampleRate * 2, true); + view.setUint16(32, 2, true); + view.setUint16(34, 16, true); + writeString(36, "data"); + view.setUint32(40, numSamples * 2, true); + + const amplitude = 0.2; + const frequency = 440; + let offset = 44; + for (let i = 0; i < numSamples; i++) { + const t = i / sampleRate; + const sample = Math.sin(2 * Math.PI * frequency * t); + const value = Math.max(-1, Math.min(1, sample * amplitude)); + view.setInt16(offset, value * 0x7fff, true); + offset += 2; + } + + return new Blob([buffer], { type: "audio/wav" }); + }; + + const getRecordingBlob = () => { + if (win.__cypressAudioBlob) { + return win.__cypressAudioBlob; + } + + if (audioBase64) { + try { + const bytes = base64ToUint8Array(audioBase64); + // Use the provided MIME type, or default to wav if not specified, + // but allow any type (e.g. audio/mpeg) to pass through. + win.__cypressAudioBlob = new Blob([bytes], { + type: audioMimeType || "audio/wav", + }); + return win.__cypressAudioBlob; + } catch (_error) {} + } + + win.__cypressAudioBlob = buildToneWavBlob(); + return win.__cypressAudioBlob; + }; + + const buildAudioStream = () => { + if (win.__cypressAudioStream) { + return win.__cypressAudioStream; + } + + try { + const AudioContextCtor = win.AudioContext || win.webkitAudioContext; + if (!AudioContextCtor) { + ensureMediaStreamCtor(); + const track = { + enabled: true, + kind: "audio", + muted: false, + readyState: "live", + stop() {}, + }; + win.__cypressAudioStream = new win.MediaStream([track]); + return win.__cypressAudioStream; + } + + const audioCtx = new AudioContextCtor(); + let destination; + try { + destination = audioCtx.createMediaStreamDestination(); + } catch (error) { + ensureMediaStreamCtor(); + const track = { + enabled: true, + kind: "audio", + muted: false, + readyState: "live", + stop() {}, + }; + destination = { stream: new win.MediaStream([track]) }; + } + + if (audioDataUrl) { + const audioEl = new win.Audio(); + audioEl.src = audioDataUrl; + audioEl.loop = true; + audioEl.preload = "auto"; + audioEl.crossOrigin = "anonymous"; + + const source = audioCtx.createMediaElementSource(audioEl); + source.connect(destination); + + const startPlayback = () => { + if (audioCtx.state === "suspended") { + audioCtx.resume().catch(() => {}); + } + audioEl.play().catch(() => {}); + }; + + audioEl.addEventListener("canplay", startPlayback); + startPlayback(); + + win.__cypressAudioElement = audioEl; + } else { + const oscillator = audioCtx.createOscillator(); + oscillator.connect(destination); + oscillator.start(); + } + + if (audioCtx.state === "suspended") { + audioCtx.resume().catch(() => {}); + } + + win.__cypressAudioContext = audioCtx; + win.__cypressAudioStream = destination.stream; + return win.__cypressAudioStream; + } catch (error) { + ensureMediaStreamCtor(); + const track = { + enabled: true, + kind: "audio", + muted: false, + readyState: "live", + stop() {}, + }; + win.__cypressAudioStream = new win.MediaStream([track]); + return win.__cypressAudioStream; + } + }; + + const getForcedStream = () => { + try { + return buildAudioStream(); + } catch (_error) { + ensureMediaStreamCtor(); + const track = { + enabled: true, + kind: "audio", + muted: false, + readyState: "live", + stop() {}, + }; + return new win.MediaStream([track]); + } + }; + + const ensurePlayback = () => { + if (win.__cypressAudioElement) { + try { + win.__cypressAudioElement.play().catch(() => {}); + } catch (_error) {} + } + }; + + const installMediaRecorderStub = () => { + // Stub if missing or if specifically in WebKit where we know it can be flaky + // Removing strict browser check allows this to run in other environments if needed + if ( + typeof win.MediaRecorder !== "undefined" && + !(Cypress.browser && Cypress.browser.name === "webkit") + ) { + return; + } + + const emit = (target, type, data) => { + const event = new win.Event(type); + if (typeof data !== "undefined") { + event.data = data; + } + target.dispatchEvent(event); + const handler = target[`on${type}`]; + if (typeof handler === "function") { + handler.call(target, event); + } + }; + + class CypressMediaRecorder { + constructor(stream, options = {}) { + this.stream = stream; + this.mimeType = options.mimeType || "audio/wav"; + this.state = "inactive"; + this._em = win.document.createDocumentFragment(); + } + + start(timeslice) { + if (this.state !== "inactive") { + return; + } + this.state = "recording"; + emit(this, "start"); + + if (timeslice) { + this._slicing = win.setInterval(() => { + if (this.state === "recording") { + const emptyBlob = new Blob([], { type: this.mimeType }); + emit(this, "dataavailable", emptyBlob); + } + }, timeslice); + } + } + + stop() { + if (this.state === "inactive") { + return; + } + if (this._slicing) { + win.clearInterval(this._slicing); + this._slicing = null; + } + const blob = getRecordingBlob(); + emit(this, "dataavailable", blob); + this.state = "inactive"; + emit(this, "stop"); + } + + pause() { + if (this.state !== "recording") { + return; + } + this.state = "paused"; + emit(this, "pause"); + } + + resume() { + if (this.state !== "paused") { + return; + } + this.state = "recording"; + emit(this, "resume"); + } + + requestData() { + if (this.state === "inactive") { + return; + } + const emptyBlob = new Blob([], { type: this.mimeType }); + emit(this, "dataavailable", emptyBlob); + } + + addEventListener(...args) { + this._em.addEventListener(...args); + } + + removeEventListener(...args) { + this._em.removeEventListener(...args); + } + + dispatchEvent(...args) { + this._em.dispatchEvent(...args); + } + } + + CypressMediaRecorder.isTypeSupported = (mimeType) => + mimeType === "audio/wav" || mimeType === audioMimeType; + CypressMediaRecorder.prototype.mimeType = audioMimeType || "audio/wav"; + + win.MediaRecorder = CypressMediaRecorder; + }; + + const applyMediaStubs = () => { + if (!win.navigator.permissions) { + safeDefine(win.navigator, "permissions", {}); + } + + if (win.navigator.permissions) { + const permissionsQuery = (desc) => { + if (!desc || desc.name === "microphone" || desc.name === "camera") { + win.__cypressMicPermissionGranted = true; + return Promise.resolve({ + onchange: null, + state: "granted", + }); + } + return Promise.resolve({ + onchange: null, + state: "prompt", + }); + }; + + safeDefine(win.navigator.permissions, "query", permissionsQuery); + + const permissionsProto = + (win.Permissions && win.Permissions.prototype) || + Object.getPrototypeOf(win.navigator.permissions); + + safeDefine(permissionsProto, "query", permissionsQuery); + } + + const stubMediaDevices = { + addEventListener() {}, + dispatchEvent() {}, + enumerateDevices: () => Promise.resolve(fallbackDevices), + getUserMedia: () => { + win.__cypressMicPermissionGranted = true; + return Promise.resolve(getForcedStream()); + }, + removeEventListener() {}, + }; + + if (win.Navigator && win.Navigator.prototype) { + safeDefineGetter( + win.Navigator.prototype, + "mediaDevices", + () => stubMediaDevices, + ); + } + + safeDefineGetter(win.navigator, "mediaDevices", () => stubMediaDevices); + + if (!win.navigator.mediaDevices) { + return; + } + + const fallbackDevices = [ + { + deviceId: "default", + groupId: "default_group_id", + kind: "audioinput", + label: "Default Microphone", + }, + { + deviceId: "communications", + groupId: "communications_group_id", + kind: "audioinput", + label: "Communications Microphone", + }, + ]; + + const patchMediaDevices = (target) => { + if (!target) { + return; + } + + safeDefine(target, "enumerateDevices", () => + Promise.resolve(fallbackDevices), + ); + + safeDefine(target, "getUserMedia", () => { + win.__cypressMicPermissionGranted = true; + return Promise.resolve(getForcedStream()); + }); + }; + + patchMediaDevices(win.navigator.mediaDevices); + patchMediaDevices(Object.getPrototypeOf(win.navigator.mediaDevices)); + + if (win.MediaDevices && win.MediaDevices.prototype) { + patchMediaDevices(win.MediaDevices.prototype); + } + + safeDefine(win.navigator, "getUserMedia", (..._args) => { + win.__cypressMicPermissionGranted = true; + return Promise.resolve(getForcedStream()); + }); + + safeDefine(win.navigator, "webkitGetUserMedia", (..._args) => { + win.__cypressMicPermissionGranted = true; + return Promise.resolve(getForcedStream()); + }); + + installMediaRecorderStub(); + }; + + win.__cypressBuildAudioStream = buildAudioStream; + win.__cypressApplyMediaStubs = applyMediaStubs; + win.__cypressEnsureAudioPlayback = ensurePlayback; + win.__cypressGetAudioBlob = getRecordingBlob; + + applyMediaStubs(); + installMediaRecorderStub(); + }); +}; + +// ============= Legacy Functions ============= + +export const addParticipant = (details) => { + cy.log("Adding participant", details); +}; + +// ============= Functions needed by Test 14 ============= + +/** + * Reapply audio stubs after navigation + */ +export const reapplyParticipantAudioStubs = () => { + cy.window({ log: false }).then((win) => { + if (win.__cypressApplyMediaStubs) { + win.__cypressApplyMediaStubs(); + } + if (win.__cypressEnsureAudioPlayback) { + win.__cypressEnsureAudioPlayback(); + } + }); +}; + +/** + * Prime microphone access + */ +export const primeMicrophoneAccess = () => { + cy.window({ log: false }).then((win) => { + if (win.__cypressApplyMediaStubs) { + win.__cypressApplyMediaStubs(); + } + if (win.__cypressEnsureAudioPlayback) { + win.__cypressEnsureAudioPlayback(); + } + + const mediaDevices = win.navigator && win.navigator.mediaDevices; + if (mediaDevices && typeof mediaDevices.getUserMedia === "function") { + return mediaDevices + .getUserMedia({ audio: true }) + .then((stream) => { + win.__cypressGrantedStream = stream; + if (mediaDevices.dispatchEvent && win.Event) { + try { + mediaDevices.dispatchEvent(new win.Event("devicechange")); + } catch (_error) {} + } + return stream; + }) + .catch(() => {}); + } + }); +}; + +/** + * Handle microphone access denied modal + */ +export const handleMicrophoneAccessDenied = () => { + cy.log("Handling microphone access denied"); + cy.get("body").then(($body) => { + if ($body.text().includes("microphone access was denied")) { + cy.contains("button", "Check microphone access").click({ force: true }); + cy.wait(2000); + } + }); +}; + +/** + * Confirm finish conversation in modal + */ +export const confirmFinishConversation = () => { + cy.log("Confirming finish conversation"); + cy.get("body").then(($body) => { + if ( + $body.text().includes("Finish Conversation") || + $body.text().includes("Are you sure") + ) { + cy.contains("button", "Yes").click({ force: true }); + cy.wait(2000); + } + }); +}; + +/** + * Finish recording from the pause/stop modal + */ +export const finishRecordingFromModal = () => { + cy.log("Finishing recording from modal"); + cy.get("body").then(($body) => { + if ($body.find('button:contains("Finish")').length > 0) { + cy.contains("button", "Finish").click({ force: true }); + cy.wait(1000); + } + }); +}; + +/** + * Retry recording if access was denied + */ +export const retryRecordingIfAccessDenied = () => { + reapplyParticipantAudioStubs(); + primeMicrophoneAccess(); + cy.wait(1000); + cy.get("body").then(($body) => { + if ($body.text().includes("microphone access was denied")) { + cy.contains("button", "Check microphone access").click({ force: true }); + cy.wait(2000); + reapplyParticipantAudioStubs(); + primeMicrophoneAccess(); + cy.contains("button", "Record", { timeout: 15000 }) + .should("be.visible") + .click({ force: true }); + } + }); +}; + +/** + * Prepare for recording - handle any pre-recording states + */ +export const prepareForRecording = () => { + reapplyParticipantAudioStubs(); + primeMicrophoneAccess(); +}; diff --git a/echo/cypress/support/functions/portal/index.js b/echo/cypress/support/functions/portal/index.js new file mode 100644 index 00000000..358d2668 --- /dev/null +++ b/echo/cypress/support/functions/portal/index.js @@ -0,0 +1,370 @@ +export const openPortalEditor = () => { + cy.log('Opening Portal Editor'); + // Click on the "Portal Editor" tab using data-testid + cy.get('[data-testid="project-overview-tab-portal-editor"]') + .scrollIntoView() + .should('be.visible') + .click({ force: true }); + cy.wait(1000); +}; + +export const selectTutorial = (tutorialName = 'Basic') => { + cy.log(`Selecting Tutorial: ${tutorialName}`); + // The tutorial selector uses data-testid + cy.get('[data-testid="portal-editor-tutorial-select"]') + .scrollIntoView() + .should('be.visible') + .select(tutorialName.toLowerCase()); +}; + +export const addTag = (tagName) => { + cy.log(`Adding Tag: ${tagName}`); + // 1. Find Tag Input using data-testid + cy.get('[data-testid="portal-editor-tags-input"]') + .scrollIntoView() + .should('be.visible') + .type(tagName); + + // 2. Add the Tag using data-testid + cy.get('[data-testid="portal-editor-add-tag-button"]').should('be.visible').click(); + + // 3. Verify Tag Added + cy.contains(tagName).should('be.visible'); +}; + +export const updatePortalContent = (title, content, thankYouContent) => { + cy.log('Updating Portal Content'); + + // Page Title using data-testid + if (title) { + cy.get('[data-testid="portal-editor-page-title-input"]') + .scrollIntoView() + .should('be.visible') + .clear() + .type(title); + } + + // Page Content - MDX Editor using data-testid + if (content) { + cy.get('[data-testid="portal-editor-page-content-editor"]') + .find('[data-lexical-editor="true"]') + .scrollIntoView() + .should('be.visible') + .click() + .clear() + .type(content); + } + + // Thank You Page Content - MDX Editor using data-testid + if (thankYouContent) { + cy.get('[data-testid="portal-editor-thank-you-content-editor"]') + .find('[data-lexical-editor="true"]') + .scrollIntoView() + .should('be.visible') + .click() + .clear() + .type(thankYouContent); + } + + // Auto-save is in effect, just wait for it + cy.wait(3000); + // cy.contains(/saved/i).should('exist'); +}; + +export const changePortalLanguage = (langCode) => { + cy.log(`Changing Portal Language to: ${langCode}`); + // The language selector uses data-testid + cy.get('[data-testid="portal-editor-language-select"]') + .scrollIntoView() + .should('be.visible') + .select(langCode); + + // Wait for auto-save + cy.wait(2000); +}; + +// Toggle "Ask for Name" checkbox +export const toggleAskForName = (enable = true) => { + cy.log(`Toggling Ask for Name: ${enable}`); + cy.get('[data-testid="portal-editor-ask-name-checkbox"]').then(($checkbox) => { + const isChecked = $checkbox.is(':checked'); + if ((enable && !isChecked) || (!enable && isChecked)) { + cy.wrap($checkbox).click({ force: true }); + } + }); +}; + +// Toggle "Ask for Email" checkbox +export const toggleAskForEmail = (enable = true) => { + cy.log(`Toggling Ask for Email: ${enable}`); + cy.contains('label', 'Ask for Email?') + .scrollIntoView() + .should('be.visible') + .then(($label) => { + const $checkbox = $label.closest('.mantine-Checkbox-root').find('input[type="checkbox"]'); + const isChecked = $checkbox.is(':checked'); + if ((enable && !isChecked) || (!enable && isChecked)) { + cy.wrap($checkbox).click({ force: true }); + } + }); +}; + +// Toggle "Make it Concrete" feature +export const toggleMakeItConcrete = (enable = true) => { + cy.log(`Toggling Make it Concrete: ${enable}`); + + // 1. Get input (it might be hidden due to Mantine styling) + cy.get('[data-testid="portal-editor-make-concrete-switch"]') + .should('exist') + .then(($input) => { + // Find the visible label/wrapper + const $label = $input.closest('label'); + cy.wrap($label).scrollIntoView().should('be.visible'); + + const isChecked = $input.is(':checked'); + if ((enable && !isChecked) || (!enable && isChecked)) { + // Click the label for robust interaction + cy.wrap($label).click({ force: true }); + + // Wait for potential auto-save or UI update + cy.wait(1000); + } + }); + + // 2. Hard check as requested + if (enable) { + cy.get('[data-testid="portal-editor-make-concrete-switch"]').should('be.checked'); + } else { + cy.get('[data-testid="portal-editor-make-concrete-switch"]').should('not.be.checked'); + } + + // 3. Verify 'Saved' state to ensure persistence + // cy.contains(/saved/i).should('exist'); +}; + + +export const toggleGoDeeper = (enable = true) => { + cy.log(`Toggling Go Deeper: ${enable}`); + + // 1. Get input (it might be hidden due to Mantine styling) + cy.get('[data-testid="portal-editor-go-deeper-switch"]') + .should('exist') + .then(($input) => { + // Find the visible label/wrapper + const $label = $input.closest('label'); + cy.wrap($label).scrollIntoView().should('be.visible'); + + const isChecked = $input.is(':checked'); + if ((enable && !isChecked) || (!enable && isChecked)) { + // Click the label for robust interaction + cy.wrap($label).click({ force: true }); + + // Wait for potential auto-save or UI update + cy.wait(1000); + } + }); + + // 2. Hard check as requested + if (enable) { + cy.get('[data-testid="portal-editor-go-deeper-switch"]').should('be.checked'); + } else { + cy.get('[data-testid="portal-editor-go-deeper-switch"]').should('not.be.checked'); + } + + // 3. Verify 'Saved' state to ensure persistence + // cy.contains(/saved/i).should('exist'); +}; + +// Toggle "Report Notifications" feature +export const toggleReportNotifications = (enable = true) => { + cy.log(`Toggling Report Notifications: ${enable}`); + cy.get('[data-testid="portal-editor-report-notifications-switch"]').then(($switch) => { + const isChecked = $switch.is(':checked'); + if ((enable && !isChecked) || (!enable && isChecked)) { + cy.wrap($switch).click({ force: true }); + } + }); +}; + +// Toggle Preview mode +export const togglePreview = () => { + cy.log('Toggling Portal Editor Preview'); + cy.get('[data-testid="portal-editor-preview-toggle"]').should('be.visible').click(); +}; + +// Verify QR Code is visible +export const verifyQrCodeVisible = () => { + cy.log('Verifying QR Code is visible'); + cy.get('[data-testid="project-qr-code"]').should('be.visible'); +}; + +// Click Share button +export const clickShareButton = () => { + cy.log('Clicking Share button'); + cy.get('[data-testid="project-share-button"]').should('be.visible').click(); +}; + +// Click Copy Link button +export const clickCopyLinkButton = () => { + cy.log('Clicking Copy Link button'); + cy.get('[data-testid="project-copy-link-button"]').should('be.visible').click(); +}; + +// Toggle Open for Participation +export const toggleOpenForParticipation = (enable = true) => { + cy.log(`Toggling Open for Participation: ${enable}`); + + // Wait for the toggle to be present and the page to be fully loaded + cy.get('[data-testid="dashboard-open-for-participation-toggle"]', { timeout: 30000 }) + .should('exist'); + + cy.wait(2000); // Let React fully render and settle + + cy.get('[data-testid="dashboard-open-for-participation-toggle"]', { timeout: 20000 }) + .then(($inputs) => { + // Find a visible input + const $visibleInput = Cypress.$($inputs).filter((_, el) => { + return Cypress.$(el).closest('.mantine-Switch-root').is(':visible'); + }).first(); + + const $input = $visibleInput.length > 0 ? $visibleInput : Cypress.$($inputs.first()); + const currentlyChecked = $input.prop('checked'); + + cy.log(`Toggle current state: checked=${currentlyChecked}, desired=${enable}`); + + if (currentlyChecked === enable) { + cy.log('Toggle already in desired state, skipping'); + return; + } + + // Use native DOM click - this properly triggers browser checkbox behavior + // and React's event delegation (synthetic onChange handler) + $input[0].click(); + }); + + cy.wait(3000); // Wait for API mutation to complete + + // Reload and verify persisted state + cy.reload(); + cy.wait(3000); + + // Check state after reload, retry once if needed + cy.get('[data-testid="dashboard-open-for-participation-toggle"]', { timeout: 20000 }) + .then(($inputs) => { + const $visibleInput = Cypress.$($inputs).filter((_, el) => { + return Cypress.$(el).closest('.mantine-Switch-root').is(':visible'); + }).first(); + + const $input = $visibleInput.length > 0 ? $visibleInput : Cypress.$($inputs.first()); + const currentlyChecked = $input.prop('checked'); + + cy.log(`After reload: checked=${currentlyChecked}, desired=${enable}`); + + if (currentlyChecked !== enable) { + // State didn't persist - try clicking again + cy.log('State did not persist, retrying click'); + $input[0].click(); + cy.wait(3000); + cy.reload(); + cy.wait(3000); + } + }); + + // Final verification + cy.get('[data-testid="dashboard-open-for-participation-toggle"]', { timeout: 20000 }) + .then(($inputs) => { + const $visibleInput = Cypress.$($inputs).filter((_, el) => { + return Cypress.$(el).closest('.mantine-Switch-root').is(':visible'); + }).first(); + const $input = $visibleInput.length > 0 ? $visibleInput : Cypress.$($inputs.first()); + expect($input.prop('checked'), 'open for participation final state').to.equal(enable); + }); +}; + +// Select Reply Mode (default, brainstorm, custom) +export const selectReplyMode = (mode = 'default') => { + cy.log(`Selecting Reply Mode: ${mode}`); + const testId = `portal-editor-reply-mode-${mode}`; + cy.get(`[data-testid="${testId}"]`).scrollIntoView().should('be.visible').click(); +}; + +// Set custom reply prompt (only works when reply mode is custom) +export const setReplyPrompt = (promptText) => { + cy.log('Setting custom reply prompt'); + cy.get('[data-testid="portal-editor-reply-prompt-textarea"]') + .scrollIntoView() + .should('be.visible') + .clear() + .type(promptText); +}; + +// Set specific context +export const setSpecificContext = (contextText) => { + cy.log('Setting specific context'); + cy.get('[data-testid="portal-editor-specific-context-input"]') + .scrollIntoView() + .should('be.visible') + .clear() + .type(contextText); +}; + +// ============= Project Search Functions ============= + +export const searchProject = (searchTerm) => { + cy.log(`Searching for project: ${searchTerm}`); + cy.get('[data-testid="project-search-input"]') + .should('be.visible') + .clear() + .type(searchTerm); +}; + +export const clearProjectSearch = () => { + cy.log('Clearing project search'); + cy.get('[data-testid="project-search-clear-button"]').should('be.visible').click(); +}; + +// ============= Project Clone Functions ============= + +export const cloneProject = (newName) => { + cy.log(`Cloning project with name: ${newName}`); + + // Click clone button + cy.get('[data-testid="project-actions-clone-button"]').scrollIntoView().should('be.visible').click(); + cy.wait(1000); + + // Fill in new name in modal + cy.get('[data-testid="project-clone-modal"]').should('be.visible'); + cy.get('[data-testid="project-clone-name-input"]').clear().type(newName); + + // Confirm clone + cy.get('[data-testid="project-clone-confirm-button"]').click(); + cy.wait(5000); // Wait for clone operation +}; + +// ============= Announcement Functions ============= + +export const openAnnouncementDrawer = () => { + cy.log('Opening announcement drawer'); + cy.get('[data-testid="announcement-icon-button"]').filter(':visible').first().click(); + cy.get('[data-testid="announcement-drawer"]').should('be.visible'); +}; + +export const closeAnnouncementDrawer = () => { + cy.log('Closing announcement drawer'); + cy.get('[data-testid="announcement-close-drawer-button"]').should('be.visible').click(); +}; + +export const verifyNoAnnouncements = () => { + cy.log('Verifying no announcements available'); + cy.get('[data-testid="announcement-empty-state"]').should('be.visible'); +}; + +export const markAllAnnouncementsRead = () => { + cy.log('Marking all announcements as read'); + cy.get('[data-testid="announcement-mark-all-read-button"]').should('be.visible').click(); +}; + +export const getUnreadAnnouncementCount = () => { + cy.log('Getting unread announcement count'); + return cy.get('[data-testid="announcement-unread-count"]').invoke('text'); +}; diff --git a/echo/cypress/support/functions/project/index.js b/echo/cypress/support/functions/project/index.js new file mode 100644 index 00000000..e2be72bc --- /dev/null +++ b/echo/cypress/support/functions/project/index.js @@ -0,0 +1,189 @@ +export const createProject = () => { + cy.log("Creating New Project"); + + // 1. Click Create Button using data-testid + cy.get('[data-testid="project-home-create-button"]') + .should("be.visible") + .click(); + + // 2. Wait for Project Creation (Automatic Navigation) + cy.wait(8000); + + // 3. Verify Navigation to Project Overview + cy.url().should("include", "/projects/"); + cy.url().should("include", "/overview"); + + // 4. Capture Project ID and Store it + cy.url().then((url) => { + const parts = url.split("/"); + const projectIndex = parts.indexOf("projects"); + if (projectIndex !== -1 && parts[projectIndex + 1]) { + const projectId = parts[projectIndex + 1]; + cy.log("Captured Project ID:", projectId); + + const filePath = "fixtures/createdProjects.json"; + cy.task("log", `Project Created: ${projectId}`); + + cy.readFile(filePath).then((projects) => { + if (!projects) projects = []; + projects.push({ + createdAt: new Date().toISOString(), + id: projectId, + name: "New Project", + }); + console.log(projects); + cy.writeFile(filePath, projects); + }); + } + }); +}; + +export const verifyProjectPage = (expectedName = "New Project") => { + cy.log("Verifying Project Page"); + + // Verify project name input has expected value using data-testid + cy.get('[data-testid="project-settings-name-input"]') + .should("be.visible") + .should("have.value", expectedName); +}; + +export const navigateToHome = () => { + cy.log("Navigating Back to Home"); + + cy.window().then((win) => { + const isMobile = win.innerWidth < 768; + + if (isMobile) { + // On mobile, use direct navigation + cy.url().then((currentUrl) => { + const locale = currentUrl.includes("/en-US/") + ? "en-US" + : currentUrl.includes("/nl-NL/") + ? "nl-NL" + : "en-US"; + cy.visit(`/${locale}/projects`); + }); + } else { + // On desktop, click the home breadcrumb using data-testid (filter visible for mobile/desktop duplicates) + cy.get('[data-testid="project-breadcrumb-home"]') + .filter(":visible") + .first() + .click(); + } + + // Verify we are back on the list page + cy.url().should("match", /\/projects$/); + cy.wait(2000); + }); +}; + +export const deleteProject = (projectId) => { + cy.log(`Deleting Project: ${projectId}`); + + // 1. Navigate to Project Settings using data-testid + cy.get('[data-testid="project-overview-tab-overview"]') + .should("be.visible") + .click(); + cy.wait(5000); + + // 2. Click "Delete Project" button using data-testid + cy.get('[data-testid="project-actions-delete-button"]') + .scrollIntoView() + .should("be.visible") + .click(); + cy.wait(5000); + // 3. Wait for modal to appear and confirm deletion + cy.get('[data-testid="project-delete-confirm-button"]', { timeout: 15000 }) + .should("be.visible") + .click(); + + // 4. Wait for Deletion and Redirect + cy.wait(5000); + + // 5. Verify Redirect to Projects Dashboard + cy.url().should("match", /\/projects$/); + + // 6. Verify Project ID is NOT present in the list + cy.get(`a[href*="${projectId}"]`).should("not.exist"); + + // 7. Remove from JSON fixture + const filePath = "fixtures/createdProjects.json"; + cy.readFile(filePath).then((projects) => { + if (projects && projects.length > 0) { + const updatedProjects = projects.filter((p) => p.id !== projectId); + cy.writeFile(filePath, updatedProjects); + cy.log(`Removed project ${projectId} from fixture.`); + } + }); +}; +export const deleteProjectInsideProjectSettings = (projectId) => { + cy.log(`Deleting Project: ${projectId}`); + + // Click "Delete Project" button using data-testid + cy.get('[data-testid="project-actions-delete-button"]') + .scrollIntoView() + .should("be.visible") + .click(); + + // Wait for modal to appear and confirm deletion + cy.get('[data-testid="project-delete-confirm-button"]', { timeout: 10000 }) + .should("be.visible") + .click(); + + // Wait for Deletion and Redirect + cy.wait(5000); + + // Verify Redirect to Projects Dashboard + cy.url().should("match", /\/projects$/); + + // Verify Project ID is NOT present in the list + cy.get(`a[href*="${projectId}"]`).should("not.exist"); + + // Remove from JSON fixture + const filePath = "fixtures/createdProjects.json"; + cy.readFile(filePath).then((projects) => { + if (projects && projects.length > 0) { + const updatedProjects = projects.filter((p) => p.id !== projectId); + cy.writeFile(filePath, updatedProjects); + cy.log(`Removed project ${projectId} from fixture.`); + } + }); +}; + +export const updateProjectName = (newName) => { + cy.log(`Updating Project Name to: ${newName}`); + + // 1. Ensure we are on Project Settings using data-testid + cy.get('[data-testid="project-overview-tab-overview"]') + .should("be.visible") + .click(); + cy.wait(1000); + + // 2. Find and update Name Input using data-testid + cy.get('[data-testid="project-settings-name-input"]') + .should("be.visible") + .clear() + .type(newName) + .blur(); + + // 3. Handle Auto-Save + cy.wait(3000); + + // Verify "saved" indication exists + // cy.contains(/saved/i).should('exist'); +}; + +export const openProjectSettings = () => { + cy.log("Opening Project Settings Tab"); + cy.get('[data-testid="project-overview-tab-overview"]') + .scrollIntoView() + .click({ force: true }); + cy.wait(1000); +}; + +export const exportProjectTranscripts = () => { + cy.log("Exporting Project Transcripts"); + cy.get('[data-testid="project-export-transcripts-button"]') + .scrollIntoView() + .click({ force: true }); +}; diff --git a/echo/cypress/support/functions/report/index.js b/echo/cypress/support/functions/report/index.js new file mode 100644 index 00000000..eabdc10e --- /dev/null +++ b/echo/cypress/support/functions/report/index.js @@ -0,0 +1,319 @@ +/** + * Report Functions + * Helper functions for the Report feature in the Echo application. + * Updated to use data-testid selectors for robust testing. + */ + +// ============= Report Creation ============= + +/** + * Opens the report create modal + */ +export const openReportCreateModal = () => { + cy.log('Opening Report Create Modal'); + cy.get('[data-testid="report-create-modal"]').should('be.visible'); +}; + +/** + * Selects a language for the report + */ +export const selectReportLanguage = (langCode) => { + cy.log('Selecting report language:', langCode); + cy.get('[data-testid="report-language-select"]').should('be.visible').select(langCode); +}; + +/** + * Creates the report + */ +export const createReport = () => { + cy.log('Creating Report'); + cy.get('[data-testid="report-create-button"]').should('be.visible').click(); + cy.wait(10000); // Wait for report generation +}; + +/** + * Generates a report (complete flow) + */ +export const generateReport = (langCode = 'en') => { + cy.log('Generating report with language:', langCode); + selectReportLanguage(langCode); + createReport(); +}; + +// ============= Report Actions ============= + +/** + * Clicks the share button (mobile) + */ +export const shareReport = () => { + cy.log('Sharing Report'); + cy.get('[data-testid="report-share-button"]').should('be.visible').click(); +}; + +/** + * Copies the report link + */ +export const copyReportLink = () => { + cy.log('Copying Report Link'); + cy.get('[data-testid="report-copy-link-button"]').should('be.visible').click(); +}; + +/** + * Prints the report + */ +export const printReport = () => { + cy.log('Printing Report'); + cy.get('[data-testid="report-print-button"]').should('be.visible').click(); +}; + +/** + * Toggles report publish status + */ +export const togglePublishReport = () => { + cy.log('Toggling Report Publish'); + cy.get('[data-testid="report-publish-toggle"]').click(); +}; + +/** + * Publishes the report. + * Confirmation modal is optional and handled only when present. + */ +export const publishReportWithConfirmation = () => { + cy.log('Publishing Report (optional confirmation)'); + togglePublishReport(); + + cy.wait(2000); +}; + +/** + * Cancels the publish confirmation + */ +export const cancelPublishReport = () => { + cy.log('Canceling Report Publish'); + cy.get('[data-testid="report-publish-cancel-button"]').should('be.visible').click(); +}; + +// ============= Report Settings ============= + +/** + * Toggles the portal link inclusion in report + */ +export const toggleIncludePortalLink = () => { + cy.log('Toggling Include Portal Link'); + cy.get('[data-testid="report-include-portal-link-checkbox"]').click(); +}; + +/** + * Toggles editing mode + */ +export const toggleEditingMode = () => { + cy.log('Toggling Editing Mode'); + cy.get('[data-testid="report-editing-mode-toggle"]').click(); +}; + +// ============= Report View/Render ============= + +/** + * Verifies the report renderer is visible + */ +export const verifyReportRendered = () => { + cy.log('Verifying Report Rendered'); + cy.get('[data-testid="report-renderer-container"]').should('be.visible'); +}; + +/** + * Verifies the report is loading + */ +export const verifyReportLoading = () => { + cy.log('Verifying Report Loading'); + cy.get('[data-testid="report-renderer-loading"]').should('be.visible'); +}; + +/** + * Waits for report to finish loading + */ +export const waitForReportLoad = (timeout = 30000) => { + cy.log('Waiting for Report to Load'); + cy.get('[data-testid="report-renderer-loading"]', { timeout }).should('not.exist'); + cy.get('[data-testid="report-renderer-container"]').should('be.visible'); +}; + +/** + * Verifies no report is available + */ +export const verifyNoReportAvailable = () => { + cy.log('Verifying No Report Available'); + cy.get('[data-testid="report-renderer-not-found"]').should('be.visible'); +}; + +// ============= Public Report View ============= + +/** + * Verifies the public report view is visible + */ +export const verifyPublicReportView = () => { + cy.log('Verifying Public Report View'); + cy.get('[data-testid="public-report-view"]').should('be.visible'); +}; + +/** + * Verifies report is not available publicly + */ +export const verifyReportNotPublished = () => { + cy.log('Verifying Report Not Published'); + cy.get('[data-testid="public-report-not-available"]').should('be.visible'); +}; + +// ============= Conversation Status ============= + +/** + * Verifies the conversation status modal + */ +export const verifyConversationStatusModal = () => { + cy.log('Verifying Conversation Status Modal'); + cy.get('[data-testid="report-conversation-status-modal"]').should('be.visible'); +}; + +// ============= Report Test Flow Helpers ============= + +/** + * Registers common exception handling used by report E2E suites. + */ +export const registerReportFlowExceptionHandling = () => { + cy.on('uncaught:exception', (err) => { + if (err.message.includes('Syntax error, unrecognized expression') || + err.message.includes('BODY[style=') || + err.message.includes('ResizeObserver loop limit exceeded')) { + return false; + } + return true; + }); +}; + +/** + * Ensures report publish switch reaches target state and persists after reload. + */ +export const setReportPublishState = (expectedChecked, options = {}) => { + const { + checkTimeout = 20000, + persistRetries = 6, + persistWaitMs = 3000, + confirmModal = true + } = options; + + cy.get('[data-testid="report-publish-toggle"]', { timeout: checkTimeout }) + .should('exist') + .then(($toggle) => { + const isChecked = $toggle.prop('checked'); + if (isChecked !== expectedChecked) { + if (expectedChecked) { + cy.wrap($toggle).check({ force: true }); + } else { + cy.wrap($toggle).uncheck({ force: true }); + } + } + }); + + cy.get('[data-testid="report-publish-toggle"]', { timeout: checkTimeout }) + .should(expectedChecked ? 'be.checked' : 'not.be.checked'); + + const verifyStateAfterReload = (retriesLeft) => { + cy.reload(); + cy.get('[data-testid="report-publish-toggle"]', { timeout: checkTimeout }) + .should('exist') + .then(($toggle) => { + const isChecked = $toggle.prop('checked'); + if (isChecked === expectedChecked) { + return; + } + + if (retriesLeft <= 0) { + throw new Error(`Report publish state did not persist as ${expectedChecked}.`); + } + + cy.wait(persistWaitMs); + verifyStateAfterReload(retriesLeft - 1); + }); + }; + + verifyStateAfterReload(persistRetries); +}; + +/** + * Ensures include-portal-link checkbox reaches target state. + */ +export const setReportPortalLinkState = (expectedChecked, timeout = 20000) => { + cy.get('[data-testid="report-include-portal-link-checkbox"]', { timeout }) + .should('exist') + .then(($input) => { + const isChecked = $input.prop('checked'); + if (isChecked !== expectedChecked) { + if (expectedChecked) { + cy.wrap($input).check({ force: true }); + } else { + cy.wrap($input).uncheck({ force: true }); + } + } + }); + + cy.get('[data-testid="report-include-portal-link-checkbox"]') + .should(expectedChecked ? 'be.checked' : 'not.be.checked'); +}; + +/** + * Waits until public report becomes available. + */ +export const waitForPublicReportPublished = (options = {}) => { + const { + retries, + maxWaitMs = 120000, + waitMs = 5000, + timeout = 20000, + reloadBetweenChecks = true + } = options; + + const startedAt = Date.now(); + const effectiveMaxWaitMs = typeof retries === 'number' + ? (retries + 1) * (waitMs + timeout) + : maxWaitMs; + + const poll = () => { + return cy.get('body', { timeout }).then(($body) => { + const hasPublicViewWrapper = $body.find('[data-testid="public-report-view"]').length > 0; + const hasRenderedReport = $body.find('[data-testid="report-renderer-container"]').length > 0; + const hasRendererLoading = $body.find('[data-testid="report-renderer-loading"]').length > 0; + const hasNotAvailableState = $body.find('[data-testid="public-report-not-available"]').length > 0; + const hasRenderedMarkdownFallback = $body.find('.prose').length > 0; + const hasParticipantLoading = $body.find('[data-testid="participant-report-loading"]').length > 0; + const elapsedMs = Date.now() - startedAt; + const timedOut = elapsedMs >= effectiveMaxWaitMs; + + if (!hasNotAvailableState && (hasRenderedReport || hasRenderedMarkdownFallback)) { + if (hasRenderedReport) { + return cy.get('[data-testid="report-renderer-container"]').should('be.visible'); + } + return cy.get('.prose').should('be.visible'); + } + + if (timedOut) { + throw new Error( + `Public report was not published in time (${effectiveMaxWaitMs}ms). ` + + `publicView=${hasPublicViewWrapper}, ` + + `renderer=${hasRenderedReport}, ` + + `markdownFallback=${hasRenderedMarkdownFallback}, ` + + `rendererLoading=${hasRendererLoading}, ` + + `participantLoading=${hasParticipantLoading}, ` + + `notAvailable=${hasNotAvailableState}` + ); + } + + cy.wait(waitMs); + if (reloadBetweenChecks) { + cy.reload(); + } + return poll(); + }); + }; + + return poll(); +}; diff --git a/echo/cypress/support/functions/settings/index.js b/echo/cypress/support/functions/settings/index.js new file mode 100644 index 00000000..0242f06b --- /dev/null +++ b/echo/cypress/support/functions/settings/index.js @@ -0,0 +1,41 @@ +export const openSettingsMenu = () => { + cy.log('Opening Settings Menu'); + // Wait for stability (handling detached DOM / hydration re-render issues) + cy.wait(2000); + // Using data-testid for robust selection - filter visible for mobile/desktop duplicates + cy.get('[data-testid="header-settings-gear-button"]').filter(':visible').first().click(); + cy.wait(1000); // Wait for menu animation +}; + +export const changeLanguage = (langCode) => { + cy.log('Changing language to:', langCode); + + // The language selector uses data-testid="header-language-picker" + cy.get('[data-testid="header-language-picker"]').filter(':visible').first().select(langCode); + + // Wait for page reload/navigation if it occurs + cy.wait(2000); +}; + +export const verifyLanguage = (expectedLogoutText, expectedUrlLocale) => { + cy.log('Verifying language change'); + + // 1. Verify URL contains the locale (e.g., /es-ES/) + if (expectedUrlLocale) { + cy.url().should('include', `/${expectedUrlLocale}/`); + } + + // 2. Verify Logout button is visible using data-testid + // The menu should be open to check this. + cy.get('body').then(($body) => { + // If the dropdown isn't visible, re-open the menu + if ($body.find('div[role="menu"]').length === 0 && $body.find('.mantine-Menu-dropdown').length === 0) { + openSettingsMenu(); + } + + // Check Logout button text using data-testid - filter visible for mobile/desktop duplicates + cy.get('[data-testid="header-logout-menu-item"]').filter(':visible').first() + .contains(expectedLogoutText); + }); +}; + diff --git a/echo/cypress/test-suites/desktop/run-chrome-suite.ps1 b/echo/cypress/test-suites/desktop/run-chrome-suite.ps1 new file mode 100644 index 00000000..49fd9ffd --- /dev/null +++ b/echo/cypress/test-suites/desktop/run-chrome-suite.ps1 @@ -0,0 +1,10 @@ +$ErrorActionPreference = "Continue" + +Push-Location "$PSScriptRoot\..\.." +try { + & .\test-suites\run-core-suite.ps1 -ViewportWidth 1440 -ViewportHeight 900 -Browser "chrome" -SuiteId "desktop" + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/echo/cypress/test-suites/desktop/run-edge-suite.ps1 b/echo/cypress/test-suites/desktop/run-edge-suite.ps1 new file mode 100644 index 00000000..1ba33210 --- /dev/null +++ b/echo/cypress/test-suites/desktop/run-edge-suite.ps1 @@ -0,0 +1,29 @@ +$ErrorActionPreference = "Continue" + +Push-Location "$PSScriptRoot\..\.." +try { + # Skip recording/audio tests for Edge + $specFilesToRun = "" + $allSpecFiles = Get-ChildItem -Path "e2e/suites" -Filter "*.cy.js" -File + $edgeExcludePattern = 'uploadAudioFile|openUploadModal|portal-onboarding-mic|portal-audio-|installParticipantAudioStubs|retranscribeConversation|videoplayback\.mp3|sampleaudio\.mp3|test-audio\.wav|startRecording|stopRecording|getPortalUrl' + + foreach ($specFile in $allSpecFiles) { + $content = Get-Content -Path $specFile.FullName -Raw + if ($content -notmatch $edgeExcludePattern) { + $specFilesToRun += "e2e/suites/$($specFile.Name)," + } + } + + $specFilesToRun = $specFilesToRun.TrimEnd(',') + + if ([string]::IsNullOrWhiteSpace($specFilesToRun)) { + Write-Host "No specs to run after filtering." -ForegroundColor Yellow + exit 0 + } + + & .\test-suites\run-core-suite.ps1 -ViewportWidth 1440 -ViewportHeight 900 -Browser "edge" -SuiteId "desktop" -SpecPattern $specFilesToRun + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/echo/cypress/test-suites/desktop/run-webkit-suite.ps1 b/echo/cypress/test-suites/desktop/run-webkit-suite.ps1 new file mode 100644 index 00000000..687e4379 --- /dev/null +++ b/echo/cypress/test-suites/desktop/run-webkit-suite.ps1 @@ -0,0 +1,10 @@ +$ErrorActionPreference = "Continue" + +Push-Location "$PSScriptRoot\..\.." +try { + & .\test-suites\run-core-suite.ps1 -ViewportWidth 1440 -ViewportHeight 900 -Browser "webkit" -SuiteId "desktop" + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/echo/cypress/test-suites/mobile/run-chrome-suite.ps1 b/echo/cypress/test-suites/mobile/run-chrome-suite.ps1 new file mode 100644 index 00000000..45905bfe --- /dev/null +++ b/echo/cypress/test-suites/mobile/run-chrome-suite.ps1 @@ -0,0 +1,10 @@ +$ErrorActionPreference = "Continue" + +Push-Location "$PSScriptRoot\..\.." +try { + & .\test-suites\run-core-suite.ps1 -ViewportWidth 375 -ViewportHeight 667 -Browser "chrome" -SuiteId "mobile" + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/echo/cypress/test-suites/mobile/run-edge-suite.ps1 b/echo/cypress/test-suites/mobile/run-edge-suite.ps1 new file mode 100644 index 00000000..188507e2 --- /dev/null +++ b/echo/cypress/test-suites/mobile/run-edge-suite.ps1 @@ -0,0 +1,29 @@ +$ErrorActionPreference = "Continue" + +Push-Location "$PSScriptRoot\..\.." +try { + # Skip recording/audio tests for Edge + $specFilesToRun = "" + $allSpecFiles = Get-ChildItem -Path "e2e/suites" -Filter "*.cy.js" -File + $edgeExcludePattern = 'uploadAudioFile|openUploadModal|portal-onboarding-mic|portal-audio-|installParticipantAudioStubs|retranscribeConversation|videoplayback\.mp3|sampleaudio\.mp3|test-audio\.wav|startRecording|stopRecording|getPortalUrl' + + foreach ($specFile in $allSpecFiles) { + $content = Get-Content -Path $specFile.FullName -Raw + if ($content -notmatch $edgeExcludePattern) { + $specFilesToRun += "e2e/suites/$($specFile.Name)," + } + } + + $specFilesToRun = $specFilesToRun.TrimEnd(',') + + if ([string]::IsNullOrWhiteSpace($specFilesToRun)) { + Write-Host "No specs to run after filtering." -ForegroundColor Yellow + exit 0 + } + + & .\test-suites\run-core-suite.ps1 -ViewportWidth 375 -ViewportHeight 667 -Browser "edge" -SuiteId "mobile" -SpecPattern $specFilesToRun + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/echo/cypress/test-suites/mobile/run-webkit-suite.ps1 b/echo/cypress/test-suites/mobile/run-webkit-suite.ps1 new file mode 100644 index 00000000..c1c40cf8 --- /dev/null +++ b/echo/cypress/test-suites/mobile/run-webkit-suite.ps1 @@ -0,0 +1,10 @@ +$ErrorActionPreference = "Continue" + +Push-Location "$PSScriptRoot\..\.." +try { + & .\test-suites\run-core-suite.ps1 -ViewportWidth 375 -ViewportHeight 667 -Browser "webkit" -SuiteId "mobile" + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/echo/cypress/test-suites/run-browser-tests.ps1 b/echo/cypress/test-suites/run-browser-tests.ps1 new file mode 100644 index 00000000..af0eba34 --- /dev/null +++ b/echo/cypress/test-suites/run-browser-tests.ps1 @@ -0,0 +1,325 @@ +# Suite 1: Cross-browser run (Chrome + Firefox + WebKit) in parallel +# - 3 parallel jobs (one per browser) +# - Retries are per-spec (only failed specs rerun) + +param( + [string]$SpecPattern = "e2e/suites/[0-9]*.cy.js", + [string]$Version = "staging", + [int]$ViewportWidth = 1440, + [int]$ViewportHeight = 900, + [int]$MaxRetries = 2, + [string[]]$Browsers = @("chrome", "firefox", "webkit") +) + +$ErrorActionPreference = "Continue" + +# Ensure Cypress launches Electron app mode, not Node mode. +if (Test-Path Env:ELECTRON_RUN_AS_NODE) { + Remove-Item Env:ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue +} + +# Use project-local Cypress cache for stability in this environment. +$env:CYPRESS_CACHE_FOLDER = "$PSScriptRoot\.cypress-cache" +$cypressExe = Get-ChildItem -Path $env:CYPRESS_CACHE_FOLDER -Recurse -Filter "Cypress.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 +if (-not $cypressExe) { + Write-Host "Cypress binary not found in local cache. Installing..." -ForegroundColor Yellow + npx cypress install +} + +$suiteRoot = "reports/suite-1-cross-browser" +$logsDir = "$suiteRoot/logs" +$screenshotsRoot = "$suiteRoot/screenshots" +$supportedBrowsers = @("chrome", "firefox", "webkit") +$browsers = @($Browsers | ForEach-Object { $_.ToLowerInvariant() } | Select-Object -Unique) +$invalidBrowsers = @($browsers | Where-Object { $_ -notin $supportedBrowsers }) + +if (-not $browsers -or $browsers.Count -eq 0) { + throw "No browsers provided. Use -Browsers chrome,firefox,webkit" +} + +if ($invalidBrowsers.Count -gt 0) { + throw "Unsupported browser(s): $($invalidBrowsers -join ', '). Supported: $($supportedBrowsers -join ', ')" +} + +function Remove-DirectoryHard { + param( + [string]$PathToRemove + ) + + if (Test-Path $PathToRemove) { + cmd /c "if exist `"$PathToRemove`" rmdir /s /q `"$PathToRemove`"" + } +} + +function Get-SpecFilesFromPattern { + param( + [string]$Pattern + ) + + $normalizedPattern = $Pattern -replace '/', '\\' + $specFiles = Get-ChildItem -Path $normalizedPattern -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like "*.cy.js" -and $_.Name -notlike "*.original.*" } | + Sort-Object FullName + + if (-not $specFiles -or $specFiles.Count -eq 0) { + throw "No spec files found for pattern: $Pattern" + } + + return $specFiles +} + +function To-RelativePosixPath { + param( + [string]$BasePath, + [string]$TargetPath + ) + + $resolvedBase = (Resolve-Path -Path $BasePath).Path + $resolvedTarget = (Resolve-Path -Path $TargetPath).Path + + if ($resolvedTarget.StartsWith($resolvedBase, [System.StringComparison]::OrdinalIgnoreCase)) { + $relative = $resolvedTarget.Substring($resolvedBase.Length).TrimStart('\', '/') + } + else { + $relative = $resolvedTarget + } + + return $relative -replace '\\', '/' +} + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " SUITE 1: CROSS-BROWSER (PARALLEL=3)" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "Browsers: $($browsers -join ', ')" -ForegroundColor Yellow +Write-Host "Spec Pattern: $SpecPattern" -ForegroundColor Yellow +Write-Host "Viewport: $ViewportWidth x $ViewportHeight" -ForegroundColor Yellow +Write-Host "Spec retries: max $MaxRetries retries per failed spec" -ForegroundColor Yellow +Write-Host "" + +Remove-DirectoryHard -PathToRemove $suiteRoot +New-Item -ItemType Directory -Path $logsDir -Force | Out-Null +New-Item -ItemType Directory -Path $screenshotsRoot -Force | Out-Null + +$jobs = @() +$workDir = (Get-Location).Path +$allSpecFiles = Get-SpecFilesFromPattern -Pattern $SpecPattern +$allSpecPaths = $allSpecFiles | ForEach-Object { To-RelativePosixPath -BasePath $workDir -TargetPath $_.FullName } + +# Exclude recording/ingest-related flows from Firefox. +$firefoxIngestPattern = 'uploadAudioFile|openUploadModal|portal-onboarding-mic|portal-audio-|installParticipantAudioStubs|retranscribeConversation|videoplayback\.mp3|sampleaudio\.mp3|test-audio\.wav' +$firefoxAllowedSpecs = @() +$firefoxExcludedSpecs = @() + +foreach ($spec in $allSpecFiles) { + $content = Get-Content -Path $spec.FullName -Raw + $relativeSpec = To-RelativePosixPath -BasePath $workDir -TargetPath $spec.FullName + + if ($content -match $firefoxIngestPattern) { + $firefoxExcludedSpecs += $relativeSpec + } + else { + $firefoxAllowedSpecs += $relativeSpec + } +} + +Write-Host "Total specs resolved: $($allSpecPaths.Count)" -ForegroundColor Yellow +Write-Host "Firefox excluded ingest specs: $($firefoxExcludedSpecs.Count)" -ForegroundColor Yellow +if ($firefoxExcludedSpecs.Count -gt 0) { + foreach ($excludedSpec in $firefoxExcludedSpecs) { + Write-Host " - $excludedSpec" -ForegroundColor DarkYellow + } +} +Write-Host "" + +foreach ($browser in $browsers) { + $effectiveSpecs = if ($browser -eq "firefox") { $firefoxAllowedSpecs } else { $allSpecPaths } + + if (-not $effectiveSpecs -or $effectiveSpecs.Count -eq 0) { + Write-Host "[SKIP] $browser has no matching specs after filtering." -ForegroundColor DarkYellow + continue + } + + $runReportDir = "$suiteRoot/$browser" + $browserScreenshotsDir = "$screenshotsRoot/$browser" + $logFile = "$logsDir/$browser.log" + $effectiveSpecArg = $effectiveSpecs -join ',' + + $job = Start-Job -Name "suite1-$browser" -ScriptBlock { + param($workDir, $browser, $effectiveSpecArg, $version, $viewportWidth, $viewportHeight, $runReportDir, $browserScreenshotsDir, $logFile, $maxRetries) + + function Remove-DirectoryHard { + param([string]$PathToRemove) + if (Test-Path $PathToRemove) { + cmd /c "if exist `"$PathToRemove`" rmdir /s /q `"$PathToRemove`"" + } + } + + Set-Location $workDir + if (Test-Path Env:ELECTRON_RUN_AS_NODE) { + Remove-Item Env:ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue + } + + $env:CYPRESS_CACHE_FOLDER = "$workDir\.cypress-cache" + $env:CYPRESS_viewportWidth = $viewportWidth + $env:CYPRESS_viewportHeight = $viewportHeight + + Remove-DirectoryHard -PathToRemove $runReportDir + New-Item -ItemType Directory -Path $runReportDir -Force | Out-Null + + Remove-DirectoryHard -PathToRemove $browserScreenshotsDir + New-Item -ItemType Directory -Path $browserScreenshotsDir -Force | Out-Null + + $specList = $effectiveSpecArg -split ',' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + $allOutput = @() + $failedSpecs = @() + $totalAttempts = 0 + + foreach ($spec in $specList) { + $specPassed = $false + $attempt = 0 + $maxAttempts = $maxRetries + 1 + + while ($attempt -lt $maxAttempts -and -not $specPassed) { + $attempt++ + $totalAttempts++ + + $attemptId = [guid]::NewGuid().ToString('N') + $tmpReportDir = Join-Path $runReportDir "tmp-report-$attemptId" + $tmpScreenshotsDir = Join-Path $browserScreenshotsDir "tmp-shots-$attemptId" + + New-Item -ItemType Directory -Path $tmpReportDir -Force | Out-Null + New-Item -ItemType Directory -Path $tmpScreenshotsDir -Force | Out-Null + + $env:CYPRESS_MOCHAWESOME_REPORT_DIR = $tmpReportDir + $attemptOutput = & npx cypress run --config-file cypress.config.js --spec $spec --env "version=$version" --browser $browser --config "screenshotsFolder=$tmpScreenshotsDir" --reporter mochawesome 2>&1 + $exitCode = $LASTEXITCODE + Remove-Item Env:CYPRESS_MOCHAWESOME_REPORT_DIR -ErrorAction SilentlyContinue + + $allOutput += "===== Spec: $spec | Attempt $attempt/$maxAttempts ($browser) =====" + $allOutput += $attemptOutput + $allOutput += "" + + $tmpJsonFiles = Get-ChildItem -Path $tmpReportDir -Filter "mochawesome*.json" -ErrorAction SilentlyContinue + $isFinalAttempt = ($attempt -eq $maxAttempts) + + if ($exitCode -eq 0) { + foreach ($jsonFile in $tmpJsonFiles) { + Move-Item -Path $jsonFile.FullName -Destination $runReportDir -Force + } + + Remove-DirectoryHard -PathToRemove $tmpReportDir + Remove-DirectoryHard -PathToRemove $tmpScreenshotsDir + $specPassed = $true + } + else { + if ($isFinalAttempt) { + foreach ($jsonFile in $tmpJsonFiles) { + Move-Item -Path $jsonFile.FullName -Destination $runReportDir -Force + } + + $finalShots = Get-ChildItem -Path $tmpScreenshotsDir -Recurse -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime + if ($finalShots.Count -gt 0) { + $specSlug = [System.IO.Path]::GetFileNameWithoutExtension($spec) -replace '[^A-Za-z0-9._-]', '_' + $destSpecDir = Join-Path $browserScreenshotsDir $specSlug + New-Item -ItemType Directory -Path $destSpecDir -Force | Out-Null + $singleShotName = "$($specSlug)-final-failure$($finalShots[0].Extension)" + Copy-Item -Path $finalShots[0].FullName -Destination (Join-Path $destSpecDir $singleShotName) -Force + } + + Remove-DirectoryHard -PathToRemove $tmpReportDir + Remove-DirectoryHard -PathToRemove $tmpScreenshotsDir + $failedSpecs += $spec + } + else { + Remove-DirectoryHard -PathToRemove $tmpReportDir + Remove-DirectoryHard -PathToRemove $tmpScreenshotsDir + } + } + } + } + + $allOutput | Out-File -FilePath $logFile -Encoding utf8 + + Remove-Item Env:CYPRESS_viewportWidth -ErrorAction SilentlyContinue + Remove-Item Env:CYPRESS_viewportHeight -ErrorAction SilentlyContinue + + return @{ + Name = $browser + ExitCode = if ($failedSpecs.Count -gt 0) { 1 } else { 0 } + LogFile = $logFile + TotalSpecs = $specList.Count + FailedSpecCount = $failedSpecs.Count + FailedSpecs = $failedSpecs + TotalAttempts = $totalAttempts + MaxRetries = $maxRetries + } + } -ArgumentList $workDir, $browser, $effectiveSpecArg, $Version, $ViewportWidth, $ViewportHeight, $runReportDir, $browserScreenshotsDir, $logFile, $MaxRetries + + $jobs += $job +} + +Write-Host "Started $($jobs.Count) jobs in parallel. Waiting..." -ForegroundColor Green +Write-Host "" + +$jobs | Wait-Job | Out-Null + +$results = @() +foreach ($job in $jobs) { + $result = Receive-Job -Job $job + $results += $result + $status = if ($result.ExitCode -eq 0) { "PASS" } else { "FAIL" } + $color = if ($result.ExitCode -eq 0) { "Green" } else { "Red" } + + Write-Host "[$status] $($result.Name) specs=$($result.TotalSpecs) failedSpecs=$($result.FailedSpecCount) totalAttempts=$($result.TotalAttempts) (log: $($result.LogFile))" -ForegroundColor $color + + if ($result.FailedSpecCount -gt 0) { + foreach ($failedSpec in $result.FailedSpecs) { + Write-Host " - failed after retries: $failedSpec" -ForegroundColor DarkRed + } + } +} + +$jobs | Remove-Job -Force + +$jsonFiles = Get-ChildItem -Path $suiteRoot -Recurse -Filter "mochawesome*.json" | +Where-Object { $_.DirectoryName -notmatch 'tmp-report-' } | +Select-Object -ExpandProperty FullName +$combinedJson = "$suiteRoot/combined-report.json" +$htmlReportName = "suite-1-report" + +if ($jsonFiles.Count -gt 0) { + $jsonFilesForNode = $jsonFiles | ConvertTo-Json + @" +const fs = require('fs'); +const { merge } = require('mochawesome-merge'); +const files = $jsonFilesForNode; +const outFile = '$combinedJson'.replace(/\\\\/g, '/'); + +merge({ files }) + .then((report) => { + fs.writeFileSync(outFile, JSON.stringify(report, null, 2)); + console.log('Reports merged to ' + outFile); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); +"@ | node - + + & npx marge $combinedJson --reportDir $suiteRoot --reportFilename $htmlReportName + Write-Host "" + Write-Host "Suite 1 HTML report: $suiteRoot/$htmlReportName.html" -ForegroundColor Cyan +} +else { + Write-Host "No mochawesome JSON files found for Suite 1." -ForegroundColor Red +} + +$failed = ($results | Where-Object { $_.ExitCode -ne 0 }).Count +Write-Host "" +Write-Host "Suite 1 Summary: Passed=$($results.Count - $failed), Failed=$failed, Total=$($results.Count)" -ForegroundColor White + +if ($failed -gt 0) { + exit 1 +} + +exit 0 diff --git a/echo/cypress/test-suites/run-core-suite.ps1 b/echo/cypress/test-suites/run-core-suite.ps1 new file mode 100644 index 00000000..31485272 --- /dev/null +++ b/echo/cypress/test-suites/run-core-suite.ps1 @@ -0,0 +1,207 @@ +param( + [string]$SpecPattern = "e2e/suites/[0-9]*.cy.js", + [string]$Version = "staging", + [int]$ViewportWidth, + [int]$ViewportHeight, + [int]$MaxRetries = 2, + [string]$Browser, + [string]$SuiteId +) + +$ErrorActionPreference = "Continue" + +if (Test-Path Env:ELECTRON_RUN_AS_NODE) { + Remove-Item Env:ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue +} + +$env:CYPRESS_CACHE_FOLDER = "$PSScriptRoot\.cypress-cache" +$cypressExe = Get-ChildItem -Path $env:CYPRESS_CACHE_FOLDER -Recurse -Filter "Cypress.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 +if (-not $cypressExe) { + Write-Host "Cypress binary not found in local cache. Installing..." -ForegroundColor Yellow + npx cypress install +} + +$suiteRoot = "reports/$SuiteId" +$logsDir = "$suiteRoot/logs" +$browserScreenshotsDir = "$suiteRoot/screenshots/$Browser" +$runReportDir = "$suiteRoot/$Browser" + +function Remove-DirectoryHard { + param([string]$PathToRemove) + if (Test-Path $PathToRemove) { + cmd /c "if exist `"$PathToRemove`" rmdir /s /q `"$PathToRemove`"" + } +} + +function Get-SpecFilesFromPattern { + param([string]$Pattern) + $normalizedPattern = $Pattern -replace '/', '\\' + $specFiles = Get-ChildItem -Path $normalizedPattern -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like "*.cy.js" -and $_.Name -notlike "*.original.*" } | + Sort-Object FullName + if (-not $specFiles -or $specFiles.Count -eq 0) { + throw "No spec files found for pattern: $Pattern" + } + return $specFiles +} + +function To-RelativePosixPath { + param([string]$BasePath, [string]$TargetPath) + $resolvedBase = (Resolve-Path -Path $BasePath).Path + $resolvedTarget = (Resolve-Path -Path $TargetPath).Path + if ($resolvedTarget.StartsWith($resolvedBase, [System.StringComparison]::OrdinalIgnoreCase)) { + $relative = $resolvedTarget.Substring($resolvedBase.Length).TrimStart('\', '/') + } + else { + $relative = $resolvedTarget + } + return $relative -replace '\\', '/' +} + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " SUITE ($SuiteId): BROWSER: $Browser ($ViewportWidth x $ViewportHeight)" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan + +Remove-DirectoryHard -PathToRemove $runReportDir +Remove-DirectoryHard -PathToRemove $browserScreenshotsDir +$null = New-Item -ItemType Directory -Path $runReportDir -Force +$null = New-Item -ItemType Directory -Path $browserScreenshotsDir -Force +$null = New-Item -ItemType Directory -Path $logsDir -Force + +$workDir = (Get-Location).Path +$allSpecFiles = Get-SpecFilesFromPattern -Pattern $SpecPattern + +if ($Browser.ToLowerInvariant() -eq "edge") { + $edgeExcludePattern = 'uploadAudioFile|openUploadModal|portal-onboarding-mic|portal-audio-|installParticipantAudioStubs|retranscribeConversation|videoplayback\.mp3|sampleaudio\.mp3|test-audio\.wav|startRecording|stopRecording|getPortalUrl' + $filteredFiles = @() + foreach ($specFile in $allSpecFiles) { + $content = Get-Content -Path $specFile.FullName -Raw + if ($content -match $edgeExcludePattern) { + Write-Host "[SKIP] $($specFile.Name) excluded for Edge (Recording Flow)" -ForegroundColor DarkYellow + } + else { + $filteredFiles += $specFile + } + } + $allSpecFiles = $filteredFiles +} + +$allSpecPaths = $allSpecFiles | ForEach-Object { To-RelativePosixPath -BasePath $workDir -TargetPath $_.FullName } + +$env:CYPRESS_viewportWidth = $ViewportWidth +$env:CYPRESS_viewportHeight = $ViewportHeight + +$allOutput = @() +$failedSpecs = @() +$totalAttempts = 0 + +foreach ($spec in $allSpecPaths) { + $specPassed = $false + $attempt = 0 + $maxAttempts = $MaxRetries + 1 + + while ($attempt -lt $maxAttempts -and -not $specPassed) { + $attempt++ + $totalAttempts++ + $attemptId = [guid]::NewGuid().ToString('N') + $tmpReportDir = Join-Path $runReportDir "tmp-report-$attemptId" + $tmpScreenshotsDir = Join-Path $browserScreenshotsDir "tmp-shots-$attemptId" + + $null = New-Item -ItemType Directory -Path $tmpReportDir -Force + $null = New-Item -ItemType Directory -Path $tmpScreenshotsDir -Force + + $env:CYPRESS_MOCHAWESOME_REPORT_DIR = $tmpReportDir + $attemptOutput = & npx cypress run --config-file cypress.config.js --spec $spec --env "version=$Version" --browser $Browser --config "screenshotsFolder=$tmpScreenshotsDir" --reporter mochawesome 2>&1 + $exitCode = $LASTEXITCODE + Remove-Item Env:CYPRESS_MOCHAWESOME_REPORT_DIR -ErrorAction SilentlyContinue + + $allOutput += "===== Spec: $spec | Attempt $attempt/$maxAttempts ($Browser) =====" + $allOutput += $attemptOutput + $allOutput += "" + + $tmpJsonFiles = Get-ChildItem -Path $tmpReportDir -Filter "mochawesome*.json" -ErrorAction SilentlyContinue + $isFinalAttempt = ($attempt -eq $maxAttempts) + + if ($exitCode -eq 0) { + foreach ($jsonFile in $tmpJsonFiles) { + Move-Item -Path $jsonFile.FullName -Destination $runReportDir -Force + } + Remove-DirectoryHard -PathToRemove $tmpReportDir + Remove-DirectoryHard -PathToRemove $tmpScreenshotsDir + $specPassed = $true + Write-Host "[PASS] $spec" -ForegroundColor Green + } + else { + if ($isFinalAttempt) { + foreach ($jsonFile in $tmpJsonFiles) { + Move-Item -Path $jsonFile.FullName -Destination $runReportDir -Force + } + $finalShots = Get-ChildItem -Path $tmpScreenshotsDir -Recurse -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime + if ($finalShots.Count -gt 0) { + $specSlug = [System.IO.Path]::GetFileNameWithoutExtension($spec) -replace '[^A-Za-z0-9._-]', '_' + $destSpecDir = Join-Path $browserScreenshotsDir $specSlug + $null = New-Item -ItemType Directory -Path $destSpecDir -Force + $singleShotName = "$($specSlug)-final-failure$($finalShots[0].Extension)" + Copy-Item -Path $finalShots[0].FullName -Destination (Join-Path $destSpecDir $singleShotName) -Force + } + Remove-DirectoryHard -PathToRemove $tmpReportDir + Remove-DirectoryHard -PathToRemove $tmpScreenshotsDir + $failedSpecs += $spec + Write-Host "[FAIL] $spec after $maxAttempts attempts" -ForegroundColor Red + } + else { + Remove-DirectoryHard -PathToRemove $tmpReportDir + Remove-DirectoryHard -PathToRemove $tmpScreenshotsDir + Write-Host "[RETRY] $spec failed on attempt $attempt" -ForegroundColor Yellow + } + } + } +} + +$logFile = "$logsDir/$Browser.log" +$allOutput | Out-File -FilePath $logFile -Encoding utf8 + +Remove-Item Env:CYPRESS_viewportWidth -ErrorAction SilentlyContinue +Remove-Item Env:CYPRESS_viewportHeight -ErrorAction SilentlyContinue + +# Merge reports and create html +$jsonFiles = Get-ChildItem -Path $runReportDir -Filter "mochawesome*.json" | Select-Object -ExpandProperty FullName +$combinedJson = "$runReportDir/combined-report.json" +$htmlReportName = "test-report" + +if ($jsonFiles.Count -gt 0) { + # Provide json files content as an array + $jsonFilesForNode = $jsonFiles | ConvertTo-Json + @" +const fs = require('fs'); +const { merge } = require('mochawesome-merge'); +const files = $jsonFilesForNode; +const outFile = '$combinedJson'.replace(/\\\\/g, '/'); + +merge({ files }) + .then((report) => { + fs.writeFileSync(outFile, JSON.stringify(report, null, 2)); + console.log('Reports merged to ' + outFile); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); +"@ | node - + + & npx marge $combinedJson --reportDir $runReportDir --reportFilename $htmlReportName + Write-Host "" + Write-Host "HTML report generated: $runReportDir/$htmlReportName.html" -ForegroundColor Cyan +} + +if ($failedSpecs.Count -gt 0) { + Write-Host "" + Write-Host "FAILED SPECS:" -ForegroundColor Red + foreach ($failedSpec in $failedSpecs) { + Write-Host " - $failedSpec" -ForegroundColor DarkRed + } + exit 1 +} + +Write-Host "All specs passed for $Browser ($SuiteId)!" -ForegroundColor Green +exit 0 diff --git a/echo/cypress/test-suites/run-parallel-tests.ps1 b/echo/cypress/test-suites/run-parallel-tests.ps1 new file mode 100644 index 00000000..6e141f68 --- /dev/null +++ b/echo/cypress/test-suites/run-parallel-tests.ps1 @@ -0,0 +1,125 @@ +# Parallel Cypress Test Runner (Fixed) +# Runs all test suites in parallel using PowerShell background jobs + +param( + [int]$MaxParallel = 5, # Max concurrent tests (adjust based on CPU/RAM) + [string]$Browser = "chrome", + [switch]$Headed, + [string]$Version = "staging" +) + +$ErrorActionPreference = "Continue" + +# Get all test files (exclude .original files) +$testDir = "e2e/suites" +$testFiles = Get-ChildItem -Path $testDir -Filter "*.cy.js" | +Where-Object { $_.Name -notlike "*.original.*" } | +Sort-Object Name + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Parallel Cypress Test Runner" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "Tests found: $($testFiles.Count)" -ForegroundColor Yellow +Write-Host "Max parallel: $MaxParallel" -ForegroundColor Yellow +Write-Host "Browser: $Browser" -ForegroundColor Yellow +Write-Host "" + +# Create results directory +$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$resultsDir = "parallel-results-$timestamp" +New-Item -ItemType Directory -Path $resultsDir -Force | Out-Null + +Write-Host "Starting parallel execution..." -ForegroundColor Green +Write-Host "Results will be saved to: $resultsDir" -ForegroundColor Gray +Write-Host "" + +# Track jobs and results +$jobs = @() +$headedFlag = if ($Headed) { "--headed" } else { "--headless" } + +# Launch tests in batches +foreach ($testFile in $testFiles) { + $testName = $testFile.BaseName + $specPath = "$testDir/$($testFile.Name)" + $logFile = Join-Path (Get-Location) "$resultsDir\$testName.log" + + # Wait if we've hit max parallel jobs + while (($jobs | Where-Object { $_.State -eq "Running" }).Count -ge $MaxParallel) { + Start-Sleep -Seconds 3 + } + + Write-Host "Starting: $testName" -ForegroundColor Gray + + # Start background job - capture exit code properly + $job = Start-Job -Name $testName -ScriptBlock { + param($specPath, $version, $browser, $headedFlag, $logFile, $workDir) + + Set-Location $workDir + $env:CYPRESS_viewportWidth = 1440 + $env:CYPRESS_viewportHeight = 900 + + # Run cypress and capture output + exit code + $output = & npx cypress run --spec $specPath --env version=$version --browser $browser $headedFlag 2>&1 + $exitCode = $LASTEXITCODE + + # Save output to log file + $output | Out-File -FilePath $logFile -Encoding utf8 + + # Return exit code as the job result + return $exitCode + } -ArgumentList $specPath, $Version, $Browser, $headedFlag, $logFile, (Get-Location).Path + + $jobs += $job + Start-Sleep -Milliseconds 500 +} + +Write-Host "" +Write-Host "All $($jobs.Count) tests launched. Waiting for completion..." -ForegroundColor Yellow +Write-Host "" + +# Wait for all jobs to complete +$jobs | Wait-Job | Out-Null + +# Collect results +$results = @{} +foreach ($job in $jobs) { + $exitCode = Receive-Job -Job $job + + # Determine status from exit code + $status = if ($exitCode -eq 0) { "PASS" } else { "FAIL" } + $color = if ($exitCode -eq 0) { "Green" } else { "Red" } + + Write-Host "[$status] $($job.Name)" -ForegroundColor $color + $results[$job.Name] = @{ Status = $status; ExitCode = $exitCode } +} + +# Cleanup jobs +$jobs | Remove-Job -Force + +# Summary +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " SUMMARY" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan + +$passed = ($results.Values | Where-Object { $_.Status -eq "PASS" }).Count +$failed = ($results.Values | Where-Object { $_.Status -eq "FAIL" }).Count +$total = $results.Count + +Write-Host "Passed: $passed" -ForegroundColor Green +Write-Host "Failed: $failed" -ForegroundColor $(if ($failed -gt 0) { "Red" } else { "Green" }) +Write-Host "Total: $total" -ForegroundColor White +Write-Host "" +Write-Host "Logs saved to: $resultsDir" -ForegroundColor Gray + +# List failed tests +if ($failed -gt 0) { + Write-Host "" + Write-Host "Failed Tests:" -ForegroundColor Red + $results.GetEnumerator() | Where-Object { $_.Value.Status -eq "FAIL" } | ForEach-Object { + Write-Host " - $($_.Key)" -ForegroundColor Red + } +} + +# Exit with appropriate code +if ($failed -gt 0) { exit 1 } else { exit 0 } diff --git a/echo/cypress/test-suites/run-test-suites.ps1 b/echo/cypress/test-suites/run-test-suites.ps1 new file mode 100644 index 00000000..a13ab2c8 --- /dev/null +++ b/echo/cypress/test-suites/run-test-suites.ps1 @@ -0,0 +1,51 @@ +# Runs both suites and generates a final merged HTML report +# Suite 1: cross-browser (chrome, edge, webkit) +# Suite 2: chrome in 3 viewports + +$ErrorActionPreference = "Continue" + +# Ensure Cypress launches Electron app mode, not Node mode. +if (Test-Path Env:ELECTRON_RUN_AS_NODE) { + Remove-Item Env:ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue +} + +# Use project-local Cypress cache for stability in this environment. +$env:CYPRESS_CACHE_FOLDER = "$PSScriptRoot\.cypress-cache" + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " RUNNING BOTH CYPRESS SUITES" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +& "$PSScriptRoot/run-browser-tests.ps1" +$suite1Exit = $LASTEXITCODE + +Write-Host "" + +& "$PSScriptRoot/run-viewport-tests.ps1" +$suite2Exit = $LASTEXITCODE + +$finalRoot = "reports/final" +if (Test-Path $finalRoot) { + Remove-Item -Recurse -Force $finalRoot +} +New-Item -ItemType Directory -Path $finalRoot -Force | Out-Null + +$jsonFiles = Get-ChildItem -Path "reports/suite-1-cross-browser", "reports/suite-2-chrome-viewports" -Recurse -Filter "mochawesome*.json" | Select-Object -ExpandProperty FullName +$combinedJson = "$finalRoot/final-combined-report.json" +$finalHtmlName = "final-test-report" + +if ($jsonFiles.Count -gt 0) { + & npx mochawesome-merge @jsonFiles -o $combinedJson + & npx marge $combinedJson --reportDir $finalRoot --reportFilename $finalHtmlName + Write-Host "" + Write-Host "Final HTML report: $finalRoot/$finalHtmlName.html" -ForegroundColor Green +} else { + Write-Host "No mochawesome JSON files found to generate final report." -ForegroundColor Red +} + +if ($suite1Exit -ne 0 -or $suite2Exit -ne 0) { + exit 1 +} + +exit 0 diff --git a/echo/cypress/test-suites/tablet/run-chrome-suite.ps1 b/echo/cypress/test-suites/tablet/run-chrome-suite.ps1 new file mode 100644 index 00000000..09b57e37 --- /dev/null +++ b/echo/cypress/test-suites/tablet/run-chrome-suite.ps1 @@ -0,0 +1,10 @@ +$ErrorActionPreference = "Continue" + +Push-Location "$PSScriptRoot\..\.." +try { + & .\test-suites\run-core-suite.ps1 -ViewportWidth 768 -ViewportHeight 1024 -Browser "chrome" -SuiteId "tablet" + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/echo/cypress/test-suites/tablet/run-edge-suite.ps1 b/echo/cypress/test-suites/tablet/run-edge-suite.ps1 new file mode 100644 index 00000000..b5caf50d --- /dev/null +++ b/echo/cypress/test-suites/tablet/run-edge-suite.ps1 @@ -0,0 +1,29 @@ +$ErrorActionPreference = "Continue" + +Push-Location "$PSScriptRoot\..\.." +try { + # Skip recording/audio tests for Edge + $specFilesToRun = "" + $allSpecFiles = Get-ChildItem -Path "e2e/suites" -Filter "*.cy.js" -File + $edgeExcludePattern = 'uploadAudioFile|openUploadModal|portal-onboarding-mic|portal-audio-|installParticipantAudioStubs|retranscribeConversation|videoplayback\.mp3|sampleaudio\.mp3|test-audio\.wav|startRecording|stopRecording|getPortalUrl' + + foreach ($specFile in $allSpecFiles) { + $content = Get-Content -Path $specFile.FullName -Raw + if ($content -notmatch $edgeExcludePattern) { + $specFilesToRun += "e2e/suites/$($specFile.Name)," + } + } + + $specFilesToRun = $specFilesToRun.TrimEnd(',') + + if ([string]::IsNullOrWhiteSpace($specFilesToRun)) { + Write-Host "No specs to run after filtering." -ForegroundColor Yellow + exit 0 + } + + & .\test-suites\run-core-suite.ps1 -ViewportWidth 768 -ViewportHeight 1024 -Browser "edge" -SuiteId "tablet" -SpecPattern $specFilesToRun + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/echo/cypress/test-suites/tablet/run-webkit-suite.ps1 b/echo/cypress/test-suites/tablet/run-webkit-suite.ps1 new file mode 100644 index 00000000..8af80342 --- /dev/null +++ b/echo/cypress/test-suites/tablet/run-webkit-suite.ps1 @@ -0,0 +1,10 @@ +$ErrorActionPreference = "Continue" + +Push-Location "$PSScriptRoot\..\.." +try { + & .\test-suites\run-core-suite.ps1 -ViewportWidth 768 -ViewportHeight 1024 -Browser "webkit" -SuiteId "tablet" + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/echo/directus/sync/snapshot/collections/project_agentic_run.json b/echo/directus/sync/snapshot/collections/project_agentic_run.json new file mode 100644 index 00000000..69585e57 --- /dev/null +++ b/echo/directus/sync/snapshot/collections/project_agentic_run.json @@ -0,0 +1,28 @@ +{ + "collection": "project_agentic_run", + "meta": { + "accountability": "all", + "archive_app_filter": true, + "archive_field": null, + "archive_value": null, + "collapse": "open", + "collection": "project_agentic_run", + "color": null, + "display_template": null, + "group": null, + "hidden": false, + "icon": null, + "item_duplication_fields": null, + "note": null, + "preview_url": null, + "singleton": false, + "sort": 11, + "sort_field": null, + "translations": null, + "unarchive_value": null, + "versioning": false + }, + "schema": { + "name": "project_agentic_run" + } +} diff --git a/echo/directus/sync/snapshot/collections/project_agentic_run_event.json b/echo/directus/sync/snapshot/collections/project_agentic_run_event.json new file mode 100644 index 00000000..d8feac03 --- /dev/null +++ b/echo/directus/sync/snapshot/collections/project_agentic_run_event.json @@ -0,0 +1,28 @@ +{ + "collection": "project_agentic_run_event", + "meta": { + "accountability": "all", + "archive_app_filter": true, + "archive_field": null, + "archive_value": null, + "collapse": "open", + "collection": "project_agentic_run_event", + "color": null, + "display_template": null, + "group": null, + "hidden": false, + "icon": null, + "item_duplication_fields": null, + "note": null, + "preview_url": null, + "singleton": false, + "sort": 12, + "sort_field": null, + "translations": null, + "unarchive_value": null, + "versioning": false + }, + "schema": { + "name": "project_agentic_run_event" + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/agent_thread_id.json b/echo/directus/sync/snapshot/fields/project_agentic_run/agent_thread_id.json new file mode 100644 index 00000000..5db3e4ca --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/agent_thread_id.json @@ -0,0 +1,44 @@ +{ + "collection": "project_agentic_run", + "field": "agent_thread_id", + "type": "string", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": null, + "display_options": null, + "field": "agent_thread_id", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 8, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "agent_thread_id", + "table": "project_agentic_run", + "data_type": "character varying", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/completed_at.json b/echo/directus/sync/snapshot/fields/project_agentic_run/completed_at.json new file mode 100644 index 00000000..1f233fc2 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/completed_at.json @@ -0,0 +1,46 @@ +{ + "collection": "project_agentic_run", + "field": "completed_at", + "type": "timestamp", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": "datetime", + "display_options": { + "relative": true + }, + "field": "completed_at", + "group": null, + "hidden": false, + "interface": "datetime", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 14, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "completed_at", + "table": "project_agentic_run", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/created_at.json b/echo/directus/sync/snapshot/fields/project_agentic_run/created_at.json new file mode 100644 index 00000000..41132a9e --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/created_at.json @@ -0,0 +1,48 @@ +{ + "collection": "project_agentic_run", + "field": "created_at", + "type": "timestamp", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": "datetime", + "display_options": { + "relative": true + }, + "field": "created_at", + "group": null, + "hidden": false, + "interface": "datetime", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 2, + "special": [ + "date-created" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "created_at", + "table": "project_agentic_run", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/directus_user_id.json b/echo/directus/sync/snapshot/fields/project_agentic_run/directus_user_id.json new file mode 100644 index 00000000..601d530b --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/directus_user_id.json @@ -0,0 +1,44 @@ +{ + "collection": "project_agentic_run", + "field": "directus_user_id", + "type": "string", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": null, + "display_options": null, + "field": "directus_user_id", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": true, + "searchable": true, + "sort": 6, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "directus_user_id", + "table": "project_agentic_run", + "data_type": "character varying", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/events.json b/echo/directus/sync/snapshot/fields/project_agentic_run/events.json new file mode 100644 index 00000000..8efe68c5 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/events.json @@ -0,0 +1,35 @@ +{ + "collection": "project_agentic_run", + "field": "events", + "type": "alias", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": null, + "display_options": null, + "field": "events", + "group": null, + "hidden": false, + "interface": "list-o2m", + "note": null, + "options": { + "fields": [ + "seq", + "event_type", + "timestamp" + ], + "layout": "table" + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 15, + "special": [ + "o2m" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/id.json b/echo/directus/sync/snapshot/fields/project_agentic_run/id.json new file mode 100644 index 00000000..b12d2746 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/id.json @@ -0,0 +1,46 @@ +{ + "collection": "project_agentic_run", + "field": "id", + "type": "uuid", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": null, + "display_options": null, + "field": "id", + "group": null, + "hidden": true, + "interface": "input", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 1, + "special": [ + "uuid" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "id", + "table": "project_agentic_run", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": false, + "is_unique": true, + "is_indexed": false, + "is_primary_key": true, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/last_event_seq.json b/echo/directus/sync/snapshot/fields/project_agentic_run/last_event_seq.json new file mode 100644 index 00000000..05ac6e2f --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/last_event_seq.json @@ -0,0 +1,44 @@ +{ + "collection": "project_agentic_run", + "field": "last_event_seq", + "type": "integer", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": null, + "display_options": null, + "field": "last_event_seq", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 9, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "last_event_seq", + "table": "project_agentic_run", + "data_type": "integer", + "default_value": 0, + "max_length": null, + "numeric_precision": 32, + "numeric_scale": 0, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/latest_error.json b/echo/directus/sync/snapshot/fields/project_agentic_run/latest_error.json new file mode 100644 index 00000000..ed607718 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/latest_error.json @@ -0,0 +1,44 @@ +{ + "collection": "project_agentic_run", + "field": "latest_error", + "type": "text", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": null, + "display_options": null, + "field": "latest_error", + "group": null, + "hidden": false, + "interface": "input-multiline", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 11, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "latest_error", + "table": "project_agentic_run", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/latest_error_code.json b/echo/directus/sync/snapshot/fields/project_agentic_run/latest_error_code.json new file mode 100644 index 00000000..341cd044 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/latest_error_code.json @@ -0,0 +1,44 @@ +{ + "collection": "project_agentic_run", + "field": "latest_error_code", + "type": "string", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": null, + "display_options": null, + "field": "latest_error_code", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 12, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "latest_error_code", + "table": "project_agentic_run", + "data_type": "character varying", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/latest_output.json b/echo/directus/sync/snapshot/fields/project_agentic_run/latest_output.json new file mode 100644 index 00000000..6f398cbd --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/latest_output.json @@ -0,0 +1,44 @@ +{ + "collection": "project_agentic_run", + "field": "latest_output", + "type": "text", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": null, + "display_options": null, + "field": "latest_output", + "group": null, + "hidden": false, + "interface": "input-multiline", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 10, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "latest_output", + "table": "project_agentic_run", + "data_type": "text", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/project_chat_id.json b/echo/directus/sync/snapshot/fields/project_agentic_run/project_chat_id.json new file mode 100644 index 00000000..77dbb917 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/project_chat_id.json @@ -0,0 +1,49 @@ +{ + "collection": "project_agentic_run", + "field": "project_chat_id", + "type": "uuid", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": null, + "display_options": null, + "field": "project_chat_id", + "group": null, + "hidden": false, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "enableLink": true, + "template": "{{name}}" + }, + "readonly": false, + "required": false, + "searchable": true, + "sort": 5, + "special": [ + "m2o" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "project_chat_id", + "table": "project_agentic_run", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "project_chat", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/project_id.json b/echo/directus/sync/snapshot/fields/project_agentic_run/project_id.json new file mode 100644 index 00000000..f960961c --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/project_id.json @@ -0,0 +1,49 @@ +{ + "collection": "project_agentic_run", + "field": "project_id", + "type": "uuid", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": null, + "display_options": null, + "field": "project_id", + "group": null, + "hidden": false, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "enableLink": true, + "template": "{{name}}" + }, + "readonly": false, + "required": true, + "searchable": true, + "sort": 4, + "special": [ + "m2o" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "project_id", + "table": "project_agentic_run", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "project", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/started_at.json b/echo/directus/sync/snapshot/fields/project_agentic_run/started_at.json new file mode 100644 index 00000000..d7034179 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/started_at.json @@ -0,0 +1,46 @@ +{ + "collection": "project_agentic_run", + "field": "started_at", + "type": "timestamp", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": "datetime", + "display_options": { + "relative": true + }, + "field": "started_at", + "group": null, + "hidden": false, + "interface": "datetime", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 13, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "started_at", + "table": "project_agentic_run", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/status.json b/echo/directus/sync/snapshot/fields/project_agentic_run/status.json new file mode 100644 index 00000000..0f4afde6 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/status.json @@ -0,0 +1,106 @@ +{ + "collection": "project_agentic_run", + "field": "status", + "type": "string", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": "labels", + "display_options": { + "choices": [ + { + "background": "var(--theme--background-normal)", + "color": "var(--theme--foreground)", + "foreground": "var(--theme--foreground)", + "text": "queued", + "value": "queued" + }, + { + "background": "var(--theme--primary-background)", + "color": "var(--theme--primary)", + "foreground": "var(--theme--primary)", + "text": "running", + "value": "running" + }, + { + "background": "var(--theme--success-background)", + "color": "var(--theme--success)", + "foreground": "var(--theme--success)", + "text": "completed", + "value": "completed" + }, + { + "background": "var(--theme--danger-background)", + "color": "var(--theme--danger)", + "foreground": "var(--theme--danger)", + "text": "failed", + "value": "failed" + }, + { + "background": "var(--theme--warning-background)", + "color": "var(--theme--warning)", + "foreground": "var(--theme--warning)", + "text": "timeout", + "value": "timeout" + } + ], + "showAsDot": true + }, + "field": "status", + "group": null, + "hidden": false, + "interface": "select-dropdown", + "note": null, + "options": { + "choices": [ + { + "text": "queued", + "value": "queued" + }, + { + "text": "running", + "value": "running" + }, + { + "text": "completed", + "value": "completed" + }, + { + "text": "failed", + "value": "failed" + }, + { + "text": "timeout", + "value": "timeout" + } + ] + }, + "readonly": false, + "required": true, + "searchable": true, + "sort": 7, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "status", + "table": "project_agentic_run", + "data_type": "character varying", + "default_value": "queued", + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run/updated_at.json b/echo/directus/sync/snapshot/fields/project_agentic_run/updated_at.json new file mode 100644 index 00000000..cc6f1fc5 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run/updated_at.json @@ -0,0 +1,48 @@ +{ + "collection": "project_agentic_run", + "field": "updated_at", + "type": "timestamp", + "meta": { + "collection": "project_agentic_run", + "conditions": null, + "display": "datetime", + "display_options": { + "relative": true + }, + "field": "updated_at", + "group": null, + "hidden": false, + "interface": "datetime", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 3, + "special": [ + "date-updated" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "updated_at", + "table": "project_agentic_run", + "data_type": "timestamp with time zone", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run_event/event_type.json b/echo/directus/sync/snapshot/fields/project_agentic_run_event/event_type.json new file mode 100644 index 00000000..324f6962 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run_event/event_type.json @@ -0,0 +1,44 @@ +{ + "collection": "project_agentic_run_event", + "field": "event_type", + "type": "string", + "meta": { + "collection": "project_agentic_run_event", + "conditions": null, + "display": null, + "display_options": null, + "field": "event_type", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": true, + "searchable": true, + "sort": 4, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "event_type", + "table": "project_agentic_run_event", + "data_type": "character varying", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run_event/id.json b/echo/directus/sync/snapshot/fields/project_agentic_run_event/id.json new file mode 100644 index 00000000..a981ec33 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run_event/id.json @@ -0,0 +1,44 @@ +{ + "collection": "project_agentic_run_event", + "field": "id", + "type": "bigInteger", + "meta": { + "collection": "project_agentic_run_event", + "conditions": null, + "display": null, + "display_options": null, + "field": "id", + "group": null, + "hidden": true, + "interface": "input", + "note": null, + "options": null, + "readonly": true, + "required": false, + "searchable": true, + "sort": 1, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "id", + "table": "project_agentic_run_event", + "data_type": "bigint", + "default_value": "nextval('project_agentic_run_event_id_seq'::regclass)", + "max_length": null, + "numeric_precision": 64, + "numeric_scale": 0, + "is_nullable": false, + "is_unique": true, + "is_indexed": false, + "is_primary_key": true, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": true, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run_event/payload.json b/echo/directus/sync/snapshot/fields/project_agentic_run_event/payload.json new file mode 100644 index 00000000..149ab12a --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run_event/payload.json @@ -0,0 +1,46 @@ +{ + "collection": "project_agentic_run_event", + "field": "payload", + "type": "json", + "meta": { + "collection": "project_agentic_run_event", + "conditions": null, + "display": null, + "display_options": null, + "field": "payload", + "group": null, + "hidden": false, + "interface": "input-code", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 5, + "special": [ + "cast-json" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "payload", + "table": "project_agentic_run_event", + "data_type": "json", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run_event/project_agentic_run_id.json b/echo/directus/sync/snapshot/fields/project_agentic_run_event/project_agentic_run_id.json new file mode 100644 index 00000000..c73d7666 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run_event/project_agentic_run_id.json @@ -0,0 +1,49 @@ +{ + "collection": "project_agentic_run_event", + "field": "project_agentic_run_id", + "type": "uuid", + "meta": { + "collection": "project_agentic_run_event", + "conditions": null, + "display": null, + "display_options": null, + "field": "project_agentic_run_id", + "group": null, + "hidden": false, + "interface": "select-dropdown-m2o", + "note": null, + "options": { + "enableLink": true, + "template": "{{id}}" + }, + "readonly": false, + "required": true, + "searchable": true, + "sort": 2, + "special": [ + "m2o" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "project_agentic_run_id", + "table": "project_agentic_run_event", + "data_type": "uuid", + "default_value": null, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": "project_agentic_run", + "foreign_key_column": "id" + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run_event/seq.json b/echo/directus/sync/snapshot/fields/project_agentic_run_event/seq.json new file mode 100644 index 00000000..845cf313 --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run_event/seq.json @@ -0,0 +1,44 @@ +{ + "collection": "project_agentic_run_event", + "field": "seq", + "type": "integer", + "meta": { + "collection": "project_agentic_run_event", + "conditions": null, + "display": null, + "display_options": null, + "field": "seq", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": true, + "searchable": true, + "sort": 3, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "seq", + "table": "project_agentic_run_event", + "data_type": "integer", + "default_value": null, + "max_length": null, + "numeric_precision": 32, + "numeric_scale": 0, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_agentic_run_event/timestamp.json b/echo/directus/sync/snapshot/fields/project_agentic_run_event/timestamp.json new file mode 100644 index 00000000..f28fc35c --- /dev/null +++ b/echo/directus/sync/snapshot/fields/project_agentic_run_event/timestamp.json @@ -0,0 +1,46 @@ +{ + "collection": "project_agentic_run_event", + "field": "timestamp", + "type": "timestamp", + "meta": { + "collection": "project_agentic_run_event", + "conditions": null, + "display": "datetime", + "display_options": { + "relative": true + }, + "field": "timestamp", + "group": null, + "hidden": false, + "interface": "datetime", + "note": null, + "options": null, + "readonly": false, + "required": false, + "searchable": true, + "sort": 6, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "timestamp", + "table": "project_agentic_run_event", + "data_type": "timestamp with time zone", + "default_value": "CURRENT_TIMESTAMP", + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/echo/directus/sync/snapshot/fields/project_chat/chat_mode.json b/echo/directus/sync/snapshot/fields/project_chat/chat_mode.json index 6f1c654f..588b9302 100644 --- a/echo/directus/sync/snapshot/fields/project_chat/chat_mode.json +++ b/echo/directus/sync/snapshot/fields/project_chat/chat_mode.json @@ -22,6 +22,10 @@ { "text": "deep_dive", "value": "deep_dive" + }, + { + "text": "agentic", + "value": "agentic" } ] }, diff --git a/echo/directus/sync/snapshot/relations/project_agentic_run/project_chat_id.json b/echo/directus/sync/snapshot/relations/project_agentic_run/project_chat_id.json new file mode 100644 index 00000000..4bc5651c --- /dev/null +++ b/echo/directus/sync/snapshot/relations/project_agentic_run/project_chat_id.json @@ -0,0 +1,25 @@ +{ + "collection": "project_agentic_run", + "field": "project_chat_id", + "related_collection": "project_chat", + "meta": { + "junction_field": null, + "many_collection": "project_agentic_run", + "many_field": "project_chat_id", + "one_allowed_collections": null, + "one_collection": "project_chat", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": null, + "sort_field": null + }, + "schema": { + "table": "project_agentic_run", + "column": "project_chat_id", + "foreign_key_table": "project_chat", + "foreign_key_column": "id", + "constraint_name": "project_agentic_run_project_chat_id_foreign", + "on_update": "NO ACTION", + "on_delete": "SET NULL" + } +} diff --git a/echo/directus/sync/snapshot/relations/project_agentic_run/project_id.json b/echo/directus/sync/snapshot/relations/project_agentic_run/project_id.json new file mode 100644 index 00000000..ac8a7598 --- /dev/null +++ b/echo/directus/sync/snapshot/relations/project_agentic_run/project_id.json @@ -0,0 +1,25 @@ +{ + "collection": "project_agentic_run", + "field": "project_id", + "related_collection": "project", + "meta": { + "junction_field": null, + "many_collection": "project_agentic_run", + "many_field": "project_id", + "one_allowed_collections": null, + "one_collection": "project", + "one_collection_field": null, + "one_deselect_action": "nullify", + "one_field": null, + "sort_field": null + }, + "schema": { + "table": "project_agentic_run", + "column": "project_id", + "foreign_key_table": "project", + "foreign_key_column": "id", + "constraint_name": "project_agentic_run_project_id_foreign", + "on_update": "NO ACTION", + "on_delete": "CASCADE" + } +} diff --git a/echo/directus/sync/snapshot/relations/project_agentic_run_event/project_agentic_run_id.json b/echo/directus/sync/snapshot/relations/project_agentic_run_event/project_agentic_run_id.json new file mode 100644 index 00000000..2eeffd3f --- /dev/null +++ b/echo/directus/sync/snapshot/relations/project_agentic_run_event/project_agentic_run_id.json @@ -0,0 +1,25 @@ +{ + "collection": "project_agentic_run_event", + "field": "project_agentic_run_id", + "related_collection": "project_agentic_run", + "meta": { + "junction_field": null, + "many_collection": "project_agentic_run_event", + "many_field": "project_agentic_run_id", + "one_allowed_collections": null, + "one_collection": "project_agentic_run", + "one_collection_field": null, + "one_deselect_action": "delete", + "one_field": "events", + "sort_field": null + }, + "schema": { + "table": "project_agentic_run_event", + "column": "project_agentic_run_id", + "foreign_key_table": "project_agentic_run", + "foreign_key_column": "id", + "constraint_name": "project_agentic_run_event_project_agentic_run_id_foreign", + "on_update": "NO ACTION", + "on_delete": "CASCADE" + } +} diff --git a/echo/frontend/src/components/chat/AgenticChatPanel.tsx b/echo/frontend/src/components/chat/AgenticChatPanel.tsx new file mode 100644 index 00000000..85b32adf --- /dev/null +++ b/echo/frontend/src/components/chat/AgenticChatPanel.tsx @@ -0,0 +1,666 @@ +import { t } from "@lingui/core/macro"; +import { Trans } from "@lingui/react/macro"; +import { + Alert, + Box, + Button, + Divider, + Group, + Loader, + Stack, + Text, + Textarea, + Title, +} from "@mantine/core"; +import { IconAlertCircle, IconPlayerStop, IconSend } from "@tabler/icons-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { + AgenticRunEvent, + AgenticRunEventsResponse, + AgenticRunStatus, +} from "@/lib/api"; +import { + appendAgenticRunMessage, + createAgenticRun, + getAgenticRun, + getAgenticRunEvents, + stopAgenticRun, + streamAgenticRun, +} from "@/lib/api"; +import { Markdown } from "../common/Markdown"; +import { toast } from "../common/Toaster"; +import { + extractTopLevelToolActivity, + type ToolActivity, +} from "./agenticToolActivity"; +import { ChatMessage } from "./ChatMessage"; +import { ChatTemplatesMenu } from "./ChatTemplatesMenu"; + +type AgenticChatPanelProps = { + chatId: string; + projectId: string; +}; + +type RenderMessage = { + id: string; + role: "user" | "assistant" | "dembrane"; + content: string; +}; + +type TimelineItem = + | (RenderMessage & { + kind: "message"; + }) + | (ToolActivity & { + kind: "tool"; + }); + +const storageKeyForChat = (chatId: string) => `agentic-run:${chatId}`; + +const isTerminalStatus = (status: AgenticRunStatus | null) => + status === "completed" || status === "failed" || status === "timeout"; + +const isInFlightStatus = (status: AgenticRunStatus | null) => + status === "queued" || status === "running"; + +const asObject = (value: unknown): Record | null => { + if (value && typeof value === "object") + return value as Record; + return null; +}; + +const toMessage = (event: AgenticRunEvent): RenderMessage | null => { + const payload = asObject(event.payload); + + const content = + typeof payload?.content === "string" + ? payload.content + : typeof payload?.message === "string" + ? payload.message + : null; + + if (event.event_type === "agent.nudge") { + return null; + } + + if (event.event_type === "user.message" && content) { + return { content, id: `u-${event.seq}`, role: "user" }; + } + + if (event.event_type === "assistant.message" && content) { + return { content, id: `a-${event.seq}`, role: "assistant" }; + } + + if (event.event_type === "run.failed" || event.event_type === "run.timeout") { + return { + content: content ?? "Agent run failed", + id: `s-${event.seq}`, + role: "dembrane", + }; + } + + if (event.event_type === "on_copilotkit_error") { + const data = + payload?.data && typeof payload.data === "object" + ? (payload.data as Record) + : null; + const nestedError = + data?.error && typeof data.error === "object" + ? (data.error as Record) + : null; + const errorMessage = + typeof nestedError?.message === "string" + ? nestedError.message + : typeof data?.message === "string" + ? data.message + : "Agent run failed"; + return { + content: errorMessage, + id: `e-${event.seq}`, + role: "dembrane", + }; + } + + return null; +}; + +const TOOL_STATUS_META: Record< + ToolActivity["status"], + { badgeClass: string; label: string } +> = { + completed: { + badgeClass: "border-emerald-300 bg-emerald-100 text-emerald-800", + label: "✓", + }, + error: { + badgeClass: "border-red-300 bg-red-100 text-red-800", + label: "Error", + }, + running: { + badgeClass: "border-amber-300 bg-amber-100 text-amber-800", + label: "Running", + }, +}; + +export const AgenticChatPanel = ({ + chatId, + projectId, +}: AgenticChatPanelProps) => { + const [runId, setRunId] = useState(null); + const [runStatus, setRunStatus] = useState(null); + const [afterSeq, setAfterSeq] = useState(0); + const [events, setEvents] = useState([]); + const [input, setInput] = useState(""); + const [templateKey, setTemplateKey] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isStopping, setIsStopping] = useState(false); + const [isStreaming, setIsStreaming] = useState(false); + const [streamFailureCount, setStreamFailureCount] = useState(0); + const [error, setError] = useState(null); + const streamAbortRef = useRef(null); + + const timeline = useMemo(() => { + const sorted = [...events].sort((a, b) => a.seq - b.seq); + const byId = new Map(); + const orderedIds: string[] = []; + + const upsertItem = (item: TimelineItem) => { + if (!byId.has(item.id)) { + orderedIds.push(item.id); + } + byId.set(item.id, item); + }; + + for (const event of sorted) { + const topLevelMessage = toMessage(event); + if (topLevelMessage) { + upsertItem({ + ...topLevelMessage, + kind: "message", + }); + } + + for (const activity of extractTopLevelToolActivity(event)) { + upsertItem({ + ...activity, + kind: "tool", + }); + } + } + + return orderedIds + .map((id) => byId.get(id)) + .filter((item): item is TimelineItem => item !== undefined); + }, [events]); + + const mergeEvents = useCallback((incoming: AgenticRunEvent[]) => { + if (incoming.length === 0) return; + setEvents((previous) => { + const bySeq = new Map(); + for (const event of previous) bySeq.set(event.seq, event); + for (const event of incoming) bySeq.set(event.seq, event); + return Array.from(bySeq.values()).sort((a, b) => a.seq - b.seq); + }); + }, []); + + const refreshEvents = useCallback( + async ( + targetRunId: string, + fromSeq: number, + ): Promise => { + const payload = await getAgenticRunEvents(targetRunId, fromSeq); + mergeEvents(payload.events); + setAfterSeq(payload.next_seq); + setRunStatus(payload.status); + return payload; + }, + [mergeEvents], + ); + + const stopStream = useCallback(() => { + if (streamAbortRef.current) { + streamAbortRef.current.abort(); + streamAbortRef.current = null; + } + setIsStreaming(false); + }, []); + + const startStream = useCallback( + async (targetRunId: string, fromSeq: number) => { + stopStream(); + + const abortController = new AbortController(); + streamAbortRef.current = abortController; + setIsStreaming(true); + + try { + await streamAgenticRun(targetRunId, { + afterSeq: fromSeq, + onEvent: (event) => { + mergeEvents([event]); + setAfterSeq((previous) => Math.max(previous, event.seq)); + setStreamFailureCount(0); + if (event.event_type === "run.failed") { + setRunStatus("failed"); + } + if (event.event_type === "run.timeout") { + setRunStatus("timeout"); + } + }, + signal: abortController.signal, + }); + } catch (streamError) { + if (abortController.signal.aborted) { + return; + } + + setStreamFailureCount((count) => { + const next = count + 1; + if (next >= 2) { + setError("Live stream interrupted. Falling back to polling."); + } + return next; + }); + + if (streamError instanceof Error) { + console.warn("Agentic stream failed", streamError); + } + } finally { + if (streamAbortRef.current === abortController) { + streamAbortRef.current = null; + setIsStreaming(false); + } + try { + const run = await getAgenticRun(targetRunId); + setRunStatus(run.status); + } catch { + // Ignore status refresh failures; polling fallback will retry. + } + } + }, + [mergeEvents, stopStream], + ); + + // biome-ignore lint/correctness/useExhaustiveDependencies: Reset panel state whenever chatId changes. + useEffect(() => { + stopStream(); + setRunId(null); + setRunStatus(null); + setAfterSeq(0); + setEvents([]); + setError(null); + setTemplateKey(null); + setIsStopping(false); + setIsSubmitting(false); + setStreamFailureCount(0); + }, [chatId, stopStream]); + + useEffect(() => { + if (!chatId) return; + const key = storageKeyForChat(chatId); + const storedRunId = window.localStorage.getItem(key); + if (!storedRunId) return; + + let active = true; + (async () => { + try { + const run = await getAgenticRun(storedRunId); + if (!active) return; + setRunId(storedRunId); + setRunStatus(run.status); + const payload = await refreshEvents(storedRunId, 0); + if (!active) return; + if (!isTerminalStatus(payload.status)) { + void startStream(storedRunId, payload.next_seq); + } + } catch { + window.localStorage.removeItem(key); + } + })(); + + return () => { + active = false; + }; + }, [chatId, refreshEvents, startStream]); + + useEffect(() => { + if (!runId || !runStatus || isTerminalStatus(runStatus)) return; + if (isStreaming || streamFailureCount < 2) return; + + let active = true; + const interval = window.setInterval(async () => { + if (!active) return; + try { + await refreshEvents(runId, afterSeq); + } catch { + // Keep fallback polling retries lightweight. + } + }, 1500); + + return () => { + active = false; + window.clearInterval(interval); + }; + }, [ + runId, + runStatus, + afterSeq, + isStreaming, + streamFailureCount, + refreshEvents, + ]); + + useEffect(() => { + if (runStatus && isTerminalStatus(runStatus)) { + stopStream(); + } + }, [runStatus, stopStream]); + + useEffect(() => { + return () => { + stopStream(); + }; + }, [stopStream]); + + const handleTemplateSelect = ({ + content, + key, + }: { + content: string; + key: string; + }) => { + const previousInput = input.trim(); + const previousTemplateKey = templateKey; + + setInput(content); + setTemplateKey(key); + + if (previousInput !== "") { + toast(t`Template applied`, { + action: { + label: t`Undo`, + onClick: () => { + setInput(previousInput); + setTemplateKey(previousTemplateKey); + }, + }, + duration: 5000, + }); + } + }; + + useEffect(() => { + if (input.trim() === "" && templateKey) { + setTemplateKey(null); + } + }, [input, templateKey]); + + const handleSubmit = async () => { + const message = input.trim(); + if (!message || !projectId || !chatId) return; + if (isInFlightStatus(runStatus)) return; + + setError(null); + setIsSubmitting(true); + setInput(""); + + try { + let targetRunId = runId; + + if (!targetRunId) { + const created = await createAgenticRun({ + message, + project_chat_id: chatId, + project_id: projectId, + }); + targetRunId = created.id; + setRunId(targetRunId); + setRunStatus(created.status); + window.localStorage.setItem(storageKeyForChat(chatId), targetRunId); + const payload = await refreshEvents(targetRunId, 0); + if (!isTerminalStatus(payload.status)) { + void startStream(targetRunId, payload.next_seq); + } + } else { + const updated = await appendAgenticRunMessage(targetRunId, message); + setRunStatus(updated.status); + const payload = await refreshEvents(targetRunId, afterSeq); + if (!isTerminalStatus(payload.status)) { + void startStream(targetRunId, payload.next_seq); + } + } + } catch (submitError) { + const message = + submitError instanceof Error + ? submitError.message + : "Failed to submit agentic message"; + setError(message); + } finally { + setIsSubmitting(false); + } + }; + + const handleStop = async () => { + if (!runId || !isInFlightStatus(runStatus)) return; + setIsStopping(true); + setError(null); + try { + await stopAgenticRun(runId); + } catch (stopError) { + const message = + stopError instanceof Error ? stopError.message : "Failed to stop run"; + setError(message); + } finally { + setIsStopping(false); + } + }; + + const isRunInFlight = isInFlightStatus(runStatus); + + return ( + + + + + <Trans>Agentic Chat</Trans> + + + {runStatus && ( + + Run status: {runStatus} + + )} + {isRunInFlight && ( + + )} + + + + + + + + {error && ( + } + title={Error} + > + {error} + + )} + + + + {timeline.length === 0 && ( + + Send a message to start an agentic run. + + )} + {timeline.map((item) => { + if (item.kind === "message") { + return ( + + + + ); + } + + const statusMeta = TOOL_STATUS_META[item.status]; + const hasRawData = + item.rawInput || item.rawOutput || item.rawError; + const showStatusBadge = item.status !== "running"; + + return ( + + +
+ + + + {item.headline} + + {showStatusBadge && ( + + {statusMeta.label} + + )} + + + {(item.summaryLines.length > 0 || hasRawData) && ( + + {item.summaryLines.map((line) => ( + + {line} + + ))} + {hasRawData && ( +
+ + Raw data + + + {item.rawInput && ( + + + Input + + + {item.rawInput} + + + )} + {item.rawOutput && ( + + + Output + + + {item.rawOutput} + + + )} + {item.rawError && ( + + + Error + + + {item.rawError} + + + )} + +
+ )} +
+ )} +
+
+
+ ); + })} +
+
+
+
+ + + + + + +