refactor(chat): eliminate /api/ prefix -> /v1/chat/* (namespaced, no-collision)

App REST surface moves from /api/* to /v1/chat/* in lockstep across server
mounts, client URL builders, SSE stream paths, OAuth redirect_uris, telemetry
route-grouping, RUM proxy, and the nginx bridge.

Namespace /v1/chat/* (not flat /v1/) to avoid colliding with the OpenAI-compat
inference surface (/v1/chat/completions, /v1/models -> router_backend) and to
match the canonical chat/openapi.yaml. Social OIDC login (/oauth/*) and /health
are unchanged (login-safe).

Levers:
- server: api/server/index.js 26 app.use('/v1/chat/*') mounts (+ experimental.js)
- client: packages/data-provider/src/api-endpoints.ts (75 builders) + config.ts
Lockstep: checkBan matcher, Files/Code download path, actions/mcp CSRF cookie
paths, ActionService + mcp/oauth handler redirect_uris, admin OpenID callback,
telemetry middleware/sdk/stream, rum proxy path, vite dev proxy + PWA denylist,
robots.txt, .env.example, pr-preview health-path (/api/health -> /health).

Third-party APIs chat CALLS are untouched (Ollama, Wolfram, Discord, GitHub
Enterprise, Mistral, OpenRouter, DataDog). Live-surface /api/: 683 -> 0.
Also untracks runtime log artifacts (api/logs, client/junit.xml; already gitignored).
This commit is contained in:
hanzo-dev
2026-07-03 13:55:43 -07:00
parent 3e83bc6779
commit ec32864a00
149 changed files with 1486 additions and 3011 deletions
+1 -1
View File
@@ -119,7 +119,7 @@ HANZO_API_KEY=
# Canonical Hanzo Cloud agents (/v1/agents). Lets signed-in users run their own
# cloud agents from chat via `/agent <name>` or the @mention picker. The chat
# backend proxies /api/agents/cloud/* to this host, forwarding the user's
# backend proxies /v1/chat/agents/cloud/* to this host, forwarding the user's
# hanzo.id token server-side (never to the browser); cloud scopes to their org.
# Optional: if unset, derived from OPENAI_BASE_URL (its host, minus /v1).
# HANZO_CLOUD_URL=https://api.hanzo.ai
+1 -1
View File
@@ -16,6 +16,6 @@ jobs:
service: chat
image: ghcr.io/hanzoai/chat
port: "3080"
health-path: /api/health
health-path: /health
e2e-dir: e2e
secrets: inherit
+7 -7
View File
@@ -106,14 +106,14 @@ Client render path (guest === chat, not a marketing gate):
the chat surface (only truly anonymous, non-guest, non-guest-enabled users go to
`/login`). `Root` shows the chat shell for `isAuthenticated || isGuest`.
- `ChatRoute` renders `ChatView` for `canChat = isAuthenticated || isGuest`; the
`/api/models` + `/api/endpoints` queries run for guests (both routes use
`/v1/chat/models` + `/v1/chat/endpoints` queries run for guests (both routes use
`requireGuestOrJwtAuth` and return the guest-scoped single-model config), and the
roles gate treats a guest as loaded (guests have no agent access). Files:
`client/src/routes/{ChatRoute,useAuthRedirect}.tsx`, `hooks/useGuestAuth.ts`,
`hooks/AuthContext.tsx`, `components/Auth/GuestLimitDialog.tsx`.
Security model (fail-closed, server-enforced):
- `POST /api/auth/guest` issues a short-lived guest JWT (`{guest:true}`,
- `POST /v1/chat/auth/guest` issues a short-lived guest JWT (`{guest:true}`,
per-token random id) signed with `JWT_SECRET`. Rate-limited per IP
(`guestTokenLimiter`, `GUEST_TOKEN_MAX`/`GUEST_TOKEN_WINDOW`) so tokens can't be
spam-minted.
@@ -147,7 +147,7 @@ agent builder, which is untouched.
- Two surfaces, ONE run path: the `/agent <name> [prompt]` slash command and the
@mention picker (cloud agents appear as a `cloudAgent` type). Both funnel
through `useRunCloudAgent``POST /api/agents/cloud/:name/run`. The @mention /
through `useRunCloudAgent``POST /v1/chat/agents/cloud/:name/run`. The @mention /
`/agent` picker arms `/agent <name> ` in the composer; submit is intercepted in
`ChatForm` (`parseAgentCommand`) and dispatched to the run path.
- Server proxy + auth (token never reaches the browser): the chat backend reads
@@ -179,7 +179,7 @@ agent builder, which is untouched.
`session.openidTokens.refreshToken` is written ONLY in REUSE mode (where
`refreshController`/`logoutController` read it). That keeps login, refresh AND
logout on the local-JWT path byte-identical to a non-OpenID login; that flag
SOLELY gates whether `/api/auth/refresh` performs the OIDC refresh-grant.
SOLELY gates whether `/v1/chat/auth/refresh` performs the OIDC refresh-grant.
The ~1h id_token is used while valid; durable refresh (hanzo.id/Casdoor OIDC
refresh or an RFC-8693 token-exchange from the chat session) is a tracked
FOLLOW-UP — the login-breaking refresh-grant is NOT enabled here.
@@ -197,7 +197,7 @@ agent builder, which is untouched.
`client/src/components/Chat/Input/AgentsCommand.tsx`, and the @mention wiring in
`client/src/hooks/Input/useMentions.ts` + `Mention.tsx`.
- Env: `HANZO_CLOUD_URL` (optional; falls back to the `OPENAI_BASE_URL` host).
- Convergence path (later): chat's LibreChat-legacy `/api/agents` CRUD should
- Convergence path (later): chat's LibreChat-legacy `/v1/chat/agents` CRUD should
converge onto cloud `/v1/agents`; this step only ADDS cloud-agent RUN.
## Unified cloud architecture (2026-07) — investigate-before-ripping map
@@ -209,7 +209,7 @@ Verified by full call-graph + route-table trace; do NOT rip blind.
### What already routes through the Go backend `api.hanzo.ai/v1` (no shadow LLM)
- **Chat completions**: client `useSSE``POST /api/agents/chat/Hanzo` (all
- **Chat completions**: client `useSSE``POST /v1/chat/agents/chat/Hanzo` (all
chat, incl. plain-model, goes through the agents framework) → custom-endpoint
resolver (`packages/api/src/endpoints/custom/initialize.ts`) reads
`HANZO_API_KEY` + literal `baseURL https://api.hanzo.ai/v1` from the loaded
@@ -219,7 +219,7 @@ Verified by full call-graph + route-table trace; do NOT rip blind.
- **Code interpreter** → `LIBRECHAT_CODE_BASEURL` = cloud `/v1/exec`.
- **Web search** → `webSearch` block (searxng+firecrawl contracts) = cloud
`/v1/websearch`.
- **Cloud agents** → `POST /api/agents/cloud/:name/run` server-proxies to cloud
- **Cloud agents** → `POST /v1/chat/agents/cloud/:name/run` server-proxies to cloud
`/v1/agents` with the user's hanzo.id bearer (see "Cloud Agents" section).
- **Model list**: curated **zen-only** (`fetch:false`) in the loaded config —
NO raw upstream names (brand policy). Authoritative prod list lives in the
+6 -6
View File
@@ -49,7 +49,7 @@ Artifacts are for substantial, self-contained content that users might modify or
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- Mermaid Diagrams: "application/vnd.mermaid"
- The user interface will render Mermaid diagrams placed within the artifact tags.
@@ -63,7 +63,7 @@ Artifacts are for substantial, self-contained content that users might modify or
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
6. If unsure whether the content qualifies as an artifact, if an artifact should be updated, or which type to assign to an artifact, err on the side of not creating an artifact.
@@ -162,7 +162,7 @@ Artifacts are for substantial, self-contained content that users might modify or
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- SVG: "image/svg+xml"
- The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags.
@@ -186,7 +186,7 @@ Artifacts are for substantial, self-contained content that users might modify or
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- When iterating on code, ensure that the code is complete and functional without any snippets, placeholders, or ellipses.
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
@@ -367,7 +367,7 @@ Artifacts are for substantial, self-contained content that users might modify or
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- SVG: "image/svg+xml"
- The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags.
@@ -391,7 +391,7 @@ Artifacts are for substantial, self-contained content that users might modify or
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- When iterating on code, ensure that the code is complete and functional without any snippets, placeholders, or ellipses.
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
@@ -1,15 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/Users/z/work/hanzo/chat/api/logs/.124163c776d287793643ac0e08ac1d35d67ad894-audit.json",
"files": [
{
"date": 1771557162318,
"name": "/Users/z/work/hanzo/chat/api/logs/meiliSync-2026-02-19.log",
"hash": "a54018af15e4552979b0e91a2379ef40b95019fe56fe70074bb13f91952d908f"
}
],
"hashType": "sha256"
}
@@ -1,15 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/Users/z/work/hanzo/chat/api/logs/.35252977fe148e605b7b92f19cd71e590a6f0a53-audit.json",
"files": [
{
"date": 1771557162297,
"name": "/Users/z/work/hanzo/chat/api/logs/error-2026-02-19.log",
"hash": "63688154da6c0a28cba1e30ddeef3eb867682460096d9b007dcfd2ddc25202b0"
}
],
"hashType": "sha256"
}
@@ -1,45 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/Users/z/work/hanzo/chat/api/logs/.5a07238e142f72f47174c381d68b979f0f4a60b3-audit.json",
"files": [
{
"date": 1750474453457,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-20.log",
"hash": "696c5dfed20dbc87cd7113adb058ad5df3e53b93b9fc24840b7e236724ac4823"
},
{
"date": 1750715218180,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-23.log",
"hash": "c802b606c51329d1d65fe6c877dff02200e4d57e6565de3697ce4d8938bd8366"
},
{
"date": 1750883760400,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-25.log",
"hash": "202106c1ae63bd10f256f9249f6f32e20e495b9ca7f5ab7844f6dff28716805a"
},
{
"date": 1750950599470,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-26.log",
"hash": "e5f73d742f92938cef296a5c97cf8bb7f4e3db0198b8bc53bb0eee3ddf0f1e84"
},
{
"date": 1751049961804,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-27.log",
"hash": "15a471f383c1ea303252a3b35f88e44cc4bd1905666617dc9afc4b415d3ddac9"
},
{
"date": 1751124937592,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-28.log",
"hash": "77a7dd41fff34914a5ce25239c9fd77fafb8c3617a7a118550af69fbcc577643"
},
{
"date": 1751599940774,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-07-03.log",
"hash": "c86e337d49f3edad24952270eb6bebba4588201f1e14eab31b2482ee0c68be00"
}
],
"hashType": "sha256"
}
@@ -1,30 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/Users/z/work/hanzo/chat/api/logs/.b5a17c43715e8a0a84a729c6012dc1ba16b1828b-audit.json",
"files": [
{
"date": 1750954055845,
"name": "/Users/z/work/hanzo/chat/api/logs/meiliSync-2025-06-26.log",
"hash": "980f8267840afde6278855f4218760f010a3fb215aa86ef5bb69dc08bb4b1b50"
},
{
"date": 1751049961800,
"name": "/Users/z/work/hanzo/chat/api/logs/meiliSync-2025-06-27.log",
"hash": "cebe79495cabf77d359dbcd3168502d3a5c4d8993e1bed0663fa40d850ccf6d5"
},
{
"date": 1751124937590,
"name": "/Users/z/work/hanzo/chat/api/logs/meiliSync-2025-06-28.log",
"hash": "bb29a81af67a7fce02315648194ef210ef2ad3c89e7083220d9a63103dc762cc"
},
{
"date": 1751599940771,
"name": "/Users/z/work/hanzo/chat/api/logs/meiliSync-2025-07-03.log",
"hash": "9b454a63526db0eb56a27269fd38a86ac3d35dba85338ae89c91ea9895d467cb"
}
],
"hashType": "sha256"
}
-8
View File
@@ -1,8 +0,0 @@
2025-06-25T20:37:18.651Z info: [Optional] Redis not initialized.
2025-06-25T20:37:20.168Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-25T20:39:50.405Z info: [Optional] Redis not initialized.
2025-06-25T20:39:51.070Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-25T20:40:07.114Z info: [Optional] Redis not initialized.
2025-06-25T20:40:07.770Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-25T20:40:53.234Z info: [Optional] Redis not initialized.
2025-06-25T20:40:53.905Z info: [Optional] IoRedis not initialized for rate limiters.
-115
View File
@@ -1,115 +0,0 @@
2025-06-26T15:10:34.209Z info: [Optional] Redis not initialized.
2025-06-26T15:10:35.166Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T16:21:35.171Z info: [Optional] Redis not initialized.
2025-06-26T16:21:36.574Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T16:22:06.626Z error: connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T16:23:03.561Z info: [Optional] Redis not initialized.
2025-06-26T16:23:04.255Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T16:23:04.300Z info: Connected to MongoDB
2025-06-26T16:23:04.301Z info: [indexSync] Starting index synchronization check...
2025-06-26T16:23:04.420Z error: Invalid custom config file at /Users/z/work/hanzo/chat/chat.yaml:
{
"issues": [
{
"code": "unrecognized_keys",
"keys": [
"ag... [truncated]
2025-06-26T16:23:04.420Z debug: Web search serperApiKey: Using environment variable SERPER_API_KEY (not set in environment, user provided value)
2025-06-26T16:23:04.420Z debug: Web search firecrawlApiKey: Using environment variable FIRECRAWL_API_KEY (not set in environment, user provided value)
2025-06-26T16:23:04.420Z debug: Web search firecrawlApiUrl: Using environment variable FIRECRAWL_API_URL (not set in environment, user provided value)
2025-06-26T16:23:04.421Z debug: Web search jinaApiKey: Using environment variable JINA_API_KEY (not set in environment, user provided value)
2025-06-26T16:23:04.421Z debug: Web search cohereApiKey: Using environment variable COHERE_API_KEY (not set in environment, user provided value)
2025-06-26T16:23:04.421Z warn: Default value for CREDS_KEY is being used.
2025-06-26T16:23:04.421Z warn: Default value for CREDS_IV is being used.
2025-06-26T16:23:04.421Z warn: Default value for JWT_SECRET is being used.
2025-06-26T16:23:04.421Z warn: Default value for JWT_REFRESH_SECRET is being used.
2025-06-26T16:23:04.421Z info: Please replace any default secret values.
2025-06-26T16:23:04.421Z info:
For your convenience, use this tool to generate your own secret values:
https://www.hanzo.ai/toolkit/creds_generator
2025-06-26T16:23:04.421Z warn: RAG API is either not running or not reachable at undefined, you may experience errors with file uploads.
2025-06-26T16:23:04.429Z info: No changes needed for 'USER' role permissions
2025-06-26T16:23:04.431Z info: No changes needed for 'ADMIN' role permissions
2025-06-26T16:23:04.431Z info: Turnstile is DISABLED (no siteKey provided).
2025-06-26T16:24:16.068Z info: [Optional] Redis not initialized.
2025-06-26T16:24:17.688Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T16:24:17.756Z info: Connected to MongoDB
2025-06-26T16:24:17.757Z info: [indexSync] Starting index synchronization check...
2025-06-26T16:24:17.811Z error: Invalid custom config file at /Users/z/work/hanzo/chat/chat.yaml:
{
"issues": [
{
"code": "unrecognized_keys",
"keys": [
"ag... [truncated]
2025-06-26T16:24:17.812Z debug: Web search serperApiKey: Using environment variable SERPER_API_KEY (not set in environment, user provided value)
2025-06-26T16:24:17.812Z debug: Web search firecrawlApiKey: Using environment variable FIRECRAWL_API_KEY (not set in environment, user provided value)
2025-06-26T16:24:17.812Z debug: Web search firecrawlApiUrl: Using environment variable FIRECRAWL_API_URL (not set in environment, user provided value)
2025-06-26T16:24:17.812Z debug: Web search jinaApiKey: Using environment variable JINA_API_KEY (not set in environment, user provided value)
2025-06-26T16:24:17.812Z debug: Web search cohereApiKey: Using environment variable COHERE_API_KEY (not set in environment, user provided value)
2025-06-26T16:24:17.812Z warn: Default value for CREDS_KEY is being used.
2025-06-26T16:24:17.812Z warn: Default value for CREDS_IV is being used.
2025-06-26T16:24:17.812Z warn: Default value for JWT_SECRET is being used.
2025-06-26T16:24:17.812Z warn: Default value for JWT_REFRESH_SECRET is being used.
2025-06-26T16:24:17.812Z info: Please replace any default secret values.
2025-06-26T16:24:17.812Z info:
For your convenience, use this tool to generate your own secret values:
https://www.hanzo.ai/toolkit/creds_generator
2025-06-26T16:24:17.812Z warn: RAG API is either not running or not reachable at undefined, you may experience errors with file uploads.
2025-06-26T16:24:17.823Z info: No changes needed for 'USER' role permissions
2025-06-26T16:24:17.825Z info: No changes needed for 'ADMIN' role permissions
2025-06-26T16:24:17.825Z info: Turnstile is DISABLED (no siteKey provided).
2025-06-26T16:24:17.832Z info: Server listening at http://localhost:3080
2025-06-26T16:24:18.010Z debug: [MEILI_SYNC:meili-index-sync] Creating initial flow state
2025-06-26T16:24:18.020Z info: [indexSync] Messages are fully synced: 0/0
2025-06-26T16:24:18.023Z info: [indexSync] Conversations are fully synced: 0/0
2025-06-26T16:24:18.024Z debug: [indexSync] No sync was needed
2025-06-26T16:24:37.105Z error: invalid signature
2025-06-26T17:16:21.631Z info: Cleaning up FlowStateManager intervals...
2025-06-26T17:28:31.428Z info: [Optional] Redis not initialized.
2025-06-26T19:16:07.812Z info: [Optional] Redis not initialized.
2025-06-26T19:16:09.408Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T19:16:39.465Z error: connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T19:28:57.619Z info: [Optional] Redis not initialized.
2025-06-26T19:28:58.798Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:18:29.425Z info: [Optional] Redis not initialized.
2025-06-26T21:18:30.678Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:19:00.734Z error: getaddrinfo ENOTFOUND mongodb
2025-06-26T21:20:04.085Z info: [Optional] Redis not initialized.
2025-06-26T21:20:04.788Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:20:34.832Z error: connect ECONNREFUSED ::1:27017, connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T21:35:21.338Z info: [Optional] Redis not initialized.
2025-06-26T21:35:22.746Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:35:52.803Z error: connect ECONNREFUSED ::1:27017, connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T21:39:16.497Z info: [Optional] Redis not initialized.
2025-06-26T21:39:17.655Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:39:47.696Z error: connect ECONNREFUSED ::1:27017, connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T21:43:04.982Z info: [Optional] Redis not initialized.
2025-06-26T21:43:06.587Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:07.208Z info: [Optional] Redis not initialized.
2025-06-26T21:43:07.990Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:09.790Z info: [Optional] Redis not initialized.
2025-06-26T21:43:10.572Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:12.993Z info: [Optional] Redis not initialized.
2025-06-26T21:43:14.084Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:16.627Z info: [Optional] Redis not initialized.
2025-06-26T21:43:17.383Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:19.449Z info: [Optional] Redis not initialized.
2025-06-26T21:43:20.212Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:34.812Z info: [Optional] Redis not initialized.
2025-06-26T21:43:36.179Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:50.258Z error: connect ECONNREFUSED ::1:27017, connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T21:44:17.565Z info: [Optional] Redis not initialized.
2025-06-26T21:44:18.322Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-27T03:16:07.463Z info: [Optional] Redis not initialized.
2025-06-27T03:16:08.973Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-27T03:16:17.650Z debug: [indexSync] Clearing sync timeouts before exiting...
2025-06-27T03:19:09.423Z info: [Optional] Redis not initialized.
2025-06-27T03:19:10.741Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-27T03:19:15.742Z debug: [indexSync] Clearing sync timeouts before exiting...
-3
View File
@@ -1,3 +0,0 @@
2025-06-27T18:46:02.321Z info: [Optional] Redis not initialized.
2025-06-27T18:46:03.818Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-27T18:46:23.237Z debug: [indexSync] Clearing sync timeouts before exiting...
-3
View File
@@ -1,3 +0,0 @@
2025-06-28T15:35:37.951Z info: [Optional] Redis not initialized.
2025-06-28T15:35:39.502Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-28T15:36:09.559Z error: connect ECONNREFUSED ::1:27017, connect ECONNREFUSED 127.0.0.1:27017
-3
View File
@@ -1,3 +0,0 @@
2025-07-04T03:32:21.282Z info: [Optional] Redis not initialized.
2025-07-04T03:32:22.739Z info: [Optional] IoRedis not initialized for rate limiters.
2025-07-04T03:32:40.543Z debug: [indexSync] Clearing sync timeouts before exiting...
-4
View File
@@ -1,4 +0,0 @@
{"level":"error","message":"[mongoMeili] Error checking index convos: fetch failed","name":"MeiliSearchCommunicationError","stack":"MeiliSearchCommunicationError: fetch failed\n at node:internal/deps/undici/undici:13510:13\n at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"}
{"level":"error","message":"[mongoMeili] Error checking index messages: fetch failed","name":"MeiliSearchCommunicationError","stack":"MeiliSearchCommunicationError: fetch failed\n at node:internal/deps/undici/undici:13510:13\n at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"}
{"level":"error","message":"[mongoMeili] Error checking index convos: fetch failed","name":"MeiliSearchCommunicationError","stack":"MeiliSearchCommunicationError: fetch failed\n at node:internal/deps/undici/undici:13510:13\n at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"}
{"level":"error","message":"[mongoMeili] Error checking index messages: fetch failed","name":"MeiliSearchCommunicationError","stack":"MeiliSearchCommunicationError: fetch failed\n at node:internal/deps/undici/undici:13510:13\n at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"}
-4
View File
@@ -1,4 +0,0 @@
{"level":"debug","message":"[mongoMeili] Index convos already exists"}
{"level":"debug","message":"[mongoMeili] Index convos already exists"}
{"level":"debug","message":"[mongoMeili] Index messages already exists"}
{"level":"debug","message":"[mongoMeili] Index messages already exists"}
-4
View File
@@ -1,4 +0,0 @@
{"level":"debug","message":"[mongoMeili] Index messages already exists"}
{"level":"debug","message":"[mongoMeili] Index convos already exists"}
{"level":"debug","message":"[mongoMeili] Index messages already exists"}
{"level":"debug","message":"[mongoMeili] Index convos already exists"}
+6 -6
View File
@@ -2189,13 +2189,13 @@ describe('models/Agent', () => {
const actions = [
{
action_id: '123',
metadata: { version: '1.0', endpoints: ['GET /api/test'], schema: { type: 'object' } },
metadata: { version: '1.0', endpoints: ['GET /v1/chat/test'], schema: { type: 'object' } },
},
{
action_id: '456',
metadata: {
version: '2.0',
endpoints: ['POST /api/example'],
endpoints: ['POST /v1/chat/example'],
schema: { type: 'string' },
},
},
@@ -2212,10 +2212,10 @@ describe('models/Agent', () => {
test('should generate different hashes for different action metadata', async () => {
const actionIds = ['test.com_action_123'];
const actions1 = [
{ action_id: '123', metadata: { version: '1.0', endpoints: ['GET /api/test'] } },
{ action_id: '123', metadata: { version: '1.0', endpoints: ['GET /v1/chat/test'] } },
];
const actions2 = [
{ action_id: '123', metadata: { version: '2.0', endpoints: ['GET /api/test'] } },
{ action_id: '123', metadata: { version: '2.0', endpoints: ['GET /v1/chat/test'] } },
];
const hash1 = await generateActionMetadataHash(actionIds, actions1);
@@ -2284,8 +2284,8 @@ describe('models/Agent', () => {
},
},
endpoints: [
{ path: '/api/test', method: 'GET', params: ['id'] },
{ path: '/api/create', method: 'POST', body: true },
{ path: '/v1/chat/test', method: 'GET', params: ['id'] },
{ path: '/v1/chat/create', method: 'POST', body: true },
],
},
},
@@ -45,7 +45,7 @@ const validateResourceType = (resourceType) => {
/**
* Bulk update permissions for a resource (grant, update, remove)
* @route PUT /api/{resourceType}/{resourceId}/permissions
* @route PUT /v1/chat/{resourceType}/{resourceId}/permissions
* @param {Object} req - Express request object
* @param {Object} req.params - Route parameters
* @param {string} req.params.resourceType - Resource type (e.g., 'agent')
@@ -178,7 +178,7 @@ const updateResourcePermissions = async (req, res) => {
/**
* Get principals with their permission roles for a resource (UI-friendly format)
* Uses efficient aggregation pipeline to join User/Group data in single query
* @route GET /api/permissions/{resourceType}/{resourceId}
* @route GET /v1/chat/permissions/{resourceType}/{resourceId}
*/
const getResourcePermissions = async (req, res) => {
try {
@@ -311,7 +311,7 @@ const getResourcePermissions = async (req, res) => {
/**
* Get available roles for a resource type
* @route GET /api/{resourceType}/roles
* @route GET /v1/chat/{resourceType}/roles
*/
const getResourceRoles = async (req, res) => {
try {
@@ -339,7 +339,7 @@ const getResourceRoles = async (req, res) => {
/**
* Get user's effective permission bitmask for a resource
* @route GET /api/{resourceType}/{resourceId}/effective
* @route GET /v1/chat/{resourceType}/{resourceId}/effective
*/
const getUserEffectivePermissions = async (req, res) => {
try {
@@ -370,7 +370,7 @@ const getUserEffectivePermissions = async (req, res) => {
/**
* Search for users and groups to grant permissions
* Supports hybrid local database + Entra ID search when configured
* @route GET /api/permissions/search-principals
* @route GET /v1/chat/permissions/search-principals
*/
const searchPrincipals = async (req, res) => {
try {
@@ -487,7 +487,7 @@ const searchPrincipals = async (req, res) => {
/**
* Get user's effective permissions for all accessible resources of a type
* @route GET /api/permissions/{resourceType}/effective/all
* @route GET /v1/chat/permissions/{resourceType}/effective/all
*/
const getAllEffectivePermissions = async (req, res) => {
try {
@@ -60,7 +60,7 @@ describe('guest bootstrap controllers', () => {
process.env = originalEnv;
});
describe('GET /api/endpoints', () => {
describe('GET /v1/chat/endpoints', () => {
it('returns ONLY the guest endpoint, no DB/config read', async () => {
const req = guestReq();
const res = mockRes();
@@ -80,7 +80,7 @@ describe('guest bootstrap controllers', () => {
});
});
describe('GET /api/models', () => {
describe('GET /v1/chat/models', () => {
it('returns ONLY the single guest model under the guest endpoint', async () => {
const req = guestReq();
const res = mockRes();
+1 -1
View File
@@ -940,7 +940,7 @@ class AgentClient extends BaseClient {
}
/** Skip token spending if aborted - the abort handler (abortMiddleware.js) handles it
This prevents double-spending when user aborts via `/api/agents/chat/abort` */
This prevents double-spending when user aborts via `/v1/chat/agents/chat/abort` */
const wasAborted = abortController?.signal?.aborted;
if (!wasAborted) {
await this.recordCollectedUsage({
+2 -2
View File
@@ -64,7 +64,7 @@ function createOAuthHandler(redirectUri = domains.client) {
if (isEnabled(process.env.OPENID_REUSE_TOKENS) === true) {
/**
* REUSE path: the OIDC tokenset drives BOTH the app auth token and the
* refresh grant (`/api/auth/refresh` performs an OIDC refresh). Unchanged.
* refresh grant (`/v1/chat/auth/refresh` performs an OIDC refresh). Unchanged.
* setOpenIDAuthTokens already persists the id_token to the session.
*/
await syncUserEntraGroupMemberships(req.user, req.user.tokenset.access_token);
@@ -74,7 +74,7 @@ function createOAuthHandler(redirectUri = domains.client) {
* Decoupled path (the live default, REUSE disabled). Keep refresh on the
* local-JWT path so login/refresh cookies stay byte-identical, but ALSO
* persist the id_token server-side so downstream on-behalf-of cloud calls
* (POST /api/agents/cloud/:name/run -> cloud /v1/agents) can run as this
* (POST /v1/chat/agents/cloud/:name/run -> cloud /v1/agents) can run as this
* hanzo.id principal. OPENID_REUSE_TOKENS still SOLELY gates the OIDC
* refresh-grant; it no longer gates id_token persistence.
*/
+4 -4
View File
@@ -172,7 +172,7 @@ const getMCPTools = async (req, res) => {
};
/**
* Get all MCP servers with permissions
* @route GET /api/mcp/servers
* @route GET /v1/chat/mcp/servers
*/
const getMCPServersList = async (req, res) => {
try {
@@ -193,7 +193,7 @@ const getMCPServersList = async (req, res) => {
/**
* Create MCP server
* @route POST /api/mcp/servers
* @route POST /v1/chat/mcp/servers
*/
const createMCPServerController = async (req, res) => {
try {
@@ -252,7 +252,7 @@ const getMCPServerById = async (req, res) => {
/**
* Update MCP server
* @route PATCH /api/mcp/servers/:serverName
* @route PATCH /v1/chat/mcp/servers/:serverName
*/
const updateMCPServerController = async (req, res) => {
try {
@@ -287,7 +287,7 @@ const updateMCPServerController = async (req, res) => {
/**
* Delete MCP server
* @route DELETE /api/mcp/servers/:serverName
* @route DELETE /v1/chat/mcp/servers/:serverName
*/
const deleteMCPServerController = async (req, res) => {
try {
+26 -26
View File
@@ -296,33 +296,33 @@ if (cluster.isMaster) {
/** Routes */
app.use('/oauth', routes.oauth);
app.use('/api/auth', routes.auth);
app.use('/api/actions', routes.actions);
app.use('/api/keys', routes.keys);
app.use('/api/api-keys', routes.apiKeys);
app.use('/api/user', routes.user);
app.use('/api/search', routes.search);
app.use('/api/messages', routes.messages);
app.use('/api/convos', routes.convos);
app.use('/api/presets', routes.presets);
app.use('/api/prompts', routes.prompts);
app.use('/api/categories', routes.categories);
app.use('/api/endpoints', routes.endpoints);
app.use('/api/balance', routes.balance);
app.use('/api/models', routes.models);
app.use('/api/plugins', routes.plugins);
app.use('/api/config', routes.config);
app.use('/api/assistants', routes.assistants);
app.use('/api/files', await routes.files.initialize());
app.use('/v1/chat/auth', routes.auth);
app.use('/v1/chat/actions', routes.actions);
app.use('/v1/chat/keys', routes.keys);
app.use('/v1/chat/api-keys', routes.apiKeys);
app.use('/v1/chat/user', routes.user);
app.use('/v1/chat/search', routes.search);
app.use('/v1/chat/messages', routes.messages);
app.use('/v1/chat/convos', routes.convos);
app.use('/v1/chat/presets', routes.presets);
app.use('/v1/chat/prompts', routes.prompts);
app.use('/v1/chat/categories', routes.categories);
app.use('/v1/chat/endpoints', routes.endpoints);
app.use('/v1/chat/balance', routes.balance);
app.use('/v1/chat/models', routes.models);
app.use('/v1/chat/plugins', routes.plugins);
app.use('/v1/chat/config', routes.config);
app.use('/v1/chat/assistants', routes.assistants);
app.use('/v1/chat/files', await routes.files.initialize());
app.use('/images/', createValidateImageRequest(appConfig.secureImageLinks), routes.staticRoute);
app.use('/api/share', routes.share);
app.use('/api/roles', routes.roles);
app.use('/api/agents', routes.agents);
app.use('/api/banner', routes.banner);
app.use('/api/memories', routes.memories);
app.use('/api/permissions', routes.accessPermissions);
app.use('/api/tags', routes.tags);
app.use('/api/mcp', routes.mcp);
app.use('/v1/chat/share', routes.share);
app.use('/v1/chat/roles', routes.roles);
app.use('/v1/chat/agents', routes.agents);
app.use('/v1/chat/banner', routes.banner);
app.use('/v1/chat/memories', routes.memories);
app.use('/v1/chat/permissions', routes.accessPermissions);
app.use('/v1/chat/tags', routes.tags);
app.use('/v1/chat/mcp', routes.mcp);
/** Error handler */
app.use(ErrorController);
+26 -26
View File
@@ -174,34 +174,34 @@ const startServer = async () => {
app.use('/oauth', routes.oauth);
/* API Endpoints */
app.use('/api/auth', routes.auth);
app.use('/api/admin', routes.adminAuth);
app.use('/api/actions', routes.actions);
app.use('/api/keys', routes.keys);
app.use('/api/api-keys', routes.apiKeys);
app.use('/api/user', routes.user);
app.use('/api/search', routes.search);
app.use('/api/messages', routes.messages);
app.use('/api/convos', routes.convos);
app.use('/api/presets', routes.presets);
app.use('/api/prompts', routes.prompts);
app.use('/api/categories', routes.categories);
app.use('/api/endpoints', routes.endpoints);
app.use('/api/balance', routes.balance);
app.use('/api/models', routes.models);
app.use('/api/config', routes.config);
app.use('/api/assistants', routes.assistants);
app.use('/api/files', await routes.files.initialize());
app.use('/v1/chat/auth', routes.auth);
app.use('/v1/chat/admin', routes.adminAuth);
app.use('/v1/chat/actions', routes.actions);
app.use('/v1/chat/keys', routes.keys);
app.use('/v1/chat/api-keys', routes.apiKeys);
app.use('/v1/chat/user', routes.user);
app.use('/v1/chat/search', routes.search);
app.use('/v1/chat/messages', routes.messages);
app.use('/v1/chat/convos', routes.convos);
app.use('/v1/chat/presets', routes.presets);
app.use('/v1/chat/prompts', routes.prompts);
app.use('/v1/chat/categories', routes.categories);
app.use('/v1/chat/endpoints', routes.endpoints);
app.use('/v1/chat/balance', routes.balance);
app.use('/v1/chat/models', routes.models);
app.use('/v1/chat/config', routes.config);
app.use('/v1/chat/assistants', routes.assistants);
app.use('/v1/chat/files', await routes.files.initialize());
app.use('/images/', createValidateImageRequest(appConfig.secureImageLinks), routes.staticRoute);
app.use('/api/share', routes.share);
app.use('/api/roles', routes.roles);
app.use('/api/agents', routes.agents);
app.use('/api/banner', routes.banner);
app.use('/api/memories', routes.memories);
app.use('/api/permissions', routes.accessPermissions);
app.use('/v1/chat/share', routes.share);
app.use('/v1/chat/roles', routes.roles);
app.use('/v1/chat/agents', routes.agents);
app.use('/v1/chat/banner', routes.banner);
app.use('/v1/chat/memories', routes.memories);
app.use('/v1/chat/permissions', routes.accessPermissions);
app.use('/api/tags', routes.tags);
app.use('/api/mcp', routes.mcp);
app.use('/v1/chat/tags', routes.tags);
app.use('/v1/chat/mcp', routes.mcp);
app.use(ErrorController);
+1 -1
View File
@@ -111,7 +111,7 @@ describe('Server Configuration', () => {
});
try {
const response = await request(app).post('/api/auth/login').send({
const response = await request(app).post('/v1/chat/auth/login').send({
email: 'test@example.com',
password: 'password123',
});
@@ -72,7 +72,7 @@ const buildRouter = () => {
const buildApp = () => {
const app = express();
app.use(express.json());
app.use('/api/agents', buildRouter());
app.use('/v1/chat/agents', buildRouter());
return app;
};
@@ -96,7 +96,7 @@ describe('guest chat middleware chain', () => {
it('lets a guest reach the completion route, pinned to the free endpoint/model', async () => {
const res = await request(buildApp())
.post('/api/agents/chat')
.post('/v1/chat/agents/chat')
.set('Authorization', `Bearer ${guestToken()}`)
.set('x-test-ip', '10.1.0.1')
.send({ endpoint: 'Hanzo', model: 'zen3-nano', text: 'hi' });
@@ -109,7 +109,7 @@ describe('guest chat middleware chain', () => {
const ip = '10.1.0.2';
const send = () =>
request(app)
.post('/api/agents/chat')
.post('/v1/chat/agents/chat')
.set('Authorization', `Bearer ${guestToken()}`)
.set('x-test-ip', ip)
.send({ endpoint: 'Hanzo', model: 'zen3-nano', text: 'hi' });
@@ -123,7 +123,7 @@ describe('guest chat middleware chain', () => {
it('routes reserved /chat/abort to the JWT-only handler, not the guest router', async () => {
const res = await request(buildApp())
.post('/api/agents/chat/abort')
.post('/v1/chat/agents/chat/abort')
.set('Authorization', `Bearer ${guestToken()}`)
.set('x-test-ip', '10.1.0.3')
.send({ streamId: 'x' });
@@ -46,7 +46,7 @@ const buildEndpointOption = require('./buildEndpointOption');
const createReq = (body, config = {}) => ({
body,
config,
baseUrl: '/api/chat',
baseUrl: '/v1/chat/chat',
});
const createRes = () => ({
+1 -1
View File
@@ -26,7 +26,7 @@ const banResponse = async (req, res) => {
const { baseUrl, originalUrl } = req;
if (!ua.browser.name) {
return res.status(403).json({ message });
} else if (baseUrl === '/api/agents' && originalUrl.startsWith('/api/agents/chat')) {
} else if (baseUrl === '/v1/chat/agents' && originalUrl.startsWith('/v1/chat/agents/chat')) {
return await denyRequest(req, res, { type: ViolationTypes.BAN });
}
@@ -4,7 +4,7 @@ const { ViolationTypes } = require('librechat-data-provider');
const logViolation = require('~/cache/logViolation');
/**
* Per-user rate limiter for the cloud-agents proxy (`/api/agents/cloud/*`). A run
* Per-user rate limiter for the cloud-agents proxy (`/v1/chat/agents/cloud/*`). A run
* is a real, billable cloud chat completion that holds an upstream socket for up
* to 30s, yet it bypasses the message limiters that guard the sibling completion
* path. This caps a signed-in user to CLOUD_AGENT_USER_MAX requests per window so
+11 -11
View File
@@ -36,7 +36,7 @@ function createApp(user) {
next();
});
}
app.use('/api/config', configRoute);
app.use('/v1/chat/config', configRoute);
return app;
}
@@ -70,7 +70,7 @@ afterEach(() => {
delete process.env.RUM_ENVIRONMENT;
});
describe('GET /api/config RUM config', () => {
describe('GET /v1/chat/config RUM config', () => {
it('includes public-token RUM config when enabled with valid env', async () => {
mockGetAppConfig.mockResolvedValue(baseAppConfig);
process.env.RUM_ENABLED = 'true';
@@ -82,7 +82,7 @@ describe('GET /api/config RUM config', () => {
process.env.RUM_ENVIRONMENT = 'test';
const app = createApp(null);
const response = await request(app).get('/api/config');
const response = await request(app).get('/v1/chat/config');
expect(response.body.rum).toEqual({
provider: 'hyperdx',
@@ -107,7 +107,7 @@ describe('GET /api/config RUM config', () => {
process.env.RUM_PUBLIC_TOKEN = 'public-token';
const app = createApp(null);
const response = await request(app).get('/api/config');
const response = await request(app).get('/v1/chat/config');
expect(response.body).not.toHaveProperty('rum');
});
@@ -119,12 +119,12 @@ describe('GET /api/config RUM config', () => {
process.env.RUM_PROXY_TARGET_URL = 'http://otel-collector:4318';
const app = createApp(mockUser);
const response = await request(app).get('/api/config');
const response = await request(app).get('/v1/chat/config');
expect(response.body.rum).toEqual({
provider: 'hyperdx',
enabled: true,
url: '/api/rum',
url: '/v1/chat/rum',
serviceName: 'librechat-web',
authMode: 'proxy',
consoleCapture: false,
@@ -139,7 +139,7 @@ describe('GET /api/config RUM config', () => {
process.env.RUM_AUTH_MODE = 'proxy';
const app = createApp(mockUser);
const response = await request(app).get('/api/config');
const response = await request(app).get('/v1/chat/config');
expect(response.body).not.toHaveProperty('rum');
});
@@ -151,7 +151,7 @@ describe('GET /api/config RUM config', () => {
process.env.RUM_PUBLIC_TOKEN = 'public-token';
const app = createApp(null);
const response = await request(app).get('/api/config');
const response = await request(app).get('/v1/chat/config');
expect(response.body).not.toHaveProperty('rum');
});
@@ -163,7 +163,7 @@ describe('GET /api/config RUM config', () => {
process.env.RUM_PUBLIC_TOKEN = 'public-token';
const app = createApp(null);
const response = await request(app).get('/api/config');
const response = await request(app).get('/v1/chat/config');
expect(response.body.rum?.url).toBe('http://[::1]:4318');
});
@@ -175,7 +175,7 @@ describe('GET /api/config RUM config', () => {
process.env.RUM_AUTH_MODE = 'userJwt';
const app = createApp(mockUser);
const response = await request(app).get('/api/config');
const response = await request(app).get('/v1/chat/config');
expect(response.body).not.toHaveProperty('rum');
});
@@ -187,7 +187,7 @@ describe('GET /api/config RUM config', () => {
process.env.RUM_AUTH_MODE = 'userJwt';
const app = createApp(null);
const response = await request(app).get('/api/config');
const response = await request(app).get('/v1/chat/config');
expect(response.body).not.toHaveProperty('rum');
});
+1 -1
View File
@@ -5,7 +5,7 @@ const configRoute = require('../config');
// file deepcode ignore UseCsurfForExpress/test: test
const app = express();
app.disable('x-powered-by');
app.use('/api/config', configRoute);
app.use('/v1/chat/config', configRoute);
afterEach(() => {
delete process.env.APP_TITLE;
@@ -39,7 +39,7 @@ jest.mock('multer', () => require(MOCKS).multerLib());
jest.mock('~/server/services/Endpoints/azureAssistants', () => require(MOCKS).assistantEndpoint());
jest.mock('~/server/services/Endpoints/assistants', () => require(MOCKS).assistantEndpoint());
describe('POST /api/convos/duplicate - Rate Limiting', () => {
describe('POST /v1/chat/convos/duplicate - Rate Limiting', () => {
let app;
let duplicateConversation;
const savedEnv = {};
@@ -73,7 +73,7 @@ describe('POST /api/convos/duplicate - Rate Limiting', () => {
req.user = { id: 'rate-limit-test-user' };
next();
});
app.use('/api/convos', convosRouter);
app.use('/v1/chat/convos', convosRouter);
});
duplicateConversation.mockResolvedValue({
@@ -95,13 +95,13 @@ describe('POST /api/convos/duplicate - Rate Limiting', () => {
for (let i = 0; i < userMax; i++) {
const res = await request(app)
.post('/api/convos/duplicate')
.post('/v1/chat/convos/duplicate')
.send({ conversationId: 'conv-123' });
expect(res.status).toBe(201);
}
const res = await request(app)
.post('/api/convos/duplicate')
.post('/v1/chat/convos/duplicate')
.send({ conversationId: 'conv-123' });
expect(res.status).toBe(429);
expect(res.body.message).toMatch(/too many/i);
@@ -122,13 +122,13 @@ describe('POST /api/convos/duplicate - Rate Limiting', () => {
for (let i = 0; i < ipMax; i++) {
const res = await request(app)
.post('/api/convos/duplicate')
.post('/v1/chat/convos/duplicate')
.send({ conversationId: 'conv-123' });
expect(res.status).toBe(201);
}
const res = await request(app)
.post('/api/convos/duplicate')
.post('/v1/chat/convos/duplicate')
.send({ conversationId: 'conv-123' });
expect(res.status).toBe(429);
expect(res.body.message).toMatch(/too many/i);
+31 -31
View File
@@ -124,7 +124,7 @@ describe('Convos Routes', () => {
next();
});
app.use('/api/convos', convosRouter);
app.use('/v1/chat/convos', convosRouter);
});
beforeEach(() => {
@@ -145,7 +145,7 @@ describe('Convos Routes', () => {
deletedCount: 3,
});
const response = await request(app).delete('/api/convos/all');
const response = await request(app).delete('/v1/chat/convos/all');
expect(response.status).toBe(201);
expect(response.body).toEqual(mockDbResponse);
@@ -176,7 +176,7 @@ describe('Convos Routes', () => {
deletedCount: 0,
});
const response = await request(app).delete('/api/convos/all');
const response = await request(app).delete('/v1/chat/convos/all');
expect(response.status).toBe(201);
expect(deleteAllSharedLinks).toHaveBeenCalledWith('test-user-123');
@@ -186,7 +186,7 @@ describe('Convos Routes', () => {
const errorMessage = 'Database connection error';
deleteConvos.mockRejectedValue(new Error(errorMessage));
const response = await request(app).delete('/api/convos/all');
const response = await request(app).delete('/v1/chat/convos/all');
expect(response.status).toBe(500);
expect(response.text).toBe('Error clearing conversations');
@@ -200,7 +200,7 @@ describe('Convos Routes', () => {
deleteConvos.mockResolvedValue({ deletedCount: 5 });
deleteToolCalls.mockRejectedValue(new Error('Tool calls deletion failed'));
const response = await request(app).delete('/api/convos/all');
const response = await request(app).delete('/v1/chat/convos/all');
expect(response.status).toBe(500);
expect(response.text).toBe('Error clearing conversations');
@@ -211,7 +211,7 @@ describe('Convos Routes', () => {
deleteToolCalls.mockResolvedValue({ deletedCount: 10 });
deleteAllSharedLinks.mockRejectedValue(new Error('Shared links deletion failed'));
const response = await request(app).delete('/api/convos/all');
const response = await request(app).delete('/v1/chat/convos/all');
expect(response.status).toBe(500);
expect(response.text).toBe('Error clearing conversations');
@@ -223,7 +223,7 @@ describe('Convos Routes', () => {
deleteToolCalls.mockResolvedValue({ deletedCount: 5 });
deleteAllSharedLinks.mockResolvedValue({ deletedCount: 2 });
let response = await request(app).delete('/api/convos/all');
let response = await request(app).delete('/v1/chat/convos/all');
expect(response.status).toBe(201);
expect(deleteAllSharedLinks).toHaveBeenCalledWith('test-user-123');
@@ -237,13 +237,13 @@ describe('Convos Routes', () => {
req.user = { id: 'test-user-456' };
next();
});
app2.use('/api/convos', require('../convos'));
app2.use('/v1/chat/convos', require('../convos'));
deleteConvos.mockResolvedValue({ deletedCount: 7 });
deleteToolCalls.mockResolvedValue({ deletedCount: 12 });
deleteAllSharedLinks.mockResolvedValue({ deletedCount: 4 });
response = await request(app2).delete('/api/convos/all');
response = await request(app2).delete('/v1/chat/convos/all');
expect(response.status).toBe(201);
expect(deleteAllSharedLinks).toHaveBeenCalledWith('test-user-456');
@@ -267,7 +267,7 @@ describe('Convos Routes', () => {
return Promise.resolve({ deletedCount: 3 });
});
await request(app).delete('/api/convos/all');
await request(app).delete('/v1/chat/convos/all');
/** Verify all three functions were called */
expect(executionOrder).toEqual(['deleteConvos', 'deleteToolCalls', 'deleteAllSharedLinks']);
@@ -286,7 +286,7 @@ describe('Convos Routes', () => {
deleteToolCalls.mockResolvedValue(mockToolCallsDeleted);
deleteAllSharedLinks.mockResolvedValue(mockSharedLinksDeleted);
const response = await request(app).delete('/api/convos/all');
const response = await request(app).delete('/v1/chat/convos/all');
expect(response.status).toBe(201);
@@ -314,7 +314,7 @@ describe('Convos Routes', () => {
});
const response = await request(app)
.delete('/api/convos')
.delete('/v1/chat/convos')
.send({
arg: {
conversationId: mockConversationId,
@@ -341,7 +341,7 @@ describe('Convos Routes', () => {
deleteToolCalls.mockResolvedValue({ deletedCount: 0 });
const response = await request(app)
.delete('/api/convos')
.delete('/v1/chat/convos')
.send({
arg: {
source: 'button',
@@ -363,7 +363,7 @@ describe('Convos Routes', () => {
});
const response = await request(app)
.delete('/api/convos')
.delete('/v1/chat/convos')
.send({
arg: {
conversationId: mockConversationId,
@@ -375,7 +375,7 @@ describe('Convos Routes', () => {
});
it('should return 400 when no parameters provided', async () => {
const response = await request(app).delete('/api/convos').send({
const response = await request(app).delete('/v1/chat/convos').send({
arg: {},
});
@@ -386,7 +386,7 @@ describe('Convos Routes', () => {
});
it('should return 400 when request body is empty (DoS prevention)', async () => {
const response = await request(app).delete('/api/convos').send({});
const response = await request(app).delete('/v1/chat/convos').send({});
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'no parameters provided' });
@@ -394,7 +394,7 @@ describe('Convos Routes', () => {
});
it('should return 400 when arg is null (DoS prevention)', async () => {
const response = await request(app).delete('/api/convos').send({ arg: null });
const response = await request(app).delete('/v1/chat/convos').send({ arg: null });
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'no parameters provided' });
@@ -402,7 +402,7 @@ describe('Convos Routes', () => {
});
it('should return 400 when arg is undefined (DoS prevention)', async () => {
const response = await request(app).delete('/api/convos').send({ arg: undefined });
const response = await request(app).delete('/v1/chat/convos').send({ arg: undefined });
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'no parameters provided' });
@@ -411,7 +411,7 @@ describe('Convos Routes', () => {
it('should return 400 when request body is null (DoS prevention)', async () => {
const response = await request(app)
.delete('/api/convos')
.delete('/v1/chat/convos')
.set('Content-Type', 'application/json')
.send('null');
@@ -427,7 +427,7 @@ describe('Convos Routes', () => {
deleteConvoSharedLink.mockRejectedValue(new Error('Failed to delete shared links'));
const response = await request(app)
.delete('/api/convos')
.delete('/v1/chat/convos')
.send({
arg: {
conversationId: mockConversationId,
@@ -458,7 +458,7 @@ describe('Convos Routes', () => {
});
await request(app)
.delete('/api/convos')
.delete('/v1/chat/convos')
.send({
arg: {
conversationId: mockConversationId,
@@ -479,7 +479,7 @@ describe('Convos Routes', () => {
});
const response = await request(app)
.delete('/api/convos')
.delete('/v1/chat/convos')
.send({
arg: {
conversationId: mockConversationId,
@@ -509,7 +509,7 @@ describe('Convos Routes', () => {
saveConvo.mockResolvedValue(mockArchivedConvo);
const response = await request(app)
.post('/api/convos/archive')
.post('/v1/chat/convos/archive')
.send({
arg: {
conversationId: mockConversationId,
@@ -522,7 +522,7 @@ describe('Convos Routes', () => {
expect(saveConvo).toHaveBeenCalledWith(
expect.objectContaining({ user: { id: 'test-user-123' } }),
{ conversationId: mockConversationId, isArchived: true },
{ context: `POST /api/convos/archive ${mockConversationId}` },
{ context: `POST /v1/chat/convos/archive ${mockConversationId}` },
);
});
@@ -538,7 +538,7 @@ describe('Convos Routes', () => {
saveConvo.mockResolvedValue(mockUnarchivedConvo);
const response = await request(app)
.post('/api/convos/archive')
.post('/v1/chat/convos/archive')
.send({
arg: {
conversationId: mockConversationId,
@@ -551,13 +551,13 @@ describe('Convos Routes', () => {
expect(saveConvo).toHaveBeenCalledWith(
expect.objectContaining({ user: { id: 'test-user-123' } }),
{ conversationId: mockConversationId, isArchived: false },
{ context: `POST /api/convos/archive ${mockConversationId}` },
{ context: `POST /v1/chat/convos/archive ${mockConversationId}` },
);
});
it('should return 400 when conversationId is missing', async () => {
const response = await request(app)
.post('/api/convos/archive')
.post('/v1/chat/convos/archive')
.send({
arg: {
isArchived: true,
@@ -571,7 +571,7 @@ describe('Convos Routes', () => {
it('should return 400 when isArchived is not a boolean', async () => {
const response = await request(app)
.post('/api/convos/archive')
.post('/v1/chat/convos/archive')
.send({
arg: {
conversationId: 'conv-123',
@@ -586,7 +586,7 @@ describe('Convos Routes', () => {
it('should return 400 when isArchived is undefined', async () => {
const response = await request(app)
.post('/api/convos/archive')
.post('/v1/chat/convos/archive')
.send({
arg: {
conversationId: 'conv-123',
@@ -603,7 +603,7 @@ describe('Convos Routes', () => {
saveConvo.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.post('/api/convos/archive')
.post('/v1/chat/convos/archive')
.send({
arg: {
conversationId: mockConversationId,
@@ -619,7 +619,7 @@ describe('Convos Routes', () => {
});
it('should handle empty arg object', async () => {
const response = await request(app).post('/api/convos/archive').send({
const response = await request(app).post('/v1/chat/convos/archive').send({
arg: {},
});
+9 -9
View File
@@ -81,7 +81,7 @@ function createApp(user) {
router.get('/:principalType/:principalId', handlers.getPrincipalGrants);
router.post('/', handlers.assignGrant);
router.delete('/:principalType/:principalId/:capability', handlers.revokeGrant);
app.use('/api/admin/grants', router);
app.use('/v1/chat/admin/grants', router);
return app;
}
@@ -96,7 +96,7 @@ describe('Admin Grants Routes — Integration', () => {
it('GET / returns seeded admin grants', async () => {
const app = createApp(adminUser);
const res = await request(app).get('/api/admin/grants').expect(200);
const res = await request(app).get('/v1/chat/admin/grants').expect(200);
expect(res.body).toHaveProperty('grants');
expect(res.body).toHaveProperty('total');
@@ -107,7 +107,7 @@ describe('Admin Grants Routes — Integration', () => {
it('GET /effective returns capabilities for admin', async () => {
const app = createApp(adminUser);
const res = await request(app).get('/api/admin/grants/effective').expect(200);
const res = await request(app).get('/v1/chat/admin/grants/effective').expect(200);
expect(res.body).toHaveProperty('capabilities');
expect(res.body.capabilities).toContain('access:admin');
@@ -119,7 +119,7 @@ describe('Admin Grants Routes — Integration', () => {
// Assign
const assignRes = await request(app)
.post('/api/admin/grants')
.post('/v1/chat/admin/grants')
.send({
principalType: PrincipalType.ROLE,
principalId: SystemRoles.USER,
@@ -135,19 +135,19 @@ describe('Admin Grants Routes — Integration', () => {
// Verify via GET
const getRes = await request(app)
.get(`/api/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}`)
.get(`/v1/chat/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}`)
.expect(200);
expect(getRes.body.grants.some((g) => g.capability === 'read:users')).toBe(true);
// Revoke
await request(app)
.delete(`/api/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}/read:users`)
.delete(`/v1/chat/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}/read:users`)
.expect(200);
// Verify revoked
const afterRes = await request(app)
.get(`/api/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}`)
.get(`/v1/chat/admin/grants/${PrincipalType.ROLE}/${SystemRoles.USER}`)
.expect(200);
expect(afterRes.body.grants.some((g) => g.capability === 'read:users')).toBe(false);
@@ -157,7 +157,7 @@ describe('Admin Grants Routes — Integration', () => {
const app = createApp(adminUser);
const res = await request(app)
.post('/api/admin/grants')
.post('/v1/chat/admin/grants')
.send({
principalType: PrincipalType.ROLE,
principalId: 'nonexistent-role',
@@ -172,7 +172,7 @@ describe('Admin Grants Routes — Integration', () => {
const app = createApp(undefined);
const res = await request(app)
.post('/api/admin/grants')
.post('/v1/chat/admin/grants')
.send({
principalType: PrincipalType.ROLE,
principalId: SystemRoles.USER,
@@ -5,7 +5,7 @@
*
* Proves:
* - a guest token on a JWT-only route returns a clean 401 (NOT 500/CastError),
* - /api/endpoints, /api/user, /api/convos return guest-scoped data,
* - /v1/chat/endpoints, /v1/chat/user, /v1/chat/convos return guest-scoped data,
* - a non-bootstrap protected route still 401s for a guest.
*/
@@ -52,19 +52,19 @@ const buildApp = () => {
passport.use(jwtLogin());
// Bootstrap (guest-aware) routes.
app.get('/api/endpoints', requireGuestOrJwtAuth, (req, res) => {
app.get('/v1/chat/endpoints', requireGuestOrJwtAuth, (req, res) => {
if (req.user?.guest === true) {
return res.send(JSON.stringify(buildGuestEndpointsConfig()));
}
return res.send(JSON.stringify({ openAI: {}, Hanzo: {} }));
});
app.get('/api/user', requireGuestOrJwtAuth, (req, res) => {
app.get('/v1/chat/user', requireGuestOrJwtAuth, (req, res) => {
if (req.user?.guest === true) {
return res.status(200).send(buildGuestUser(req.user));
}
return res.status(200).send(req.user);
});
app.get('/api/convos', requireGuestOrJwtAuth, (req, res) => {
app.get('/v1/chat/convos', requireGuestOrJwtAuth, (req, res) => {
if (req.user?.guest === true) {
return res.status(200).json({ conversations: [], nextCursor: null });
}
@@ -72,7 +72,7 @@ const buildApp = () => {
});
// Non-bootstrap protected route (JWT-only): must reject guests.
app.get('/api/prompts', requireJwtAuth, (req, res) => res.status(200).json({ ok: true }));
app.get('/v1/chat/prompts', requireJwtAuth, (req, res) => res.status(200).json({ ok: true }));
return app;
};
@@ -91,9 +91,9 @@ describe('guest bootstrap auth chain (integration)', () => {
afterEach(() => jest.clearAllMocks());
it('GET /api/endpoints → 200 with ONLY the guest endpoint', async () => {
it('GET /v1/chat/endpoints → 200 with ONLY the guest endpoint', async () => {
const res = await request(app)
.get('/api/endpoints')
.get('/v1/chat/endpoints')
.set('Authorization', `Bearer ${guestToken}`);
expect(res.status).toBe(200);
const body = JSON.parse(res.text);
@@ -101,36 +101,36 @@ describe('guest bootstrap auth chain (integration)', () => {
expect(getUserById).not.toHaveBeenCalled();
});
it('GET /api/user → 200 ephemeral guest principal (no email, no DB lookup)', async () => {
const res = await request(app).get('/api/user').set('Authorization', `Bearer ${guestToken}`);
it('GET /v1/chat/user → 200 ephemeral guest principal (no email, no DB lookup)', async () => {
const res = await request(app).get('/v1/chat/user').set('Authorization', `Bearer ${guestToken}`);
expect(res.status).toBe(200);
expect(res.body).toMatchObject({ id: 'guest_abc-123', role: 'GUEST', guest: true });
expect(res.body).not.toHaveProperty('email');
expect(getUserById).not.toHaveBeenCalled();
});
it('GET /api/convos → 200 empty list for a guest', async () => {
const res = await request(app).get('/api/convos').set('Authorization', `Bearer ${guestToken}`);
it('GET /v1/chat/convos → 200 empty list for a guest', async () => {
const res = await request(app).get('/v1/chat/convos').set('Authorization', `Bearer ${guestToken}`);
expect(res.status).toBe(200);
expect(res.body).toEqual({ conversations: [], nextCursor: null });
});
it('GET /api/prompts (JWT-only) → clean 401 for a guest, NEVER 500/CastError', async () => {
const res = await request(app).get('/api/prompts').set('Authorization', `Bearer ${guestToken}`);
it('GET /v1/chat/prompts (JWT-only) → clean 401 for a guest, NEVER 500/CastError', async () => {
const res = await request(app).get('/v1/chat/prompts').set('Authorization', `Bearer ${guestToken}`);
expect(res.status).toBe(401);
// The guest id must never reach getUserById (that is what caused the CastError → 500).
expect(getUserById).not.toHaveBeenCalled();
});
it('GET /api/prompts → clean 401 for a real-but-missing user (no 500)', async () => {
it('GET /v1/chat/prompts → clean 401 for a real-but-missing user (no 500)', async () => {
const realToken = jwt.sign({ id: '64b2f0c0c0c0c0c0c0c0c0c0' }, JWT_SECRET);
const res = await request(app).get('/api/prompts').set('Authorization', `Bearer ${realToken}`);
const res = await request(app).get('/v1/chat/prompts').set('Authorization', `Bearer ${realToken}`);
expect(res.status).toBe(401);
expect(getUserById).toHaveBeenCalledTimes(1);
});
it('GET /api/prompts → 401 with no token (fail closed)', async () => {
const res = await request(app).get('/api/prompts');
it('GET /v1/chat/prompts → 401 with no token (fail closed)', async () => {
const res = await request(app).get('/v1/chat/prompts');
expect(res.status).toBe(401);
});
});
+10 -10
View File
@@ -28,7 +28,7 @@ describe('Keys Routes', () => {
next();
});
app.use('/api/keys', keysRouter);
app.use('/v1/chat/keys', keysRouter);
});
beforeEach(() => {
@@ -40,7 +40,7 @@ describe('Keys Routes', () => {
updateUserKey.mockResolvedValue({});
const response = await request(app)
.put('/api/keys')
.put('/v1/chat/keys')
.send({ name: 'openAI', value: 'sk-test-key-123', expiresAt: '2026-12-31' });
expect(response.status).toBe(201);
@@ -56,7 +56,7 @@ describe('Keys Routes', () => {
it('should not allow userId override via request body (IDOR prevention)', async () => {
updateUserKey.mockResolvedValue({});
const response = await request(app).put('/api/keys').send({
const response = await request(app).put('/v1/chat/keys').send({
userId: 'attacker-injected-id',
name: 'openAI',
value: 'sk-attacker-key',
@@ -74,7 +74,7 @@ describe('Keys Routes', () => {
it('should ignore extraneous fields from request body', async () => {
updateUserKey.mockResolvedValue({});
const response = await request(app).put('/api/keys').send({
const response = await request(app).put('/v1/chat/keys').send({
name: 'openAI',
value: 'sk-test-key',
expiresAt: '2026-12-31',
@@ -96,7 +96,7 @@ describe('Keys Routes', () => {
updateUserKey.mockResolvedValue({});
const response = await request(app)
.put('/api/keys')
.put('/v1/chat/keys')
.send({ name: 'anthropic', value: 'sk-ant-key' });
expect(response.status).toBe(201);
@@ -110,7 +110,7 @@ describe('Keys Routes', () => {
it('should return 400 when request body is null', async () => {
const response = await request(app)
.put('/api/keys')
.put('/v1/chat/keys')
.set('Content-Type', 'application/json')
.send('null');
@@ -123,7 +123,7 @@ describe('Keys Routes', () => {
it('should delete a user key by name', async () => {
deleteUserKey.mockResolvedValue({});
const response = await request(app).delete('/api/keys/openAI');
const response = await request(app).delete('/v1/chat/keys/openAI');
expect(response.status).toBe(204);
expect(deleteUserKey).toHaveBeenCalledWith({
@@ -138,7 +138,7 @@ describe('Keys Routes', () => {
it('should delete all keys when all=true', async () => {
deleteUserKey.mockResolvedValue({});
const response = await request(app).delete('/api/keys?all=true');
const response = await request(app).delete('/v1/chat/keys?all=true');
expect(response.status).toBe(204);
expect(deleteUserKey).toHaveBeenCalledWith({
@@ -148,7 +148,7 @@ describe('Keys Routes', () => {
});
it('should return 400 when all query param is not true', async () => {
const response = await request(app).delete('/api/keys');
const response = await request(app).delete('/v1/chat/keys');
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Specify either all=true to delete.' });
@@ -161,7 +161,7 @@ describe('Keys Routes', () => {
const mockExpiry = { expiresAt: '2026-12-31' };
getUserKeyExpiry.mockResolvedValue(mockExpiry);
const response = await request(app).get('/api/keys?name=openAI');
const response = await request(app).get('/v1/chat/keys?name=openAI');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockExpiry);
+4 -4
View File
@@ -12,7 +12,7 @@ jest.mock('@hanzochat/api', () => ({
const app = express();
// Mock the route handler
app.get('/api/config', (req, res) => {
app.get('/v1/chat/config', (req, res) => {
const ldapConfig = getLdapConfig();
res.json({ ldap: ldapConfig });
});
@@ -26,7 +26,7 @@ describe('LDAP Config Tests', () => {
getLdapConfig.mockReturnValue({ enabled: true, username: true });
isEnabled.mockReturnValue(true);
const response = await request(app).get('/api/config');
const response = await request(app).get('/v1/chat/config');
expect(response.statusCode).toBe(200);
expect(response.body.ldap).toEqual({
@@ -39,7 +39,7 @@ describe('LDAP Config Tests', () => {
getLdapConfig.mockReturnValue({ enabled: true });
isEnabled.mockReturnValue(false);
const response = await request(app).get('/api/config');
const response = await request(app).get('/v1/chat/config');
expect(response.statusCode).toBe(200);
expect(response.body.ldap).toEqual({
@@ -50,7 +50,7 @@ describe('LDAP Config Tests', () => {
it('should not return LDAP config when LDAP is not enabled', async () => {
getLdapConfig.mockReturnValue(undefined);
const response = await request(app).get('/api/config');
const response = await request(app).get('/v1/chat/config');
expect(response.statusCode).toBe(200);
expect(response.body.ldap).toBeUndefined();
+82 -82
View File
@@ -147,7 +147,7 @@ describe('MCP Routes', () => {
next();
});
app.use('/api/mcp', mcpRouter);
app.use('/v1/chat/mcp', mcpRouter);
});
afterAll(async () => {
@@ -182,7 +182,7 @@ describe('MCP Routes', () => {
flowId: 'test-user-id:test-server',
});
const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({
const response = await request(app).get('/v1/chat/mcp/test-server/oauth/initiate').query({
userId: 'test-user-id',
flowId: 'test-user-id:test-server',
});
@@ -199,7 +199,7 @@ describe('MCP Routes', () => {
});
it('should return 403 when userId does not match authenticated user', async () => {
const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({
const response = await request(app).get('/v1/chat/mcp/test-server/oauth/initiate').query({
userId: 'different-user-id',
flowId: 'test-user-id:test-server',
});
@@ -216,7 +216,7 @@ describe('MCP Routes', () => {
getLogStores.mockReturnValue({});
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({
const response = await request(app).get('/v1/chat/mcp/test-server/oauth/initiate').query({
userId: 'test-user-id',
flowId: 'non-existent-flow-id',
});
@@ -237,7 +237,7 @@ describe('MCP Routes', () => {
getLogStores.mockReturnValue({});
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({
const response = await request(app).get('/v1/chat/mcp/test-server/oauth/initiate').query({
userId: 'test-user-id',
flowId: 'test-user-id:test-server',
});
@@ -254,7 +254,7 @@ describe('MCP Routes', () => {
getLogStores.mockReturnValue({});
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({
const response = await request(app).get('/v1/chat/mcp/test-server/oauth/initiate').query({
userId: 'test-user-id',
flowId: 'test-user-id:test-server',
});
@@ -274,7 +274,7 @@ describe('MCP Routes', () => {
getLogStores.mockReturnValue({});
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
const response = await request(app).get('/api/mcp/test-server/oauth/initiate').query({
const response = await request(app).get('/v1/chat/mcp/test-server/oauth/initiate').query({
userId: 'test-user-id',
flowId: 'test-user-id:test-server',
});
@@ -289,7 +289,7 @@ describe('MCP Routes', () => {
const { getLogStores } = require('~/cache');
it('should redirect to error page when OAuth error is received', async () => {
const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({
const response = await request(app).get('/v1/chat/mcp/test-server/oauth/callback').query({
error: 'access_denied',
state: 'test-user-id:test-server',
});
@@ -300,7 +300,7 @@ describe('MCP Routes', () => {
});
it('should redirect to error page when code is missing', async () => {
const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({
const response = await request(app).get('/v1/chat/mcp/test-server/oauth/callback').query({
state: 'test-user-id:test-server',
});
const basePath = getBasePath();
@@ -310,7 +310,7 @@ describe('MCP Routes', () => {
});
it('should redirect to error page when state is missing', async () => {
const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({
const response = await request(app).get('/v1/chat/mcp/test-server/oauth/callback').query({
code: 'test-auth-code',
});
const basePath = getBasePath();
@@ -320,7 +320,7 @@ describe('MCP Routes', () => {
});
it('should redirect to error page when CSRF cookie is missing', async () => {
const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({
const response = await request(app).get('/v1/chat/mcp/test-server/oauth/callback').query({
code: 'test-auth-code',
state: 'test-user-id:test-server',
});
@@ -335,7 +335,7 @@ describe('MCP Routes', () => {
it('should redirect to error page when CSRF cookie does not match state', async () => {
const csrfToken = generateTestCsrfToken('different-flow-id');
const response = await request(app)
.get('/api/mcp/test-server/oauth/callback')
.get('/v1/chat/mcp/test-server/oauth/callback')
.set('Cookie', [`oauth_csrf=${csrfToken}`])
.query({
code: 'test-auth-code',
@@ -355,7 +355,7 @@ describe('MCP Routes', () => {
const csrfToken = generateTestCsrfToken(flowId);
const response = await request(app)
.get('/api/mcp/test-server/oauth/callback')
.get('/v1/chat/mcp/test-server/oauth/callback')
.set('Cookie', [`oauth_csrf=${csrfToken}`])
.query({
code: 'test-auth-code',
@@ -419,7 +419,7 @@ describe('MCP Routes', () => {
const csrfToken = generateTestCsrfToken(flowId);
const response = await request(app)
.get('/api/mcp/test-server/oauth/callback')
.get('/v1/chat/mcp/test-server/oauth/callback')
.set('Cookie', [`oauth_csrf=${csrfToken}`])
.query({
code: 'test-auth-code',
@@ -464,7 +464,7 @@ describe('MCP Routes', () => {
const csrfToken = generateTestCsrfToken(flowId);
const response = await request(app)
.get('/api/mcp/test-server/oauth/callback')
.get('/v1/chat/mcp/test-server/oauth/callback')
.set('Cookie', [`oauth_csrf=${csrfToken}`])
.query({
code: 'test-auth-code',
@@ -506,7 +506,7 @@ describe('MCP Routes', () => {
const csrfToken = generateTestCsrfToken(flowId);
const response = await request(app)
.get('/api/mcp/test-server/oauth/callback')
.get('/v1/chat/mcp/test-server/oauth/callback')
.set('Cookie', [`oauth_csrf=${csrfToken}`])
.query({
code: 'test-auth-code',
@@ -558,7 +558,7 @@ describe('MCP Routes', () => {
const csrfToken = generateTestCsrfToken(flowId);
const response = await request(app)
.get('/api/mcp/test-server/oauth/callback')
.get('/v1/chat/mcp/test-server/oauth/callback')
.set('Cookie', [`oauth_csrf=${csrfToken}`])
.query({
code: 'test-auth-code',
@@ -606,7 +606,7 @@ describe('MCP Routes', () => {
const csrfToken = generateTestCsrfToken(flowId);
const response = await request(app)
.get('/api/mcp/test-server/oauth/callback')
.get('/v1/chat/mcp/test-server/oauth/callback')
.set('Cookie', [`oauth_csrf=${csrfToken}`])
.query({
code: 'test-auth-code',
@@ -671,7 +671,7 @@ describe('MCP Routes', () => {
const csrfToken = generateTestCsrfToken(flowId);
const response = await request(app)
.get('/api/mcp/test-server/oauth/callback')
.get('/v1/chat/mcp/test-server/oauth/callback')
.set('Cookie', [`oauth_csrf=${csrfToken}`])
.query({
code: 'test-auth-code',
@@ -718,7 +718,7 @@ describe('MCP Routes', () => {
const csrfToken = generateTestCsrfToken(flowId);
const response = await request(app)
.get('/api/mcp/test-server/oauth/callback')
.get('/v1/chat/mcp/test-server/oauth/callback')
.set('Cookie', [`oauth_csrf=${csrfToken}`])
.query({
code: 'test-auth-code',
@@ -751,7 +751,7 @@ describe('MCP Routes', () => {
getLogStores.mockReturnValue({});
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
const response = await request(app).get('/api/mcp/oauth/tokens/test-user-id:flow-123');
const response = await request(app).get('/v1/chat/mcp/oauth/tokens/test-user-id:flow-123');
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -769,16 +769,16 @@ describe('MCP Routes', () => {
req.user = null;
next();
});
unauthApp.use('/api/mcp', mcpRouter);
unauthApp.use('/v1/chat/mcp', mcpRouter);
const response = await request(unauthApp).get('/api/mcp/oauth/tokens/test-flow-id');
const response = await request(unauthApp).get('/v1/chat/mcp/oauth/tokens/test-flow-id');
expect(response.status).toBe(401);
expect(response.body).toEqual({ error: 'User not authenticated' });
});
it('should return 403 when user tries to access flow they do not own', async () => {
const response = await request(app).get('/api/mcp/oauth/tokens/other-user-id:flow-123');
const response = await request(app).get('/v1/chat/mcp/oauth/tokens/other-user-id:flow-123');
expect(response.status).toBe(403);
expect(response.body).toEqual({ error: 'Access denied' });
@@ -793,7 +793,7 @@ describe('MCP Routes', () => {
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
const response = await request(app).get(
'/api/mcp/oauth/tokens/test-user-id:non-existent-flow',
'/v1/chat/mcp/oauth/tokens/test-user-id:non-existent-flow',
);
expect(response.status).toBe(404);
@@ -811,7 +811,7 @@ describe('MCP Routes', () => {
getLogStores.mockReturnValue({});
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
const response = await request(app).get('/api/mcp/oauth/tokens/test-user-id:pending-flow');
const response = await request(app).get('/v1/chat/mcp/oauth/tokens/test-user-id:pending-flow');
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Flow not completed' });
@@ -822,7 +822,7 @@ describe('MCP Routes', () => {
throw new Error('Database connection failed');
});
const response = await request(app).get('/api/mcp/oauth/tokens/test-user-id:error-flow');
const response = await request(app).get('/v1/chat/mcp/oauth/tokens/test-user-id:error-flow');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to get tokens' });
@@ -843,7 +843,7 @@ describe('MCP Routes', () => {
getLogStores.mockReturnValue({});
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
const response = await request(app).get('/api/mcp/oauth/status/test-user-id:test-server');
const response = await request(app).get('/v1/chat/mcp/oauth/status/test-user-id:test-server');
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -855,7 +855,7 @@ describe('MCP Routes', () => {
});
it('should return 403 when flowId does not match authenticated user', async () => {
const response = await request(app).get('/api/mcp/oauth/status/other-user-id:test-server');
const response = await request(app).get('/v1/chat/mcp/oauth/status/other-user-id:test-server');
expect(response.status).toBe(403);
expect(response.body).toEqual({ error: 'Access denied' });
@@ -869,7 +869,7 @@ describe('MCP Routes', () => {
getLogStores.mockReturnValue({});
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
const response = await request(app).get('/api/mcp/oauth/status/test-user-id:non-existent');
const response = await request(app).get('/v1/chat/mcp/oauth/status/test-user-id:non-existent');
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'Flow not found' });
@@ -883,7 +883,7 @@ describe('MCP Routes', () => {
getLogStores.mockReturnValue({});
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
const response = await request(app).get('/api/mcp/oauth/status/test-user-id:error-server');
const response = await request(app).get('/v1/chat/mcp/oauth/status/test-user-id:error-server');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to get flow status' });
@@ -906,7 +906,7 @@ describe('MCP Routes', () => {
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
MCPOAuthHandler.generateFlowId.mockReturnValue('test-user-id:test-server');
const response = await request(app).post('/api/mcp/oauth/cancel/test-server');
const response = await request(app).post('/v1/chat/mcp/oauth/cancel/test-server');
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -930,7 +930,7 @@ describe('MCP Routes', () => {
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
MCPOAuthHandler.generateFlowId.mockReturnValue('test-user-id:test-server');
const response = await request(app).post('/api/mcp/oauth/cancel/test-server');
const response = await request(app).post('/v1/chat/mcp/oauth/cancel/test-server');
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -949,7 +949,7 @@ describe('MCP Routes', () => {
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
MCPOAuthHandler.generateFlowId.mockReturnValue('test-user-id:test-server');
const response = await request(app).post('/api/mcp/oauth/cancel/test-server');
const response = await request(app).post('/v1/chat/mcp/oauth/cancel/test-server');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to cancel OAuth flow' });
@@ -962,9 +962,9 @@ describe('MCP Routes', () => {
req.user = null;
next();
});
unauthApp.use('/api/mcp', mcpRouter);
unauthApp.use('/v1/chat/mcp', mcpRouter);
const response = await request(unauthApp).post('/api/mcp/oauth/cancel/test-server');
const response = await request(unauthApp).post('/v1/chat/mcp/oauth/cancel/test-server');
expect(response.status).toBe(401);
expect(response.body).toEqual({ error: 'User not authenticated' });
@@ -984,7 +984,7 @@ describe('MCP Routes', () => {
require('~/config').getFlowStateManager.mockReturnValue({});
require('~/cache').getLogStores.mockReturnValue({});
const response = await request(app).post('/api/mcp/non-existent-server/reinitialize');
const response = await request(app).post('/v1/chat/mcp/non-existent-server/reinitialize');
expect(response.status).toBe(404);
expect(response.body).toEqual({
@@ -1018,7 +1018,7 @@ describe('MCP Routes', () => {
oauthUrl: 'https://oauth.example.com/auth',
});
const response = await request(app).post('/api/mcp/oauth-server/reinitialize');
const response = await request(app).post('/v1/chat/mcp/oauth-server/reinitialize');
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -1043,7 +1043,7 @@ describe('MCP Routes', () => {
require('~/cache').getLogStores.mockReturnValue({});
require('~/server/services/Tools/mcp').reinitMCPServer.mockResolvedValue(null);
const response = await request(app).post('/api/mcp/error-server/reinitialize');
const response = await request(app).post('/v1/chat/mcp/error-server/reinitialize');
expect(response.status).toBe(500);
expect(response.body).toEqual({
@@ -1061,7 +1061,7 @@ describe('MCP Routes', () => {
});
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
const response = await request(app).post('/api/mcp/test-server/reinitialize');
const response = await request(app).post('/v1/chat/mcp/test-server/reinitialize');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Internal server error' });
@@ -1074,9 +1074,9 @@ describe('MCP Routes', () => {
req.user = null;
next();
});
unauthApp.use('/api/mcp', mcpRouter);
unauthApp.use('/v1/chat/mcp', mcpRouter);
const response = await request(unauthApp).post('/api/mcp/test-server/reinitialize');
const response = await request(unauthApp).post('/v1/chat/mcp/test-server/reinitialize');
expect(response.status).toBe(401);
expect(response.body).toEqual({ error: 'User not authenticated' });
@@ -1116,7 +1116,7 @@ describe('MCP Routes', () => {
oauthUrl: null,
});
const response = await request(app).post('/api/mcp/test-server/reinitialize');
const response = await request(app).post('/v1/chat/mcp/test-server/reinitialize');
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -1174,7 +1174,7 @@ describe('MCP Routes', () => {
oauthUrl: null,
});
const response = await request(app).post('/api/mcp/test-server/reinitialize');
const response = await request(app).post('/v1/chat/mcp/test-server/reinitialize');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
@@ -1212,7 +1212,7 @@ describe('MCP Routes', () => {
requiresOAuth: true,
});
const response = await request(app).get('/api/mcp/connection/status');
const response = await request(app).get('/v1/chat/mcp/connection/status');
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -1236,7 +1236,7 @@ describe('MCP Routes', () => {
it('should return 404 when MCP config is not found', async () => {
getMCPSetupData.mockRejectedValue(new Error('MCP config not found'));
const response = await request(app).get('/api/mcp/connection/status');
const response = await request(app).get('/v1/chat/mcp/connection/status');
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'MCP config not found' });
@@ -1245,7 +1245,7 @@ describe('MCP Routes', () => {
it('should return 500 when connection status check fails', async () => {
getMCPSetupData.mockRejectedValue(new Error('Database error'));
const response = await request(app).get('/api/mcp/connection/status');
const response = await request(app).get('/v1/chat/mcp/connection/status');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to get connection status' });
@@ -1258,9 +1258,9 @@ describe('MCP Routes', () => {
req.user = null;
next();
});
unauthApp.use('/api/mcp', mcpRouter);
unauthApp.use('/v1/chat/mcp', mcpRouter);
const response = await request(unauthApp).get('/api/mcp/connection/status');
const response = await request(unauthApp).get('/v1/chat/mcp/connection/status');
expect(response.status).toBe(401);
expect(response.body).toEqual({ error: 'User not authenticated' });
@@ -1287,7 +1287,7 @@ describe('MCP Routes', () => {
requiresOAuth: true,
});
const response = await request(app).get('/api/mcp/connection/status/oauth-server');
const response = await request(app).get('/v1/chat/mcp/connection/status/oauth-server');
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -1308,7 +1308,7 @@ describe('MCP Routes', () => {
oauthServers: [],
});
const response = await request(app).get('/api/mcp/connection/status/non-existent-server');
const response = await request(app).get('/v1/chat/mcp/connection/status/non-existent-server');
expect(response.status).toBe(404);
expect(response.body).toEqual({
@@ -1319,7 +1319,7 @@ describe('MCP Routes', () => {
it('should return 404 when MCP config is not found', async () => {
getMCPSetupData.mockRejectedValue(new Error('MCP config not found'));
const response = await request(app).get('/api/mcp/connection/status/test-server');
const response = await request(app).get('/v1/chat/mcp/connection/status/test-server');
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'MCP config not found' });
@@ -1328,7 +1328,7 @@ describe('MCP Routes', () => {
it('should return 500 when connection status check fails', async () => {
getMCPSetupData.mockRejectedValue(new Error('Database connection failed'));
const response = await request(app).get('/api/mcp/connection/status/test-server');
const response = await request(app).get('/v1/chat/mcp/connection/status/test-server');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to get connection status' });
@@ -1341,9 +1341,9 @@ describe('MCP Routes', () => {
req.user = null;
next();
});
unauthApp.use('/api/mcp', mcpRouter);
unauthApp.use('/v1/chat/mcp', mcpRouter);
const response = await request(unauthApp).get('/api/mcp/connection/status/test-server');
const response = await request(unauthApp).get('/v1/chat/mcp/connection/status/test-server');
expect(response.status).toBe(401);
expect(response.body).toEqual({ error: 'User not authenticated' });
@@ -1366,7 +1366,7 @@ describe('MCP Routes', () => {
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
getUserPluginAuthValue.mockResolvedValueOnce('some-api-key-value').mockResolvedValueOnce('');
const response = await request(app).get('/api/mcp/test-server/auth-values');
const response = await request(app).get('/v1/chat/mcp/test-server/auth-values');
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -1387,7 +1387,7 @@ describe('MCP Routes', () => {
mockRegistryInstance.getServerConfig.mockResolvedValue(null);
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
const response = await request(app).get('/api/mcp/non-existent-server/auth-values');
const response = await request(app).get('/v1/chat/mcp/non-existent-server/auth-values');
expect(response.status).toBe(404);
expect(response.body).toEqual({
@@ -1406,7 +1406,7 @@ describe('MCP Routes', () => {
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
getUserPluginAuthValue.mockRejectedValue(new Error('Database error'));
const response = await request(app).get('/api/mcp/test-server/auth-values');
const response = await request(app).get('/v1/chat/mcp/test-server/auth-values');
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -1426,7 +1426,7 @@ describe('MCP Routes', () => {
});
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
const response = await request(app).get('/api/mcp/test-server/auth-values');
const response = await request(app).get('/v1/chat/mcp/test-server/auth-values');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to check auth value flags' });
@@ -1440,7 +1440,7 @@ describe('MCP Routes', () => {
});
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
const response = await request(app).get('/api/mcp/test-server/auth-values');
const response = await request(app).get('/v1/chat/mcp/test-server/auth-values');
expect(response.status).toBe(200);
expect(response.body).toEqual({
@@ -1453,9 +1453,9 @@ describe('MCP Routes', () => {
it('should return 401 when user is not authenticated in auth-values endpoint', async () => {
const appWithoutAuth = express();
appWithoutAuth.use(express.json());
appWithoutAuth.use('/api/mcp', mcpRouter);
appWithoutAuth.use('/v1/chat/mcp', mcpRouter);
const response = await request(appWithoutAuth).get('/api/mcp/test-server/auth-values');
const response = await request(appWithoutAuth).get('/v1/chat/mcp/test-server/auth-values');
expect(response.status).toBe(401);
expect(response.body).toEqual({ error: 'User not authenticated' });
@@ -1502,7 +1502,7 @@ describe('MCP Routes', () => {
const csrfToken = generateTestCsrfToken(flowId);
const response = await request(app)
.get(`/api/mcp/test-server/oauth/callback?code=test-code&state=${flowId}`)
.get(`/v1/chat/mcp/test-server/oauth/callback?code=test-code&state=${flowId}`)
.set('Cookie', [`oauth_csrf=${csrfToken}`])
.expect(302);
@@ -1556,7 +1556,7 @@ describe('MCP Routes', () => {
const csrfToken = generateTestCsrfToken(flowId);
const response = await request(app)
.get(`/api/mcp/test-server/oauth/callback?code=test-code&state=${flowId}`)
.get(`/v1/chat/mcp/test-server/oauth/callback?code=test-code&state=${flowId}`)
.set('Cookie', [`oauth_csrf=${csrfToken}`])
.expect(302);
@@ -1583,7 +1583,7 @@ describe('MCP Routes', () => {
mockRegistryInstance.getAllServerConfigs.mockResolvedValue(mockServerConfigs);
const response = await request(app).get('/api/mcp/servers');
const response = await request(app).get('/v1/chat/mcp/servers');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockServerConfigs);
@@ -1593,7 +1593,7 @@ describe('MCP Routes', () => {
it('should return empty object when no servers are configured', async () => {
mockRegistryInstance.getAllServerConfigs.mockResolvedValue({});
const response = await request(app).get('/api/mcp/servers');
const response = await request(app).get('/v1/chat/mcp/servers');
expect(response.status).toBe(200);
expect(response.body).toEqual({});
@@ -1606,9 +1606,9 @@ describe('MCP Routes', () => {
req.user = null;
next();
});
unauthApp.use('/api/mcp', mcpRouter);
unauthApp.use('/v1/chat/mcp', mcpRouter);
const response = await request(unauthApp).get('/api/mcp/servers');
const response = await request(unauthApp).get('/v1/chat/mcp/servers');
expect(response.status).toBe(401);
expect(response.body).toEqual({ message: 'Unauthorized' });
@@ -1617,7 +1617,7 @@ describe('MCP Routes', () => {
it('should return 500 when server config retrieval fails', async () => {
mockRegistryInstance.getAllServerConfigs.mockRejectedValue(new Error('Database error'));
const response = await request(app).get('/api/mcp/servers');
const response = await request(app).get('/v1/chat/mcp/servers');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Database error' });
@@ -1638,7 +1638,7 @@ describe('MCP Routes', () => {
config: validConfig,
});
const response = await request(app).post('/api/mcp/servers').send({ config: validConfig });
const response = await request(app).post('/v1/chat/mcp/servers').send({ config: validConfig });
expect(response.status).toBe(201);
expect(response.body).toEqual({
@@ -1664,7 +1664,7 @@ describe('MCP Routes', () => {
title: 'Test Stdio Server',
};
const response = await request(app).post('/api/mcp/servers').send({ config: stdioConfig });
const response = await request(app).post('/v1/chat/mcp/servers').send({ config: stdioConfig });
// Stdio transport is not allowed via API - only admins can configure it via YAML
expect(response.status).toBe(400);
@@ -1678,7 +1678,7 @@ describe('MCP Routes', () => {
title: 'Invalid Server',
};
const response = await request(app).post('/api/mcp/servers').send({ config: invalidConfig });
const response = await request(app).post('/v1/chat/mcp/servers').send({ config: invalidConfig });
expect(response.status).toBe(400);
expect(response.body.message).toBe('Invalid configuration');
@@ -1692,7 +1692,7 @@ describe('MCP Routes', () => {
title: 'Invalid Protocol Server',
};
const response = await request(app).post('/api/mcp/servers').send({ config: invalidConfig });
const response = await request(app).post('/v1/chat/mcp/servers').send({ config: invalidConfig });
expect(response.status).toBe(400);
expect(response.body.message).toBe('Invalid configuration');
@@ -1707,7 +1707,7 @@ describe('MCP Routes', () => {
mockRegistryInstance.addServer.mockRejectedValue(new Error('Database connection failed'));
const response = await request(app).post('/api/mcp/servers').send({ config: validConfig });
const response = await request(app).post('/v1/chat/mcp/servers').send({ config: validConfig });
expect(response.status).toBe(500);
expect(response.body).toEqual({ message: 'Database connection failed' });
@@ -1724,7 +1724,7 @@ describe('MCP Routes', () => {
mockRegistryInstance.getServerConfig.mockResolvedValue(mockConfig);
const response = await request(app).get('/api/mcp/servers/test-server');
const response = await request(app).get('/v1/chat/mcp/servers/test-server');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockConfig);
@@ -1737,7 +1737,7 @@ describe('MCP Routes', () => {
it('should return 404 when server not found', async () => {
mockRegistryInstance.getServerConfig.mockResolvedValue(null);
const response = await request(app).get('/api/mcp/servers/non-existent-server');
const response = await request(app).get('/v1/chat/mcp/servers/non-existent-server');
expect(response.status).toBe(404);
expect(response.body).toEqual({ message: 'MCP server not found' });
@@ -1746,7 +1746,7 @@ describe('MCP Routes', () => {
it('should return 500 when registry throws error', async () => {
mockRegistryInstance.getServerConfig.mockRejectedValue(new Error('Database error'));
const response = await request(app).get('/api/mcp/servers/error-server');
const response = await request(app).get('/v1/chat/mcp/servers/error-server');
expect(response.status).toBe(500);
expect(response.body).toEqual({ message: 'Database error' });
@@ -1765,7 +1765,7 @@ describe('MCP Routes', () => {
mockRegistryInstance.updateServer.mockResolvedValue(updatedConfig);
const response = await request(app)
.patch('/api/mcp/servers/test-server')
.patch('/v1/chat/mcp/servers/test-server')
.send({ config: updatedConfig });
expect(response.status).toBe(200);
@@ -1789,7 +1789,7 @@ describe('MCP Routes', () => {
};
const response = await request(app)
.patch('/api/mcp/servers/test-server')
.patch('/v1/chat/mcp/servers/test-server')
.send({ config: invalidConfig });
expect(response.status).toBe(400);
@@ -1807,7 +1807,7 @@ describe('MCP Routes', () => {
mockRegistryInstance.updateServer.mockRejectedValue(new Error('Update failed'));
const response = await request(app)
.patch('/api/mcp/servers/test-server')
.patch('/v1/chat/mcp/servers/test-server')
.send({ config: validConfig });
expect(response.status).toBe(500);
@@ -1819,7 +1819,7 @@ describe('MCP Routes', () => {
it('should delete server successfully', async () => {
mockRegistryInstance.removeServer.mockResolvedValue(undefined);
const response = await request(app).delete('/api/mcp/servers/test-server');
const response = await request(app).delete('/v1/chat/mcp/servers/test-server');
expect(response.status).toBe(200);
expect(response.body).toEqual({ message: 'MCP server deleted successfully' });
@@ -1833,7 +1833,7 @@ describe('MCP Routes', () => {
it('should return 500 when registry throws error', async () => {
mockRegistryInstance.removeServer.mockRejectedValue(new Error('Deletion failed'));
const response = await request(app).delete('/api/mcp/servers/error-server');
const response = await request(app).delete('/v1/chat/mcp/servers/error-server');
expect(response.status).toBe(500);
expect(response.body).toEqual({ message: 'Deletion failed' });
@@ -155,7 +155,7 @@ describe('DELETE /:conversationId/:messageId route handler', () => {
req.user = { id: authenticatedUserId };
next();
});
app.use('/api/messages', messagesRouter);
app.use('/v1/chat/messages', messagesRouter);
});
beforeEach(() => {
@@ -165,7 +165,7 @@ describe('DELETE /:conversationId/:messageId route handler', () => {
it('should pass user and conversationId in the deleteMessages filter', async () => {
deleteMessages.mockResolvedValue({ deletedCount: 1 });
await request(app).delete('/api/messages/convo-1/msg-1');
await request(app).delete('/v1/chat/messages/convo-1/msg-1');
expect(deleteMessages).toHaveBeenCalledTimes(1);
expect(deleteMessages).toHaveBeenCalledWith({
@@ -178,7 +178,7 @@ describe('DELETE /:conversationId/:messageId route handler', () => {
it('should return 204 on successful deletion', async () => {
deleteMessages.mockResolvedValue({ deletedCount: 1 });
const response = await request(app).delete('/api/messages/convo-1/msg-owned');
const response = await request(app).delete('/v1/chat/messages/convo-1/msg-owned');
expect(response.status).toBe(204);
expect(deleteMessages).toHaveBeenCalledWith({
@@ -191,7 +191,7 @@ describe('DELETE /:conversationId/:messageId route handler', () => {
it('should return 500 when deleteMessages throws', async () => {
deleteMessages.mockRejectedValue(new Error('DB failure'));
const response = await request(app).delete('/api/messages/convo-1/msg-1');
const response = await request(app).delete('/v1/chat/messages/convo-1/msg-1');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Internal server error' });
@@ -213,7 +213,7 @@ describe('message route conversation ownership filters', () => {
req.user = { id: authenticatedUserId };
next();
});
app.use('/api/messages', messagesRouter);
app.use('/v1/chat/messages', messagesRouter);
});
beforeEach(() => {
@@ -233,7 +233,7 @@ describe('message route conversation ownership filters', () => {
saveMessage.mockResolvedValue(savedMessage);
saveConvo.mockResolvedValue({ conversationId: urlConversationId });
const response = await request(app).post(`/api/messages/${urlConversationId}`).send({
const response = await request(app).post(`/v1/chat/messages/${urlConversationId}`).send({
messageId: savedMessage.messageId,
conversationId: bodyConversationId,
text: savedMessage.text,
@@ -248,20 +248,20 @@ describe('message route conversation ownership filters', () => {
text: savedMessage.text,
user: authenticatedUserId,
}),
{ context: 'POST /api/messages/:conversationId' },
{ context: 'POST /v1/chat/messages/:conversationId' },
);
expect(saveMessage.mock.calls[0][1].conversationId).not.toBe(bodyConversationId);
expect(saveConvo).toHaveBeenCalledWith(
expect.objectContaining({ userId: authenticatedUserId }),
savedMessage,
{ context: 'POST /api/messages/:conversationId' },
{ context: 'POST /v1/chat/messages/:conversationId' },
);
});
it('should filter conversation message reads by authenticated user', async () => {
getMessages.mockResolvedValue([{ messageId: 'message-1', conversationId: 'convo-1' }]);
const response = await request(app).get('/api/messages/convo-1');
const response = await request(app).get('/v1/chat/messages/convo-1');
expect(response.status).toBe(200);
expect(getMessages).toHaveBeenCalledWith(
@@ -273,7 +273,7 @@ describe('message route conversation ownership filters', () => {
it('should filter single message reads by authenticated user', async () => {
getMessages.mockResolvedValue([{ messageId: 'message-1', conversationId: 'convo-1' }]);
const response = await request(app).get('/api/messages/convo-1/message-1');
const response = await request(app).get('/v1/chat/messages/convo-1/message-1');
expect(response.status).toBe(200);
expect(getMessages).toHaveBeenCalledWith(
+13 -13
View File
@@ -28,7 +28,7 @@ function createApp(user) {
req.user = user;
next();
});
app.use('/api/roles', rolesRouter);
app.use('/v1/chat/roles', rolesRouter);
return app;
}
@@ -48,12 +48,12 @@ beforeEach(() => {
mockGetRoleByName.mockResolvedValue(null);
});
describe('GET /api/roles/:roleName — isOwnRole authorization', () => {
describe('GET /v1/chat/roles/:roleName — isOwnRole authorization', () => {
it('allows a custom role user to fetch their own role', async () => {
mockGetRoleByName.mockResolvedValue(staffRole);
const app = createApp({ id: 'u1', role: 'STAFF' });
const res = await request(app).get('/api/roles/STAFF');
const res = await request(app).get('/v1/chat/roles/STAFF');
expect(res.status).toBe(200);
expect(res.body.name).toBe('STAFF');
@@ -63,7 +63,7 @@ describe('GET /api/roles/:roleName — isOwnRole authorization', () => {
it('returns 403 when a custom role user requests a different custom role', async () => {
const app = createApp({ id: 'u1', role: 'STAFF' });
const res = await request(app).get('/api/roles/MANAGER');
const res = await request(app).get('/v1/chat/roles/MANAGER');
expect(res.status).toBe(403);
expect(mockGetRoleByName).not.toHaveBeenCalled();
@@ -72,7 +72,7 @@ describe('GET /api/roles/:roleName — isOwnRole authorization', () => {
it('returns 403 when a custom role user requests ADMIN', async () => {
const app = createApp({ id: 'u1', role: 'STAFF' });
const res = await request(app).get('/api/roles/ADMIN');
const res = await request(app).get('/v1/chat/roles/ADMIN');
expect(res.status).toBe(403);
expect(mockGetRoleByName).not.toHaveBeenCalled();
@@ -82,7 +82,7 @@ describe('GET /api/roles/:roleName — isOwnRole authorization', () => {
mockGetRoleByName.mockResolvedValue(userRole);
const app = createApp({ id: 'u1', role: SystemRoles.USER });
const res = await request(app).get(`/api/roles/${SystemRoles.USER}`);
const res = await request(app).get(`/v1/chat/roles/${SystemRoles.USER}`);
expect(res.status).toBe(200);
});
@@ -90,7 +90,7 @@ describe('GET /api/roles/:roleName — isOwnRole authorization', () => {
it('returns 403 when USER requests the ADMIN role', async () => {
const app = createApp({ id: 'u1', role: SystemRoles.USER });
const res = await request(app).get(`/api/roles/${SystemRoles.ADMIN}`);
const res = await request(app).get(`/v1/chat/roles/${SystemRoles.ADMIN}`);
expect(res.status).toBe(403);
});
@@ -100,7 +100,7 @@ describe('GET /api/roles/:roleName — isOwnRole authorization', () => {
mockGetRoleByName.mockResolvedValue(adminRole);
const app = createApp({ id: 'u1', role: SystemRoles.ADMIN });
const res = await request(app).get(`/api/roles/${SystemRoles.ADMIN}`);
const res = await request(app).get(`/v1/chat/roles/${SystemRoles.ADMIN}`);
expect(res.status).toBe(200);
});
@@ -110,7 +110,7 @@ describe('GET /api/roles/:roleName — isOwnRole authorization', () => {
mockGetRoleByName.mockResolvedValue(staffRole);
const app = createApp({ id: 'u1', role: SystemRoles.USER });
const res = await request(app).get('/api/roles/STAFF');
const res = await request(app).get('/v1/chat/roles/STAFF');
expect(res.status).toBe(200);
expect(res.body.name).toBe('STAFF');
@@ -120,7 +120,7 @@ describe('GET /api/roles/:roleName — isOwnRole authorization', () => {
mockGetRoleByName.mockResolvedValue(null);
const app = createApp({ id: 'u1', role: 'GHOST' });
const res = await request(app).get('/api/roles/GHOST');
const res = await request(app).get('/v1/chat/roles/GHOST');
expect(res.status).toBe(404);
});
@@ -129,7 +129,7 @@ describe('GET /api/roles/:roleName — isOwnRole authorization', () => {
mockGetRoleByName.mockRejectedValue(new Error('db error'));
const app = createApp({ id: 'u1', role: SystemRoles.USER });
const res = await request(app).get(`/api/roles/${SystemRoles.USER}`);
const res = await request(app).get(`/v1/chat/roles/${SystemRoles.USER}`);
expect(res.status).toBe(500);
});
@@ -137,7 +137,7 @@ describe('GET /api/roles/:roleName — isOwnRole authorization', () => {
it('returns 403 for prototype property names like constructor (no prototype pollution)', async () => {
const app = createApp({ id: 'u1', role: 'STAFF' });
const res = await request(app).get('/api/roles/constructor');
const res = await request(app).get('/v1/chat/roles/constructor');
expect(res.status).toBe(403);
expect(mockGetRoleByName).not.toHaveBeenCalled();
@@ -148,7 +148,7 @@ describe('GET /api/roles/:roleName — isOwnRole authorization', () => {
const app = createApp({ id: 'u1', role: 'STAFF' });
mockGetRoleByName.mockResolvedValue(staffRole);
const res = await request(app).get('/api/roles/STAFF');
const res = await request(app).get('/v1/chat/roles/STAFF');
expect(res.status).toBe(200);
});
+12 -12
View File
@@ -64,7 +64,7 @@ const buildApp = ({ retentionMode = RetentionMode.TEMPORARY } = {}) => {
req.config = { interfaceConfig: { retentionMode } };
next();
});
app.use('/api/share', shareRouter);
app.use('/v1/chat/share', shareRouter);
return app;
};
@@ -78,7 +78,7 @@ describe('share routes retention', () => {
createSharedLink.mockResolvedValue({ shareId: 'share-123' });
const response = await request(buildApp())
.post('/api/share/convo-123')
.post('/v1/chat/share/convo-123')
.send({ targetMessageId: 'msg-123' });
expect(response.status).toBe(200);
@@ -113,7 +113,7 @@ describe('share routes retention', () => {
createSharedLink.mockResolvedValue({ shareId: 'share-123' });
const response = await request(buildApp())
.post('/api/share/convo-123')
.post('/v1/chat/share/convo-123')
.send({ targetMessageId: 'msg-123' });
expect(response.status).toBe(404);
@@ -125,7 +125,7 @@ describe('share routes retention', () => {
createSharedLink.mockResolvedValue({ shareId: 'share-123' });
const response = await request(buildApp({ retentionMode: RetentionMode.ALL }))
.post('/api/share/convo-123')
.post('/v1/chat/share/convo-123')
.send({ targetMessageId: 'msg-123' });
expect(response.status).toBe(404);
@@ -137,7 +137,7 @@ describe('share routes retention', () => {
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
updateSharedLink.mockResolvedValue({ shareId: 'share-456' });
const response = await request(buildApp()).patch('/api/share/share-123');
const response = await request(buildApp()).patch('/v1/chat/share/share-123');
expect(response.status).toBe(200);
expect(mongoose.models.SharedLink.findOne).toHaveBeenCalledWith(
@@ -169,7 +169,7 @@ describe('share routes retention', () => {
mockGetSharedLinkExpiration.mockResolvedValue(expiredExpiration);
updateSharedLink.mockResolvedValue({ shareId: 'share-456' });
const response = await request(buildApp()).patch('/api/share/share-123');
const response = await request(buildApp()).patch('/v1/chat/share/share-123');
expect(response.status).toBe(404);
expect(updateSharedLink).not.toHaveBeenCalled();
@@ -181,7 +181,7 @@ describe('share routes retention', () => {
updateSharedLink.mockResolvedValue({ shareId: 'share-456' });
const response = await request(buildApp({ retentionMode: RetentionMode.ALL })).patch(
'/api/share/share-123',
'/v1/chat/share/share-123',
);
expect(response.status).toBe(404);
@@ -197,7 +197,7 @@ describe('share routes retention', () => {
mockGetSharedLinkExpiration.mockResolvedValue(null);
updateSharedLink.mockResolvedValue({ shareId: 'share-456' });
const response = await request(buildApp()).patch('/api/share/share-123');
const response = await request(buildApp()).patch('/v1/chat/share/share-123');
expect(response.status).toBe(200);
expect(updateSharedLink).toHaveBeenCalledWith('user-123', 'share-123', undefined, null);
@@ -208,7 +208,7 @@ describe('share routes retention', () => {
mockGetSharedLinkExpiration.mockResolvedValue(undefined);
updateSharedLink.mockResolvedValue({ shareId: 'share-456' });
const response = await request(buildApp()).patch('/api/share/share-123');
const response = await request(buildApp()).patch('/v1/chat/share/share-123');
expect(response.status).toBe(200);
expect(updateSharedLink).toHaveBeenCalledWith('user-123', 'share-123', undefined, undefined);
@@ -223,7 +223,7 @@ describe('share routes retention', () => {
});
updateSharedLink.mockResolvedValue({ shareId: 'share-456' });
const response = await request(buildApp()).patch('/api/share/share-123');
const response = await request(buildApp()).patch('/v1/chat/share/share-123');
expect(response.status).toBe(200);
expect(logger.error).toHaveBeenCalledWith(
@@ -239,7 +239,7 @@ describe('share routes retention', () => {
updateSharedLink.mockResolvedValue({ shareId: 'share-456', targetMessageId: 'msg-456' });
const response = await request(buildApp())
.patch('/api/share/share-123')
.patch('/v1/chat/share/share-123')
.send({ targetMessageId: 'msg-456' });
expect(response.status).toBe(200);
@@ -253,7 +253,7 @@ describe('share routes retention', () => {
it('rejects non-string target message updates', async () => {
const response = await request(buildApp())
.patch('/api/share/share-123')
.patch('/v1/chat/share/share-123')
.send({ targetMessageId: 123 });
expect(response.status).toBe(400);
+7 -7
View File
@@ -22,17 +22,17 @@ router.use(uaParser);
/**
* Generic routes for resource permissions
* Pattern: /api/permissions/{resourceType}/{resourceId}
* Pattern: /v1/chat/permissions/{resourceType}/{resourceId}
*/
/**
* GET /api/permissions/search-principals
* GET /v1/chat/permissions/search-principals
* Search for users and groups to grant permissions
*/
router.get('/search-principals', checkPeoplePickerAccess, searchPrincipals);
/**
* GET /api/permissions/{resourceType}/roles
* GET /v1/chat/permissions/{resourceType}/roles
* Get available roles for a resource type
*/
router.get('/:resourceType/roles', getResourceRoles);
@@ -84,7 +84,7 @@ const checkResourcePermissionAccess = (requiredPermission) => (req, res, next) =
};
/**
* GET /api/permissions/{resourceType}/{resourceId}
* GET /v1/chat/permissions/{resourceType}/{resourceId}
* Get all permissions for a specific resource
* SECURITY: Requires SHARE permission to view resource permissions
*/
@@ -95,7 +95,7 @@ router.get(
);
/**
* PUT /api/permissions/{resourceType}/{resourceId}
* PUT /v1/chat/permissions/{resourceType}/{resourceId}
* Bulk update permissions for a specific resource
* SECURITY: Requires SHARE permission to modify resource permissions
* SECURITY: Requires SHARE_PUBLIC permission to enable public sharing
@@ -108,13 +108,13 @@ router.put(
);
/**
* GET /api/permissions/{resourceType}/effective/all
* GET /v1/chat/permissions/{resourceType}/effective/all
* Get user's effective permissions for all accessible resources of a type
*/
router.get('/:resourceType/effective/all', getAllEffectivePermissions);
/**
* GET /api/permissions/{resourceType}/{resourceId}/effective
* GET /v1/chat/permissions/{resourceType}/{resourceId}/effective
* Get user's effective permissions for a specific resource
*/
router.get('/:resourceType/:resourceId/effective', getUserEffectivePermissions);
@@ -126,7 +126,7 @@ describe('Access permissions share policy', () => {
req.user = { id: 'skill-owner', role: SystemRoles.USER };
next();
});
app.use('/api/permissions', accessPermissionsRouter);
app.use('/v1/chat/permissions', accessPermissionsRouter);
});
it.each(sharePolicyCases)(
@@ -142,7 +142,7 @@ describe('Access permissions share policy', () => {
});
const response = await request(app)
.put(`/api/permissions/${resourceType}/${resourceId}`)
.put(`/v1/chat/permissions/${resourceType}/${resourceId}`)
.send({ updated: [createUpdatedPrincipal(accessRoleId)], public: false });
expect(response.status).toBe(403);
@@ -166,7 +166,7 @@ describe('Access permissions share policy', () => {
});
const response = await request(app)
.put(`/api/permissions/${ResourceType.SKILL}/${resourceId}`)
.put(`/v1/chat/permissions/${ResourceType.SKILL}/${resourceId}`)
.send({ updated: [createUpdatedPrincipal(AccessRoleIds.SKILL_VIEWER)], public: false });
expect(response.status).toBe(200);
@@ -178,7 +178,7 @@ describe('Access permissions share policy', () => {
hasCapability.mockResolvedValue(true);
const response = await request(app)
.put(`/api/permissions/${ResourceType.SKILL}/${resourceId}`)
.put(`/v1/chat/permissions/${ResourceType.SKILL}/${resourceId}`)
.send({ updated: [createUpdatedPrincipal(AccessRoleIds.SKILL_VIEWER)], public: false });
expect(response.status).toBe(200);
@@ -198,7 +198,7 @@ describe('Access permissions share policy', () => {
});
const response = await request(app)
.put(`/api/permissions/${ResourceType.SKILL}/${resourceId}`)
.put(`/v1/chat/permissions/${ResourceType.SKILL}/${resourceId}`)
.send({ public: true, publicAccessRoleId: AccessRoleIds.SKILL_VIEWER });
expect(response.status).toBe(403);
+1 -1
View File
@@ -19,7 +19,7 @@ const { getLogStores } = require('~/cache');
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET;
const OAUTH_CSRF_COOKIE_PATH = '/api/actions';
const OAUTH_CSRF_COOKIE_PATH = '/v1/chat/actions';
/**
* Sets a CSRF cookie binding the action OAuth flow to the current browser session.
+1 -1
View File
@@ -80,7 +80,7 @@ const EXCHANGE_CODE_PATTERN = /^[a-f0-9]{64}$/i;
* This endpoint is called server-to-server by the admin panel.
* The code is one-time-use and expires in 30 seconds.
*
* POST /api/admin/oauth/exchange
* POST /v1/chat/admin/oauth/exchange
* Body: { code: string }
* Response: { token: string, refreshToken: string, user: object }
*/
+4 -4
View File
@@ -134,7 +134,7 @@ describe('admin auth OpenID refresh route', () => {
app = express();
app.use(express.json());
app.use('/api/admin', adminAuthRouter);
app.use('/v1/chat/admin', adminAuthRouter);
isEnabled.mockReturnValue(true);
getOpenIdConfig.mockReturnValue(openIdConfig);
@@ -187,7 +187,7 @@ describe('admin auth OpenID refresh route', () => {
Object.assign(process.env, env);
const response = await request(app)
.post('/api/admin/oauth/refresh')
.post('/v1/chat/admin/oauth/refresh')
.send({ refresh_token: 'incoming-refresh-token' });
expect(response.status).toBe(200);
@@ -211,7 +211,7 @@ describe('admin auth OpenID refresh route', () => {
});
const response = await request(app)
.post('/api/admin/oauth/refresh')
.post('/v1/chat/admin/oauth/refresh')
.send({ refresh_token: 'incoming-refresh-token' });
expect(response.status).toBe(401);
@@ -227,7 +227,7 @@ describe('admin auth OpenID refresh route', () => {
process.env.OPENID_REFRESH_AUDIENCE = 'https://api.example.com';
await request(app)
.post('/api/admin/oauth/refresh')
.post('/v1/chat/admin/oauth/refresh')
.send({ refresh_token: 'incoming-refresh-token' });
expect(logger.debug).toHaveBeenCalledWith('[admin/oauth/refresh] OpenID refresh params', {
@@ -69,7 +69,7 @@ describe('Agent Abort Endpoint', () => {
beforeAll(() => {
app = express();
app.use(express.json());
app.use('/api/agents', agentRoutes);
app.use('/v1/chat/agents', agentRoutes);
});
beforeEach(() => {
@@ -86,7 +86,7 @@ describe('Agent Abort Endpoint', () => {
});
const response = await request(app)
.post('/api/agents/chat/abort')
.post('/v1/chat/agents/chat/abort')
.send({ conversationId: jobStreamId });
expect(response.status).toBe(403);
@@ -112,7 +112,7 @@ describe('Agent Abort Endpoint', () => {
});
const response = await request(app)
.post('/api/agents/chat/abort')
.post('/v1/chat/agents/chat/abort')
.send({ conversationId: jobStreamId });
expect(response.status).toBe(200);
@@ -135,7 +135,7 @@ describe('Agent Abort Endpoint', () => {
});
const response = await request(app)
.post('/api/agents/chat/abort')
.post('/v1/chat/agents/chat/abort')
.send({ conversationId: jobStreamId });
expect(response.status).toBe(200);
@@ -163,7 +163,7 @@ describe('Agent Abort Endpoint', () => {
});
const response = await request(app)
.post('/api/agents/chat/abort')
.post('/v1/chat/agents/chat/abort')
.send({ conversationId: jobStreamId });
expect(response.status).toBe(200);
@@ -189,7 +189,7 @@ describe('Agent Abort Endpoint', () => {
});
const response = await request(app)
.post('/api/agents/chat/abort')
.post('/v1/chat/agents/chat/abort')
.send({ conversationId: jobStreamId });
expect(response.status).toBe(200);
@@ -224,7 +224,7 @@ describe('Agent Abort Endpoint', () => {
mockSaveMessage.mockResolvedValue();
const response = await request(app)
.post('/api/agents/chat/abort')
.post('/v1/chat/agents/chat/abort')
.send({ conversationId: jobStreamId });
expect(response.status).toBe(200);
@@ -271,7 +271,7 @@ describe('Agent Abort Endpoint', () => {
mockSaveMessage.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.post('/api/agents/chat/abort')
.post('/v1/chat/agents/chat/abort')
.send({ conversationId: jobStreamId });
// Should still return success even if save fails
@@ -289,7 +289,7 @@ describe('Agent Abort Endpoint', () => {
mockGenerationJobManager.getActiveJobIdsForUser.mockResolvedValue([]);
const response = await request(app)
.post('/api/agents/chat/abort')
.post('/v1/chat/agents/chat/abort')
.send({ conversationId: 'non-existent-job' });
expect(response.status).toBe(404);
@@ -63,7 +63,7 @@ function buildApp(session, cookie) {
}
next();
});
app.use('/api/agents/cloud', cloudRouter);
app.use('/v1/chat/agents/cloud', cloudRouter);
return app;
}
@@ -79,14 +79,14 @@ describe('cloud agents proxy route', () => {
describe('token handling (no leak, fail-secure)', () => {
it('401s when the openid principal has no hanzo.id session token', async () => {
const res = await request(buildApp({})).get('/api/agents/cloud');
const res = await request(buildApp({})).get('/v1/chat/agents/cloud');
expect(res.status).toBe(401);
expect(mockClient.list).not.toHaveBeenCalled();
});
it('forwards the session id_token (never the browser) to cloud', async () => {
mockClient.list.mockResolvedValue({ agents: [{ name: 'researcher' }] });
const res = await request(buildApp(withToken)).get('/api/agents/cloud');
const res = await request(buildApp(withToken)).get('/v1/chat/agents/cloud');
expect(res.status).toBe(200);
expect(mockClient.list).toHaveBeenCalledWith(VALID_ID);
expect(res.body).toEqual({ agents: [{ name: 'researcher' }], enabled: true });
@@ -97,7 +97,7 @@ describe('cloud agents proxy route', () => {
// hanzo.id issues JWT access tokens too; the fallback stays principal-bound.
const accJwt = mintIdToken();
const app = buildApp({ openidTokens: { accessToken: accJwt } });
await request(app).get('/api/agents/cloud');
await request(app).get('/v1/chat/agents/cloud');
expect(mockClient.list).toHaveBeenCalledWith(accJwt);
});
@@ -106,7 +106,7 @@ describe('cloud agents proxy route', () => {
// forward. (This is the path a selective `openid_access_token` cookie
// injection would take; the binding requirement closes it.)
const app = buildApp({ openidTokens: { accessToken: 'OPAQUE_NO_BINDING' } });
const res = await request(app).get('/api/agents/cloud');
const res = await request(app).get('/v1/chat/agents/cloud');
expect(res.status).toBe(401);
expect(mockClient.list).not.toHaveBeenCalled();
});
@@ -114,7 +114,7 @@ describe('cloud agents proxy route', () => {
it('401s (never forwards) an access_token JWT that names a different principal', async () => {
const foreignAcc = mintIdToken({ sub: 'sub-someone-else' });
const app = buildApp({ openidTokens: { accessToken: foreignAcc } });
const res = await request(app).get('/api/agents/cloud');
const res = await request(app).get('/v1/chat/agents/cloud');
expect(res.status).toBe(401);
expect(mockClient.list).not.toHaveBeenCalled();
});
@@ -122,7 +122,7 @@ describe('cloud agents proxy route', () => {
it('reads the httpOnly openid_id_token cookie when the session is empty', async () => {
mockClient.list.mockResolvedValue({ agents: [] });
const app = buildApp({}, `openid_id_token=${VALID_ID}`);
await request(app).get('/api/agents/cloud');
await request(app).get('/v1/chat/agents/cloud');
expect(mockClient.list).toHaveBeenCalledWith(VALID_ID);
});
});
@@ -131,7 +131,7 @@ describe('cloud agents proxy route', () => {
it('honest 401 when the id_token is past its own exp (no forward)', async () => {
const expired = mintIdToken({ exp: Math.floor(Date.now() / 1000) - 60 });
const app = buildApp({ openidTokens: { idToken: expired } });
const res = await request(app).get('/api/agents/cloud');
const res = await request(app).get('/v1/chat/agents/cloud');
expect(res.status).toBe(401);
expect(mockClient.list).not.toHaveBeenCalled();
});
@@ -139,14 +139,14 @@ describe('cloud agents proxy route', () => {
it('honest 401 when the id_token names a different principal (sub mismatch)', async () => {
const foreign = mintIdToken({ sub: 'sub-someone-else' });
const app = buildApp({ openidTokens: { idToken: foreign } });
const res = await request(app).get('/api/agents/cloud');
const res = await request(app).get('/v1/chat/agents/cloud');
expect(res.status).toBe(401);
expect(mockClient.list).not.toHaveBeenCalled();
});
it('honest 401 when the id_token is not a decodable JWT', async () => {
const app = buildApp({ openidTokens: { idToken: 'not-a-jwt' } });
const res = await request(app).get('/api/agents/cloud');
const res = await request(app).get('/v1/chat/agents/cloud');
expect(res.status).toBe(401);
expect(mockClient.list).not.toHaveBeenCalled();
});
@@ -158,14 +158,14 @@ describe('cloud agents proxy route', () => {
// Forwarding those tokens would run as the wrong principal — deny at the
// identity layer (req.user.provider), independent of any cookie.
mockPrincipal = LOCAL_USER;
const res = await request(buildApp(withToken)).get('/api/agents/cloud');
const res = await request(buildApp(withToken)).get('/v1/chat/agents/cloud');
expect(res.status).toBe(401);
expect(mockClient.list).not.toHaveBeenCalled();
});
it('401s for a principal that carries no provider', async () => {
mockPrincipal = { id: 'u_unknown' };
const res = await request(buildApp(withToken)).get('/api/agents/cloud');
const res = await request(buildApp(withToken)).get('/v1/chat/agents/cloud');
expect(res.status).toBe(401);
expect(mockClient.list).not.toHaveBeenCalled();
});
@@ -175,7 +175,7 @@ describe('cloud agents proxy route', () => {
// asserted, so NO token is forwarded — even one whose sub would have matched
// a normal user. Fail-secure closes the null-binding gap.
mockPrincipal = { id: 'u_openid_no_sub', provider: 'openid' };
const res = await request(buildApp(withToken)).get('/api/agents/cloud');
const res = await request(buildApp(withToken)).get('/v1/chat/agents/cloud');
expect(res.status).toBe(401);
expect(mockClient.list).not.toHaveBeenCalled();
});
@@ -186,7 +186,7 @@ describe('cloud agents proxy route', () => {
// binding rejects it — the forwarded principal can only ever be req.user.
const foreign = mintIdToken({ sub: 'victim-sub-xyz' });
const app = buildApp({}, `openid_id_token=${foreign}`);
const res = await request(app).get('/api/agents/cloud');
const res = await request(app).get('/v1/chat/agents/cloud');
expect(res.status).toBe(401);
expect(mockClient.list).not.toHaveBeenCalled();
});
@@ -195,7 +195,7 @@ describe('cloud agents proxy route', () => {
describe('disabled deployment', () => {
it('returns an empty, disabled list when cloud agents are not configured', async () => {
getCloudAgentsClient.mockReturnValue(null);
const res = await request(buildApp(withToken)).get('/api/agents/cloud');
const res = await request(buildApp(withToken)).get('/v1/chat/agents/cloud');
expect(res.status).toBe(200);
expect(res.body).toEqual({ agents: [], enabled: false });
});
@@ -205,7 +205,7 @@ describe('cloud agents proxy route', () => {
it('forwards {input} and returns the RunResult', async () => {
mockClient.run.mockResolvedValue({ id: 'run_1', status: 'ok', output: 'done' });
const res = await request(buildApp(withToken))
.post('/api/agents/cloud/researcher/run')
.post('/v1/chat/agents/cloud/researcher/run')
.send({ input: 'summarize' });
expect(res.status).toBe(200);
expect(mockClient.run).toHaveBeenCalledWith(VALID_ID, 'researcher', 'summarize');
@@ -219,7 +219,7 @@ describe('cloud agents proxy route', () => {
});
mockClient.run.mockRejectedValue(err);
const res = await request(buildApp(withToken))
.post('/api/agents/cloud/researcher/run')
.post('/v1/chat/agents/cloud/researcher/run')
.send({ input: 'x' });
expect(res.status).toBe(502);
expect(res.body).toEqual({ status: 'error', error: 'model down' });
@@ -229,7 +229,7 @@ describe('cloud agents proxy route', () => {
const err = Object.assign(new Error('invalid agent name'), { status: 400 });
mockClient.run.mockRejectedValue(err);
const res = await request(buildApp(withToken))
.post('/api/agents/cloud/researcher/run')
.post('/v1/chat/agents/cloud/researcher/run')
.send({ input: 'x' });
// simulate the client rejecting after the boundary let a valid name through
expect(res.status).toBe(400);
@@ -250,7 +250,7 @@ describe('cloud agents proxy route', () => {
for (const name of smuggles) {
it(`rejects "${name}" with 400 and never calls the client`, async () => {
const res = await request(buildApp(withToken))
.post(`/api/agents/cloud/${name}/run`)
.post(`/v1/chat/agents/cloud/${name}/run`)
.send({ input: 'x' });
expect(res.status).toBe(400);
expect(mockClient.run).not.toHaveBeenCalled();
@@ -258,7 +258,7 @@ describe('cloud agents proxy route', () => {
}
it('rejects a bad name on GET /:name too', async () => {
const res = await request(buildApp(withToken)).get('/api/agents/cloud/..%2Fetc');
const res = await request(buildApp(withToken)).get('/v1/chat/agents/cloud/..%2Fetc');
expect(res.status).toBe(400);
expect(mockClient.get).not.toHaveBeenCalled();
});
@@ -411,7 +411,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
// Mount the responses routes
const responsesRoutes = require('~/server/routes/agents/responses');
app.use('/api/agents/v1/responses', responsesRoutes);
app.use('/v1/chat/agents/v1/responses', responsesRoutes);
// Create test user
testUser = await User.create({
@@ -531,7 +531,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
describe('basic-response', () => {
it('should return a valid ResponseResource for a simple text request', async () => {
const response = await authRequest()
.post('/api/agents/v1/responses')
.post('/v1/chat/agents/v1/responses')
.send({
model: testAgent.id,
input: [
@@ -570,7 +570,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
describe('streaming-response', () => {
it('should return valid SSE streaming events', async () => {
const response = await authRequest()
.post('/api/agents/v1/responses')
.post('/v1/chat/agents/v1/responses')
.send({
model: testAgent.id,
input: [
@@ -642,7 +642,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
it('should emit valid event types per Open Responses spec', async () => {
const response = await authRequest()
.post('/api/agents/v1/responses')
.post('/v1/chat/agents/v1/responses')
.send({
model: testAgent.id,
input: [
@@ -679,7 +679,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
it('should include logprobs array in output_text events', async () => {
const response = await authRequest()
.post('/api/agents/v1/responses')
.post('/v1/chat/agents/v1/responses')
.send({
model: testAgent.id,
input: [
@@ -736,7 +736,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
// gets merged into the system prompt, or we test with a simple user message
// that instructs the behavior.
const response = await authRequest()
.post('/api/agents/v1/responses')
.post('/v1/chat/agents/v1/responses')
.send({
model: testAgent.id,
input: [
@@ -762,7 +762,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
describe('multi-turn', () => {
it('should handle multi-turn conversation history', async () => {
const response = await authRequest()
.post('/api/agents/v1/responses')
.post('/v1/chat/agents/v1/responses')
.send({
model: testAgent.id,
input: [
@@ -802,7 +802,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
describe('string-input', () => {
it('should accept simple string input', async () => {
const response = await authRequest().post('/api/agents/v1/responses').send({
const response = await authRequest().post('/v1/chat/agents/v1/responses').send({
model: testAgent.id,
input: 'Hello!',
});
@@ -822,7 +822,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
describe('Extended Thinking', () => {
it('should return reasoning output when thinking is enabled', async () => {
const response = await authRequest()
.post('/api/agents/v1/responses')
.post('/v1/chat/agents/v1/responses')
.send({
model: thinkingAgent.id,
input: [
@@ -863,7 +863,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
it('should stream reasoning events when thinking is enabled', async () => {
const response = await authRequest()
.post('/api/agents/v1/responses')
.post('/v1/chat/agents/v1/responses')
.send({
model: thinkingAgent.id,
input: [
@@ -938,7 +938,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
describe('Schema Validation', () => {
it('should include all required fields in response', async () => {
const response = await authRequest().post('/api/agents/v1/responses').send({
const response = await authRequest().post('/v1/chat/agents/v1/responses').send({
model: testAgent.id,
input: 'Test',
});
@@ -984,7 +984,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
});
it('should have valid message item structure', async () => {
const response = await authRequest().post('/api/agents/v1/responses').send({
const response = await authRequest().post('/v1/chat/agents/v1/responses').send({
model: testAgent.id,
input: 'Hello',
});
@@ -1034,7 +1034,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
describe('Response Storage', () => {
it('should store response when store: true and retrieve it', async () => {
// Create a stored response
const createResponse = await authRequest().post('/api/agents/v1/responses').send({
const createResponse = await authRequest().post('/v1/chat/agents/v1/responses').send({
model: testAgent.id,
input: 'Remember this: The answer is 42.',
store: true,
@@ -1050,7 +1050,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
await new Promise((resolve) => setTimeout(resolve, 500));
// Retrieve the stored response
const getResponseResult = await authRequest().get(`/api/agents/v1/responses/${responseId}`);
const getResponseResult = await authRequest().get(`/v1/chat/agents/v1/responses/${responseId}`);
// Note: The response might be stored under conversationId, not responseId
// If we get 404, that's expected behavior for now since we store by conversationId
@@ -1062,7 +1062,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
});
it('should return 404 for non-existent response', async () => {
const response = await authRequest().get('/api/agents/v1/responses/resp_nonexistent123');
const response = await authRequest().get('/v1/chat/agents/v1/responses/resp_nonexistent123');
expect(response.status).toBe(404);
expect(response.body.error).toBeDefined();
@@ -1075,7 +1075,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
describe('Error Handling', () => {
it('should return error for missing model', async () => {
const response = await authRequest().post('/api/agents/v1/responses').send({
const response = await authRequest().post('/v1/chat/agents/v1/responses').send({
input: 'Hello',
});
@@ -1084,7 +1084,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
});
it('should return error for missing input', async () => {
const response = await authRequest().post('/api/agents/v1/responses').send({
const response = await authRequest().post('/v1/chat/agents/v1/responses').send({
model: testAgent.id,
});
@@ -1093,7 +1093,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
});
it('should return error for non-existent agent', async () => {
const response = await authRequest().post('/api/agents/v1/responses').send({
const response = await authRequest().post('/v1/chat/agents/v1/responses').send({
model: 'agent_nonexistent123456789',
input: 'Hello',
});
@@ -1109,7 +1109,7 @@ describeWithApiKey('Open Responses API Integration Tests', () => {
describe('GET /v1/responses/models', () => {
it('should list available agents as models', async () => {
const response = await authRequest().get('/api/agents/v1/responses/models');
const response = await authRequest().get('/v1/chat/agents/v1/responses/models');
expect(response.status).toBe(200);
expect(response.body.object).toBe('list');
+7 -7
View File
@@ -7,11 +7,11 @@ const { getCloudAgentsClient, AGENT_NAME_RE } = require('~/server/services/Cloud
/**
* Cloud agents router lets a signed-in chat user RUN their own canonical Hanzo
* Cloud agents (`/v1/agents`) from the chat thread. Mounted at `/api/agents/cloud`.
* Cloud agents (`/v1/agents`) from the chat thread. Mounted at `/v1/chat/agents/cloud`.
*
* GET /api/agents/cloud list the caller's cloud agents
* GET /api/agents/cloud/:name one agent's detail + recent runs
* POST /api/agents/cloud/:name/run run the agent {input} -> RunResult
* GET /v1/chat/agents/cloud list the caller's cloud agents
* GET /v1/chat/agents/cloud/:name one agent's detail + recent runs
* POST /v1/chat/agents/cloud/:name/run run the agent {input} -> RunResult
*
* Auth: `requireJwtAuth` gates every route (guests are rejected). The chat
* backend then forwards the user's hanzo.id id_token to cloud as a Bearer;
@@ -144,7 +144,7 @@ function sendCloudError(res, err, action) {
return res.status(status).json({ error: err.message || 'cloud agents request failed' });
}
/** GET /api/agents/cloud — list the caller's cloud agents. */
/** GET /v1/chat/agents/cloud — list the caller's cloud agents. */
router.get('/', async (req, res) => {
const client = getCloudAgentsClient();
if (!client) {
@@ -162,7 +162,7 @@ router.get('/', async (req, res) => {
}
});
/** GET /api/agents/cloud/:name — one agent's detail + recent runs. */
/** GET /v1/chat/agents/cloud/:name — one agent's detail + recent runs. */
router.get('/:name', async (req, res) => {
const client = getCloudAgentsClient();
if (!client) {
@@ -180,7 +180,7 @@ router.get('/:name', async (req, res) => {
}
});
/** POST /api/agents/cloud/:name/run — run the agent, return its RunResult. */
/** POST /v1/chat/agents/cloud/:name/run — run the agent, return its RunResult. */
router.post('/:name/run', async (req, res) => {
const client = getCloudAgentsClient();
if (!client) {
+3 -3
View File
@@ -25,7 +25,7 @@ const router = express.Router();
/**
* Open Responses API routes (API key authentication handled in route file)
* Mounted at /agents/v1/responses (full path: /api/agents/v1/responses)
* Mounted at /agents/v1/responses (full path: /v1/chat/agents/v1/responses)
* NOTE: Must be mounted BEFORE /v1 to avoid being caught by the less specific route
* @see https://openresponses.org/specification
*/
@@ -33,7 +33,7 @@ router.use('/v1/responses', responses);
/**
* OpenAI-compatible API routes (API key authentication handled in route file)
* Mounted at /agents/v1 (full path: /api/agents/v1/chat/completions)
* Mounted at /agents/v1 (full path: /v1/chat/agents/v1/chat/completions)
*/
router.use('/v1', openai);
@@ -103,7 +103,7 @@ router.use(uaParser);
* Canonical Hanzo Cloud agents (`/v1/agents`). Mounted BEFORE the legacy `/`
* (v1) router so `/cloud/*` is not shadowed by v1's `GET /:id`. The cloud router
* carries its own `requireJwtAuth` and forwards the user's hanzo.id bearer to
* cloud server-side (see cloud.js). Legacy `/api/agents` CRUD stays untouched.
* cloud server-side (see cloud.js). Legacy `/v1/chat/agents` CRUD stays untouched.
*/
router.use('/cloud', cloud);
+6 -6
View File
@@ -79,18 +79,18 @@ jest.mock('~/server/middleware', () => {
const authRouter = require('./auth');
describe('POST /api/auth/cloudfront/refresh', () => {
describe('POST /v1/chat/auth/cloudfront/refresh', () => {
let app;
beforeEach(() => {
jest.clearAllMocks();
app = express();
app.use(express.json());
app.use('/api/auth', authRouter);
app.use('/v1/chat/auth', authRouter);
});
it('requires authentication', async () => {
await request(app).post('/api/auth/cloudfront/refresh').expect(401);
await request(app).post('/v1/chat/auth/cloudfront/refresh').expect(401);
expect(mockForceRefreshCloudFrontAuthCookies).not.toHaveBeenCalled();
});
@@ -104,7 +104,7 @@ describe('POST /api/auth/cloudfront/refresh', () => {
});
const response = await request(app)
.post('/api/auth/cloudfront/refresh')
.post('/v1/chat/auth/cloudfront/refresh')
.set('Authorization', 'Bearer ok')
.expect(404);
@@ -121,7 +121,7 @@ describe('POST /api/auth/cloudfront/refresh', () => {
});
const response = await request(app)
.post('/api/auth/cloudfront/refresh')
.post('/v1/chat/auth/cloudfront/refresh')
.set('Authorization', 'Bearer ok')
.expect(200);
@@ -139,7 +139,7 @@ describe('POST /api/auth/cloudfront/refresh', () => {
it('reuses the auth middleware refresh result instead of minting cookies twice', async () => {
const response = await request(app)
.post('/api/auth/cloudfront/refresh')
.post('/v1/chat/auth/cloudfront/refresh')
.set('Authorization', 'Bearer ok')
.set('x-cloudfront-warmed', 'true')
.expect(200);
+2 -2
View File
@@ -186,7 +186,7 @@ router.post('/archive', validateConvoAccess, async (req, res) => {
const dbResponse = await saveConvo(
req,
{ conversationId, isArchived },
{ context: `POST /api/convos/archive ${conversationId}` },
{ context: `POST /v1/chat/convos/archive ${conversationId}` },
);
res.status(200).json(dbResponse);
} catch (error) {
@@ -226,7 +226,7 @@ router.post('/update', validateConvoAccess, async (req, res) => {
const dbResponse = await saveConvo(
req,
{ conversationId, title: sanitizedTitle },
{ context: `POST /api/convos/update ${conversationId}` },
{ context: `POST /v1/chat/convos/update ${conversationId}` },
);
res.status(201).json(dbResponse);
} catch (error) {
+8 -8
View File
@@ -106,7 +106,7 @@ describe('file upload routes restore strict isolation context after multer', ()
beforeAll(async () => {
const { initialize } = require('./index');
app = express();
app.use('/api/files', await initialize());
app.use('/v1/chat/files', await initialize());
});
beforeEach(() => {
@@ -123,12 +123,12 @@ describe('file upload routes restore strict isolation context after multer', ()
});
it.each([
['files', '/api/files'],
['images', '/api/files/images'],
['avatar', '/api/files/images/avatar'],
['agent-avatar', '/api/files/images/agents/agent-1/avatar'],
['assistant-avatar', '/api/files/images/assistants/asst-1/avatar'],
['speech-stt', '/api/files/speech/stt'],
['files', '/v1/chat/files'],
['images', '/v1/chat/files/images'],
['avatar', '/v1/chat/files/images/avatar'],
['agent-avatar', '/v1/chat/files/images/agents/agent-1/avatar'],
['assistant-avatar', '/v1/chat/files/images/assistants/asst-1/avatar'],
['speech-stt', '/v1/chat/files/speech/stt'],
])('restores context for %s upload', async (route, url) => {
const res = await request(app).post(url);
@@ -142,7 +142,7 @@ describe('file upload routes restore strict isolation context after multer', ()
role: 'USER',
};
const res = await request(app).post('/api/files');
const res = await request(app).post('/v1/chat/files');
expect(res.status).toBe(403);
expect(res.body.error).toMatch(/Tenant context required/);
+2 -2
View File
@@ -23,7 +23,7 @@ describe('Multer Configuration', () => {
mockReq = {
user: { id: 'test-user-123' },
body: {},
originalUrl: '/api/files/upload',
originalUrl: '/v1/chat/files/upload',
config: {
paths: {
uploads: tempDir,
@@ -256,7 +256,7 @@ describe('Multer Configuration', () => {
});
it('should handle audio files for speech-to-text endpoint with real config', async () => {
mockReq.originalUrl = '/api/speech/stt';
mockReq.originalUrl = '/v1/chat/speech/stt';
const multerInstance = await createMulterInstance();
expect(multerInstance).toBeDefined();
+6 -6
View File
@@ -47,7 +47,7 @@ const { getLogStores } = require('~/cache');
const router = Router();
const OAUTH_CSRF_COOKIE_PATH = '/api/mcp';
const OAUTH_CSRF_COOKIE_PATH = '/v1/chat/mcp';
/**
* Get all MCP tools available to the user
@@ -677,7 +677,7 @@ const checkMCPCreate = generateCheckAccess({
/**
* Get list of accessible MCP servers
* @route GET /api/mcp/servers
* @route GET /v1/chat/mcp/servers
* @param {Object} req.query - Query parameters for pagination and search
* @param {number} [req.query.limit] - Number of results per page
* @param {string} [req.query.after] - Pagination cursor
@@ -688,7 +688,7 @@ router.get('/servers', requireJwtAuth, checkMCPUsePermissions, getMCPServersList
/**
* Create a new MCP server
* @route POST /api/mcp/servers
* @route POST /v1/chat/mcp/servers
* @param {MCPServerCreateParams} req.body - The MCP server creation parameters.
* @returns {MCPServer} 201 - Success response - application/json
*/
@@ -696,7 +696,7 @@ router.post('/servers', requireJwtAuth, checkMCPCreate, createMCPServerControlle
/**
* Get single MCP server by ID
* @route GET /api/mcp/servers/:serverName
* @route GET /v1/chat/mcp/servers/:serverName
* @param {string} req.params.serverName - MCP server identifier.
* @returns {MCPServer} 200 - Success response - application/json
*/
@@ -713,7 +713,7 @@ router.get(
/**
* Update MCP server
* @route PATCH /api/mcp/servers/:serverName
* @route PATCH /v1/chat/mcp/servers/:serverName
* @param {string} req.params.serverName - MCP server identifier.
* @param {MCPServerUpdateParams} req.body - The MCP server update parameters.
* @returns {MCPServer} 200 - Success response - application/json
@@ -731,7 +731,7 @@ router.patch(
/**
* Delete MCP server
* @route DELETE /api/mcp/servers/:serverName
* @route DELETE /v1/chat/mcp/servers/:serverName
* @param {string} req.params.serverName - MCP server identifier.
* @returns {Object} 200 - Success response - application/json
*/
+4 -4
View File
@@ -188,7 +188,7 @@ router.post('/branch', async (req, res) => {
};
const savedMessage = await saveMessage(req, newMessage, {
context: 'POST /api/messages/branch',
context: 'POST /v1/chat/messages/branch',
});
if (!savedMessage) {
@@ -265,7 +265,7 @@ router.post('/artifact/:messageId', async (req, res) => {
content: message.content,
user: req.user.id,
},
{ context: 'POST /api/messages/artifact/:messageId' },
{ context: 'POST /v1/chat/messages/artifact/:messageId' },
);
res.status(200).json({
@@ -297,12 +297,12 @@ router.post('/:conversationId', validateMessageReq, async (req, res) => {
const savedMessage = await saveMessage(
req,
{ ...message, user: req.user.id },
{ context: 'POST /api/messages/:conversationId' },
{ context: 'POST /v1/chat/messages/:conversationId' },
);
if (!savedMessage) {
return res.status(400).json({ error: 'Message not saved' });
}
await saveConvo(req, savedMessage, { context: 'POST /api/messages/:conversationId' });
await saveConvo(req, savedMessage, { context: 'POST /v1/chat/messages/:conversationId' });
res.status(201).json(savedMessage);
} catch (error) {
logger.error('Error saving message:', error);
+36 -36
View File
@@ -83,7 +83,7 @@ beforeAll(async () => {
// Import routes after middleware is set up
promptRoutes = require('./prompts');
app.use('/api/prompts', promptRoutes);
app.use('/v1/chat/prompts', promptRoutes);
});
afterEach(() => {
@@ -180,13 +180,13 @@ describe('Prompt Routes - ACL Permissions', () => {
// Simple test to verify route is loaded
it('should have routes loaded', async () => {
// This should at least not crash
const response = await request(app).get('/api/prompts/test-404');
const response = await request(app).get('/v1/chat/prompts/test-404');
// We expect a 401 or 404, not 500
expect(response.status).not.toBe(500);
});
describe('POST /api/prompts - Create Prompt', () => {
describe('POST /v1/chat/prompts - Create Prompt', () => {
afterEach(async () => {
await Prompt.deleteMany({});
await PromptGroup.deleteMany({});
@@ -204,7 +204,7 @@ describe('Prompt Routes - ACL Permissions', () => {
},
};
const response = await request(app).post('/api/prompts').send(promptData);
const response = await request(app).post('/v1/chat/prompts').send(promptData);
expect(response.status).toBe(200);
expect(response.body.prompt).toBeDefined();
@@ -234,7 +234,7 @@ describe('Prompt Routes - ACL Permissions', () => {
},
};
const response = await request(app).post('/api/prompts').send(promptData).expect(200);
const response = await request(app).post('/v1/chat/prompts').send(promptData).expect(200);
expect(response.body.prompt).toBeDefined();
expect(response.body.group).toBeDefined();
@@ -252,7 +252,7 @@ describe('Prompt Routes - ACL Permissions', () => {
});
});
describe('GET /api/prompts/:promptId - Get Prompt', () => {
describe('GET /v1/chat/prompts/:promptId - Get Prompt', () => {
let testPrompt;
let testGroup;
@@ -293,7 +293,7 @@ describe('Prompt Routes - ACL Permissions', () => {
grantedBy: testUsers.owner._id,
});
const response = await request(app).get(`/api/prompts/${testPrompt._id}`);
const response = await request(app).get(`/v1/chat/prompts/${testPrompt._id}`);
expect(response.status).toBe(200);
expect(response.body._id).toBe(testPrompt._id.toString());
expect(response.body.prompt).toBe(testPrompt.prompt);
@@ -303,7 +303,7 @@ describe('Prompt Routes - ACL Permissions', () => {
// Change the user to one without access
setTestUser(app, testUsers.noAccess);
const response = await request(app).get(`/api/prompts/${testPrompt._id}`).expect(403);
const response = await request(app).get(`/v1/chat/prompts/${testPrompt._id}`).expect(403);
// Verify error response
expect(response.body.error).toBe('Forbidden');
@@ -314,13 +314,13 @@ describe('Prompt Routes - ACL Permissions', () => {
// Set admin user
setTestUser(app, testUsers.admin);
const response = await request(app).get(`/api/prompts/${testPrompt._id}`).expect(200);
const response = await request(app).get(`/v1/chat/prompts/${testPrompt._id}`).expect(200);
expect(response.body._id).toBe(testPrompt._id.toString());
});
});
describe('DELETE /api/prompts/:promptId - Delete Prompt', () => {
describe('DELETE /v1/chat/prompts/:promptId - Delete Prompt', () => {
let testPrompt;
let testGroup;
@@ -366,7 +366,7 @@ describe('Prompt Routes - ACL Permissions', () => {
it('should delete prompt when user has delete permissions', async () => {
const response = await request(app)
.delete(`/api/prompts/${testPrompt._id}`)
.delete(`/v1/chat/prompts/${testPrompt._id}`)
.query({ groupId: testGroup._id.toString() })
.expect(200);
@@ -408,7 +408,7 @@ describe('Prompt Routes - ACL Permissions', () => {
setTestUser(app, testUsers.viewer);
await request(app)
.delete(`/api/prompts/${authorPrompt._id}`)
.delete(`/v1/chat/prompts/${authorPrompt._id}`)
.query({ groupId: testGroup._id.toString() })
.expect(403);
@@ -418,7 +418,7 @@ describe('Prompt Routes - ACL Permissions', () => {
});
});
describe('PATCH /api/prompts/:promptId/tags/production - Make Production', () => {
describe('PATCH /v1/chat/prompts/:promptId/tags/production - Make Production', () => {
let testPrompt;
let testGroup;
@@ -462,7 +462,7 @@ describe('Prompt Routes - ACL Permissions', () => {
setTestUser(app, testUsers.owner);
const response = await request(app)
.patch(`/api/prompts/${testPrompt._id}/tags/production`)
.patch(`/v1/chat/prompts/${testPrompt._id}/tags/production`)
.expect(200);
expect(response.body.message).toBe('Prompt production made successfully');
@@ -486,7 +486,7 @@ describe('Prompt Routes - ACL Permissions', () => {
// Set viewer user
setTestUser(app, testUsers.viewer);
await request(app).patch(`/api/prompts/${testPrompt._id}/tags/production`).expect(403);
await request(app).patch(`/v1/chat/prompts/${testPrompt._id}/tags/production`).expect(403);
// Verify prompt hasn't changed
const unchangedGroup = await PromptGroup.findById(testGroup._id);
@@ -538,13 +538,13 @@ describe('Prompt Routes - ACL Permissions', () => {
// Change user to someone without explicit permissions
setTestUser(app, testUsers.noAccess);
const response = await request(app).get(`/api/prompts/${publicPrompt._id}`).expect(200);
const response = await request(app).get(`/v1/chat/prompts/${publicPrompt._id}`).expect(200);
expect(response.body._id).toBe(publicPrompt._id.toString());
});
});
describe('PATCH /api/prompts/groups/:groupId - Update Prompt Group Security', () => {
describe('PATCH /v1/chat/prompts/groups/:groupId - Update Prompt Group Security', () => {
let testGroup;
beforeEach(async () => {
@@ -581,7 +581,7 @@ describe('Prompt Routes - ACL Permissions', () => {
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.patch(`/v1/chat/prompts/groups/${testGroup._id}`)
.send(updateData)
.expect(200);
@@ -597,7 +597,7 @@ describe('Prompt Routes - ACL Permissions', () => {
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.patch(`/v1/chat/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
@@ -613,7 +613,7 @@ describe('Prompt Routes - ACL Permissions', () => {
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.patch(`/v1/chat/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
@@ -629,7 +629,7 @@ describe('Prompt Routes - ACL Permissions', () => {
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.patch(`/v1/chat/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
@@ -645,7 +645,7 @@ describe('Prompt Routes - ACL Permissions', () => {
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.patch(`/v1/chat/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
@@ -661,7 +661,7 @@ describe('Prompt Routes - ACL Permissions', () => {
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.patch(`/v1/chat/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
@@ -676,7 +676,7 @@ describe('Prompt Routes - ACL Permissions', () => {
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.patch(`/v1/chat/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
@@ -696,7 +696,7 @@ describe('Prompt Routes - ACL Permissions', () => {
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.patch(`/v1/chat/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
@@ -741,7 +741,7 @@ describe('Prompt Routes - ACL Permissions', () => {
it('should correctly indicate hasMore when there are more pages', async () => {
const response = await request(app)
.get('/api/prompts/groups')
.get('/v1/chat/prompts/groups')
.query({ limit: '10' })
.expect(200);
@@ -755,7 +755,7 @@ describe('Prompt Routes - ACL Permissions', () => {
it('should correctly indicate no more pages on the last page', async () => {
// First get the cursor for page 2
const firstPage = await request(app)
.get('/api/prompts/groups')
.get('/v1/chat/prompts/groups')
.query({ limit: '10' })
.expect(200);
@@ -764,7 +764,7 @@ describe('Prompt Routes - ACL Permissions', () => {
// Now fetch the second page using the cursor
const response = await request(app)
.get('/api/prompts/groups')
.get('/v1/chat/prompts/groups')
.query({ limit: '10', cursor: firstPage.body.after })
.expect(200);
@@ -775,7 +775,7 @@ describe('Prompt Routes - ACL Permissions', () => {
it('should support cursor-based pagination', async () => {
// First page
const firstPage = await request(app)
.get('/api/prompts/groups')
.get('/v1/chat/prompts/groups')
.query({ limit: '5' })
.expect(200);
@@ -785,7 +785,7 @@ describe('Prompt Routes - ACL Permissions', () => {
// Second page using cursor
const secondPage = await request(app)
.get('/api/prompts/groups')
.get('/v1/chat/prompts/groups')
.query({ limit: '5', cursor: firstPage.body.after })
.expect(200);
@@ -848,7 +848,7 @@ describe('Prompt Routes - ACL Permissions', () => {
// Test pagination with category filter
const firstPage = await request(app)
.get('/api/prompts/groups')
.get('/v1/chat/prompts/groups')
.query({ limit: '5', category: 'test-cat-1' })
.expect(200);
@@ -858,7 +858,7 @@ describe('Prompt Routes - ACL Permissions', () => {
expect(firstPage.body.after).toBeTruthy();
const secondPage = await request(app)
.get('/api/prompts/groups')
.get('/v1/chat/prompts/groups')
.query({ limit: '5', cursor: firstPage.body.after, category: 'test-cat-1' })
.expect(200);
@@ -916,7 +916,7 @@ describe('Prompt Routes - ACL Permissions', () => {
// Test pagination with name filter
const firstPage = await request(app)
.get('/api/prompts/groups')
.get('/v1/chat/prompts/groups')
.query({ limit: '10', name: 'Search' })
.expect(200);
@@ -926,7 +926,7 @@ describe('Prompt Routes - ACL Permissions', () => {
expect(firstPage.body.after).toBeTruthy();
const secondPage = await request(app)
.get('/api/prompts/groups')
.get('/v1/chat/prompts/groups')
.query({ limit: '10', cursor: firstPage.body.after, name: 'Search' })
.expect(200);
@@ -984,7 +984,7 @@ describe('Prompt Routes - ACL Permissions', () => {
// Test pagination with both filters
const response = await request(app)
.get('/api/prompts/groups')
.get('/v1/chat/prompts/groups')
.query({ limit: '5', name: 'API', category: 'api-category' })
.expect(200);
@@ -999,7 +999,7 @@ describe('Prompt Routes - ACL Permissions', () => {
// Page 2
const page2 = await request(app)
.get('/api/prompts/groups')
.get('/v1/chat/prompts/groups')
.query({ limit: '5', cursor: response.body.after, name: 'API', category: 'api-category' })
.expect(200);
+8 -8
View File
@@ -103,7 +103,7 @@ const createPermissionUpdateHandler = (permissionKey) => {
};
/**
* GET /api/roles/:roleName
* GET /v1/chat/roles/:roleName
* Get a specific role by name
*/
router.get('/:roleName', async (req, res) => {
@@ -131,43 +131,43 @@ router.get('/:roleName', async (req, res) => {
});
/**
* PUT /api/roles/:roleName/prompts
* PUT /v1/chat/roles/:roleName/prompts
* Update prompt permissions for a specific role
*/
router.put('/:roleName/prompts', checkAdmin, createPermissionUpdateHandler('prompts'));
/**
* PUT /api/roles/:roleName/agents
* PUT /v1/chat/roles/:roleName/agents
* Update agent permissions for a specific role
*/
router.put('/:roleName/agents', checkAdmin, createPermissionUpdateHandler('agents'));
/**
* PUT /api/roles/:roleName/memories
* PUT /v1/chat/roles/:roleName/memories
* Update memory permissions for a specific role
*/
router.put('/:roleName/memories', checkAdmin, createPermissionUpdateHandler('memories'));
/**
* PUT /api/roles/:roleName/people-picker
* PUT /v1/chat/roles/:roleName/people-picker
* Update people picker permissions for a specific role
*/
router.put('/:roleName/people-picker', checkAdmin, createPermissionUpdateHandler('people-picker'));
/**
* PUT /api/roles/:roleName/mcp-servers
* PUT /v1/chat/roles/:roleName/mcp-servers
* Update MCP servers permissions for a specific role
*/
router.put('/:roleName/mcp-servers', checkAdmin, createPermissionUpdateHandler('mcp-servers'));
/**
* PUT /api/roles/:roleName/marketplace
* PUT /v1/chat/roles/:roleName/marketplace
* Update marketplace permissions for a specific role
*/
router.put('/:roleName/marketplace', checkAdmin, createPermissionUpdateHandler('marketplace'));
/**
* PUT /api/roles/:roleName/remote-agents
* PUT /v1/chat/roles/:roleName/remote-agents
* Update remote agents (API) permissions for a specific role
*/
router.put('/:roleName/remote-agents', checkAdmin, createPermissionUpdateHandler('remote-agents'));
+7 -7
View File
@@ -141,7 +141,7 @@ beforeAll(async () => {
});
currentTestUser = testUsers.owner;
app.use('/api/skills', require('./skills'));
app.use('/v1/chat/skills', require('./skills'));
});
afterEach(async () => {
@@ -202,7 +202,7 @@ async function setupTestData() {
async function createSkillAsOwner(overrides = {}) {
return request(app)
.post('/api/skills')
.post('/v1/chat/skills')
.send({
name: 'strict-file-skill',
description: 'A strict tenant skill used in multipart route tests.',
@@ -227,7 +227,7 @@ describe('Skill multipart routes under strict tenant isolation', () => {
zip.file('scripts/run.sh', 'echo strict');
const buffer = await zip.generateAsync({ type: 'nodebuffer' });
const res = await request(app).post('/api/skills/import').attach('file', buffer, {
const res = await request(app).post('/v1/chat/skills/import').attach('file', buffer, {
filename: 'strict-import.skill',
contentType: 'application/zip',
});
@@ -274,7 +274,7 @@ describe('Skill multipart routes under strict tenant isolation', () => {
});
const res = await request(app)
.post('/api/skills/import')
.post('/v1/chat/skills/import')
.attach('file', Buffer.from('# Request Tenant Markdown'), {
filename: 'request-tenant.md',
contentType: 'text/markdown',
@@ -301,7 +301,7 @@ describe('Skill multipart routes under strict tenant isolation', () => {
});
const res = await request(app)
.post('/api/skills/import')
.post('/v1/chat/skills/import')
.attach('file', Buffer.from('# No Tenant'), {
filename: 'no-tenant.md',
contentType: 'text/markdown',
@@ -316,7 +316,7 @@ describe('Skill multipart routes under strict tenant isolation', () => {
expect(created.status).toBe(201);
const res = await request(app)
.post(`/api/skills/${created.body._id}/files`)
.post(`/v1/chat/skills/${created.body._id}/files`)
.field('relativePath', 'scripts/manual.sh')
.attach('file', Buffer.from('echo manual'), {
filename: 'manual.sh',
@@ -351,7 +351,7 @@ describe('Skill multipart routes under strict tenant isolation', () => {
});
const res = await request(app)
.post(`/api/skills/${created.body._id}/files`)
.post(`/v1/chat/skills/${created.body._id}/files`)
.field('relativePath', 'scripts/request-tenant.sh')
.attach('file', Buffer.from('echo request tenant'), {
filename: 'request-tenant.sh',
+36 -36
View File
@@ -143,7 +143,7 @@ beforeAll(async () => {
currentTestUser = testUsers.owner;
skillRoutes = require('./skills');
app.use('/api/skills', skillRoutes);
app.use('/v1/chat/skills', skillRoutes);
});
afterEach(async () => {
@@ -223,7 +223,7 @@ async function createSkillAsOwner(overrides = {}) {
// Description is deliberately kept above the 20-char short-description
// warning threshold so existing tests don't trip the coaching warning.
const res = await request(app)
.post('/api/skills')
.post('/v1/chat/skills')
.send({
name: 'demo-skill',
description: 'A small demo skill used in routing integration tests.',
@@ -240,7 +240,7 @@ describe('Skill routes', () => {
});
afterEach(() => errSpy.mockRestore());
describe('POST /api/skills', () => {
describe('POST /v1/chat/skills', () => {
it('creates a skill and grants SKILL_OWNER ACL', async () => {
const res = await createSkillAsOwner();
expect(res.status).toBe(201);
@@ -306,7 +306,7 @@ describe('Skill routes', () => {
});
it('rejects missing description with 400', async () => {
const res = await request(app).post('/api/skills').send({ name: 'x-skill', body: '' });
const res = await request(app).post('/v1/chat/skills').send({ name: 'x-skill', body: '' });
expect(res.status).toBe(400);
});
@@ -324,7 +324,7 @@ describe('Skill routes', () => {
});
});
describe('POST /api/skills/import', () => {
describe('POST /v1/chat/skills/import', () => {
it('enforces fileConfig.skills.fileSizeLimit before import handling', async () => {
mockFileConfig = {
skills: {
@@ -333,7 +333,7 @@ describe('Skill routes', () => {
};
const res = await request(app)
.post('/api/skills/import')
.post('/v1/chat/skills/import')
.attach('file', Buffer.alloc(2 * 1024 * 1024), {
filename: 'too-large.skill',
contentType: 'application/zip',
@@ -368,7 +368,7 @@ describe('Skill routes', () => {
zip.file('scripts/imported-script.sh', 'echo imported');
const buffer = await zip.generateAsync({ type: 'nodebuffer' });
const res = await request(app).post('/api/skills/import').attach('file', buffer, {
const res = await request(app).post('/v1/chat/skills/import').attach('file', buffer, {
filename: 'imported-skill.skill',
contentType: 'application/zip',
});
@@ -395,7 +395,7 @@ describe('Skill routes', () => {
});
});
describe('GET /api/skills', () => {
describe('GET /v1/chat/skills', () => {
it('returns only skills the caller can access', async () => {
const mine = await createSkillAsOwner({ name: 'mine-skill' });
expect(mine.status).toBe(201);
@@ -407,36 +407,36 @@ describe('Skill routes', () => {
// users see their own skill only.
setTestUser(testUsers.owner);
const res = await request(app).get('/api/skills');
const res = await request(app).get('/v1/chat/skills');
expect(res.status).toBe(200);
expect(res.body.skills.length).toBe(1);
expect(res.body.skills[0].name).toBe('mine-skill');
});
});
describe('GET /api/skills/:id', () => {
describe('GET /v1/chat/skills/:id', () => {
it('returns 403 when the user has no access', async () => {
const created = await createSkillAsOwner();
expect(created.status).toBe(201);
setTestUser(testUsers.noAccess);
const res = await request(app).get(`/api/skills/${created.body._id}`);
const res = await request(app).get(`/v1/chat/skills/${created.body._id}`);
expect(res.status).toBe(403);
});
it('returns the skill to the owner with isPublic flag', async () => {
const created = await createSkillAsOwner();
const res = await request(app).get(`/api/skills/${created.body._id}`);
const res = await request(app).get(`/v1/chat/skills/${created.body._id}`);
expect(res.status).toBe(200);
expect(res.body.name).toBe('demo-skill');
expect(res.body.isPublic).toBe(false);
});
});
describe('PATCH /api/skills/:id (optimistic concurrency)', () => {
describe('PATCH /v1/chat/skills/:id (optimistic concurrency)', () => {
it('updates with correct expectedVersion and bumps version', async () => {
const created = await createSkillAsOwner();
const res = await request(app)
.patch(`/api/skills/${created.body._id}`)
.patch(`/v1/chat/skills/${created.body._id}`)
.send({ expectedVersion: 1, description: 'Updated description' });
expect(res.status).toBe(200);
expect(res.body.version).toBe(2);
@@ -446,12 +446,12 @@ describe('Skill routes', () => {
it('returns 409 on stale expectedVersion', async () => {
const created = await createSkillAsOwner();
const first = await request(app)
.patch(`/api/skills/${created.body._id}`)
.patch(`/v1/chat/skills/${created.body._id}`)
.send({ expectedVersion: 1, description: 'First' });
expect(first.status).toBe(200);
const stale = await request(app)
.patch(`/api/skills/${created.body._id}`)
.patch(`/v1/chat/skills/${created.body._id}`)
.send({ expectedVersion: 1, description: 'Stale' });
expect(stale.status).toBe(409);
expect(stale.body.error).toBe('skill_version_conflict');
@@ -461,7 +461,7 @@ describe('Skill routes', () => {
it('rejects updates without expectedVersion', async () => {
const created = await createSkillAsOwner();
const res = await request(app)
.patch(`/api/skills/${created.body._id}`)
.patch(`/v1/chat/skills/${created.body._id}`)
.send({ description: 'no version' });
expect(res.status).toBe(400);
});
@@ -470,16 +470,16 @@ describe('Skill routes', () => {
const created = await createSkillAsOwner();
setTestUser(testUsers.noAccess);
const res = await request(app)
.patch(`/api/skills/${created.body._id}`)
.patch(`/v1/chat/skills/${created.body._id}`)
.send({ expectedVersion: 1, description: 'nope' });
expect(res.status).toBe(403);
});
});
describe('DELETE /api/skills/:id', () => {
describe('DELETE /v1/chat/skills/:id', () => {
it('deletes and cascades ACL entries', async () => {
const created = await createSkillAsOwner();
const res = await request(app).delete(`/api/skills/${created.body._id}`);
const res = await request(app).delete(`/v1/chat/skills/${created.body._id}`);
expect(res.status).toBe(200);
expect(res.body.deleted).toBe(true);
@@ -493,33 +493,33 @@ describe('Skill routes', () => {
it('returns 403 for a non-owner', async () => {
const created = await createSkillAsOwner();
setTestUser(testUsers.noAccess);
const res = await request(app).delete(`/api/skills/${created.body._id}`);
const res = await request(app).delete(`/v1/chat/skills/${created.body._id}`);
expect(res.status).toBe(403);
});
});
describe('GET /api/skills/:id/files', () => {
describe('GET /v1/chat/skills/:id/files', () => {
it('returns an empty list for a skill with no files', async () => {
const created = await createSkillAsOwner();
const res = await request(app).get(`/api/skills/${created.body._id}/files`);
const res = await request(app).get(`/v1/chat/skills/${created.body._id}/files`);
expect(res.status).toBe(200);
expect(res.body.files).toEqual([]);
});
});
describe('POST /api/skills/:id/files (live)', () => {
describe('POST /v1/chat/skills/:id/files (live)', () => {
it('returns 400 when no file is provided', async () => {
const created = await createSkillAsOwner();
const res = await request(app).post(`/api/skills/${created.body._id}/files`);
const res = await request(app).post(`/v1/chat/skills/${created.body._id}/files`);
expect(res.status).toBe(400);
expect(res.body.error).toMatch(/no file/i);
});
});
describe('GET /api/skills/:id/files/:relativePath', () => {
describe('GET /v1/chat/skills/:id/files/:relativePath', () => {
it('returns SKILL.md content from skill body', async () => {
const created = await createSkillAsOwner();
const res = await request(app).get(`/api/skills/${created.body._id}/files/SKILL.md`);
const res = await request(app).get(`/v1/chat/skills/${created.body._id}/files/SKILL.md`);
expect(res.status).toBe(200);
expect(res.body.mimeType).toBe('text/markdown');
expect(res.body.isBinary).toBe(false);
@@ -530,13 +530,13 @@ describe('Skill routes', () => {
it('returns 404 for a nonexistent file', async () => {
const created = await createSkillAsOwner();
const res = await request(app).get(
`/api/skills/${created.body._id}/files/scripts%2Fmissing.sh`,
`/v1/chat/skills/${created.body._id}/files/scripts%2Fmissing.sh`,
);
expect(res.status).toBe(404);
});
});
describe('DELETE /api/skills/:id/files/:relativePath', () => {
describe('DELETE /v1/chat/skills/:id/files/:relativePath', () => {
const { upsertSkillFile } = require('~/models');
it('deletes an existing skill file, bumps skill version, and returns 200', async () => {
@@ -553,12 +553,12 @@ describe('Skill routes', () => {
author: testUsers.owner._id,
});
const beforeSkill = await request(app).get(`/api/skills/${created.body._id}`);
const beforeSkill = await request(app).get(`/v1/chat/skills/${created.body._id}`);
expect(beforeSkill.body.fileCount).toBe(1);
expect(beforeSkill.body.version).toBe(2);
const res = await request(app).delete(
`/api/skills/${created.body._id}/files/scripts%2Fparse.sh`,
`/v1/chat/skills/${created.body._id}/files/scripts%2Fparse.sh`,
);
expect(res.status).toBe(200);
expect(res.body).toEqual({
@@ -567,7 +567,7 @@ describe('Skill routes', () => {
deleted: true,
});
const afterSkill = await request(app).get(`/api/skills/${created.body._id}`);
const afterSkill = await request(app).get(`/v1/chat/skills/${created.body._id}`);
expect(afterSkill.body.fileCount).toBe(0);
expect(afterSkill.body.version).toBe(3);
});
@@ -575,7 +575,7 @@ describe('Skill routes', () => {
it('returns 404 when the file does not exist', async () => {
const created = await createSkillAsOwner();
const res = await request(app).delete(
`/api/skills/${created.body._id}/files/scripts%2Fmissing.sh`,
`/v1/chat/skills/${created.body._id}/files/scripts%2Fmissing.sh`,
);
expect(res.status).toBe(404);
});
@@ -584,7 +584,7 @@ describe('Skill routes', () => {
const created = await createSkillAsOwner();
setTestUser(testUsers.noAccess);
const res = await request(app).delete(
`/api/skills/${created.body._id}/files/scripts%2Fparse.sh`,
`/v1/chat/skills/${created.body._id}/files/scripts%2Fparse.sh`,
);
expect(res.status).toBe(403);
});
@@ -604,12 +604,12 @@ describe('Skill routes', () => {
setTestUser(testUsers.editor);
const res = await request(app)
.patch(`/api/skills/${created.body._id}`)
.patch(`/v1/chat/skills/${created.body._id}`)
.send({ expectedVersion: 1, description: 'Edited by editor' });
expect(res.status).toBe(200);
// Editor should NOT be able to delete
const del = await request(app).delete(`/api/skills/${created.body._id}`);
const del = await request(app).delete(`/v1/chat/skills/${created.body._id}`);
expect(del.status).toBe(403);
});
});
+2 -2
View File
@@ -176,7 +176,7 @@ async function createActionTool({
const stateToken = jwt.sign(statePayload, JWT_SECRET, { expiresIn: '10m' });
try {
const redirectUri = `${process.env.DOMAIN_CLIENT}/api/actions/${action_id}/oauth/callback`;
const redirectUri = `${process.env.DOMAIN_CLIENT}/v1/chat/actions/${action_id}/oauth/callback`;
const params = new URLSearchParams({
client_id: metadata.oauth_client_id,
scope: metadata.auth.scope,
@@ -222,7 +222,7 @@ async function createActionTool({
state: stateToken,
userId: userId,
client_url: metadata.auth.client_url,
redirect_uri: `${process.env.DOMAIN_SERVER}/api/actions/${action_id}/oauth/callback`,
redirect_uri: `${process.env.DOMAIN_SERVER}/v1/chat/actions/${action_id}/oauth/callback`,
token_exchange_method: metadata.auth.token_exchange_method,
/** Encrypted values */
encrypted_oauth_client_id: encrypted.oauth_client_id,
+1 -1
View File
@@ -421,7 +421,7 @@ const setAuthTokens = async (userId, res, _session = null) => {
*
* This is DELIBERATELY decoupled from the token-refresh strategy. It runs on
* EVERY OpenID login regardless of `OPENID_REUSE_TOKENS`; that flag alone still
* governs whether `/api/auth/refresh` performs an OIDC refresh-grant. It writes
* governs whether `/v1/chat/auth/refresh` performs an OIDC refresh-grant. It writes
* only server-side session state (no `token_provider`, `refreshToken`, or other
* auth cookie), so it cannot alter the browser-facing login/refresh cookies a
* REUSE-disabled login stays byte-identical.
+1 -1
View File
@@ -48,7 +48,7 @@ const createDownloadFallback = ({
const basePath = getBasePath();
return {
filename: name,
filepath: `${basePath}/api/files/code/download/${session_id}/${id}`,
filepath: `${basePath}/v1/chat/files/code/download/${session_id}/${id}`,
expiresAt,
conversationId,
toolCallId,
@@ -286,7 +286,7 @@ describe('Code Process', () => {
const result = await processCodeOutput(baseParams);
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('exceeds size limit'));
expect(result.filepath).toContain('/api/files/code/download/session-123/file-id-123');
expect(result.filepath).toContain('/v1/chat/files/code/download/session-123/file-id-123');
expect(result.expiresAt).toBeDefined();
// Should not call createFile for oversized files (fallback path)
expect(createFile).not.toHaveBeenCalled();
@@ -307,7 +307,7 @@ describe('Code Process', () => {
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('saveBuffer not available'),
);
expect(result.filepath).toContain('/api/files/code/download/');
expect(result.filepath).toContain('/v1/chat/files/code/download/');
expect(result.filename).toBe('test-file.txt');
});
@@ -316,7 +316,7 @@ describe('Code Process', () => {
const result = await processCodeOutput(baseParams);
expect(result.filepath).toContain('/api/files/code/download/session-123/file-id-123');
expect(result.filepath).toContain('/v1/chat/files/code/download/session-123/file-id-123');
expect(result.conversationId).toBe('conv-123');
expect(result.messageId).toBe('msg-123');
expect(result.toolCallId).toBe('tool-call-123');
+1 -1
View File
@@ -55,7 +55,7 @@ const buildGuestPrincipal = (id) => ({
});
/**
* Builds the guest-scoped `/api/user` response: the ephemeral principal only.
* Builds the guest-scoped `/v1/chat/user` response: the ephemeral principal only.
* Mirrors the safe-field shape the client expects (no password/totp/email/db id).
*
* @param {{ id: string }} principal
+1 -1
View File
@@ -674,7 +674,7 @@ const setupOpenIdAdmin = (openidConfig) => {
scope: process.env.OPENID_SCOPE,
usePKCE: isEnabled(process.env.OPENID_USE_PKCE),
clockTolerance: process.env.OPENID_CLOCK_TOLERANCE || 300,
callbackURL: process.env.DOMAIN_SERVER + '/api/admin/oauth/openid/callback',
callbackURL: process.env.DOMAIN_SERVER + '/v1/chat/admin/oauth/openid/callback',
},
createOpenIDCallback(true),
);
+1 -1
View File
@@ -61,7 +61,7 @@ const createReq = (overrides = {}) => ({
headers: { 'user-agent': 'Mozilla/5.0' },
body: {},
baseUrl: '/api',
originalUrl: '/api/test',
originalUrl: '/v1/chat/test',
...overrides,
});
-763
View File
@@ -1,763 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="jest tests" tests="337" failures="0" errors="0" time="9.042">
<testsuite name="i18next translation tests" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:01" time="0.697" tests="6">
<testcase classname="i18next translation tests should return the correct translation for a valid key in English" name="i18next translation tests should return the correct translation for a valid key in English" time="0.005">
</testcase>
<testcase classname="i18next translation tests should return the correct translation for a valid key in French" name="i18next translation tests should return the correct translation for a valid key in French" time="0.001">
</testcase>
<testcase classname="i18next translation tests should return the correct translation for a valid key in Spanish" name="i18next translation tests should return the correct translation for a valid key in Spanish" time="0">
</testcase>
<testcase classname="i18next translation tests should fallback to English for an invalid language code" name="i18next translation tests should fallback to English for an invalid language code" time="0.001">
</testcase>
<testcase classname="i18next translation tests should return the key itself for an invalid key" name="i18next translation tests should return the key itself for an invalid key" time="0.001">
</testcase>
<testcase classname="i18next translation tests should correctly format placeholders in the translation" name="i18next translation tests should correctly format placeholders in the translation" time="0">
</testcase>
</testsuite>
<testsuite name="AgentFooter" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:01" time="1.005" tests="8">
<testcase classname="AgentFooter Main Functionality renders with standard components based on default state" name="AgentFooter Main Functionality renders with standard components based on default state" time="0.026">
</testcase>
<testcase classname="AgentFooter Main Functionality handles loading states for createMutation" name="AgentFooter Main Functionality handles loading states for createMutation" time="0.066">
</testcase>
<testcase classname="AgentFooter Main Functionality handles loading states for updateMutation" name="AgentFooter Main Functionality handles loading states for updateMutation" time="0.002">
</testcase>
<testcase classname="AgentFooter Conditional Rendering adjusts UI based on activePanel state" name="AgentFooter Conditional Rendering adjusts UI based on activePanel state" time="0.002">
</testcase>
<testcase classname="AgentFooter Conditional Rendering adjusts UI based on agent ID existence" name="AgentFooter Conditional Rendering adjusts UI based on agent ID existence" time="0.002">
</testcase>
<testcase classname="AgentFooter Conditional Rendering adjusts UI based on user role" name="AgentFooter Conditional Rendering adjusts UI based on user role" time="0.004">
</testcase>
<testcase classname="AgentFooter Conditional Rendering adjusts UI based on permissions" name="AgentFooter Conditional Rendering adjusts UI based on permissions" time="0.001">
</testcase>
<testcase classname="AgentFooter Edge Cases handles null agent data" name="AgentFooter Edge Cases handles null agent data" time="0.001">
</testcase>
</testsuite>
<testsuite name="Button" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:01" time="1.081" tests="2">
<testcase classname="Button renders with the correct type and children" name="Button renders with the correct type and children" time="0.023">
</testcase>
<testcase classname="Button calls onClick when clicked" name="Button calls onClick when clicked" time="0.006">
</testcase>
</testsuite>
<testsuite name="useCopyToClipboard" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:02" time="0.121" tests="15">
<testcase classname="useCopyToClipboard Basic functionality should copy plain text without citations" name="useCopyToClipboard Basic functionality should copy plain text without citations" time="0.006">
</testcase>
<testcase classname="useCopyToClipboard Basic functionality should handle content array with text types" name="useCopyToClipboard Basic functionality should handle content array with text types" time="0.002">
</testcase>
<testcase classname="useCopyToClipboard Basic functionality should reset isCopied after timeout" name="useCopyToClipboard Basic functionality should reset isCopied after timeout" time="0.001">
</testcase>
<testcase classname="useCopyToClipboard Citation formatting should format standalone search citations" name="useCopyToClipboard Citation formatting should format standalone search citations" time="0.002">
</testcase>
<testcase classname="useCopyToClipboard Citation formatting should format news citations with correct mapping" name="useCopyToClipboard Citation formatting should format news citations with correct mapping" time="0.002">
</testcase>
<testcase classname="useCopyToClipboard Citation formatting should handle highlighted text with citations" name="useCopyToClipboard Citation formatting should handle highlighted text with citations" time="0.001">
</testcase>
<testcase classname="useCopyToClipboard Citation formatting should handle composite citations" name="useCopyToClipboard Citation formatting should handle composite citations" time="0.001">
</testcase>
<testcase classname="useCopyToClipboard Citation deduplication should use same number for duplicate URLs" name="useCopyToClipboard Citation deduplication should use same number for duplicate URLs" time="0.001">
</testcase>
<testcase classname="useCopyToClipboard Citation deduplication should handle multiple citations of the same source" name="useCopyToClipboard Citation deduplication should handle multiple citations of the same source" time="0.002">
</testcase>
<testcase classname="useCopyToClipboard Edge cases should handle missing search results gracefully" name="useCopyToClipboard Edge cases should handle missing search results gracefully" time="0">
</testcase>
<testcase classname="useCopyToClipboard Edge cases should handle invalid citation indices" name="useCopyToClipboard Edge cases should handle invalid citation indices" time="0">
</testcase>
<testcase classname="useCopyToClipboard Edge cases should handle citations without links" name="useCopyToClipboard Edge cases should handle citations without links" time="0.001">
</testcase>
<testcase classname="useCopyToClipboard Edge cases should clean up orphaned citation lists at the end" name="useCopyToClipboard Edge cases should clean up orphaned citation lists at the end" time="0">
</testcase>
<testcase classname="useCopyToClipboard All citation types should handle all citation types correctly" name="useCopyToClipboard All citation types should handle all citation types correctly" time="0.001">
</testcase>
<testcase classname="useCopyToClipboard Complex scenarios should handle mixed highlighted text and composite citations" name="useCopyToClipboard Complex scenarios should handle mixed highlighted text and composite citations" time="0">
</testcase>
</testsuite>
<testsuite name="Conversation Utilities" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:02" time="0.549" tests="36">
<testcase classname="Conversation Utilities groupConversationsByDate groups conversations by date correctly" name="Conversation Utilities groupConversationsByDate groups conversations by date correctly" time="0.002">
</testcase>
<testcase classname="Conversation Utilities groupConversationsByDate skips conversations with duplicate conversationIds" name="Conversation Utilities groupConversationsByDate skips conversations with duplicate conversationIds" time="0.001">
</testcase>
<testcase classname="Conversation Utilities groupConversationsByDate sorts conversations by month correctly" name="Conversation Utilities groupConversationsByDate sorts conversations by month correctly" time="0.001">
</testcase>
<testcase classname="Conversation Utilities groupConversationsByDate handles conversations from multiple years correctly" name="Conversation Utilities groupConversationsByDate handles conversations from multiple years correctly" time="0">
</testcase>
<testcase classname="Conversation Utilities groupConversationsByDate handles conversations from the same month correctly" name="Conversation Utilities groupConversationsByDate handles conversations from the same month correctly" time="0">
</testcase>
<testcase classname="Conversation Utilities groupConversationsByDate handles conversations with null or undefined updatedAt correctly" name="Conversation Utilities groupConversationsByDate handles conversations with null or undefined updatedAt correctly" time="0">
</testcase>
<testcase classname="Conversation Utilities groupConversationsByDate correctly groups and sorts conversations for every month of the year" name="Conversation Utilities groupConversationsByDate correctly groups and sorts conversations for every month of the year" time="0.001">
</testcase>
<testcase classname="Conversation Utilities normalizeConversationData normalizes the number of items on each page after data removal" name="Conversation Utilities normalizeConversationData normalizes the number of items on each page after data removal" time="0.001">
</testcase>
<testcase classname="Conversation Utilities normalizeConversationData normalizes the number of items on each page after data addition" name="Conversation Utilities normalizeConversationData normalizes the number of items on each page after data addition" time="0.001">
</testcase>
<testcase classname="Conversation Utilities normalizeConversationData returns empty data when there is no data" name="Conversation Utilities normalizeConversationData returns empty data when there is no data" time="0">
</testcase>
<testcase classname="Conversation Utilities normalizeConversationData does not normalize data when not needed" name="Conversation Utilities normalizeConversationData does not normalize data when not needed" time="0">
</testcase>
<testcase classname="Conversation Utilities normalizeConversationData deletes pages that have no data as a result of normalization" name="Conversation Utilities normalizeConversationData deletes pages that have no data as a result of normalization" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers findConversationInInfinite finds a conversation by id in InfiniteData" name="Conversation Utilities InfiniteData helpers findConversationInInfinite finds a conversation by id in InfiniteData" time="0.001">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers findConversationInInfinite returns undefined if conversation not found" name="Conversation Utilities InfiniteData helpers findConversationInInfinite returns undefined if conversation not found" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers findConversationInInfinite returns undefined if data is undefined" name="Conversation Utilities InfiniteData helpers findConversationInInfinite returns undefined if data is undefined" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers updateInfiniteConvoPage updates a conversation in InfiniteData" name="Conversation Utilities InfiniteData helpers updateInfiniteConvoPage updates a conversation in InfiniteData" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers updateInfiniteConvoPage returns original data if conversation not found" name="Conversation Utilities InfiniteData helpers updateInfiniteConvoPage returns original data if conversation not found" time="0.001">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers updateInfiniteConvoPage returns undefined if data is undefined" name="Conversation Utilities InfiniteData helpers updateInfiniteConvoPage returns undefined if data is undefined" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers addConversationToInfinitePages adds a conversation to the first page" name="Conversation Utilities InfiniteData helpers addConversationToInfinitePages adds a conversation to the first page" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers addConversationToInfinitePages creates new InfiniteData if data is undefined" name="Conversation Utilities InfiniteData helpers addConversationToInfinitePages creates new InfiniteData if data is undefined" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers removeConvoFromInfinitePages removes a conversation by id" name="Conversation Utilities InfiniteData helpers removeConvoFromInfinitePages removes a conversation by id" time="0.008">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers removeConvoFromInfinitePages removes empty pages after deletion" name="Conversation Utilities InfiniteData helpers removeConvoFromInfinitePages removes empty pages after deletion" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers removeConvoFromInfinitePages returns original data if data is undefined" name="Conversation Utilities InfiniteData helpers removeConvoFromInfinitePages returns original data if data is undefined" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers updateConvoFieldsInfinite updates fields and bumps to front if keepPosition is false" name="Conversation Utilities InfiniteData helpers updateConvoFieldsInfinite updates fields and bumps to front if keepPosition is false" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers updateConvoFieldsInfinite updates fields and keeps position if keepPosition is true" name="Conversation Utilities InfiniteData helpers updateConvoFieldsInfinite updates fields and keeps position if keepPosition is true" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers updateConvoFieldsInfinite returns original data if conversation not found" name="Conversation Utilities InfiniteData helpers updateConvoFieldsInfinite returns original data if conversation not found" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers updateConvoFieldsInfinite returns original data if data is undefined" name="Conversation Utilities InfiniteData helpers updateConvoFieldsInfinite returns original data if data is undefined" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers storeEndpointSettings stores model for endpoint" name="Conversation Utilities InfiniteData helpers storeEndpointSettings stores model for endpoint" time="0.001">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers storeEndpointSettings stores secondaryModel for gptPlugins endpoint" name="Conversation Utilities InfiniteData helpers storeEndpointSettings stores secondaryModel for gptPlugins endpoint" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers storeEndpointSettings does nothing if conversation is null" name="Conversation Utilities InfiniteData helpers storeEndpointSettings does nothing if conversation is null" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers storeEndpointSettings does nothing if endpoint is missing" name="Conversation Utilities InfiniteData helpers storeEndpointSettings does nothing if endpoint is missing" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers QueryClient helpers addConvoToAllQueries adds new on top if not present" name="Conversation Utilities InfiniteData helpers QueryClient helpers addConvoToAllQueries adds new on top if not present" time="0.001">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers QueryClient helpers addConvoToAllQueries does not duplicate" name="Conversation Utilities InfiniteData helpers QueryClient helpers addConvoToAllQueries does not duplicate" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers QueryClient helpers updateConvoInAllQueries updates correct convo" name="Conversation Utilities InfiniteData helpers QueryClient helpers updateConvoInAllQueries updates correct convo" time="0">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers QueryClient helpers removeConvoFromAllQueries deletes conversation" name="Conversation Utilities InfiniteData helpers QueryClient helpers removeConvoFromAllQueries deletes conversation" time="0.001">
</testcase>
<testcase classname="Conversation Utilities InfiniteData helpers QueryClient helpers addConversationToAllConversationsQueries works with multiple pages" name="Conversation Utilities InfiniteData helpers QueryClient helpers addConversationToAllConversationsQueries works with multiple pages" time="0">
</testcase>
</testsuite>
<testsuite name="cleanupPreset" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.074" tests="14">
<testcase classname="cleanupPreset chatGptLabel migration should migrate chatGptLabel to modelLabel when only chatGptLabel exists" name="cleanupPreset chatGptLabel migration should migrate chatGptLabel to modelLabel when only chatGptLabel exists" time="0.001">
</testcase>
<testcase classname="cleanupPreset chatGptLabel migration should prioritize modelLabel over chatGptLabel when both exist" name="cleanupPreset chatGptLabel migration should prioritize modelLabel over chatGptLabel when both exist" time="0.006">
</testcase>
<testcase classname="cleanupPreset chatGptLabel migration should keep modelLabel when only modelLabel exists" name="cleanupPreset chatGptLabel migration should keep modelLabel when only modelLabel exists" time="0">
</testcase>
<testcase classname="cleanupPreset chatGptLabel migration should handle preset without either label" name="cleanupPreset chatGptLabel migration should handle preset without either label" time="0">
</testcase>
<testcase classname="cleanupPreset chatGptLabel migration should handle empty chatGptLabel" name="cleanupPreset chatGptLabel migration should handle empty chatGptLabel" time="0.001">
</testcase>
<testcase classname="cleanupPreset chatGptLabel migration should not migrate empty string chatGptLabel when modelLabel exists" name="cleanupPreset chatGptLabel migration should not migrate empty string chatGptLabel when modelLabel exists" time="0">
</testcase>
<testcase classname="cleanupPreset presetOverride handling should apply presetOverride and then handle label migration" name="cleanupPreset presetOverride handling should apply presetOverride and then handle label migration" time="0">
</testcase>
<testcase classname="cleanupPreset presetOverride handling should handle label migration in presetOverride" name="cleanupPreset presetOverride handling should handle label migration in presetOverride" time="0">
</testcase>
<testcase classname="cleanupPreset error handling should handle undefined preset" name="cleanupPreset error handling should handle undefined preset" time="0">
</testcase>
<testcase classname="cleanupPreset error handling should handle preset with null endpoint" name="cleanupPreset error handling should handle preset with null endpoint" time="0.001">
</testcase>
<testcase classname="cleanupPreset error handling should handle preset with empty string endpoint" name="cleanupPreset error handling should handle preset with empty string endpoint" time="0">
</testcase>
<testcase classname="cleanupPreset normal preset properties should preserve all other preset properties" name="cleanupPreset normal preset properties should preserve all other preset properties" time="0">
</testcase>
<testcase classname="cleanupPreset normal preset properties should generate default title when title is missing" name="cleanupPreset normal preset properties should generate default title when title is missing" time="0">
</testcase>
<testcase classname="cleanupPreset normal preset properties should handle null presetId" name="cleanupPreset normal preset properties should handle null presetId" time="0.001">
</testcase>
</testsuite>
<testsuite name="VersionItem" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:02" time="0.898" tests="10">
<testcase classname="VersionItem renders version number and timestamp" name="VersionItem renders version number and timestamp" time="0.05">
</testcase>
<testcase classname="VersionItem active version badge and no restore button when active" name="VersionItem active version badge and no restore button when active" time="0.05">
</testcase>
<testcase classname="VersionItem restore button and no active badge when not active" name="VersionItem restore button and no active badge when not active" time="0.002">
</testcase>
<testcase classname="VersionItem restore confirmation flow - confirmed" name="VersionItem restore confirmation flow - confirmed" time="0.032">
</testcase>
<testcase classname="VersionItem restore confirmation flow - canceled" name="VersionItem restore confirmation flow - canceled" time="0.003">
</testcase>
<testcase classname="VersionItem handles invalid timestamp" name="VersionItem handles invalid timestamp" time="0.003">
</testcase>
<testcase classname="VersionItem handles missing timestamps" name="VersionItem handles missing timestamps" time="0.009">
</testcase>
<testcase classname="VersionItem prefers updatedAt over createdAt when both exist" name="VersionItem prefers updatedAt over createdAt when both exist" time="0.002">
</testcase>
<testcase classname="VersionItem falls back to createdAt when updatedAt is missing" name="VersionItem falls back to createdAt when updatedAt is missing" time="0.002">
</testcase>
<testcase classname="VersionItem handles empty version object" name="VersionItem handles empty version object" time="0.001">
</testcase>
</testsuite>
<testsuite name="useHealthCheck" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.12" tests="14">
<testcase classname="useHealthCheck when not authenticated should not start health check" name="useHealthCheck when not authenticated should not start health check" time="0.002">
</testcase>
<testcase classname="useHealthCheck when authenticated should start health check after delay" name="useHealthCheck when authenticated should start health check after delay" time="0.002">
</testcase>
<testcase classname="useHealthCheck when authenticated should set up 10-minute interval" name="useHealthCheck when authenticated should set up 10-minute interval" time="0.003">
</testcase>
<testcase classname="useHealthCheck when authenticated should run health check continuously every 10 minutes" name="useHealthCheck when authenticated should run health check continuously every 10 minutes" time="0.002">
</testcase>
<testcase classname="useHealthCheck when authenticated should add window focus event listener" name="useHealthCheck when authenticated should add window focus event listener" time="0.001">
</testcase>
<testcase classname="useHealthCheck when authenticated should handle window focus correctly when no previous check" name="useHealthCheck when authenticated should handle window focus correctly when no previous check" time="0.001">
</testcase>
<testcase classname="useHealthCheck when authenticated should handle window focus correctly when check is recent" name="useHealthCheck when authenticated should handle window focus correctly when check is recent" time="0.001">
</testcase>
<testcase classname="useHealthCheck when authenticated should handle window focus correctly when check is old" name="useHealthCheck when authenticated should handle window focus correctly when check is old" time="0.001">
</testcase>
<testcase classname="useHealthCheck when authenticated should prevent multiple initializations" name="useHealthCheck when authenticated should prevent multiple initializations" time="0.001">
</testcase>
<testcase classname="useHealthCheck when authenticated should handle API errors gracefully" name="useHealthCheck when authenticated should handle API errors gracefully" time="0.001">
</testcase>
<testcase classname="useHealthCheck cleanup should clear intervals on unmount" name="useHealthCheck cleanup should clear intervals on unmount" time="0.001">
</testcase>
<testcase classname="useHealthCheck cleanup should remove event listeners on unmount" name="useHealthCheck cleanup should remove event listeners on unmount" time="0">
</testcase>
<testcase classname="useHealthCheck cleanup should clear timeout on unmount before initialization" name="useHealthCheck cleanup should clear timeout on unmount before initialization" time="0.001">
</testcase>
<testcase classname="useHealthCheck authentication state changes should start health check when authentication becomes true" name="useHealthCheck authentication state changes should start health check when authentication becomes true" time="0.001">
</testcase>
</testsuite>
<testsuite name="getEndpointField" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.068" tests="11">
<testcase classname="getEndpointField returns undefined if endpointsConfig is undefined" name="getEndpointField returns undefined if endpointsConfig is undefined" time="0">
</testcase>
<testcase classname="getEndpointField returns undefined if endpoint is null" name="getEndpointField returns undefined if endpoint is null" time="0">
</testcase>
<testcase classname="getEndpointField returns undefined if endpoint is undefined" name="getEndpointField returns undefined if endpoint is undefined" time="0">
</testcase>
<testcase classname="getEndpointField returns the correct value for a valid endpoint and property" name="getEndpointField returns the correct value for a valid endpoint and property" time="0.001">
</testcase>
<testcase classname="getEndpointField returns undefined for a valid endpoint but an invalid property" name="getEndpointField returns undefined for a valid endpoint but an invalid property" time="0">
</testcase>
<testcase classname="getEndpointField returns the correct value for a non-enum endpoint and valid property" name="getEndpointField returns the correct value for a non-enum endpoint and valid property" time="0">
</testcase>
<testcase classname="getEndpointField returns undefined for a non-enum endpoint with an invalid property" name="getEndpointField returns undefined for a non-enum endpoint with an invalid property" time="0">
</testcase>
<testcase classname="getEndpointsFilter returns an empty object if endpointsConfig is undefined" name="getEndpointsFilter returns an empty object if endpointsConfig is undefined" time="0">
</testcase>
<testcase classname="getEndpointsFilter returns a filter object based on endpointsConfig" name="getEndpointsFilter returns a filter object based on endpointsConfig" time="0">
</testcase>
<testcase classname="getAvailableEndpoints returns available endpoints based on filter and config" name="getAvailableEndpoints returns available endpoints based on filter and config" time="0">
</testcase>
<testcase classname="mapEndpoints returns sorted available endpoints" name="mapEndpoints returns sorted available endpoints" time="0.001">
</testcase>
</testsuite>
<testsuite name="SplitText" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.264" tests="1">
<testcase classname="SplitText renders emojis correctly" name="SplitText renders emojis correctly" time="0.04">
</testcase>
</testsuite>
<testsuite name="useQueryParams" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:02" time="1.12" tests="6">
<testcase classname="useQueryParams should process query parameters on initial render" name="useQueryParams should process query parameters on initial render" time="0.121">
</testcase>
<testcase classname="useQueryParams should auto-submit message when submit=true and no settings to apply" name="useQueryParams should auto-submit message when submit=true and no settings to apply" time="0.011">
</testcase>
<testcase classname="useQueryParams should defer submission when settings need to be applied first" name="useQueryParams should defer submission when settings need to be applied first" time="0.153">
</testcase>
<testcase classname="useQueryParams should submit after timeout if settings never get applied" name="useQueryParams should submit after timeout if settings never get applied" time="0.003">
</testcase>
<testcase classname="useQueryParams should mark as submitted when no submit parameter is present" name="useQueryParams should mark as submitted when no submit parameter is present" time="0.005">
</testcase>
<testcase classname="useQueryParams should handle empty query parameters" name="useQueryParams should handle empty query parameters" time="0.002">
</testcase>
</testsuite>
<testsuite name="VersionContent" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.081" tests="3">
<testcase classname="VersionContent renders different UI states correctly" name="VersionContent renders different UI states correctly" time="0.006">
</testcase>
<testcase classname="VersionContent restore functionality works correctly" name="VersionContent restore functionality works correctly" time="0.002">
</testcase>
<testcase classname="VersionContent handles edge cases in data" name="VersionContent handles edge cases in data" time="0.003">
</testcase>
</testsuite>
<testsuite name="presets utils" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.179" tests="27">
<testcase classname="presets utils getPresetTitle with modelLabel should use modelLabel as the label" name="presets utils getPresetTitle with modelLabel should use modelLabel as the label" time="0.001">
</testcase>
<testcase classname="presets utils getPresetTitle with modelLabel should prioritize modelLabel over deprecated chatGptLabel" name="presets utils getPresetTitle with modelLabel should prioritize modelLabel over deprecated chatGptLabel" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle with modelLabel should handle title that includes the label" name="presets utils getPresetTitle with modelLabel should handle title that includes the label" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle with modelLabel should handle case-insensitive title matching" name="presets utils getPresetTitle with modelLabel should handle case-insensitive title matching" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle with modelLabel should use label as title when label includes the title" name="presets utils getPresetTitle with modelLabel should use label as title when label includes the title" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle without modelLabel should work without modelLabel" name="presets utils getPresetTitle without modelLabel should work without modelLabel" time="0.001">
</testcase>
<testcase classname="presets utils getPresetTitle without modelLabel should handle empty modelLabel" name="presets utils getPresetTitle without modelLabel should handle empty modelLabel" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle without modelLabel should handle null modelLabel" name="presets utils getPresetTitle without modelLabel should handle null modelLabel" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle title variations should handle missing title" name="presets utils getPresetTitle title variations should handle missing title" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle title variations should handle empty title" name="presets utils getPresetTitle title variations should handle empty title" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle title variations should handle &quot;New Chat&quot; title" name="presets utils getPresetTitle title variations should handle &quot;New Chat&quot; title" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle title variations should handle title with whitespace" name="presets utils getPresetTitle title variations should handle title with whitespace" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle mention mode should return mention format with all components" name="presets utils getPresetTitle mention mode should return mention format with all components" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle mention mode should handle mention format with object tools" name="presets utils getPresetTitle mention mode should handle mention format with object tools" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle mention mode should handle mention format with minimal data" name="presets utils getPresetTitle mention mode should handle mention format with minimal data" time="0.001">
</testcase>
<testcase classname="presets utils getPresetTitle mention mode should handle mention format with only modelLabel" name="presets utils getPresetTitle mention mode should handle mention format with only modelLabel" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle mention mode should handle mention format with only promptPrefix" name="presets utils getPresetTitle mention mode should handle mention format with only promptPrefix" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle edge cases should handle missing model" name="presets utils getPresetTitle edge cases should handle missing model" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle edge cases should handle undefined model" name="presets utils getPresetTitle edge cases should handle undefined model" time="0">
</testcase>
<testcase classname="presets utils getPresetTitle edge cases should trim the final result" name="presets utils getPresetTitle edge cases should trim the final result" time="0">
</testcase>
<testcase classname="presets utils removeUnavailableTools should remove unavailable tools from string array" name="presets utils removeUnavailableTools should remove unavailable tools from string array" time="0">
</testcase>
<testcase classname="presets utils removeUnavailableTools should remove unavailable tools from object array" name="presets utils removeUnavailableTools should remove unavailable tools from object array" time="0">
</testcase>
<testcase classname="presets utils removeUnavailableTools should handle preset without tools" name="presets utils removeUnavailableTools should handle preset without tools" time="0.001">
</testcase>
<testcase classname="presets utils removeUnavailableTools should handle preset with empty tools array" name="presets utils removeUnavailableTools should handle preset with empty tools array" time="0">
</testcase>
<testcase classname="presets utils removeUnavailableTools should remove all tools when none are available" name="presets utils removeUnavailableTools should remove all tools when none are available" time="0">
</testcase>
<testcase classname="presets utils removeUnavailableTools should preserve all other preset properties" name="presets utils removeUnavailableTools should preserve all other preset properties" time="0">
</testcase>
<testcase classname="presets utils removeUnavailableTools should not mutate the original preset" name="presets utils removeUnavailableTools should not mutate the original preset" time="0">
</testcase>
</testsuite>
<testsuite name="Store Atoms Validation" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:02" time="0.56" tests="4">
<testcase classname="Store Atoms Validation should have all required atoms defined" name="Store Atoms Validation should have all required atoms defined" time="0.005">
</testcase>
<testcase classname="Store Atoms Validation should not have any undefined atoms when destructuring" name="Store Atoms Validation should not have any undefined atoms when destructuring" time="0.001">
</testcase>
<testcase classname="Store Atoms Validation should have all user atoms defined" name="Store Atoms Validation should have all user atoms defined" time="0">
</testcase>
<testcase classname="Store Atoms Validation should verify all atoms are properly typed" name="Store Atoms Validation should verify all atoms are properly typed" time="0">
</testcase>
</testsuite>
<testsuite name="useFocusChatEffect" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:02" time="1.226" tests="17">
<testcase classname="useFocusChatEffect Basic functionality should focus textarea when location.state.focusChat is true" name="useFocusChatEffect Basic functionality should focus textarea when location.state.focusChat is true" time="0.014">
</testcase>
<testcase classname="useFocusChatEffect Basic functionality should not focus textarea when location.state.focusChat is false" name="useFocusChatEffect Basic functionality should not focus textarea when location.state.focusChat is false" time="0.002">
</testcase>
<testcase classname="useFocusChatEffect Basic functionality should not focus textarea when textAreaRef.current is null" name="useFocusChatEffect Basic functionality should not focus textarea when textAreaRef.current is null" time="0.001">
</testcase>
<testcase classname="useFocusChatEffect Basic functionality should not focus textarea on touchscreen devices" name="useFocusChatEffect Basic functionality should not focus textarea on touchscreen devices" time="0.001">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should use window.location.search instead of location.search" name="useFocusChatEffect URL parameter handling should use window.location.search instead of location.search" time="0.007">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should prioritize window.location.search with agent_id parameter" name="useFocusChatEffect URL parameter handling should prioritize window.location.search with agent_id parameter" time="0">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should use empty path when window.location.search is empty" name="useFocusChatEffect URL parameter handling should use empty path when window.location.search is empty" time="0.001">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should use window.location.search when React Router search is empty" name="useFocusChatEffect URL parameter handling should use window.location.search when React Router search is empty" time="0.001">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should use window.location.search even when both have agent_id but with different values" name="useFocusChatEffect URL parameter handling should use window.location.search even when both have agent_id but with different values" time="0.001">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should handle URL parameters with special characters correctly" name="useFocusChatEffect URL parameter handling should handle URL parameters with special characters correctly" time="0.001">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should handle multiple URL parameters correctly" name="useFocusChatEffect URL parameter handling should handle multiple URL parameters correctly" time="0">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should pass through malformed URL parameters unchanged" name="useFocusChatEffect URL parameter handling should pass through malformed URL parameters unchanged" time="0.001">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should handle navigation immediately after URL parameter changes" name="useFocusChatEffect URL parameter handling should handle navigation immediately after URL parameter changes" time="0.001">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should handle undefined or null search params gracefully" name="useFocusChatEffect URL parameter handling should handle undefined or null search params gracefully" time="0">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should handle navigation when location.state is null" name="useFocusChatEffect URL parameter handling should handle navigation when location.state is null" time="0.001">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should handle navigation when location.state.focusChat is undefined" name="useFocusChatEffect URL parameter handling should handle navigation when location.state.focusChat is undefined" time="0">
</testcase>
<testcase classname="useFocusChatEffect URL parameter handling should handle navigation when both search params are empty" name="useFocusChatEffect URL parameter handling should handle navigation when both search params are empty" time="0.001">
</testcase>
</testsuite>
<testsuite name="getMemories" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.094" tests="1">
<testcase classname="getMemories should fetch memories from /api/memories" name="getMemories should fetch memories from /api/memories" time="0.004">
</testcase>
</testsuite>
<testsuite name="preprocessLaTeX" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.076" tests="33">
<testcase classname="preprocessLaTeX returns the same string if no LaTeX patterns are found" name="preprocessLaTeX returns the same string if no LaTeX patterns are found" time="0.001">
</testcase>
<testcase classname="preprocessLaTeX returns the same string if no dollar signs are present" name="preprocessLaTeX returns the same string if no dollar signs are present" time="0">
</testcase>
<testcase classname="preprocessLaTeX preserves valid inline LaTeX delimiters \(...\)" name="preprocessLaTeX preserves valid inline LaTeX delimiters \(...\)" time="0">
</testcase>
<testcase classname="preprocessLaTeX preserves valid block LaTeX delimiters \[...\]" name="preprocessLaTeX preserves valid block LaTeX delimiters \[...\]" time="0">
</testcase>
<testcase classname="preprocessLaTeX preserves valid double dollar delimiters" name="preprocessLaTeX preserves valid double dollar delimiters" time="0">
</testcase>
<testcase classname="preprocessLaTeX converts single dollar delimiters to double dollars" name="preprocessLaTeX converts single dollar delimiters to double dollars" time="0.001">
</testcase>
<testcase classname="preprocessLaTeX converts multiple single dollar expressions" name="preprocessLaTeX converts multiple single dollar expressions" time="0">
</testcase>
<testcase classname="preprocessLaTeX escapes currency dollar signs" name="preprocessLaTeX escapes currency dollar signs" time="0">
</testcase>
<testcase classname="preprocessLaTeX escapes currency with spaces" name="preprocessLaTeX escapes currency with spaces" time="0">
</testcase>
<testcase classname="preprocessLaTeX escapes currency with commas" name="preprocessLaTeX escapes currency with commas" time="0">
</testcase>
<testcase classname="preprocessLaTeX escapes currency with decimals" name="preprocessLaTeX escapes currency with decimals" time="0.001">
</testcase>
<testcase classname="preprocessLaTeX converts LaTeX expressions while escaping currency" name="preprocessLaTeX converts LaTeX expressions while escaping currency" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles Goldbach Conjecture example" name="preprocessLaTeX handles Goldbach Conjecture example" time="0">
</testcase>
<testcase classname="preprocessLaTeX does not escape already escaped dollar signs" name="preprocessLaTeX does not escape already escaped dollar signs" time="0">
</testcase>
<testcase classname="preprocessLaTeX does not convert already escaped single dollars" name="preprocessLaTeX does not convert already escaped single dollars" time="0">
</testcase>
<testcase classname="preprocessLaTeX escapes mhchem commands" name="preprocessLaTeX escapes mhchem commands" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles empty string" name="preprocessLaTeX handles empty string" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles complex mixed content" name="preprocessLaTeX handles complex mixed content" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles multiple equations with currency" name="preprocessLaTeX handles multiple equations with currency" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles inline code blocks" name="preprocessLaTeX handles inline code blocks" time="0.001">
</testcase>
<testcase classname="preprocessLaTeX handles multiline code blocks" name="preprocessLaTeX handles multiline code blocks" time="0">
</testcase>
<testcase classname="preprocessLaTeX preserves LaTeX expressions with special characters" name="preprocessLaTeX preserves LaTeX expressions with special characters" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles complex physics equations" name="preprocessLaTeX handles complex physics equations" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles financial calculations with currency" name="preprocessLaTeX handles financial calculations with currency" time="0">
</testcase>
<testcase classname="preprocessLaTeX does not convert partial or malformed expressions" name="preprocessLaTeX does not convert partial or malformed expressions" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles nested parentheses in LaTeX" name="preprocessLaTeX handles nested parentheses in LaTeX" time="0">
</testcase>
<testcase classname="preprocessLaTeX preserves spacing in equations" name="preprocessLaTeX preserves spacing in equations" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles LaTeX with newlines inside should not be converted" name="preprocessLaTeX handles LaTeX with newlines inside should not be converted" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles multiple dollar signs in text" name="preprocessLaTeX handles multiple dollar signs in text" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles complex LaTeX with currency in same expression" name="preprocessLaTeX handles complex LaTeX with currency in same expression" time="0">
</testcase>
<testcase classname="preprocessLaTeX preserves already escaped dollars in LaTeX" name="preprocessLaTeX preserves already escaped dollars in LaTeX" time="0.001">
</testcase>
<testcase classname="preprocessLaTeX handles adjacent LaTeX and currency" name="preprocessLaTeX handles adjacent LaTeX and currency" time="0">
</testcase>
<testcase classname="preprocessLaTeX handles LaTeX with special characters and currency" name="preprocessLaTeX handles LaTeX with special characters and currency" time="0">
</testcase>
</testsuite>
<testsuite name="isActiveVersion" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.074" tests="26">
<testcase classname="isActiveVersion returns true for the first version in versions array when currentAgent is null" name="isActiveVersion returns true for the first version in versions array when currentAgent is null" time="0.001">
</testcase>
<testcase classname="isActiveVersion returns true when all fields match exactly" name="isActiveVersion returns true when all fields match exactly" time="0">
</testcase>
<testcase classname="isActiveVersion returns false when names do not match" name="isActiveVersion returns false when names do not match" time="0.001">
</testcase>
<testcase classname="isActiveVersion returns false when descriptions do not match" name="isActiveVersion returns false when descriptions do not match" time="0">
</testcase>
<testcase classname="isActiveVersion returns false when instructions do not match" name="isActiveVersion returns false when instructions do not match" time="0">
</testcase>
<testcase classname="isActiveVersion returns false when artifacts do not match" name="isActiveVersion returns false when artifacts do not match" time="0">
</testcase>
<testcase classname="isActiveVersion matches tools regardless of order" name="isActiveVersion matches tools regardless of order" time="0.001">
</testcase>
<testcase classname="isActiveVersion returns false when tools arrays have different lengths" name="isActiveVersion returns false when tools arrays have different lengths" time="0">
</testcase>
<testcase classname="isActiveVersion returns false when tools do not match" name="isActiveVersion returns false when tools do not match" time="0">
</testcase>
<testcase classname="isActiveVersion matches capabilities regardless of order" name="isActiveVersion matches capabilities regardless of order" time="0">
</testcase>
<testcase classname="isActiveVersion returns false when capabilities arrays have different lengths" name="isActiveVersion returns false when capabilities arrays have different lengths" time="0">
</testcase>
<testcase classname="isActiveVersion returns false when capabilities do not match" name="isActiveVersion returns false when capabilities do not match" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles missing tools arrays" name="isActiveVersion edge cases handles missing tools arrays" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles when version has tools but agent does not" name="isActiveVersion edge cases handles when version has tools but agent does not" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles when agent has tools but version does not" name="isActiveVersion edge cases handles when agent has tools but version does not" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles missing capabilities arrays" name="isActiveVersion edge cases handles missing capabilities arrays" time="0.001">
</testcase>
<testcase classname="isActiveVersion edge cases handles when version has capabilities but agent does not" name="isActiveVersion edge cases handles when version has capabilities but agent does not" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles when agent has capabilities but version does not" name="isActiveVersion edge cases handles when agent has capabilities but version does not" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles null values in fields" name="isActiveVersion edge cases handles null values in fields" time="0.001">
</testcase>
<testcase classname="isActiveVersion edge cases handles empty versions array" name="isActiveVersion edge cases handles empty versions array" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles empty arrays for tools" name="isActiveVersion edge cases handles empty arrays for tools" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles empty arrays for capabilities" name="isActiveVersion edge cases handles empty arrays for capabilities" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles missing artifacts field" name="isActiveVersion edge cases handles missing artifacts field" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles when version has artifacts but agent does not" name="isActiveVersion edge cases handles when version has artifacts but agent does not" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles when agent has artifacts but version does not" name="isActiveVersion edge cases handles when agent has artifacts but version does not" time="0">
</testcase>
<testcase classname="isActiveVersion edge cases handles empty string for artifacts" name="isActiveVersion edge cases handles empty string for artifacts" time="0">
</testcase>
</testsuite>
<testsuite name="ConversationTag Utilities" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.077" tests="6">
<testcase classname="ConversationTag Utilities updateConversationTag updates the first tag correctly" name="ConversationTag Utilities updateConversationTag updates the first tag correctly" time="0.001">
</testcase>
<testcase classname="ConversationTag Utilities updates the third tag correctly" name="ConversationTag Utilities updates the third tag correctly" time="0.001">
</testcase>
<testcase classname="ConversationTag Utilities updates the order of other tags if the order of the tags is moving up" name="ConversationTag Utilities updates the order of other tags if the order of the tags is moving up" time="0">
</testcase>
<testcase classname="ConversationTag Utilities updates the order of other tags if the order of the tags is moving down" name="ConversationTag Utilities updates the order of other tags if the order of the tags is moving down" time="0.001">
</testcase>
<testcase classname="ConversationTag Utilities updates the order of other tags if new tag is added" name="ConversationTag Utilities updates the order of other tags if new tag is added" time="0.003">
</testcase>
<testcase classname="ConversationTag Utilities returns a new array for new tag if no tags exist" name="ConversationTag Utilities returns a new array for new tag if no tags exist" time="0">
</testcase>
</testsuite>
<testsuite name="createChatSearchParams" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.097" tests="27">
<testcase classname="createChatSearchParams conversation inputs handles basic conversation properties" name="createChatSearchParams conversation inputs handles basic conversation properties" time="0.001">
</testcase>
<testcase classname="createChatSearchParams conversation inputs applies only the endpoint property when other conversation fields are absent" name="createChatSearchParams conversation inputs applies only the endpoint property when other conversation fields are absent" time="0.001">
</testcase>
<testcase classname="createChatSearchParams conversation inputs applies only the model property when other conversation fields are absent" name="createChatSearchParams conversation inputs applies only the model property when other conversation fields are absent" time="0">
</testcase>
<testcase classname="createChatSearchParams conversation inputs includes assistant_id when endpoint is assistants" name="createChatSearchParams conversation inputs includes assistant_id when endpoint is assistants" time="0.001">
</testcase>
<testcase classname="createChatSearchParams conversation inputs includes agent_id when endpoint is agents" name="createChatSearchParams conversation inputs includes agent_id when endpoint is agents" time="0.002">
</testcase>
<testcase classname="createChatSearchParams conversation inputs excludes all parameters except assistant_id when endpoint is assistants" name="createChatSearchParams conversation inputs excludes all parameters except assistant_id when endpoint is assistants" time="0.001">
</testcase>
<testcase classname="createChatSearchParams conversation inputs excludes all parameters except agent_id when endpoint is agents" name="createChatSearchParams conversation inputs excludes all parameters except agent_id when endpoint is agents" time="0">
</testcase>
<testcase classname="createChatSearchParams conversation inputs returns empty params when agent endpoint has no agent_id" name="createChatSearchParams conversation inputs returns empty params when agent endpoint has no agent_id" time="0">
</testcase>
<testcase classname="createChatSearchParams conversation inputs returns empty params when assistants endpoint has no assistant_id" name="createChatSearchParams conversation inputs returns empty params when assistants endpoint has no assistant_id" time="0.001">
</testcase>
<testcase classname="createChatSearchParams conversation inputs ignores agent_id when it matches EPHEMERAL_AGENT_ID" name="createChatSearchParams conversation inputs ignores agent_id when it matches EPHEMERAL_AGENT_ID" time="0">
</testcase>
<testcase classname="createChatSearchParams conversation inputs handles stop arrays correctly by joining with commas" name="createChatSearchParams conversation inputs handles stop arrays correctly by joining with commas" time="0.005">
</testcase>
<testcase classname="createChatSearchParams conversation inputs filters out non-supported array properties" name="createChatSearchParams conversation inputs filters out non-supported array properties" time="0.001">
</testcase>
<testcase classname="createChatSearchParams conversation inputs includes empty arrays in output params" name="createChatSearchParams conversation inputs includes empty arrays in output params" time="0">
</testcase>
<testcase classname="createChatSearchParams conversation inputs handles non-stop arrays correctly in paramMap" name="createChatSearchParams conversation inputs handles non-stop arrays correctly in paramMap" time="0">
</testcase>
<testcase classname="createChatSearchParams conversation inputs includes empty non-stop arrays as serialized empty arrays" name="createChatSearchParams conversation inputs includes empty non-stop arrays as serialized empty arrays" time="0">
</testcase>
<testcase classname="createChatSearchParams conversation inputs excludes parameters with null or undefined values from the output" name="createChatSearchParams conversation inputs excludes parameters with null or undefined values from the output" time="0.001">
</testcase>
<testcase classname="createChatSearchParams conversation inputs handles float parameter values correctly" name="createChatSearchParams conversation inputs handles float parameter values correctly" time="0">
</testcase>
<testcase classname="createChatSearchParams conversation inputs handles integer parameter values correctly" name="createChatSearchParams conversation inputs handles integer parameter values correctly" time="0">
</testcase>
<testcase classname="createChatSearchParams preset inputs handles preset objects correctly" name="createChatSearchParams preset inputs handles preset objects correctly" time="0">
</testcase>
<testcase classname="createChatSearchParams preset inputs returns only spec param when spec property is present" name="createChatSearchParams preset inputs returns only spec param when spec property is present" time="0.001">
</testcase>
<testcase classname="createChatSearchParams record inputs includes allowed parameters from Record inputs" name="createChatSearchParams record inputs includes allowed parameters from Record inputs" time="0">
</testcase>
<testcase classname="createChatSearchParams record inputs excludes disallowed parameters from Record inputs" name="createChatSearchParams record inputs excludes disallowed parameters from Record inputs" time="0">
</testcase>
<testcase classname="createChatSearchParams record inputs includes valid values from Record inputs" name="createChatSearchParams record inputs includes valid values from Record inputs" time="0">
</testcase>
<testcase classname="createChatSearchParams record inputs excludes null or undefined values from Record inputs" name="createChatSearchParams record inputs excludes null or undefined values from Record inputs" time="0">
</testcase>
<testcase classname="createChatSearchParams record inputs handles generic object without endpoint or model properties" name="createChatSearchParams record inputs handles generic object without endpoint or model properties" time="0">
</testcase>
<testcase classname="createChatSearchParams edge cases returns an empty URLSearchParams instance when input is null" name="createChatSearchParams edge cases returns an empty URLSearchParams instance when input is null" time="0">
</testcase>
<testcase classname="createChatSearchParams edge cases returns an empty URLSearchParams instance for an empty object input" name="createChatSearchParams edge cases returns an empty URLSearchParams instance for an empty object input" time="0">
</testcase>
</testsuite>
<testsuite name="imageResize utility" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="0.074" tests="6">
<testcase classname="imageResize utility supportsClientResize should return true when all required APIs are available" name="imageResize utility supportsClientResize should return true when all required APIs are available" time="0.001">
</testcase>
<testcase classname="imageResize utility supportsClientResize should return false when HTMLCanvasElement is not available" name="imageResize utility supportsClientResize should return false when HTMLCanvasElement is not available" time="0">
</testcase>
<testcase classname="imageResize utility shouldResizeImage should return true for large image files" name="imageResize utility shouldResizeImage should return true for large image files" time="0">
</testcase>
<testcase classname="imageResize utility shouldResizeImage should return false for small image files" name="imageResize utility shouldResizeImage should return false for small image files" time="0.001">
</testcase>
<testcase classname="imageResize utility shouldResizeImage should return false for non-image files" name="imageResize utility shouldResizeImage should return false for non-image files" time="0">
</testcase>
<testcase classname="imageResize utility shouldResizeImage should return false for GIF files" name="imageResize utility shouldResizeImage should return false for GIF files" time="0">
</testcase>
</testsuite>
<testsuite name="Prompts Atoms Runtime Safety" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:02" time="1.384" tests="4">
<testcase classname="Prompts Atoms Runtime Safety should handle undefined atoms with fallback pattern" name="Prompts Atoms Runtime Safety should handle undefined atoms with fallback pattern" time="0.01">
</testcase>
<testcase classname="Prompts Atoms Runtime Safety should use alwaysMakeProd atom with fallback" name="Prompts Atoms Runtime Safety should use alwaysMakeProd atom with fallback" time="0.009">
</testcase>
<testcase classname="Prompts Atoms Runtime Safety should use promptsEditorMode atom with fallback" name="Prompts Atoms Runtime Safety should use promptsEditorMode atom with fallback" time="0.002">
</testcase>
<testcase classname="Prompts Atoms Runtime Safety should not throw error when using fallback pattern" name="Prompts Atoms Runtime Safety should not throw error when using fallback pattern" time="0.008">
</testcase>
</testsuite>
<testsuite name="VersionPanel" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:02" time="1.477" tests="4">
<testcase classname="VersionPanel renders panel UI and handles navigation" name="VersionPanel renders panel UI and handles navigation" time="0.058">
</testcase>
<testcase classname="VersionPanel VersionContent receives correct props" name="VersionPanel VersionContent receives correct props" time="0.016">
</testcase>
<testcase classname="VersionPanel handles data state variations" name="VersionPanel handles data state variations" time="0.019">
</testcase>
<testcase classname="VersionPanel memoizes agent data correctly" name="VersionPanel memoizes agent data correctly" time="0.002">
</testcase>
</testsuite>
<testsuite name="ApiKeyDialog" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:01" time="2.041" tests="8">
<testcase classname="ApiKeyDialog shows all dropdowns and both reranker fields when no config is set" name="ApiKeyDialog shows all dropdowns and both reranker fields when no config is set" time="0.221">
</testcase>
<testcase classname="ApiKeyDialog shows static text for provider and only provider input if provider is set" name="ApiKeyDialog shows static text for provider and only provider input if provider is set" time="0.051">
</testcase>
<testcase classname="ApiKeyDialog shows only Jina reranker field if rerankerType is set to jina" name="ApiKeyDialog shows only Jina reranker field if rerankerType is set to jina" time="0.036">
</testcase>
<testcase classname="ApiKeyDialog shows only Cohere reranker field if rerankerType is set to cohere" name="ApiKeyDialog shows only Cohere reranker field if rerankerType is set to cohere" time="0.033">
</testcase>
<testcase classname="ApiKeyDialog shows documentation link for the visible reranker" name="ApiKeyDialog shows documentation link for the visible reranker" time="0.057">
</testcase>
<testcase classname="ApiKeyDialog does not render provider section if SYSTEM_DEFINED" name="ApiKeyDialog does not render provider section if SYSTEM_DEFINED" time="0.039">
</testcase>
<testcase classname="ApiKeyDialog does not render scraper section if SYSTEM_DEFINED" name="ApiKeyDialog does not render scraper section if SYSTEM_DEFINED" time="0.039">
</testcase>
<testcase classname="ApiKeyDialog does not render reranker section if SYSTEM_DEFINED" name="ApiKeyDialog does not render reranker section if SYSTEM_DEFINED" time="0.04">
</testcase>
</testsuite>
<testsuite name="PluginStoreItem" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="1.839" tests="3">
<testcase classname="PluginStoreItem renders the plugin name and description" name="PluginStoreItem renders the plugin name and description" time="0.212">
</testcase>
<testcase classname="PluginStoreItem calls onInstall when the install button is clicked" name="PluginStoreItem calls onInstall when the install button is clicked" time="0.057">
</testcase>
<testcase classname="PluginStoreItem calls onUninstall when the uninstall button is clicked" name="PluginStoreItem calls onUninstall when the uninstall button is clicked" time="0.019">
</testcase>
</testsuite>
<testsuite name="Regenerate" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="1.67" tests="2">
<testcase classname="Regenerate should render the Regenerate button" name="Regenerate should render the Regenerate button" time="0.244">
</testcase>
<testcase classname="Regenerate should call onClick when the button is clicked" name="Regenerate should call onClick when the button is clicked" time="0.01">
</testcase>
</testsuite>
<testsuite name="Stop" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:04" time="1.394" tests="2">
<testcase classname="Stop should render the Stop button" name="Stop should render the Stop button" time="0.19">
</testcase>
<testcase classname="Stop should call onClick when the button is clicked" name="Stop should call onClick when the button is clicked" time="0.011">
</testcase>
</testsuite>
<testsuite name="PluginPagination" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="1.854" tests="5">
<testcase classname="PluginPagination should render the previous button as enabled when not on the first page" name="PluginPagination should render the previous button as enabled when not on the first page" time="0.249">
</testcase>
<testcase classname="PluginPagination should call onChangePage with the previous page number when the previous button is clicked" name="PluginPagination should call onChangePage with the previous page number when the previous button is clicked" time="0.051">
</testcase>
<testcase classname="PluginPagination should call onChangePage with the next page number when the next button is clicked" name="PluginPagination should call onChangePage with the next page number when the next button is clicked" time="0.033">
</testcase>
<testcase classname="PluginPagination should render the page numbers" name="PluginPagination should render the page numbers" time="0.02">
</testcase>
<testcase classname="PluginPagination should call onChangePage with the correct page number when a page number button is clicked" name="PluginPagination should call onChangePage with the correct page number when a page number button is clicked" time="0.024">
</testcase>
</testsuite>
<testsuite name="DialogTemplate" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="2.251" tests="3">
<testcase classname="DialogTemplate renders correctly with all props" name="DialogTemplate renders correctly with all props" time="0.103">
</testcase>
<testcase classname="DialogTemplate renders correctly without optional props" name="DialogTemplate renders correctly without optional props" time="0.001">
</testcase>
<testcase classname="DialogTemplate calls selectHandler when the select button is clicked" name="DialogTemplate calls selectHandler when the select button is clicked" time="0.037">
</testcase>
</testsuite>
<testsuite name="ConversationModeSwitch" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="2.48" tests="2">
<testcase classname="ConversationModeSwitch renders correctly" name="ConversationModeSwitch renders correctly" time="0.104">
</testcase>
<testcase classname="ConversationModeSwitch calls onCheckedChange when the switch is toggled" name="ConversationModeSwitch calls onCheckedChange when the switch is toggled" time="0.014">
</testcase>
</testsuite>
<testsuite name="ThemeSelector" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="2.778" tests="2">
<testcase classname="ThemeSelector renders correctly" name="ThemeSelector renders correctly" time="0.383">
</testcase>
<testcase classname="ThemeSelector calls onChange when the select value changes" name="ThemeSelector calls onChange when the select value changes" time="0.14">
</testcase>
</testsuite>
<testsuite name="CloudBrowserVoicesSwitch" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:05" time="0.615" tests="2">
<testcase classname="CloudBrowserVoicesSwitch renders correctly" name="CloudBrowserVoicesSwitch renders correctly" time="0.128">
</testcase>
<testcase classname="CloudBrowserVoicesSwitch calls onCheckedChange when the switch is toggled" name="CloudBrowserVoicesSwitch calls onCheckedChange when the switch is toggled" time="0.016">
</testcase>
</testsuite>
<testsuite name="LangSelector" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="2.891" tests="2">
<testcase classname="LangSelector renders correctly" name="LangSelector renders correctly" time="0.423">
</testcase>
<testcase classname="LangSelector calls onChange when the select value changes" name="LangSelector calls onChange when the select value changes" time="0.267">
</testcase>
</testsuite>
<testsuite name="PluginAuthForm" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:03" time="3.112" tests="2">
<testcase classname="PluginAuthForm renders the form with the correct fields" name="PluginAuthForm renders the form with the correct fields" time="0.325">
</testcase>
<testcase classname="PluginAuthForm calls the onSubmit function with the form data when submitted" name="PluginAuthForm calls the onSubmit function with the form data when submitted" time="0.266">
</testcase>
</testsuite>
<testsuite name="SpeechToTextSwitch" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:05" time="0.8" tests="2">
<testcase classname="SpeechToTextSwitch renders correctly" name="SpeechToTextSwitch renders correctly" time="0.34">
</testcase>
<testcase classname="SpeechToTextSwitch calls onCheckedChange when the switch is toggled" name="SpeechToTextSwitch calls onCheckedChange when the switch is toggled" time="0.014">
</testcase>
</testsuite>
<testsuite name="TextToSpeechSwitch" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:05" time="1.191" tests="2">
<testcase classname="TextToSpeechSwitch renders correctly" name="TextToSpeechSwitch renders correctly" time="0.133">
</testcase>
<testcase classname="TextToSpeechSwitch calls onCheckedChange when the switch is toggled" name="TextToSpeechSwitch calls onCheckedChange when the switch is toggled" time="0.015">
</testcase>
</testsuite>
<testsuite name="AutoTranscribeAudioSwitch" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:06" time="0.542" tests="2">
<testcase classname="AutoTranscribeAudioSwitch renders correctly" name="AutoTranscribeAudioSwitch renders correctly" time="0.077">
</testcase>
<testcase classname="AutoTranscribeAudioSwitch calls onCheckedChange when the switch is toggled" name="AutoTranscribeAudioSwitch calls onCheckedChange when the switch is toggled" time="0.006">
</testcase>
</testsuite>
<testsuite name="CacheTTSSwitch" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:05" time="1.15" tests="2">
<testcase classname="CacheTTSSwitch renders correctly" name="CacheTTSSwitch renders correctly" time="0.063">
</testcase>
<testcase classname="CacheTTSSwitch calls onCheckedChange when the switch is toggled" name="CacheTTSSwitch calls onCheckedChange when the switch is toggled" time="0.026">
</testcase>
</testsuite>
<testsuite name="AutomaticPlaybackSwitch" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:05" time="1.129" tests="2">
<testcase classname="AutomaticPlaybackSwitch renders correctly" name="AutomaticPlaybackSwitch renders correctly" time="0.061">
</testcase>
<testcase classname="AutomaticPlaybackSwitch calls onCheckedChange when the switch is toggled" name="AutomaticPlaybackSwitch calls onCheckedChange when the switch is toggled" time="0.015">
</testcase>
</testsuite>
<testsuite name="undefined" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:06" time="0.792" tests="3">
<testcase classname=" renders login form" name=" renders login form" time="0.054">
</testcase>
<testcase classname=" submits login form" name=" submits login form" time="0.093">
</testcase>
<testcase classname=" displays validation error messages" name=" displays validation error messages" time="0.05">
</testcase>
</testsuite>
<testsuite name="undefined" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:06" time="0.845" tests="3">
<testcase classname=" renders login form" name=" renders login form" time="0.119">
</testcase>
<testcase classname=" calls loginUser.mutate on login" name=" calls loginUser.mutate on login" time="0.095">
</testcase>
<testcase classname=" Navigates to / on successful login" name=" Navigates to / on successful login" time="0.078">
</testcase>
</testsuite>
<testsuite name="undefined" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:06" time="0.86" tests="4">
<testcase classname=" renders plugin store dialog with plugins from the available plugins query and shows install/uninstall buttons based on user plugins" name=" renders plugin store dialog with plugins from the available plugins query and shows install/uninstall buttons based on user plugins" time="0.146">
</testcase>
<testcase classname=" Displays the plugin auth form when installing a plugin with auth" name=" Displays the plugin auth form when installing a plugin with auth" time="0.109">
</testcase>
<testcase classname=" allows the user to navigate between pages" name=" allows the user to navigate between pages" time="0.091">
</testcase>
<testcase classname=" allows the user to search for plugins" name=" allows the user to search for plugins" time="0.024">
</testcase>
</testsuite>
<testsuite name="undefined" errors="0" failures="0" skipped="0" timestamp="2025-07-04T03:30:06" time="0.846" tests="3">
<testcase classname=" renders registration form" name=" renders registration form" time="0.137">
</testcase>
<testcase classname=" shows validation error messages" name=" shows validation error messages" time="0.143">
</testcase>
<testcase classname=" shows error message when registration fails" name=" shows error message when registration fails" time="0.188">
</testcase>
</testsuite>
</testsuites>
+1 -1
View File
@@ -23,7 +23,7 @@ server {
# The default limits for image uploads as of 11/22/23 is 20MB/file, and 25MB/request
client_max_body_size 25M;
location /api/ {
location /v1/chat/ {
proxy_pass http://api:3080$request_uri;
}
+1 -1
View File
@@ -1,3 +1,3 @@
User-agent: *
Disallow: /api/
Disallow: /v1/chat/
Allow: /
@@ -93,7 +93,7 @@ export default function StreamAudio({ index = 0 }) {
}
logger.log('Fetching audio...', navigator.userAgent);
const response = await fetch('/api/files/speech/tts', {
const response = await fetch('/v1/chat/files/speech/tts', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ messageId: latestMessage?.messageId, runId: activeRunId, voice }),
@@ -23,7 +23,7 @@ interface AttachmentLinkOptions {
/**
* Determines if a file is stored locally (not an external API URL).
* Files with these sources are stored on the LibreChat server and should
* use the /api/files/download endpoint instead of direct URL access.
* use the /v1/chat/files/download endpoint instead of direct URL access.
*/
const isLocallyStoredSource = (source?: string): boolean => {
if (!source) {
@@ -82,7 +82,7 @@ describe('LogLink download routing', () => {
render(
<LogLink
href="/api/files/code/download/session-1/file-1"
href="/v1/chat/files/code/download/session-1/file-1"
filename={filename}
source={FileSources.execute_code}
>
@@ -11,7 +11,7 @@ export default function ActionCallback({ action_id }: { action_id?: string }) {
const { watch } = useFormContext();
const { showToast } = useToastContext();
const [isCopying, setIsCopying] = useState(false);
const callbackURL = `${window.location.protocol}//${window.location.host}/api/actions/${action_id}/oauth/callback`;
const callbackURL = `${window.location.protocol}//${window.location.host}/v1/chat/actions/${action_id}/oauth/callback`;
const copyLink = useCopyToClipboard({ text: callbackURL });
if (!action_id) {
@@ -103,7 +103,7 @@ export default function MCPServerDialog({
!permissionsLoading;
const redirectUri = createdServerId
? `${window.location.origin}/api/mcp/${createdServerId}/oauth/callback`
? `${window.location.origin}/v1/chat/mcp/${createdServerId}/oauth/callback`
: '';
const copyLink = useCopyToClipboard({ text: redirectUri });
@@ -36,7 +36,7 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
}) as AuthorizationTypeEnum;
const redirectUri = serverName
? `${window.location.origin}/api/mcp/${serverName}/oauth/callback`
? `${window.location.origin}/v1/chat/mcp/${serverName}/oauth/callback`
: '';
const copyLink = useCopyToClipboard({ text: redirectUri });
@@ -32,7 +32,7 @@ function SkillFileViewer({ skillId, relativePath }: SkillFileViewerProps) {
const rawUrl = useMemo(
() =>
`${apiBaseUrl()}/api/skills/${skillId}/files/${encodeURIComponent(relativePath)}?raw=true`,
`${apiBaseUrl()}/v1/chat/skills/${skillId}/files/${encodeURIComponent(relativePath)}?raw=true`,
[skillId, relativePath],
);
+1 -1
View File
@@ -24,7 +24,7 @@ export interface AbortStreamResponse {
export const abortStream = async (params: AbortStreamParams): Promise<AbortStreamResponse> => {
console.log('[abortStream] Calling abort endpoint with params:', params);
const result = (await request.post(
`${apiBaseUrl()}/api/agents/chat/abort`,
`${apiBaseUrl()}/v1/chat/agents/chat/abort`,
params,
)) as AbortStreamResponse;
console.log('[abortStream] Abort response:', result);
+1 -1
View File
@@ -17,7 +17,7 @@ export const streamStatusQueryKey = (conversationId: string) => ['streamStatus',
export const fetchStreamStatus = async (conversationId: string): Promise<StreamStatusResponse> => {
return request.get<StreamStatusResponse>(
`${apiBaseUrl()}/api/agents/chat/status/${conversationId}`,
`${apiBaseUrl()}/v1/chat/agents/chat/status/${conversationId}`,
);
};
@@ -142,7 +142,7 @@ describe('useAppStartup — MCP permission gating', () => {
const startupConfig = {
cloudFront: {
cookieRefresh: {
endpoint: '/api/auth/cloudfront/refresh',
endpoint: '/v1/chat/auth/cloudfront/refresh',
domain: 'https://cdn.example.com',
},
},
@@ -31,7 +31,7 @@ interface UseAttachmentPreviewSyncResult {
* `attachment` update event arrives and the SSE handler upserts by
* `file_id`. If the stream has already closed (the model finished
* generating before the render resolved), this hook covers the gap by
* polling `GET /api/files/:file_id/preview` and writing the resolved
* polling `GET /v1/chat/files/:file_id/preview` and writing the resolved
* record back into `messageAttachmentsMap` which triggers
* re-classification through `artifactTypeForAttachment`, so the file
* chip transitions from a plain download to the rich preview card
@@ -280,8 +280,8 @@ describe('useHandleKeyUp', () => {
});
describe('paste protection — long text starting with command char', () => {
it('does NOT trigger for pasted "/api/v1/users"', () => {
const ref = makeTextAreaRef('/api/v1/users', 13);
it('does NOT trigger for pasted "/v1/chat/v1/users"', () => {
const ref = makeTextAreaRef('/v1/chat/v1/users', 13);
const { handleKeyUp, setShowPromptsPopover } = renderUseHandleKeyUp(ref);
act(() => handleKeyUp(makeKeyEvent('v')));
@@ -106,7 +106,7 @@ jest.mock('librechat-data-provider', () => {
...actual,
createPayload: jest.fn(() => ({
payload: { model: 'gpt-4o' },
server: '/api/agents/chat',
server: '/v1/chat/agents/chat',
})),
removeNullishValues: jest.fn((v: unknown) => v),
apiBaseUrl: jest.fn(() => ''),
+1 -1
View File
@@ -21,7 +21,7 @@ export default function useAttachmentHandler(queryClient?: QueryClient) {
queryClient &&
fileData?.file_id &&
fileData?.filepath &&
!fileData.filepath.includes('/api/files')
!fileData.filepath.includes('/v1/chat/files')
) {
queryClient.setQueryData([QueryKeys.files], (oldData: TFile[] | undefined) => {
if (!oldData) {
+1 -1
View File
@@ -147,7 +147,7 @@ export default function useResumableSSE(
let { userMessage } = currentSubmission;
let textIndex: number | null = null;
const baseUrl = `${apiBaseUrl()}/api/agents/chat/stream/${encodeURIComponent(currentStreamId)}`;
const baseUrl = `${apiBaseUrl()}/v1/chat/agents/chat/stream/${encodeURIComponent(currentStreamId)}`;
const url = isResume ? `${baseUrl}?resume=true` : baseUrl;
console.log('[ResumableSSE] Subscribing to stream:', url, { isResume });
+1 -1
View File
@@ -12,7 +12,7 @@ const EMPTY_FAVORITES: string[] = [];
* Hook for managing user skill favorites.
*
* Skill favorites are a flat array of skill ObjectId strings, persisted via
* `/api/user/settings/favorites/skills`. React Query is the single source of
* `/v1/chat/user/settings/favorites/skills`. React Query is the single source of
* truth toggling drives an optimistic mutation that updates the cache,
* eliminating the need for a separate atom mirror.
*/
+4 -4
View File
@@ -112,7 +112,7 @@ describe('useRum', () => {
rum: {
provider: 'hyperdx',
enabled: true,
url: '/api/rum',
url: '/v1/chat/rum',
serviceName: 'librechat-web',
authMode: 'proxy',
},
@@ -129,11 +129,11 @@ describe('useRum', () => {
disableReplay: true,
service: 'librechat-web',
tracePropagationTargets: undefined,
url: '/api/rum',
url: '/v1/chat/rum',
});
});
await window.fetch('/api/rum/v1/traces', { method: 'POST' });
await window.fetch('/v1/chat/rum/v1/traces', { method: 'POST' });
const headers = fetchMock.mock.calls[0]?.[1]?.headers;
expect(headers).toBeInstanceOf(Headers);
@@ -151,7 +151,7 @@ describe('useRum', () => {
rum: {
provider: 'hyperdx',
enabled: true,
url: '/api/rum',
url: '/v1/chat/rum',
serviceName: 'librechat-web',
authMode: 'proxy',
},
@@ -40,7 +40,7 @@ describe('downloadFile utilities', () => {
expect(isHttpDownloadTarget('https://cdn.example.com/file.pdf')).toBe(true);
expect(isHttpDownloadTarget('http://cdn.example.com/file.pdf')).toBe(true);
expect(isHttpDownloadTarget('blob:https://app.example.com/id')).toBe(false);
expect(isHttpDownloadTarget('/api/files/code/download/session/file')).toBe(false);
expect(isHttpDownloadTarget('/v1/chat/files/code/download/session/file')).toBe(false);
expect(isHttpDownloadTarget(undefined)).toBe(false);
});
+1 -1
View File
@@ -52,7 +52,7 @@ export const RESOURCE_CONFIGS: Record<ResourceType, ResourceConfig> = {
defaultViewerRoleId: AccessRoleIds.REMOTE_AGENT_VIEWER,
defaultEditorRoleId: AccessRoleIds.REMOTE_AGENT_EDITOR,
defaultOwnerRoleId: AccessRoleIds.REMOTE_AGENT_OWNER,
getResourceUrl: () => `${window.location.origin}/api/v1/responses`,
getResourceUrl: () => `${window.location.origin}/v1/chat/v1/responses`,
getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'remote agent'),
getShareMessage: (name?: string) =>
name && name !== '' ? `"${name}" (API Access)` : 'remote agent access',
+3 -3
View File
@@ -16,7 +16,7 @@ const backendURL = process.env.HOST ? `http://${process.env.HOST}:${backendPort}
// Only proxy to local backend if not using cloud gateway directly
const devProxy = hanzoApiUrl ? {} : {
'/api': {
'/v1/chat': {
target: backendURL,
changeOrigin: true,
},
@@ -63,12 +63,12 @@ export default defineConfig(({ command }) => ({
// createHandlerBoundToURL('index.html'). If it isn't precached, that
// throws "non-precached-url: index.html", the service worker breaks, and
// users are stranded on a stale shell after each deploy (they see
// /api/* 401s until they manually clear the SW). Precaching it — with
// /v1/chat/* 401s until they manually clear the SW). Precaching it — with
// registerType:'autoUpdate' above — keeps the SPA nav fallback valid and
// self-updates on every release.
globIgnores: ['images/**/*', '**/*.map'],
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
navigateFallbackDenylist: [/^\/oauth/, /^\/api/],
navigateFallbackDenylist: [/^\/oauth/, /^\/v1\//],
},
includeAssets: [],
manifest: {
+1 -1
View File
@@ -12,7 +12,7 @@ function isUUID(uuid: string) {
}
const waitForServerStream = async (response: Response) => {
const endpointCheck = response.url().includes(`/api/agents`);
const endpointCheck = response.url().includes(`/v1/chat/agents`);
return endpointCheck && response.status() === 200;
};
+26 -2
View File
@@ -80,10 +80,34 @@ http {
proxy_read_timeout 86400;
}
# Hanzo Router API Gateway
# Chat application REST API (namespaced /v1/chat/* -> chat backend).
# MUST precede location /v1/ so the OpenAI-compat inference proxy
# (router_backend) never shadows the app surface. Longest-prefix match
# also guarantees precedence, but we keep it explicit and first.
location /v1/chat/ {
limit_req zone=api burst=50 nodelay;
proxy_pass http://chat_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE / streaming (chat completions stream via the agents route)
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
proxy_read_timeout 86400;
}
# Hanzo Router API Gateway (OpenAI-compat inference: /v1/models, /v1/embeddings, ...)
location /v1/ {
limit_req zone=api burst=50 nodelay;
proxy_pass http://router_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
+376 -39
View File
@@ -23362,8 +23362,8 @@ class MCPOAuthHandler {
static getDefaultRedirectUri(serverName) {
const baseUrl = process.env.DOMAIN_SERVER || 'http://localhost:3080';
return serverName
? `${baseUrl}/api/mcp/${serverName}/oauth/callback`
: `${baseUrl}/api/mcp/oauth/callback`;
? `${baseUrl}/v1/chat/mcp/${serverName}/oauth/callback`
: `${baseUrl}/v1/chat/mcp/oauth/callback`;
}
/**
* Processes and logs a token refresh response from an OAuth server.
@@ -34042,9 +34042,10 @@ function createDedent(options) {
result = result.trim();
}
// handle escaped newlines at the end to ensure they don't get stripped too
// Unescape escapes after trimming so sequences like `\n`, `\t`,
// `\xHH` and `\u{...}` are preserved (fixes #24)
if (escapeSpecialCharacters) {
result = result.replace(/\\n/g, "\n");
result = result.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\v/g, "\v").replace(/\\b/g, "\b").replace(/\\f/g, "\f").replace(/\\0/g, "\0").replace(/\\x([\da-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16))).replace(/\\u\{([\da-fA-F]{1,6})\}/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/\\u([\da-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
}
// Workaround for Bun issue with Unicode characters
@@ -34864,7 +34865,7 @@ Artifacts are for substantial, self-contained content that users might modify or
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- SVG: "image/svg+xml"
- The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags.
@@ -34888,7 +34889,7 @@ Artifacts are for substantial, self-contained content that users might modify or
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- When iterating on code, ensure that the code is complete and functional without any snippets, placeholders, or ellipses.
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
@@ -35068,7 +35069,7 @@ Artifacts are for substantial, self-contained content that users might modify or
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- SVG: "image/svg+xml"
- The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags.
@@ -35092,7 +35093,7 @@ Artifacts are for substantial, self-contained content that users might modify or
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- When iterating on code, ensure that the code is complete and functional without any snippets, placeholders, or ellipses.
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
@@ -37240,6 +37241,331 @@ function getBedrockModels() {
return models;
}
const KEY_TTL_MS = 10 * 60 * 1000; // 10 minutes
const IAM_TIMEOUT_MS = 5000;
/** Per-user hk- key cache: cacheKey (user id|email) -> { key, expiresAt }. */
const keyCache = new Map();
function truthy(value) {
return (value !== null && value !== void 0 ? value : '').toString().trim().toLowerCase() === 'true';
}
/** IAM base URL — prefer an in-cluster override, else the OIDC issuer. */
function iamBaseUrl() {
const base = process.env.IAM_INTERNAL_URL ||
process.env.IAM_SERVER_URL ||
process.env.OPENID_ISSUER ||
'';
return base.replace(/\/+$/, '');
}
function iamClientId() {
return process.env.IAM_CLIENT_ID || process.env.OPENID_CLIENT_ID || '';
}
function iamClientSecret() {
return process.env.IAM_CLIENT_SECRET || process.env.OPENID_CLIENT_SECRET || '';
}
/**
* Whether per-user hk- billing is enabled AND fully configured. When false,
* `resolveHanzoCloudKey` returns null and the shared key is used (legacy).
*/
function isHanzoPerUserKeyEnabled() {
return (truthy(process.env.HANZO_PER_USER_KEY) &&
Boolean(iamBaseUrl()) &&
Boolean(iamClientId()) &&
Boolean(iamClientSecret()));
}
function basicAuthHeader() {
const raw = `${iamClientId()}:${iamClientSecret()}`;
return `Basic ${Buffer.from(raw).toString('base64')}`;
}
// ── Per-user billing identity + starter credit ───────────────────────────────
/**
* Orgs whose MEMBERS are billed per-user (default: the shared "hanzo" catch-all,
* the home of every unaffiliated individual signup). MUST mirror the gateway's
* PERSONAL_BILLING_ORGS / object.BillingSubject (hanzoai/ai) so chat and the
* gateway derive the identical subject.
*/
function personalBillingOrgs() {
const raw = (process.env.HANZO_PERSONAL_BILLING_ORGS ||
process.env.HANZO_DEFAULT_ORG ||
'hanzo')
.split(',')
.map((o) => o.trim().toLowerCase())
.filter(Boolean);
return new Set(raw.length ? raw : ['hanzo']);
}
/**
* Canonical Commerce billing subject for an IAM (owner, name) identity the
* account the cloud gateway debits and reads. Personal-billing org "owner/name"
* (per-user), pooled org "owner". Always lowercased so it nets against the
* gateway's usage writes. Byte-identical to object.BillingSubject in hanzoai/ai.
*/
function billingSubject(owner, name) {
const o = (owner !== null && owner !== void 0 ? owner : '').toString().trim().toLowerCase();
if (!o) {
return '';
}
if (personalBillingOrgs().has(o)) {
const n = (name !== null && name !== void 0 ? name : '').toString().trim().toLowerCase();
return n ? `${o}/${n}` : o;
}
return o;
}
function commerceBaseUrl() {
return (process.env.COMMERCE_API_URL || process.env.COMMERCE_ENDPOINT || '').replace(/\/+$/, '');
}
function commerceToken() {
return process.env.COMMERCE_TOKEN || process.env.COMMERCE_API_TOKEN || '';
}
/** Subjects whose starter credit this process has already ensured (commerce is also idempotent). */
const starterEnsured = new Set();
/**
* Ensure the new-user $5 welcome credit exists on THIS user's billing subject,
* exactly once. Idempotent two ways: an in-process set (avoids re-hitting
* commerce every key refresh) and commerce's own tag-deduped, transaction-guarded
* grant (`POST /v1/billing/grant-starter`) so concurrent chats can never
* double-grant (no bleed).
*
* Best-effort but awaited: the credit must land on the subject BEFORE we forward
* the user's hk- key to the gateway, otherwise the gateway sees a $0 balance and
* 402s the very first chat. A commerce hiccup must NOT break key resolution,
* though the gateway still enforces balance, and the next message retries.
*/
function ensureStarterCredit(subject, owner) {
return __awaiter(this, void 0, void 0, function* () {
const base = commerceBaseUrl();
if (!base || !subject || starterEnsured.has(subject)) {
return;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), IAM_TIMEOUT_MS);
try {
const headers = {
'Content-Type': 'application/json',
'X-Hanzo-Org': owner,
};
const tok = commerceToken();
if (tok) {
headers['Authorization'] = `Bearer ${tok}`;
}
const resp = yield fetch(`${base}/v1/billing/grant-starter`, {
method: 'POST',
headers,
body: JSON.stringify({ user: subject, trigger: 'chat_first_use' }),
signal: controller.signal,
});
if (resp.ok) {
starterEnsured.add(subject); // ensured this process; commerce dedupes across pods
}
else {
dataSchemas.logger.warn('[hanzoCloudKey] starter-credit grant non-OK (continuing)', {
subject,
status: resp.status,
});
}
}
catch (err) {
dataSchemas.logger.warn('[hanzoCloudKey] starter-credit grant failed (continuing; gateway enforces)', {
subject,
error: err instanceof Error ? err.message : String(err),
});
}
finally {
clearTimeout(timeoutId);
}
});
}
/**
* Single IAM HTTP primitive (confidential-client Basic auth, `/v1/iam/*` JSON
* API). Throws on transport error / non-ok status so callers fail closed.
*/
function iamRequest(path_1, params_1) {
return __awaiter(this, arguments, void 0, function* (path, params, method = 'GET') {
const url = new URL(`${iamBaseUrl()}${path}`);
for (const [k, v] of Object.entries(params)) {
if (v != null && v !== '') {
url.searchParams.set(k, v);
}
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), IAM_TIMEOUT_MS);
try {
const resp = yield fetch(url.toString(), Object.assign(Object.assign({ method, headers: {
Authorization: basicAuthHeader(),
'Content-Type': 'application/json',
} }, (method === 'POST' ? { body: '{}' } : {})), { signal: controller.signal }));
if (!resp.ok) {
throw new Error(`IAM ${method} ${path} returned ${resp.status}`);
}
return (yield resp.json());
}
finally {
clearTimeout(timeoutId);
}
});
}
/** Resolve the authoritative IAM record (owner/name/accessKey) by org + email. */
function getIamUserByOrgEmail(owner, email) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const res = yield iamRequest('/v1/iam/get-user', {
owner,
email: email.toLowerCase(),
});
if (res.status !== 'ok' || !((_a = res.data) === null || _a === void 0 ? void 0 : _a.owner) || !((_b = res.data) === null || _b === void 0 ? void 0 : _b.name)) {
return null;
}
return res.data;
});
}
/** Mint (create) the per-user hk- key for an IAM sub ("owner/name"). */
function mintUserKey(sub) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const res = yield iamRequest('/v1/iam/mint-user-keys', { id: sub }, 'POST');
if (res.status !== 'ok' || !((_a = res.data) === null || _a === void 0 ? void 0 : _a.accessKey)) {
return null;
}
return res.data.accessKey;
});
}
/**
* Resolve the authenticated user's own hk- Cloud API key, minting one on first
* use if the IAM record has none. Returns null when per-user billing is
* disabled/unconfigured, the user is a guest / has no email, or IAM cannot be
* reached (caller FAILS CLOSED on null for an authenticated user).
*/
function resolveHanzoCloudKey(user) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d;
if (!isHanzoPerUserKeyEnabled()) {
return null;
}
if (!user || user.guest) {
return null;
}
const email = ((_a = user.email) !== null && _a !== void 0 ? _a : '').toString().toLowerCase();
if (!email) {
return null;
}
const cacheKey = user.id || email;
const cached = keyCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.key;
}
const defaultOrg = process.env.HANZO_DEFAULT_ORG || 'hanzo';
const owner = ((_b = user.organization) !== null && _b !== void 0 ? _b : '').toString().trim() || defaultOrg;
try {
let record = yield getIamUserByOrgEmail(owner, email);
// A user whose stored org is stale/missing may live in the default org.
if (!record && owner !== defaultOrg) {
record = yield getIamUserByOrgEmail(defaultOrg, email);
}
if (!(record === null || record === void 0 ? void 0 : record.owner) || !(record === null || record === void 0 ? void 0 : record.name)) {
dataSchemas.logger.warn('[hanzoCloudKey] No IAM user for billing identity', {
owner,
email,
});
return null;
}
// Single authoritative identity: stamp the REAL billing org (record.owner)
// back onto req.user. The OIDC-stored `organization` can be the Casdoor
// super-org "admin" for some users, which is NOT where their hk- key bills —
// so the downstream Commerce balance gate must use this resolved owner, not
// the login-time value. This keeps the key and the gate on ONE org.
const subject = billingSubject(record.owner, record.name);
try {
user.organization = record.owner;
// Stamp the canonical billing subject so the balance gate keys on the SAME
// account the gateway debits (per-user for the shared "hanzo" catch-all).
user.billingSubject = subject;
}
catch (_e) {
/* req.user may be a frozen/lean doc — non-fatal; gate still has gateway as backstop */
}
// Ensure THIS user's one-time $5 welcome credit exists on THEIR subject
// before we hand back the key — so the gateway's first balance check sees it
// instead of 402-ing a brand-new account. Idempotent + best-effort.
yield ensureStarterCredit(subject, record.owner);
let key = ((_c = record.accessKey) !== null && _c !== void 0 ? _c : '').trim();
if (!key) {
// Mint on first chat — the key is theirs going forward.
key = (_d = (yield mintUserKey(`${record.owner}/${record.name}`))) !== null && _d !== void 0 ? _d : '';
}
if (!key) {
dataSchemas.logger.error('[hanzoCloudKey] Failed to resolve/mint hk- key', {
sub: `${record.owner}/${record.name}`,
});
return null;
}
keyCache.set(cacheKey, { key, expiresAt: Date.now() + KEY_TTL_MS });
return key;
}
catch (err) {
dataSchemas.logger.error('[hanzoCloudKey] IAM lookup failed (failing closed)', {
owner,
email,
error: err instanceof Error ? err.message : String(err),
});
return null;
}
});
}
/** Test-only: clear the in-process key cache. */
function _clearHanzoKeyCache() {
keyCache.clear();
}
/**
* Wrap an OpenAI-client `fetch` so the Hanzo Cloud gateway's HTTP-200 error
* envelope becomes a real, surfaced error instead of an opaque crash.
*
* The gateway (api.hanzo.ai) answers some failures most importantly a request
* for a premium model when the caller's balance is only the $5 starter credit
* with HTTP 200 and a JSON body `{ "status": "error", "msg": "..." }` that has no
* `choices`. The OpenAI client treats the 200 as success and parses the body to
* `undefined`; the agent run then throws the opaque
* `Cannot read properties of undefined (reading 'role')`, so NO assistant reply
* renders and the user never learns why (and the title call dies the same way on
* `reading 'message'`).
*
* This wrapper rewrites that envelope into a conventional 402 response carrying
* the gateway's own `msg`, so the OpenAI client raises a clean, non-retryable
* error and the existing error path shows the actionable message ("... requires a
* paid balance. Add funds ...") instead of crashing. Successful SSE streams
* (`text/event-stream`) and normal completions pass through untouched, and the
* request headers/body are never inspected per-user `hk-` billing is unaffected.
*/
function wrapHanzoGatewayFetch(baseFetch) {
const inner = baseFetch !== null && baseFetch !== void 0 ? baseFetch : ((input, init) => fetch(input, init));
return (input, init) => __awaiter(this, void 0, void 0, function* () {
var _a;
const response = yield inner(input, init);
/** Live SSE success streams must pass through without buffering the body. */
const contentType = (_a = response.headers.get('content-type')) !== null && _a !== void 0 ? _a : '';
if (contentType.includes('text/event-stream')) {
return response;
}
/** Read a clone so the original body stays intact for the OpenAI client. */
let envelope;
try {
envelope = (yield response.clone().json());
}
catch (_b) {
return response;
}
if (envelope && envelope.status === 'error' && envelope.choices == null) {
const message = (typeof envelope.msg === 'string' && envelope.msg.trim()) ||
'Hanzo Cloud rejected the model request.';
dataSchemas.logger.warn('[hanzoGatewayFetch] gateway returned a 200 error envelope; surfacing as 402', {
message,
});
return new Response(JSON.stringify({
error: { message, type: 'insufficient_quota', code: 'insufficient_quota' },
}), { status: 402, headers: { 'content-type': 'application/json' } });
}
return response;
});
}
const { PROXY } = process.env;
/**
* Builds custom options from endpoint configuration
@@ -37304,8 +37630,25 @@ function initializeCustom(_a) {
checkUserKeyExpiry(expiresAt, endpoint);
userValues = yield db.getUserKeyValues({ userId: (_e = (_d = req.user) === null || _d === void 0 ? void 0 : _d.id) !== null && _e !== void 0 ? _e : '', name: endpoint });
}
const apiKey = userProvidesKey ? userValues === null || userValues === void 0 ? void 0 : userValues.apiKey : CUSTOM_API_KEY;
let apiKey = userProvidesKey ? userValues === null || userValues === void 0 ? void 0 : userValues.apiKey : CUSTOM_API_KEY;
const baseURL = userProvidesURL ? userValues === null || userValues === void 0 ? void 0 : userValues.baseURL : CUSTOM_BASE_URL;
// Hanzo per-user billing: an authenticated (non-guest) user's chat must be
// billed to THEIR OWN org via THEIR OWN hk- key — never the shared key. We
// resolve (mint on first chat) their key from IAM and use it here. If it
// cannot be resolved we FAIL CLOSED (throw) rather than silently fall back to
// the shared key, so an IAM hiccup can never route an authed user's spend onto
// the shared org. Guests (anonymous preview) keep the shared, capped key.
const billingUser = req.user;
const isAuthenticatedUser = Boolean(billingUser && !billingUser.guest && billingUser.email);
if (isHanzoPerUserKeyEnabled() && isAuthenticatedUser) {
const perUserKey = yield resolveHanzoCloudKey(billingUser);
if (perUserKey) {
apiKey = perUserKey;
}
else {
throw new Error('Your Hanzo Cloud account is not linked for billing yet. Please sign out and back in, then claim your starter credit at https://billing.hanzo.ai');
}
}
if (userProvidesKey && !apiKey) {
throw new Error(JSON.stringify({
type: librechatDataProvider.ErrorTypes.NO_USER_KEY,
@@ -37348,6 +37691,17 @@ function initializeCustom(_a) {
options.useLegacyContent = true;
options.endpointTokenConfig = endpointTokenConfig;
}
// The Hanzo Cloud gateway answers some failures (e.g. a premium model requested
// against a starter-credit-only balance) with HTTP 200 + a JSON error envelope
// ({status:"error", msg}) that has no `choices`. Left as-is, the OpenAI client
// parses the choices-less 200 to `undefined` and the agent run crashes with
// `Cannot read properties of undefined (reading 'role')` — no reply renders.
// Wrap the client fetch so that envelope becomes a clean 402 carrying the
// gateway's actionable message. Scoped to the Hanzo gateway; response-only, so
// per-user hk- billing is untouched.
if ((options === null || options === void 0 ? void 0 : options.configOptions) && /(?:^|\.)hanzo\.ai(?::|\/|$)/i.test(baseURL !== null && baseURL !== void 0 ? baseURL : '')) {
options.configOptions.fetch = wrapHanzoGatewayFetch(options.configOptions.fetch);
}
const streamRate = clientOptions.streamRate;
if (streamRate) {
options.llmConfig._lc_stream_delay = streamRate;
@@ -37608,18 +37962,13 @@ function loadCustomEndpointsConfig(customEndpoints) {
(endpoint.models.fetch || endpoint.models.default));
for (let i = 0; i < filteredEndpoints.length; i++) {
const endpoint = filteredEndpoints[i];
const { baseURL, apiKey, name: configName, iconURL, modelDisplayLabel, customParams, } = endpoint;
const { baseURL, apiKey, name: configName, iconURL, modelDisplayLabel, customParams, customOrder, } = endpoint;
const name = librechatDataProvider.normalizeEndpointName(configName);
const resolvedApiKey = librechatDataProvider.extractEnvVariable(apiKey !== null && apiKey !== void 0 ? apiKey : '');
const resolvedBaseURL = librechatDataProvider.extractEnvVariable(baseURL !== null && baseURL !== void 0 ? baseURL : '');
customEndpointsConfig[name] = {
type: librechatDataProvider.EModelEndpoint.custom,
userProvide: isUserProvided(resolvedApiKey),
userProvideURL: isUserProvided(resolvedBaseURL),
customParams,
customEndpointsConfig[name] = Object.assign({ type: librechatDataProvider.EModelEndpoint.custom, userProvide: isUserProvided(resolvedApiKey), userProvideURL: isUserProvided(resolvedBaseURL), customParams,
modelDisplayLabel,
iconURL,
};
iconURL }, (customOrder != null ? { order: customOrder } : {}));
}
}
return customEndpointsConfig;
@@ -37841,7 +38190,7 @@ const primeResources = (_a) => __awaiter(void 0, [_a], void 0, function* ({ req,
* Initializes an agent for use in requests.
* Handles file processing, tool loading, provider configuration, and context token calculations.
*
* This function is exported from @librechat/api and replaces the CJS version from
* This function is exported from @hanzochat/api and replaces the CJS version from
* api/server/services/Endpoints/agents/agent.js
*
* @param params - Initialization parameters
@@ -40839,27 +41188,11 @@ function recordCollectedUsage(deps, params) {
dataSchemas.logger.error('[packages/api #recordCollectedUsage] Error spending tokens', err);
});
}
// Fire usage to Commerce if configured (fire-and-forget)
try {
// Dynamic require to avoid hard dependency — CommerceClient is in the api layer
const { getCommerceClient } = require('~/server/services/CommerceClient');
const commerceClient = getCommerceClient();
if (commerceClient && user) {
// Estimate cost in cents from total tokens (rough: 1M tokenCredits = $1 = 100 cents)
const totalTokens = input_tokens + total_output_tokens;
const estimatedCents = Math.ceil(totalTokens / 10000); // 10,000 tokens ≈ 1 cent
commerceClient.recordUsage({
userId: user,
model: model || 'unknown',
promptTokens: input_tokens,
completionTokens: total_output_tokens,
amountCents: estimatedCents,
});
}
}
catch (_f) {
// Commerce not available — local tracking is authoritative
}
// NOTE: Commerce is debited exactly once, by the cloud gateway (api.hanzo.ai),
// against the user's per-user billing subject when their hk- key is forwarded.
// Chat must NOT also record usage to Commerce here — that was a second debit
// (to a mis-keyed account) and breaks the single-debit money path. Local
// token accounting above remains for the in-app usage display.
return {
input_tokens,
output_tokens: total_output_tokens,
@@ -45482,6 +45815,7 @@ exports.RedisEventTransport = RedisEventTransport;
exports.RedisJobStore = RedisJobStore;
exports.StepTypes = StepTypes;
exports.Tokenizer = TokenizerSingleton;
exports._clearHanzoKeyCache = _clearHanzoKeyCache;
exports.agentAvatarSchema = agentAvatarSchema;
exports.agentBaseResourceSchema = agentBaseResourceSchema;
exports.agentBaseSchema = agentBaseSchema;
@@ -45498,6 +45832,7 @@ exports.applyDefaultParams = applyDefaultParams$1;
exports.azureAISearchSchema = azureAISearchSchema;
exports.backfillRemoteAgentPermissions = backfillRemoteAgentPermissions;
exports.batchDeleteKeys = batchDeleteKeys;
exports.billingSubject = billingSubject;
exports.buildAgentInstructions = buildAgentInstructions;
exports.buildAggregatedResponse = buildAggregatedResponse;
exports.buildImageToolContext = buildImageToolContext;
@@ -45723,6 +46058,7 @@ exports.isChatCompletionValidationFailure = isChatCompletionValidationFailure;
exports.isConcurrentLimitEnabled = isConcurrentLimitEnabled;
exports.isEmailDomainAllowed = isEmailDomainAllowed;
exports.isEnabled = isEnabled;
exports.isHanzoPerUserKeyEnabled = isHanzoPerUserKeyEnabled;
exports.isKnownCustomProvider = isKnownCustomProvider;
exports.isMCPDomainAllowed = isMCPDomainAllowed;
exports.isMCPDomainNotAllowedError = isMCPDomainNotAllowedError;
@@ -45799,6 +46135,7 @@ exports.refreshListAvatars = refreshListAvatars;
exports.requireAdmin = requireAdmin;
exports.resolveGraphTokenPlaceholder = resolveGraphTokenPlaceholder;
exports.resolveGraphTokensInRecord = resolveGraphTokensInRecord;
exports.resolveHanzoCloudKey = resolveHanzoCloudKey;
exports.resolveHeaders = resolveHeaders;
exports.resolveHostnameSSRF = resolveHostnameSSRF;
exports.resolveJsonSchemaRefs = resolveJsonSchemaRefs;
+1 -1
View File
File diff suppressed because one or more lines are too long
+14 -19
View File
@@ -62,19 +62,25 @@ export declare class MCPConnection extends EventEmitter {
disconnect(): Promise<void>;
fetchResources(): Promise<t.MCPResource[]>;
fetchTools(): Promise<{
inputSchema: {
[x: string]: unknown;
type: "object";
properties?: Record<string, object> | undefined;
required?: string[] | undefined;
};
name: string;
inputSchema: {
type: "object";
required?: string[] | undefined;
properties?: Record<string, object> | undefined;
};
_meta?: Record<string, unknown> | undefined;
icons?: {
src: string;
mimeType?: string | undefined;
sizes?: string[] | undefined;
}[] | undefined;
title?: string | undefined;
description?: string | undefined;
outputSchema?: {
[x: string]: unknown;
type: "object";
properties?: Record<string, object> | undefined;
required?: string[] | undefined;
properties?: Record<string, object> | undefined;
additionalProperties?: boolean | undefined;
} | undefined;
annotations?: {
title?: string | undefined;
@@ -83,17 +89,6 @@ export declare class MCPConnection extends EventEmitter {
idempotentHint?: boolean | undefined;
openWorldHint?: boolean | undefined;
} | undefined;
execution?: {
taskSupport?: "optional" | "required" | "forbidden" | undefined;
} | undefined;
_meta?: Record<string, unknown> | undefined;
icons?: {
src: string;
mimeType?: string | undefined;
sizes?: string[] | undefined;
theme?: "light" | "dark" | undefined;
}[] | undefined;
title?: string | undefined;
}[]>;
fetchPrompts(): Promise<t.MCPPrompt[]>;
isConnected(): Promise<boolean>;
+40 -358
View File
@@ -1,399 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="jest tests" tests="185" failures="0" errors="0" time="16.638">
<testsuite name="sanitizeModelName" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:15" time="0.98" tests="20">
<testcase classname="sanitizeModelName removes periods from the model name" name="sanitizeModelName removes periods from the model name" time="0.003">
<testsuites name="jest tests" tests="37" failures="0" errors="0" time="0.504">
<testsuite name="telemetryMiddleware" errors="0" failures="0" skipped="0" timestamp="2026-07-03T09:30:36" time="0.308" tests="16">
<testcase classname="telemetryMiddleware passes through without an active span" name="telemetryMiddleware passes through without an active span" time="0.002">
</testcase>
<testcase classname="sanitizeModelName leaves model name unchanged if no periods are present" name="sanitizeModelName leaves model name unchanged if no periods are present" time="0">
<testcase classname="telemetryMiddleware uses the stored request span for deferred completion attributes" name="telemetryMiddleware uses the stored request span for deferred completion attributes" time="0.001">
</testcase>
<testcase classname="genAzureEndpoint generates correct endpoint URL" name="genAzureEndpoint generates correct endpoint URL" time="0">
<testcase classname="telemetryMiddleware records safe route and identity attributes without body content" name="telemetryMiddleware records safe route and identity attributes without body content" time="0.001">
</testcase>
<testcase classname="genAzureChatCompletion prefers model name over deployment name when both are provided and feature enabled" name="genAzureChatCompletion prefers model name over deployment name when both are provided and feature enabled" time="0.001">
<testcase classname="telemetryMiddleware records identity attributes populated by downstream middleware" name="telemetryMiddleware records identity attributes populated by downstream middleware" time="0">
</testcase>
<testcase classname="genAzureChatCompletion uses deployment name when model name is not provided" name="genAzureChatCompletion uses deployment name when model name is not provided" time="0">
<testcase classname="telemetryMiddleware does not derive tenant identity from request headers" name="telemetryMiddleware does not derive tenant identity from request headers" time="0.001">
</testcase>
<testcase classname="genAzureChatCompletion uses model name when deployment name is not provided and feature enabled" name="genAzureChatCompletion uses model name when deployment name is not provided and feature enabled" time="0">
<testcase classname="telemetryMiddleware ignores health checks" name="telemetryMiddleware ignores health checks" time="0">
</testcase>
<testcase classname="genAzureChatCompletion throws error if neither deployment name nor model name is provided" name="genAzureChatCompletion throws error if neither deployment name nor model name is provided" time="0.009">
<testcase classname="telemetryMiddleware uses a low-cardinality fallback for unmatched API routes" name="telemetryMiddleware uses a low-cardinality fallback for unmatched API routes" time="0.001">
</testcase>
<testcase classname="genAzureChatCompletion ignores model name and uses deployment name when feature is disabled" name="genAzureChatCompletion ignores model name and uses deployment name when feature is disabled" time="0">
<testcase classname="telemetryMiddleware uses a low-cardinality fallback for unmatched SPA routes" name="telemetryMiddleware uses a low-cardinality fallback for unmatched SPA routes" time="0">
</testcase>
<testcase classname="genAzureChatCompletion sanitizes model name when used in URL" name="genAzureChatCompletion sanitizes model name when used in URL" time="0">
<testcase classname="telemetryMiddleware marks server responses as errored" name="telemetryMiddleware marks server responses as errored" time="0">
</testcase>
<testcase classname="genAzureChatCompletion updates client with sanitized model name when provided and feature enabled" name="genAzureChatCompletion updates client with sanitized model name when provided and feature enabled" time="0">
<testcase classname="telemetryMiddleware records completion attributes only once when finish and close both fire" name="telemetryMiddleware records completion attributes only once when finish and close both fire" time="0">
</testcase>
<testcase classname="genAzureChatCompletion does not update client when model name is not provided" name="genAzureChatCompletion does not update client when model name is not provided" time="0">
<testcase classname="telemetryMiddleware marks client disconnects before finish as aborted errors" name="telemetryMiddleware marks client disconnects before finish as aborted errors" time="0">
</testcase>
<testcase classname="genAzureChatCompletion does not update client when feature is disabled" name="genAzureChatCompletion does not update client when feature is disabled" time="0.001">
<testcase classname="telemetryErrorMiddleware records exceptions and forwards the error" name="telemetryErrorMiddleware records exceptions and forwards the error" time="0.001">
</testcase>
<testcase classname="getAzureCredentials retrieves Azure OpenAI API credentials from environment variables" name="getAzureCredentials retrieves Azure OpenAI API credentials from environment variables" time="0">
<testcase classname="telemetryErrorMiddleware records exceptions on the stored request span when available" name="telemetryErrorMiddleware records exceptions on the stored request span when available" time="0.001">
</testcase>
<testcase classname="constructAzureURL replaces both placeholders when both properties are provided" name="constructAzureURL replaces both placeholders when both properties are provided" time="0">
<testcase classname="telemetryErrorMiddleware handles non-Error values without throwing" name="telemetryErrorMiddleware handles non-Error values without throwing" time="0">
</testcase>
<testcase classname="constructAzureURL replaces only INSTANCE_NAME when only azureOpenAIApiInstanceName is provided" name="constructAzureURL replaces only INSTANCE_NAME when only azureOpenAIApiInstanceName is provided" time="0">
<testcase classname="telemetryErrorMiddleware handles null error values without throwing" name="telemetryErrorMiddleware handles null error values without throwing" time="0">
</testcase>
<testcase classname="constructAzureURL replaces only DEPLOYMENT_NAME when only azureOpenAIApiDeploymentName is provided" name="constructAzureURL replaces only DEPLOYMENT_NAME when only azureOpenAIApiDeploymentName is provided" time="0">
</testcase>
<testcase classname="constructAzureURL does not replace any placeholders when azure object is empty" name="constructAzureURL does not replace any placeholders when azure object is empty" time="0">
</testcase>
<testcase classname="constructAzureURL returns baseURL as is when `azureOptions` object is not provided" name="constructAzureURL returns baseURL as is when `azureOptions` object is not provided" time="0">
</testcase>
<testcase classname="constructAzureURL returns baseURL as is when no placeholders are set" name="constructAzureURL returns baseURL as is when no placeholders are set" time="0.001">
</testcase>
<testcase classname="constructAzureURL returns regular Azure OpenAI baseURL with placeholders set" name="constructAzureURL returns regular Azure OpenAI baseURL with placeholders set" time="0">
<testcase classname="telemetryErrorMiddleware forwards the error without an active span" name="telemetryErrorMiddleware forwards the error without an active span" time="0.001">
</testcase>
</testsuite>
<testsuite name="sanitizeFilename" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:16" time="0.114" tests="13">
<testcase classname="sanitizeFilename removes directory components (1/2)" name="sanitizeFilename removes directory components (1/2)" time="0.001">
<testsuite name="telemetry SDK lifecycle" errors="0" failures="0" skipped="0" timestamp="2026-07-03T09:30:36" time="0.332" tests="21">
<testcase classname="telemetry SDK lifecycle does not initialize when tracing is disabled by default" name="telemetry SDK lifecycle does not initialize when tracing is disabled by default" time="0.04">
</testcase>
<testcase classname="sanitizeFilename removes directory components (2/2)" name="sanitizeFilename removes directory components (2/2)" time="0">
<testcase classname="telemetry SDK lifecycle does not initialize when OTEL_SDK_DISABLED is true" name="telemetry SDK lifecycle does not initialize when OTEL_SDK_DISABLED is true" time="0">
</testcase>
<testcase classname="sanitizeFilename replaces non-alphanumeric characters" name="sanitizeFilename replaces non-alphanumeric characters" time="0">
<testcase classname="telemetry SDK lifecycle does not initialize under Bun runtime" name="telemetry SDK lifecycle does not initialize under Bun runtime" time="0.001">
</testcase>
<testcase classname="sanitizeFilename preserves dots and hyphens" name="sanitizeFilename preserves dots and hyphens" time="0">
<testcase classname="telemetry SDK lifecycle starts the Node SDK once when enabled" name="telemetry SDK lifecycle starts the Node SDK once when enabled" time="0.002">
</testcase>
<testcase classname="sanitizeFilename prepends underscore to filenames starting with a dot" name="sanitizeFilename prepends underscore to filenames starting with a dot" time="0">
<testcase classname="telemetry SDK lifecycle tracks HTTP server request spans for completion updates" name="telemetry SDK lifecycle tracks HTTP server request spans for completion updates" time="0">
</testcase>
<testcase classname="sanitizeFilename truncates long filenames" name="sanitizeFilename truncates long filenames" time="0">
<testcase classname="telemetry SDK lifecycle redacts incoming URL attributes before HTTP spans are exported" name="telemetry SDK lifecycle redacts incoming URL attributes before HTTP spans are exported" time="0.001">
</testcase>
<testcase classname="sanitizeFilename handles filenames with no extension" name="sanitizeFilename handles filenames with no extension" time="0">
<testcase classname="telemetry SDK lifecycle redacts outgoing HTTP URL query attributes before client spans are exported" name="telemetry SDK lifecycle redacts outgoing HTTP URL query attributes before client spans are exported" time="0.001">
</testcase>
<testcase classname="sanitizeFilename handles empty input" name="sanitizeFilename handles empty input" time="0.001">
<testcase classname="telemetry SDK lifecycle redacts delimiter-less outgoing query segments and preserves separate HTTP ports" name="telemetry SDK lifecycle redacts delimiter-less outgoing query segments and preserves separate HTTP ports" time="0.001">
</testcase>
<testcase classname="sanitizeFilename handles input with only special characters" name="sanitizeFilename handles input with only special characters" time="0">
<testcase classname="telemetry SDK lifecycle keeps outgoing HTTP URLs absolute when request options omit an origin" name="telemetry SDK lifecycle keeps outgoing HTTP URLs absolute when request options omit an origin" time="0.005">
</testcase>
<testcase classname="sanitizeFilename with real crypto truncates long filenames with real crypto" name="sanitizeFilename with real crypto truncates long filenames with real crypto" time="0.004">
<testcase classname="telemetry SDK lifecycle uses the agent protocol for outgoing URL attributes when request protocol is absent" name="telemetry SDK lifecycle uses the agent protocol for outgoing URL attributes when request protocol is absent" time="0.001">
</testcase>
<testcase classname="sanitizeFilename with real crypto handles filenames with no extension with real crypto" name="sanitizeFilename with real crypto handles filenames with no extension with real crypto" time="0">
<testcase classname="telemetry SDK lifecycle does not infer HTTPS from port 443 without protocol context" name="telemetry SDK lifecycle does not infer HTTPS from port 443 without protocol context" time="0">
</testcase>
<testcase classname="sanitizeFilename with real crypto generates unique suffixes for identical long filenames" name="sanitizeFilename with real crypto generates unique suffixes for identical long filenames" time="0">
<testcase classname="telemetry SDK lifecycle brackets IPv6 hostnames in outgoing HTTP URL attributes" name="telemetry SDK lifecycle brackets IPv6 hostnames in outgoing HTTP URL attributes" time="0.001">
</testcase>
<testcase classname="sanitizeFilename with real crypto real crypto produces valid hex strings" name="sanitizeFilename with real crypto real crypto produces valid hex strings" time="0.001">
<testcase classname="telemetry SDK lifecycle redacts outgoing Undici URL query attributes before fetch spans are exported" name="telemetry SDK lifecycle redacts outgoing Undici URL query attributes before fetch spans are exported" time="0">
</testcase>
</testsuite>
<testsuite name="Environment Variable Extraction (MCP)" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:15" time="1.542" tests="30">
<testcase classname="Environment Variable Extraction (MCP) StdioOptionsSchema should transform environment variables in the env field" name="Environment Variable Extraction (MCP) StdioOptionsSchema should transform environment variables in the env field" time="0.004">
<testcase classname="telemetry SDK lifecycle reflects lifecycle status from the controller getter" name="telemetry SDK lifecycle reflects lifecycle status from the controller getter" time="0.001">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) StdioOptionsSchema should handle undefined env field" name="Environment Variable Extraction (MCP) StdioOptionsSchema should handle undefined env field" time="0">
<testcase classname="telemetry SDK lifecycle handles async SDK start failures without throwing" name="telemetry SDK lifecycle handles async SDK start failures without throwing" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) StreamableHTTPOptionsSchema should validate a valid streamable-http configuration" name="Environment Variable Extraction (MCP) StreamableHTTPOptionsSchema should validate a valid streamable-http configuration" time="0.001">
<testcase classname="telemetry SDK lifecycle returns failed status without throwing when SDK start fails" name="telemetry SDK lifecycle returns failed status without throwing when SDK start fails" time="0.001">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) StreamableHTTPOptionsSchema should reject websocket URLs" name="Environment Variable Extraction (MCP) StreamableHTTPOptionsSchema should reject websocket URLs" time="0.027">
<testcase classname="telemetry SDK lifecycle shuts down the active SDK idempotently" name="telemetry SDK lifecycle shuts down the active SDK idempotently" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) StreamableHTTPOptionsSchema should reject secure websocket URLs" name="Environment Variable Extraction (MCP) StreamableHTTPOptionsSchema should reject secure websocket URLs" time="0">
<testcase classname="telemetry SDK lifecycle coalesces concurrent shutdown calls" name="telemetry SDK lifecycle coalesces concurrent shutdown calls" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) StreamableHTTPOptionsSchema should require type field to be set explicitly" name="Environment Variable Extraction (MCP) StreamableHTTPOptionsSchema should require type field to be set explicitly" time="0.001">
<testcase classname="telemetry SDK lifecycle keeps the active SDK available when shutdown fails" name="telemetry SDK lifecycle keeps the active SDK available when shutdown fails" time="0.007">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) StreamableHTTPOptionsSchema should validate headers as record of strings" name="Environment Variable Extraction (MCP) StreamableHTTPOptionsSchema should validate headers as record of strings" time="0">
<testcase classname="telemetry SDK lifecycle registers a shutdown task with the coordinator when initialized" name="telemetry SDK lifecycle registers a shutdown task with the coordinator when initialized" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should create a deep clone of the input object" name="Environment Variable Extraction (MCP) processMCPEnv should create a deep clone of the input object" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should process environment variables in env field" name="Environment Variable Extraction (MCP) processMCPEnv should process environment variables in env field" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should process user ID in headers field" name="Environment Variable Extraction (MCP) processMCPEnv should process user ID in headers field" time="0.011">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should handle null or undefined input" name="Environment Variable Extraction (MCP) processMCPEnv should handle null or undefined input" time="0.001">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should not modify objects without env or headers" name="Environment Variable Extraction (MCP) processMCPEnv should not modify objects without env or headers" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should ensure different users with same starting config get separate values" name="Environment Variable Extraction (MCP) processMCPEnv should ensure different users with same starting config get separate values" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should process headers in streamable-http options" name="Environment Variable Extraction (MCP) processMCPEnv should process headers in streamable-http options" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should maintain streamable-http type in processed options" name="Environment Variable Extraction (MCP) processMCPEnv should maintain streamable-http type in processed options" time="0.001">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should process dynamic user fields in headers" name="Environment Variable Extraction (MCP) processMCPEnv should process dynamic user fields in headers" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should handle missing user fields gracefully" name="Environment Variable Extraction (MCP) processMCPEnv should handle missing user fields gracefully" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should process user fields in env variables" name="Environment Variable Extraction (MCP) processMCPEnv should process user fields in env variables" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should process user fields in URL" name="Environment Variable Extraction (MCP) processMCPEnv should process user fields in URL" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should handle boolean user fields" name="Environment Variable Extraction (MCP) processMCPEnv should handle boolean user fields" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should not process sensitive fields like password" name="Environment Variable Extraction (MCP) processMCPEnv should not process sensitive fields like password" time="0.001">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should handle multiple occurrences of the same placeholder" name="Environment Variable Extraction (MCP) processMCPEnv should handle multiple occurrences of the same placeholder" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should support both id and _id properties for CHAT_USER_ID" name="Environment Variable Extraction (MCP) processMCPEnv should support both id and _id properties for CHAT_USER_ID" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should process customUserVars in env field" name="Environment Variable Extraction (MCP) processMCPEnv should process customUserVars in env field" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should process customUserVars in headers field" name="Environment Variable Extraction (MCP) processMCPEnv should process customUserVars in headers field" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should process customUserVars in URL field" name="Environment Variable Extraction (MCP) processMCPEnv should process customUserVars in URL field" time="0">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should prioritize customUserVars over user fields and system env vars if placeholders are the same (though not recommended)" name="Environment Variable Extraction (MCP) processMCPEnv should prioritize customUserVars over user fields and system env vars if placeholders are the same (though not recommended)" time="0.009">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should handle customUserVars with no matching placeholders" name="Environment Variable Extraction (MCP) processMCPEnv should handle customUserVars with no matching placeholders" time="0.001">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should handle placeholders with no matching customUserVars (falling back to user/system vars)" name="Environment Variable Extraction (MCP) processMCPEnv should handle placeholders with no matching customUserVars (falling back to user/system vars)" time="0.002">
</testcase>
<testcase classname="Environment Variable Extraction (MCP) processMCPEnv should correctly process a mix of all variable types" name="Environment Variable Extraction (MCP) processMCPEnv should correctly process a mix of all variable types" time="0">
</testcase>
</testsuite>
<testsuite name="Tokenizer" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:15" time="1.517" tests="9">
<testcase classname="Tokenizer should be a singleton (same instance)" name="Tokenizer should be a singleton (same instance)" time="0.007">
</testcase>
<testcase classname="Tokenizer getTokenizer should create an encoder for an explicit model name (e.g., &quot;gpt-4&quot;)" name="Tokenizer getTokenizer should create an encoder for an explicit model name (e.g., &quot;gpt-4&quot;)" time="0.12">
</testcase>
<testcase classname="Tokenizer getTokenizer should create an encoder for a known encoding (e.g., &quot;cl100k_base&quot;)" name="Tokenizer getTokenizer should create an encoder for a known encoding (e.g., &quot;cl100k_base&quot;)" time="0.036">
</testcase>
<testcase classname="Tokenizer getTokenizer should return cached tokenizer if previously fetched" name="Tokenizer getTokenizer should return cached tokenizer if previously fetched" time="0">
</testcase>
<testcase classname="Tokenizer freeAndResetAllEncoders should free all encoders and reset tokenizerCallsCount to 1" name="Tokenizer freeAndResetAllEncoders should free all encoders and reset tokenizerCallsCount to 1" time="0.039">
</testcase>
<testcase classname="Tokenizer freeAndResetAllEncoders should catch and log errors if freeing fails" name="Tokenizer freeAndResetAllEncoders should catch and log errors if freeing fails" time="0.001">
</testcase>
<testcase classname="Tokenizer getTokenCount should return the number of tokens in the given text" name="Tokenizer getTokenCount should return the number of tokens in the given text" time="0.05">
</testcase>
<testcase classname="Tokenizer getTokenCount should reset encoders if an error is thrown" name="Tokenizer getTokenCount should reset encoders if an error is thrown" time="0.202">
</testcase>
<testcase classname="Tokenizer getTokenCount should reset tokenizers after 25 calls" name="Tokenizer getTokenCount should reset tokenizers after 25 calls" time="0.137">
</testcase>
</testsuite>
<testsuite name="extractChatParams" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:15" time="1.577" tests="10">
<testcase classname="extractChatParams should return defaults when options is undefined" name="extractChatParams should return defaults when options is undefined" time="0.003">
</testcase>
<testcase classname="extractChatParams should return defaults when options is null" name="extractChatParams should return defaults when options is null" time="0">
</testcase>
<testcase classname="extractChatParams should extract all Chat params and leave model options" name="extractChatParams should extract all Chat params and leave model options" time="0">
</testcase>
<testcase classname="extractChatParams should handle null values for Chat params" name="extractChatParams should handle null values for Chat params" time="0">
</testcase>
<testcase classname="extractChatParams should use default for resendFiles when not provided" name="extractChatParams should use default for resendFiles when not provided" time="0.001">
</testcase>
<testcase classname="extractChatParams should handle empty options object" name="extractChatParams should handle empty options object" time="0">
</testcase>
<testcase classname="extractChatParams should only extract known Chat params" name="extractChatParams should only extract known Chat params" time="0">
</testcase>
<testcase classname="extractChatParams should not mutate the original options object" name="extractChatParams should not mutate the original options object" time="0">
</testcase>
<testcase classname="extractChatParams should handle undefined values for optional Chat params" name="extractChatParams should handle undefined values for optional Chat params" time="0.001">
</testcase>
<testcase classname="extractChatParams should handle mixed null and undefined values" name="extractChatParams should handle mixed null and undefined values" time="0">
</testcase>
</testsuite>
<testsuite name="isEnabled" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:16" time="0.041" tests="12">
<testcase classname="isEnabled should return true when input is &quot;true&quot;" name="isEnabled should return true when input is &quot;true&quot;" time="0">
</testcase>
<testcase classname="isEnabled should return true when input is &quot;TRUE&quot;" name="isEnabled should return true when input is &quot;TRUE&quot;" time="0">
</testcase>
<testcase classname="isEnabled should return true when input is true" name="isEnabled should return true when input is true" time="0">
</testcase>
<testcase classname="isEnabled should return false when input is &quot;false&quot;" name="isEnabled should return false when input is &quot;false&quot;" time="0">
</testcase>
<testcase classname="isEnabled should return false when input is false" name="isEnabled should return false when input is false" time="0">
</testcase>
<testcase classname="isEnabled should return false when input is null" name="isEnabled should return false when input is null" time="0">
</testcase>
<testcase classname="isEnabled should return false when input is undefined" name="isEnabled should return false when input is undefined" time="0.004">
</testcase>
<testcase classname="isEnabled should return false when input is an empty string" name="isEnabled should return false when input is an empty string" time="0">
</testcase>
<testcase classname="isEnabled should return false when input is a whitespace string" name="isEnabled should return false when input is a whitespace string" time="0">
</testcase>
<testcase classname="isEnabled should return false when input is a number" name="isEnabled should return false when input is a number" time="0.001">
</testcase>
<testcase classname="isEnabled should return false when input is an object" name="isEnabled should return false when input is an object" time="0">
</testcase>
<testcase classname="isEnabled should return false when input is an array" name="isEnabled should return false when input is an array" time="0">
</testcase>
</testsuite>
<testsuite name="resolveHeaders" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:15" time="1.599" tests="20">
<testcase classname="resolveHeaders should return empty object when headers is undefined" name="resolveHeaders should return empty object when headers is undefined" time="0.002">
</testcase>
<testcase classname="resolveHeaders should return empty object when headers is null" name="resolveHeaders should return empty object when headers is null" time="0.002">
</testcase>
<testcase classname="resolveHeaders should return empty object when headers is empty" name="resolveHeaders should return empty object when headers is empty" time="0.001">
</testcase>
<testcase classname="resolveHeaders should process environment variables in headers" name="resolveHeaders should process environment variables in headers" time="0">
</testcase>
<testcase classname="resolveHeaders should process user ID placeholder when user has id" name="resolveHeaders should process user ID placeholder when user has id" time="0">
</testcase>
<testcase classname="resolveHeaders should not process user ID placeholder when user is undefined" name="resolveHeaders should not process user ID placeholder when user is undefined" time="0">
</testcase>
<testcase classname="resolveHeaders should not process user ID placeholder when user has no id" name="resolveHeaders should not process user ID placeholder when user has no id" time="0">
</testcase>
<testcase classname="resolveHeaders should process full user object placeholders" name="resolveHeaders should process full user object placeholders" time="0.002">
</testcase>
<testcase classname="resolveHeaders should handle missing user fields gracefully" name="resolveHeaders should handle missing user fields gracefully" time="0">
</testcase>
<testcase classname="resolveHeaders should process custom user variables" name="resolveHeaders should process custom user variables" time="0">
</testcase>
<testcase classname="resolveHeaders should prioritize custom user variables over user fields" name="resolveHeaders should prioritize custom user variables over user fields" time="0">
</testcase>
<testcase classname="resolveHeaders should handle boolean user fields" name="resolveHeaders should handle boolean user fields" time="0.001">
</testcase>
<testcase classname="resolveHeaders should handle multiple occurrences of the same placeholder" name="resolveHeaders should handle multiple occurrences of the same placeholder" time="0">
</testcase>
<testcase classname="resolveHeaders should handle mixed variable types in the same headers object" name="resolveHeaders should handle mixed variable types in the same headers object" time="0">
</testcase>
<testcase classname="resolveHeaders should not modify the original headers object" name="resolveHeaders should not modify the original headers object" time="0">
</testcase>
<testcase classname="resolveHeaders should handle special characters in custom variable names" name="resolveHeaders should handle special characters in custom variable names" time="0">
</testcase>
<testcase classname="resolveHeaders should replace all allowed user field placeholders" name="resolveHeaders should replace all allowed user field placeholders" time="0.001">
</testcase>
<testcase classname="resolveHeaders should handle multiple placeholders in one value" name="resolveHeaders should handle multiple placeholders in one value" time="0.001">
</testcase>
<testcase classname="resolveHeaders should leave unknown placeholders unchanged" name="resolveHeaders should leave unknown placeholders unchanged" time="0">
</testcase>
<testcase classname="resolveHeaders should handle a mix of all types" name="resolveHeaders should handle a mix of all types" time="0">
</testcase>
</testsuite>
<testsuite name="primeResources" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:15" time="1.643" tests="23">
<testcase classname="primeResources when OCR is enabled and tool_resources has OCR file_ids should fetch OCR files and include them in attachments" name="primeResources when OCR is enabled and tool_resources has OCR file_ids should fetch OCR files and include them in attachments" time="0.004">
</testcase>
<testcase classname="primeResources when OCR is disabled should not fetch OCR files even if tool_resources has OCR file_ids" name="primeResources when OCR is disabled should not fetch OCR files even if tool_resources has OCR file_ids" time="0.001">
</testcase>
<testcase classname="primeResources when attachments are provided should process files with fileIdentifier as execute_code resources" name="primeResources when attachments are provided should process files with fileIdentifier as execute_code resources" time="0">
</testcase>
<testcase classname="primeResources when attachments are provided should process embedded files as file_search resources" name="primeResources when attachments are provided should process embedded files as file_search resources" time="0">
</testcase>
<testcase classname="primeResources when attachments are provided should process image files in requestFileSet as image_edit resources" name="primeResources when attachments are provided should process image files in requestFileSet as image_edit resources" time="0.001">
</testcase>
<testcase classname="primeResources when attachments are provided should not process image files not in requestFileSet" name="primeResources when attachments are provided should not process image files not in requestFileSet" time="0">
</testcase>
<testcase classname="primeResources when attachments are provided should not process image files without height and width" name="primeResources when attachments are provided should not process image files without height and width" time="0">
</testcase>
<testcase classname="primeResources when attachments are provided should filter out null files from attachments" name="primeResources when attachments are provided should filter out null files from attachments" time="0.001">
</testcase>
<testcase classname="primeResources when attachments are provided should merge existing tool_resources with new files" name="primeResources when attachments are provided should merge existing tool_resources with new files" time="0">
</testcase>
<testcase classname="primeResources when both OCR and attachments are provided should include both OCR files and attachment files" name="primeResources when both OCR and attachments are provided should include both OCR files and attachment files" time="0">
</testcase>
<testcase classname="primeResources when both OCR and attachments are provided should prevent duplicate files when same file exists in OCR and attachments" name="primeResources when both OCR and attachments are provided should prevent duplicate files when same file exists in OCR and attachments" time="0.001">
</testcase>
<testcase classname="primeResources when both OCR and attachments are provided should still categorize duplicate files for tool_resources" name="primeResources when both OCR and attachments are provided should still categorize duplicate files for tool_resources" time="0.004">
</testcase>
<testcase classname="primeResources when both OCR and attachments are provided should handle multiple duplicate files" name="primeResources when both OCR and attachments are provided should handle multiple duplicate files" time="0">
</testcase>
<testcase classname="primeResources when both OCR and attachments are provided should handle files without file_id gracefully" name="primeResources when both OCR and attachments are provided should handle files without file_id gracefully" time="0.001">
</testcase>
<testcase classname="primeResources when both OCR and attachments are provided should prevent duplicates from existing tool_resources" name="primeResources when both OCR and attachments are provided should prevent duplicates from existing tool_resources" time="0">
</testcase>
<testcase classname="primeResources when both OCR and attachments are provided should handle duplicates within attachments array" name="primeResources when both OCR and attachments are provided should handle duplicates within attachments array" time="0">
</testcase>
<testcase classname="primeResources when both OCR and attachments are provided should prevent duplicates across different tool_resource categories" name="primeResources when both OCR and attachments are provided should prevent duplicates across different tool_resource categories" time="0">
</testcase>
<testcase classname="primeResources when both OCR and attachments are provided should handle complex scenario with OCR, existing tool_resources, and attachments" name="primeResources when both OCR and attachments are provided should handle complex scenario with OCR, existing tool_resources, and attachments" time="0">
</testcase>
<testcase classname="primeResources error handling should handle errors gracefully and log them" name="primeResources error handling should handle errors gracefully and log them" time="0.001">
</testcase>
<testcase classname="primeResources error handling should handle promise rejection in attachments" name="primeResources error handling should handle promise rejection in attachments" time="0">
</testcase>
<testcase classname="primeResources edge cases should handle missing app.locals gracefully" name="primeResources edge cases should handle missing app.locals gracefully" time="0">
</testcase>
<testcase classname="primeResources edge cases should handle undefined tool_resources" name="primeResources edge cases should handle undefined tool_resources" time="0">
</testcase>
<testcase classname="primeResources edge cases should handle empty requestFileSet" name="primeResources edge cases should handle empty requestFileSet" time="0">
</testcase>
</testsuite>
<testsuite name="normalizeServerName" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:16" time="0.109" tests="4">
<testcase classname="normalizeServerName should not modify server names that already match the pattern" name="normalizeServerName should not modify server names that already match the pattern" time="0">
</testcase>
<testcase classname="normalizeServerName should normalize server names with non-ASCII characters" name="normalizeServerName should normalize server names with non-ASCII characters" time="0">
</testcase>
<testcase classname="normalizeServerName should normalize server names with special characters" name="normalizeServerName should normalize server names with special characters" time="0">
</testcase>
<testcase classname="normalizeServerName should trim leading and trailing underscores" name="normalizeServerName should trim leading and trailing underscores" time="0">
</testcase>
</testsuite>
<testsuite name="MistralOCR Service" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:15" time="1.662" tests="20">
<testcase classname="MistralOCR Service uploadDocumentToMistral should upload a document to Mistral API using file streaming" name="MistralOCR Service uploadDocumentToMistral should upload a document to Mistral API using file streaming" time="0.004">
</testcase>
<testcase classname="MistralOCR Service uploadDocumentToMistral should handle errors during document upload" name="MistralOCR Service uploadDocumentToMistral should handle errors during document upload" time="0.016">
</testcase>
<testcase classname="MistralOCR Service getSignedUrl should fetch signed URL from Mistral API" name="MistralOCR Service getSignedUrl should fetch signed URL from Mistral API" time="0.001">
</testcase>
<testcase classname="MistralOCR Service getSignedUrl should handle errors when fetching signed URL" name="MistralOCR Service getSignedUrl should handle errors when fetching signed URL" time="0">
</testcase>
<testcase classname="MistralOCR Service performOCR should perform OCR using Mistral API (document_url)" name="MistralOCR Service performOCR should perform OCR using Mistral API (document_url)" time="0.001">
</testcase>
<testcase classname="MistralOCR Service performOCR should perform OCR using Mistral API (image_url)" name="MistralOCR Service performOCR should perform OCR using Mistral API (image_url)" time="0">
</testcase>
<testcase classname="MistralOCR Service performOCR should handle errors during OCR processing" name="MistralOCR Service performOCR should handle errors during OCR processing" time="0">
</testcase>
<testcase classname="MistralOCR Service uploadMistralOCR should process OCR for a file with standard configuration" name="MistralOCR Service uploadMistralOCR should process OCR for a file with standard configuration" time="0.001">
</testcase>
<testcase classname="MistralOCR Service uploadMistralOCR should process OCR for an image file and use image_url type" name="MistralOCR Service uploadMistralOCR should process OCR for an image file and use image_url type" time="0.001">
</testcase>
<testcase classname="MistralOCR Service uploadMistralOCR should process variable references in configuration" name="MistralOCR Service uploadMistralOCR should process variable references in configuration" time="0.001">
</testcase>
<testcase classname="MistralOCR Service uploadMistralOCR should fall back to default values when variables are not properly formatted" name="MistralOCR Service uploadMistralOCR should fall back to default values when variables are not properly formatted" time="0">
</testcase>
<testcase classname="MistralOCR Service uploadMistralOCR should handle API errors during OCR process" name="MistralOCR Service uploadMistralOCR should handle API errors during OCR process" time="0.008">
</testcase>
<testcase classname="MistralOCR Service uploadMistralOCR should handle single page documents without page numbering" name="MistralOCR Service uploadMistralOCR should handle single page documents without page numbering" time="0">
</testcase>
<testcase classname="MistralOCR Service uploadMistralOCR should use literal values in configuration when provided directly" name="MistralOCR Service uploadMistralOCR should use literal values in configuration when provided directly" time="0.001">
</testcase>
<testcase classname="MistralOCR Service uploadMistralOCR should handle empty configuration values and use defaults" name="MistralOCR Service uploadMistralOCR should handle empty configuration values and use defaults" time="0">
</testcase>
<testcase classname="MistralOCR Service uploadMistralOCR Mixed env var and hardcoded configuration should preserve hardcoded baseURL when only apiKey is an env var" name="MistralOCR Service uploadMistralOCR Mixed env var and hardcoded configuration should preserve hardcoded baseURL when only apiKey is an env var" time="0.001">
</testcase>
<testcase classname="MistralOCR Service uploadMistralOCR Mixed env var and hardcoded configuration should preserve hardcoded apiKey when only baseURL is an env var" name="MistralOCR Service uploadMistralOCR Mixed env var and hardcoded configuration should preserve hardcoded apiKey when only baseURL is an env var" time="0">
</testcase>
<testcase classname="MistralOCR Service uploadAzureMistralOCR should process OCR using Azure Mistral with base64 encoding" name="MistralOCR Service uploadAzureMistralOCR should process OCR using Azure Mistral with base64 encoding" time="0.001">
</testcase>
<testcase classname="MistralOCR Service uploadAzureMistralOCR Mixed env var and hardcoded configuration should preserve hardcoded baseURL when only apiKey is an env var" name="MistralOCR Service uploadAzureMistralOCR Mixed env var and hardcoded configuration should preserve hardcoded baseURL when only apiKey is an env var" time="0">
</testcase>
<testcase classname="MistralOCR Service uploadAzureMistralOCR Mixed env var and hardcoded configuration should preserve hardcoded apiKey when only baseURL is an env var" name="MistralOCR Service uploadAzureMistralOCR Mixed env var and hardcoded configuration should preserve hardcoded apiKey when only baseURL is an env var" time="0">
</testcase>
</testsuite>
<testsuite name="createAxiosInstance" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:16" time="0.615" tests="6">
<testcase classname="createAxiosInstance creates an axios instance without proxy when no proxy env is set" name="createAxiosInstance creates an axios instance without proxy when no proxy env is set" time="0.001">
</testcase>
<testcase classname="createAxiosInstance configures proxy correctly with hostname and protocol" name="createAxiosInstance configures proxy correctly with hostname and protocol" time="0">
</testcase>
<testcase classname="createAxiosInstance configures proxy correctly with hostname, protocol and port" name="createAxiosInstance configures proxy correctly with hostname, protocol and port" time="0">
</testcase>
<testcase classname="createAxiosInstance handles proxy URLs with authentication" name="createAxiosInstance handles proxy URLs with authentication" time="0.001">
</testcase>
<testcase classname="createAxiosInstance throws error when proxy URL is invalid" name="createAxiosInstance throws error when proxy URL is invalid" time="0.007">
</testcase>
<testcase classname="createAxiosInstance handles edge case proxy URLs correctly" name="createAxiosInstance handles edge case proxy URLs correctly" time="0">
</testcase>
</testsuite>
<testsuite name="tempChatRetention" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:15" time="1.832" tests="12">
<testcase classname="tempChatRetention getTempChatRetentionHours should return default retention hours when no config or env var is set" name="tempChatRetention getTempChatRetentionHours should return default retention hours when no config or env var is set" time="0.003">
</testcase>
<testcase classname="tempChatRetention getTempChatRetentionHours should use environment variable when set" name="tempChatRetention getTempChatRetentionHours should use environment variable when set" time="0">
</testcase>
<testcase classname="tempChatRetention getTempChatRetentionHours should use config value when set" name="tempChatRetention getTempChatRetentionHours should use config value when set" time="0">
</testcase>
<testcase classname="tempChatRetention getTempChatRetentionHours should prioritize config over environment variable" name="tempChatRetention getTempChatRetentionHours should prioritize config over environment variable" time="0.001">
</testcase>
<testcase classname="tempChatRetention getTempChatRetentionHours should enforce minimum retention period" name="tempChatRetention getTempChatRetentionHours should enforce minimum retention period" time="0.003">
</testcase>
<testcase classname="tempChatRetention getTempChatRetentionHours should enforce maximum retention period" name="tempChatRetention getTempChatRetentionHours should enforce maximum retention period" time="0.001">
</testcase>
<testcase classname="tempChatRetention getTempChatRetentionHours should handle invalid environment variable" name="tempChatRetention getTempChatRetentionHours should handle invalid environment variable" time="0">
</testcase>
<testcase classname="tempChatRetention getTempChatRetentionHours should handle invalid config value" name="tempChatRetention getTempChatRetentionHours should handle invalid config value" time="0.001">
</testcase>
<testcase classname="tempChatRetention createTempChatExpirationDate should create expiration date with default retention period" name="tempChatRetention createTempChatExpirationDate should create expiration date with default retention period" time="0">
</testcase>
<testcase classname="tempChatRetention createTempChatExpirationDate should create expiration date with custom retention period" name="tempChatRetention createTempChatExpirationDate should create expiration date with custom retention period" time="0.001">
</testcase>
<testcase classname="tempChatRetention createTempChatExpirationDate should return a Date object" name="tempChatRetention createTempChatExpirationDate should return a Date object" time="0">
</testcase>
<testcase classname="tempChatRetention createTempChatExpirationDate should return a future date" name="tempChatRetention createTempChatExpirationDate should return a future date" time="0">
</testcase>
</testsuite>
<testsuite name="FlowStateManager" errors="0" failures="0" skipped="0" timestamp="2025-06-26T19:28:15" time="15.407" tests="6">
<testcase classname="FlowStateManager Concurrency Tests should handle concurrent flow creation and return same result" name="FlowStateManager Concurrency Tests should handle concurrent flow creation and return same result" time="2.258">
</testcase>
<testcase classname="FlowStateManager Concurrency Tests should handle flow timeout correctly" name="FlowStateManager Concurrency Tests should handle flow timeout correctly" time="2.272">
</testcase>
<testcase classname="FlowStateManager Concurrency Tests should maintain flow state consistency under high concurrency" name="FlowStateManager Concurrency Tests should maintain flow state consistency under high concurrency" time="2.254">
</testcase>
<testcase classname="FlowStateManager Concurrency Tests should handle race conditions in flow completion" name="FlowStateManager Concurrency Tests should handle race conditions in flow completion" time="2.256">
</testcase>
<testcase classname="FlowStateManager Concurrency Tests should handle concurrent flow monitoring" name="FlowStateManager Concurrency Tests should handle concurrent flow monitoring" time="2.254">
</testcase>
<testcase classname="FlowStateManager Concurrency Tests should handle concurrent success and failure attempts" name="FlowStateManager Concurrency Tests should handle concurrent success and failure attempts" time="2.255">
<testcase classname="telemetry SDK lifecycle the registered shutdown task warns when telemetry shutdown rejects" name="telemetry SDK lifecycle the registered shutdown task warns when telemetry shutdown rejects" time="0.001">
</testcase>
</testsuite>
</testsuites>
+1 -1
View File
@@ -77,7 +77,7 @@ export interface AdminGrantsDeps {
/** Currently ROLE-only; Record/Set structure preserved for future principal-type expansion. */
export type GrantPrincipalType = PrincipalType.ROLE;
/** Creates admin grant handlers with dependency injection for the /api/admin/grants routes. */
/** Creates admin grant handlers with dependency injection for the /v1/chat/admin/grants routes. */
export function createAdminGrantsHandlers(deps: AdminGrantsDeps) {
const {
listGrants,
+1 -1
View File
@@ -18,7 +18,7 @@ const MAX_DESCRIPTION_LENGTH = 2000;
const CONTROL_CHAR_RE = /\p{Cc}/u;
/**
* Role names that would create semantically ambiguous URLs.
* e.g. GET /api/admin/roles/members is that "list roles" or "get role named members"?
* e.g. GET /v1/chat/admin/roles/members is that "list roles" or "get role named members"?
* Express routing resolves this correctly (single vs multi-segment), but the URLs
* are confusing for API consumers. Keep in sync with sub-path routes in routes/admin/roles.js.
*/
+1 -1
View File
@@ -34,7 +34,7 @@ function normalize(value: string | undefined): string | null {
/**
* Resolves the current deployment's build info from (in order): explicit env vars,
* then local git metadata. Used by `/api/config` to expose commit/branch to clients
* then local git metadata. Used by `/v1/chat/config` to expose commit/branch to clients
* when `interface.buildInfo` is enabled.
*/
export function resolveBuildInfo(): BuildInfo {

Some files were not shown because too many files have changed in this diff Show More