From fa4de65be9d047cbd6d6a05a6f3a2d5bfdecf277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Wed, 8 Jul 2026 11:32:18 -0400 Subject: [PATCH 01/23] config for playwright MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Carlos Logroño Guerrero --- .github/workflows/ui-playwright.yaml | 57 +++++++++++++ ui/.gitignore | 5 ++ ui/package-lock.json | 22 ++++- ui/package.json | 4 + ui/playwright.config.ts | 60 ++++++++++++++ ui/playwright/README.md | 87 +++++++++++++++++++ ui/playwright/fixtures/test.ts | 26 ++++++ ui/playwright/mocks/data.ts | 72 ++++++++++++++++ ui/playwright/mocks/server.mjs | 120 +++++++++++++++++++++++++++ ui/playwright/tests/smoke.spec.ts | 20 +++++ ui/playwright/tsconfig.json | 7 ++ 11 files changed, 477 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ui-playwright.yaml create mode 100644 ui/playwright.config.ts create mode 100644 ui/playwright/README.md create mode 100644 ui/playwright/fixtures/test.ts create mode 100644 ui/playwright/mocks/data.ts create mode 100644 ui/playwright/mocks/server.mjs create mode 100644 ui/playwright/tests/smoke.spec.ts create mode 100644 ui/playwright/tsconfig.json diff --git a/.github/workflows/ui-playwright.yaml b/.github/workflows/ui-playwright.yaml new file mode 100644 index 0000000000..d1ccabe4fd --- /dev/null +++ b/.github/workflows/ui-playwright.yaml @@ -0,0 +1,57 @@ +name: UI Playwright E2E + +# Page-level browser E2E for the UI against a mocked backend. +# Kept separate from CI so the main workflow does not need paths-filter or +# conditional steps. No cluster required — the stub backend is booted by Playwright. + +on: + push: + branches: [main, "release/**"] + paths: + - "ui/**" + pull_request: + branches: [main, "release/**"] + paths: + - "ui/**" + workflow_dispatch: + +jobs: + playwright: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "24" + cache: "npm" + cache-dependency-path: ui/package-lock.json + + - name: Install dependencies + working-directory: ./ui + env: + CYPRESS_INSTALL_BINARY: 0 + run: npm ci + + - name: Install Playwright browser + working-directory: ./ui + run: npx playwright install --with-deps chromium + + - name: Run Playwright tests + working-directory: ./ui + run: npm run test:pw + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: | + ui/playwright-report + ui/playwright/test-results + retention-days: 14 diff --git a/ui/.gitignore b/ui/.gitignore index 9352a9952b..adb14e3028 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -42,3 +42,8 @@ next-env.d.ts # storybook /storybook-static + +# playwright +/playwright-report +/playwright/test-results +/playwright/.cache diff --git a/ui/package-lock.json b/ui/package-lock.json index 44516378f9..0a11de0827 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -58,6 +58,7 @@ "@chromatic-com/storybook": "^5.2.1", "@eslint/eslintrc": "^3.3.6", "@jest/globals": "^30.4.1", + "@playwright/test": "^1.61.0", "@storybook/addon-a11y": "^10.5.0", "@storybook/addon-docs": "^10.5.0", "@storybook/addon-onboarding": "^10.5.0", @@ -4179,6 +4180,22 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -16731,7 +16748,7 @@ "version": "1.61.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.61.1" @@ -16750,7 +16767,7 @@ "version": "1.61.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -16763,7 +16780,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ diff --git a/ui/package.json b/ui/package.json index 389745b425..43cf0e31f7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,9 @@ "test:watch": "jest --watch", "test:e2e:cypress": "cypress run --e2e", "test:e2e": "start-server-and-test dev http://localhost:8001 test:e2e:cypress", + "test:pw": "playwright test", + "test:pw:ui": "playwright test --ui", + "test:pw:debug": "playwright test --debug", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "chromatic": "chromatic --exit-zero-on-changes --storybook-build-dir storybook-static --project-token chpt_3e29f54d624610f" @@ -75,6 +78,7 @@ "@chromatic-com/storybook": "^5.2.1", "@eslint/eslintrc": "^3.3.6", "@jest/globals": "^30.4.1", + "@playwright/test": "^1.61.0", "@storybook/addon-a11y": "^10.5.0", "@storybook/addon-docs": "^10.5.0", "@storybook/addon-onboarding": "^10.5.0", diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts new file mode 100644 index 0000000000..6b6e33ecdd --- /dev/null +++ b/ui/playwright.config.ts @@ -0,0 +1,60 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Playwright E2E config for the kagent UI. + * + * Data is fetched server-side (Next.js server actions), so browser-level + * `page.route` cannot intercept `/api/**`. Instead we boot a standalone stub + * backend (playwright/mocks/server.mjs) and point Next at it via + * BACKEND_INTERNAL_URL — `getBackendUrl()` (src/lib/utils.ts) checks that env + * var first. Both servers are started by the `webServer` block below. + * + * See playwright/README.md for the full test strategy. + */ + +const CI = !!process.env.CI; + +const STUB_PORT = 8899; +const STUB_URL = `http://127.0.0.1:${STUB_PORT}`; +const APP_URL = "http://localhost:8001"; + +export default defineConfig({ + testDir: "./playwright/tests", + outputDir: "./playwright/test-results", + fullyParallel: true, + forbidOnly: CI, + retries: CI ? 1 : 0, + // Single worker for now: one shared stub backend + one Next server. Per-test + // data isolation is a Stage 1 concern (see README). + workers: CI ? 1 : undefined, + timeout: 30_000, + expect: { timeout: 10_000 }, + reporter: [["html", { open: "never" }], ["list"]], + use: { + baseURL: APP_URL, + screenshot: "only-on-failure", + trace: "retain-on-failure", + video: "off", + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], + webServer: [ + { + command: "node playwright/mocks/server.mjs", + url: `${STUB_URL}/__mock/health`, + reuseExistingServer: !CI, + timeout: 30_000, + stdout: "pipe", + stderr: "pipe", + }, + { + command: "npm run dev", + url: APP_URL, + reuseExistingServer: !CI, + timeout: 120_000, + env: { + // Redirect the server-side backend fetch to our stub. + BACKEND_INTERNAL_URL: `${STUB_URL}/api`, + }, + }, + ], +}); diff --git a/ui/playwright/README.md b/ui/playwright/README.md new file mode 100644 index 0000000000..96e8f55a1b --- /dev/null +++ b/ui/playwright/README.md @@ -0,0 +1,87 @@ +# Playwright E2E tests + +Page-level browser end-to-end tests for the kagent UI, run against a **mocked +backend**. This suite fills the one gap nothing else covers: **multi-step user +flows across components**. + +## What this suite does and does not cover + +| Layer | Tool | Not Playwright's job | +|---|---|---| +| Atoms (`src/components/ui/*`) | shadcn primitives | skip | +| Visual / render states | Storybook + Vitest-browser + Chromatic | skip | +| Unit / logic | Jest (`*.test.ts(x)`) | skip | +| Page-load smoke (`h1` renders, page reachable, onboarding steps 1–2, model-edit page opens) | Cypress (`cypress/e2e/smoke.cy.ts`) | skip | +| **Multi-step flows: form submission, payload correctness, streaming, wizard completion, error/edge states** | **Playwright (this suite)** | — | + +Rule: if Cypress / Chromatic / Jest already assert it, Playwright does not. + +## How mocking works (important) + +Nearly every `/api/**` call runs **server-side** inside Next.js server actions +(`src/app/actions/*.ts`, all `"use server"`). The browser sends an opaque RPC +POST to the route; the Node server does the actual backend `fetch`. So browser +`page.route("**/api/**")` intercepts **nothing** on load. + +Instead we run a standalone **stub backend** (`mocks/server.mjs`) and point Next +at it with `BACKEND_INTERNAL_URL` — `getBackendUrl()` (`src/lib/utils.ts`) checks +that env var first. Playwright's `webServer` (in `../playwright.config.ts`) boots +both the stub (`:8899`) and `next dev` (`:8001`). + +The one exception is A2A chat streaming (`POST /a2a/**`, SSE), which **is** +browser-originated and `page.route`-able — used in the chat spec (Stage 2). + +## Layout + +``` +playwright/ + tests/ # *.spec.ts, one per feature area + helpers/ # (Stage 1) reusable drivers: page, forms, select, dialog, nav + mocks/ + server.mjs # stub backend (runtime source of happy-path data) + data.ts # typed spec-side builders (mirror server.mjs shapes) + fixtures/ + test.ts # import { test, expect } from here in every spec + tsconfig.json +``` + +## Running + +```bash +cd ui +npm install +npx playwright install chromium # first time only +npm run test:pw # headless +npm run test:pw:ui # interactive UI mode +npm run test:pw:debug # step-through debugger +``` + +Playwright starts the stub + dev server automatically. To drive the app manually +against the stub: + +```bash +node playwright/mocks/server.mjs & +BACKEND_INTERNAL_URL=http://127.0.0.1:8899/api npm run dev +# open http://localhost:8001 +``` + +## Conventions + +- Import `{ test, expect }` from `../fixtures/test`, never `@playwright/test`. +- One spec file per feature area (`tests/agents/`, `tests/chat/`, …); use + `test.step()` for sub-phases of a multi-step flow. +- **`data-testid` policy:** prefer `getByRole` / `getByLabel`. Add `data-testid` + only where role/text is ambiguous or unstable (list rows, per-item action + buttons, wizard steps, combobox options). Add incrementally — no upfront sweep. + Do **not** remove/rename the existing `data-test` model-edit hooks; Cypress + depends on them. + +## Roadmap + +- **Stage 0 (done):** foundation — config, stub backend, CI, one smoke test. +- **Stage 1:** helper/driver library (`helpers/*`) + per-test scenario overrides + via the stub's `/__mock/scenario` endpoint. Prefer stateless scenario selection + keyed by request content (e.g. `?namespace=empty-ns` → `[]`); fall back to the + control endpoint with `workers: 1` for endpoints lacking a discriminator. +- **Stage 2:** feature flows (gap-scoped), ordered by importance — + Create Agent → Chat/session (A2A SSE mock) → Models → MCP → Onboarding completion. diff --git a/ui/playwright/fixtures/test.ts b/ui/playwright/fixtures/test.ts new file mode 100644 index 0000000000..80e650aaa6 --- /dev/null +++ b/ui/playwright/fixtures/test.ts @@ -0,0 +1,26 @@ +// Shared test fixture. Import { test, expect } from here in every spec (not +// directly from @playwright/test) so all specs get the same setup. +// +// For now it resets the stub backend before each test. Reset is a no-op until +// Stage 1 adds per-test scenarios, but establishing the pattern now means specs +// don't change when scenarios land. + +import { test as base, expect } from "@playwright/test"; + +const STUB_URL = "http://127.0.0.1:8899"; + +export const test = base.extend({ + page: async ({ page, request }, use) => { + await request.post(`${STUB_URL}/__mock/reset`).catch(() => { + // Stub not reachable (e.g. reused external server) — non-fatal for Stage 0. + }); + // Bypass the first-run onboarding wizard (AppInitializer redirects to it when + // this key is unset). Runs before any page script, on every navigation. + await page.addInitScript(() => { + window.localStorage.setItem("kagent-onboarding", "true"); + }); + await use(page); + }, +}); + +export { expect }; diff --git a/ui/playwright/mocks/data.ts b/ui/playwright/mocks/data.ts new file mode 100644 index 0000000000..8d0ba6e126 --- /dev/null +++ b/ui/playwright/mocks/data.ts @@ -0,0 +1,72 @@ +// Typed, spec-side mock builders. Use these in specs to construct payloads for +// assertions or (from Stage 1) to POST scenarios to the stub's /__mock/scenario +// endpoint. The stub backend's runtime happy-path lives in server.mjs; keep the +// shapes here in sync with it. +// +// Typed against the app's own types (via the @/ alias, see playwright/tsconfig.json) +// so drift from the real API surface fails at compile time. + +import type { + AgentResponse, + ModelConfig, + ProviderModelsResponse, + ToolsResponse, +} from "@/types"; + +export type Namespace = { name: string; status: string }; +export type ToolServerListEntry = { ref: string; groupKind: string; discoveredTools: unknown[] }; + +export function mockAgentResponse(overrides: Partial = {}): AgentResponse { + return { + id: "1", + agent: { + metadata: { name: "e2e-agent", namespace: "default" }, + spec: { type: "Declarative", description: "Seeded E2E agent" }, + }, + model: "gpt-4o", + modelProvider: "OpenAI", + modelConfigRef: "default/default-model-config", + tools: [], + deploymentReady: true, + accepted: true, + ...overrides, + }; +} + +export function mockModelsResponse(): ProviderModelsResponse { + return { openai: [{ name: "gpt-4o", function_calling: true }] }; +} + +export function mockModelConfig(overrides: Partial = {}): ModelConfig { + return { + ref: "default/default-model-config", + spec: { model: "gpt-4o", provider: "OpenAI" }, + ...overrides, + }; +} + +export function mockNamespace(overrides: Partial = {}): Namespace { + return { name: "default", status: "Active", ...overrides }; +} + +export function mockToolServer(overrides: Partial = {}): ToolServerListEntry { + return { + ref: "default/e2e-tool-server", + groupKind: "RemoteMCPServer.kagent.dev", + discoveredTools: [], + ...overrides, + }; +} + +export function mockTool(overrides: Partial = {}): ToolsResponse { + return { + id: "e2e-tool", + server_name: "e2e-tool-server", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + deleted_at: "", + description: "Seeded E2E tool", + group_kind: "RemoteMCPServer.kagent.dev", + ...overrides, + }; +} diff --git a/ui/playwright/mocks/server.mjs b/ui/playwright/mocks/server.mjs new file mode 100644 index 0000000000..43cead2954 --- /dev/null +++ b/ui/playwright/mocks/server.mjs @@ -0,0 +1,120 @@ +// Standalone stub backend for Playwright E2E. +// +// The kagent UI fetches data server-side (Next.js server actions), so the +// backend fetch happens in the Node process, not the browser — browser-level +// page.route cannot mock it. Instead we run this tiny HTTP server and point +// Next at it via BACKEND_INTERNAL_URL (see playwright.config.ts). getBackendUrl() +// in src/lib/utils.ts checks BACKEND_INTERNAL_URL first, so every /api/* call +// lands here. +// +// Dependency-free (Node built-in http only) so it runs without a TS/build step. +// Payloads mirror the shapes in src/types/index.ts; the typed spec-side builders +// live in playwright/mocks/data.ts. Keep the two in sync until Stage 1 unifies +// them behind the /__mock/scenario control endpoint. + +import { createServer } from "node:http"; + +const PORT = Number(process.env.STUB_PORT ?? 8899); + +// region Payloads (happy path) + +// Success envelope used by the Go backend: { message, data }. +const ok = (data, message = "OK") => ({ message, data }); + +const agent = { + id: "1", + agent: { + metadata: { name: "e2e-agent", namespace: "default" }, + spec: { type: "Declarative", description: "Seeded E2E agent" }, + }, + model: "gpt-4o", + modelProvider: "OpenAI", + modelConfigRef: "default/default-model-config", + tools: [], + deploymentReady: true, + accepted: true, +}; + +const modelConfig = { + ref: "default/default-model-config", + spec: { model: "gpt-4o", provider: "OpenAI" }, +}; + +const toolServer = { + ref: "default/e2e-tool-server", + groupKind: "RemoteMCPServer.kagent.dev", + discoveredTools: [], +}; + +const tool = { + id: "e2e-tool", + server_name: "e2e-tool-server", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + deleted_at: "", + description: "Seeded E2E tool", + group_kind: "RemoteMCPServer.kagent.dev", +}; + +const substrateStatus = { + enabled: false, + workerPools: [], + actorTemplates: [], + actors: [], + workers: [], +}; + +// GET route table keyed by pathname (query string stripped before lookup). +const GET_ROUTES = { + "/api/agents": () => ok([agent], "Successfully fetched agents"), + "/api/models": () => ok({ openai: [{ name: "gpt-4o", function_calling: true }] }), + "/api/modelconfigs": () => ok([modelConfig]), + "/api/namespaces": () => ok([{ name: "default", status: "Active" }], "Namespaces fetched successfully"), + "/api/toolservers": () => ok([toolServer]), + "/api/tools": () => ok([tool]), + "/api/substrate/status": () => ok(substrateStatus, "Substrate status fetched"), +}; + +// endregion + +// region Server + +const json = (res, status, body) => { + const payload = JSON.stringify(body); + res.writeHead(status, { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(payload), + }); + res.end(payload); +}; + +const server = createServer((req, res) => { + const { method, url } = req; + const pathname = url.split("?")[0]; + + // Control + health endpoints. + if (pathname === "/__mock/health") return json(res, 200, { status: "ok" }); + if (method === "POST" && pathname === "/__mock/reset") { + // No scenario state yet — happy path only. Wired up in Stage 1. + return json(res, 200, { status: "reset" }); + } + if (method === "POST" && pathname === "/__mock/scenario") { + // Placeholder for per-test overrides (Stage 1). + return json(res, 200, { status: "noop" }); + } + + if (method === "GET" && GET_ROUTES[pathname]) { + console.log(`[stub] ${method} ${url} -> 200`); + return json(res, 200, GET_ROUTES[pathname]()); + } + + // Anything unmocked is a real gap — make it loud so we notice leaks. + console.warn(`[stub] UNHANDLED ${method} ${url} -> 404`); + return json(res, 404, { error: `No stub for ${method} ${pathname}` }); +}); + +server.listen(PORT, "127.0.0.1", () => { + console.log(`[stub] kagent mock backend listening on http://127.0.0.1:${PORT}`); +}); + +// endregion diff --git a/ui/playwright/tests/smoke.spec.ts b/ui/playwright/tests/smoke.spec.ts new file mode 100644 index 0000000000..3bbc1eba53 --- /dev/null +++ b/ui/playwright/tests/smoke.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from "../fixtures/test"; + +// Stage 0 smoke test: proves the whole rig works end to end — Playwright boots +// the stub backend + `next dev`, the server-side fetch is redirected to the stub +// (via BACKEND_INTERNAL_URL), and the home page renders the mocked agent list. +test("home page renders the Agents list against the mocked backend", async ({ page }) => { + const fatalErrors: string[] = []; + page.on("pageerror", (err) => fatalErrors.push(err.message)); + + await page.goto("/"); + + // PageHeader renders

Agents

on successful load + // (present in both populated and empty states; absent in loading/error states). + await expect(page.getByRole("heading", { level: 1, name: "Agents" })).toBeVisible(); + + // ErrorState (early-return branch) renders this heading — it must not appear. + await expect(page.getByText("Error Encountered")).toHaveCount(0); + + expect(fatalErrors, `uncaught page errors: ${fatalErrors.join("; ")}`).toEqual([]); +}); diff --git a/ui/playwright/tsconfig.json b/ui/playwright/tsconfig.json new file mode 100644 index 0000000000..fd1b9b7035 --- /dev/null +++ b/ui/playwright/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["node", "@playwright/test"] + }, + "include": ["**/*.ts", "**/*.mjs"] +} From 631d1c3669c9ec23394e13c563c46e594cb69025 Mon Sep 17 00:00:00 2001 From: cjlg-soloio Date: Wed, 8 Jul 2026 12:31:07 -0400 Subject: [PATCH 02/23] Run with 1 worker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: cjlg-soloio Signed-off-by: Carlos Logroño Guerrero --- ui/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index 6b6e33ecdd..235a078091 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -26,7 +26,7 @@ export default defineConfig({ retries: CI ? 1 : 0, // Single worker for now: one shared stub backend + one Next server. Per-test // data isolation is a Stage 1 concern (see README). - workers: CI ? 1 : undefined, + workers: 1, timeout: 30_000, expect: { timeout: 10_000 }, reporter: [["html", { open: "never" }], ["list"]], From 09d28ede0ee62ad7f85783d58997bc3dd28f595b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Wed, 8 Jul 2026 12:34:40 -0400 Subject: [PATCH 03/23] fix: resolve Copilot review comments on Playwright config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Force single Playwright worker unconditionally (shared stub + Next server) - Default req.method/req.url before parsing in the stub server - Fail fast on stub reset failure in CI; stay non-fatal locally - Drop unused pull-requests:write permission from the workflow Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Carlos Logroño Guerrero --- .github/workflows/ui-playwright.yaml | 1 - ui/playwright/fixtures/test.ts | 9 ++++++--- ui/playwright/mocks/server.mjs | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ui-playwright.yaml b/.github/workflows/ui-playwright.yaml index d1ccabe4fd..90b1db9e39 100644 --- a/.github/workflows/ui-playwright.yaml +++ b/.github/workflows/ui-playwright.yaml @@ -20,7 +20,6 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v6 diff --git a/ui/playwright/fixtures/test.ts b/ui/playwright/fixtures/test.ts index 80e650aaa6..f3f95cb347 100644 --- a/ui/playwright/fixtures/test.ts +++ b/ui/playwright/fixtures/test.ts @@ -11,9 +11,12 @@ const STUB_URL = "http://127.0.0.1:8899"; export const test = base.extend({ page: async ({ page, request }, use) => { - await request.post(`${STUB_URL}/__mock/reset`).catch(() => { - // Stub not reachable (e.g. reused external server) — non-fatal for Stage 0. - }); + try { + await request.post(`${STUB_URL}/__mock/reset`); + } catch (err) { + // Stub should always be reachable in CI (managed via webServer). Fail fast. + if (process.env.CI) throw err; + } // Bypass the first-run onboarding wizard (AppInitializer redirects to it when // this key is unset). Runs before any page script, on every navigation. await page.addInitScript(() => { diff --git a/ui/playwright/mocks/server.mjs b/ui/playwright/mocks/server.mjs index 43cead2954..54d3d60ae4 100644 --- a/ui/playwright/mocks/server.mjs +++ b/ui/playwright/mocks/server.mjs @@ -89,7 +89,8 @@ const json = (res, status, body) => { }; const server = createServer((req, res) => { - const { method, url } = req; + const method = req.method ?? "GET"; + const url = req.url ?? "/"; const pathname = url.split("?")[0]; // Control + health endpoints. From 083bcbf4f3eebd17c678b66627c22b057d1a8211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Wed, 8 Jul 2026 12:42:54 -0400 Subject: [PATCH 04/23] docs: explain defensive req.method/url defaults in stub server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Carlos Logroño Guerrero --- ui/playwright/mocks/server.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/playwright/mocks/server.mjs b/ui/playwright/mocks/server.mjs index 54d3d60ae4..8181dc6dcd 100644 --- a/ui/playwright/mocks/server.mjs +++ b/ui/playwright/mocks/server.mjs @@ -89,6 +89,8 @@ const json = (res, status, body) => { }; const server = createServer((req, res) => { + // req.method/url are typed string|undefined; default them so a malformed + // request can't throw on the split below. const method = req.method ?? "GET"; const url = req.url ?? "/"; const pathname = url.split("?")[0]; From 151d0c1fa2658481f0a0f574adbe7129cf88b951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Wed, 8 Jul 2026 15:59:59 -0400 Subject: [PATCH 05/23] test(ui): disable fullyParallel until Stage 1 isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs fullyParallel with workers: 1 so both stay off until per-test data isolation lands, preventing tests from silently racing against the shared stub backend if the worker count is raised. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Carlos Logroño Guerrero --- ui/playwright.config.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index 235a078091..d907923a15 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -21,11 +21,13 @@ const APP_URL = "http://localhost:8001"; export default defineConfig({ testDir: "./playwright/tests", outputDir: "./playwright/test-results", - fullyParallel: true, + // Parallelism stays off until Stage 1 per-test data isolation lands: one + // shared stub backend + one Next server means concurrent tests would race + // against shared state (see README). Flip both `fullyParallel` and `workers` + // together when isolation is in place. + fullyParallel: false, forbidOnly: CI, retries: CI ? 1 : 0, - // Single worker for now: one shared stub backend + one Next server. Per-test - // data isolation is a Stage 1 concern (see README). workers: 1, timeout: 30_000, expect: { timeout: 10_000 }, From a0a9d0595695ccf0def7c6bf16d374ba7576b97b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Wed, 8 Jul 2026 17:22:18 -0400 Subject: [PATCH 06/23] fix(ui-test): address review comments on Playwright config and mocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pin STUB_PORT on the stub webServer so a shell-set STUB_PORT can't misbind it - Stop reusing the dev server so BACKEND_INTERNAL_URL always applies (no silent stub bypass) - Type ToolServerListEntry as the real RemoteMCPServerResponse for compile-time drift detection - Drop no-op **/*.mjs from the playwright tsconfig include Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Carlos Logroño Guerrero --- ui/playwright.config.ts | 9 ++++++++- ui/playwright/mocks/data.ts | 19 ++++++++++++++----- ui/playwright/tsconfig.json | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index d907923a15..ac77c405b7 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -47,11 +47,18 @@ export default defineConfig({ timeout: 30_000, stdout: "pipe", stderr: "pipe", + // Pin the port so a shell-exported STUB_PORT can't make the stub bind + // somewhere other than the health-check / BACKEND_INTERNAL_URL address. + env: { STUB_PORT: String(STUB_PORT) }, }, { command: "npm run dev", url: APP_URL, - reuseExistingServer: !CI, + // Never reuse an existing dev server: the BACKEND_INTERNAL_URL below is + // only applied to a server Playwright starts. A reused server (e.g. a + // hand-started `npm run dev`) would silently bypass the stub. Always + // boot our own so the redirect is guaranteed; a busy port fails loudly. + reuseExistingServer: false, timeout: 120_000, env: { // Redirect the server-side backend fetch to our stub. diff --git a/ui/playwright/mocks/data.ts b/ui/playwright/mocks/data.ts index 8d0ba6e126..3fa60f7418 100644 --- a/ui/playwright/mocks/data.ts +++ b/ui/playwright/mocks/data.ts @@ -10,13 +10,16 @@ import type { AgentResponse, ModelConfig, ProviderModelsResponse, + RemoteMCPServerResponse, ToolsResponse, } from "@/types"; export type Namespace = { name: string; status: string }; -export type ToolServerListEntry = { ref: string; groupKind: string; discoveredTools: unknown[] }; +export type ToolServerListEntry = RemoteMCPServerResponse; -export function mockAgentResponse(overrides: Partial = {}): AgentResponse { +export function mockAgentResponse( + overrides: Partial = {}, +): AgentResponse { return { id: "1", agent: { @@ -37,7 +40,9 @@ export function mockModelsResponse(): ProviderModelsResponse { return { openai: [{ name: "gpt-4o", function_calling: true }] }; } -export function mockModelConfig(overrides: Partial = {}): ModelConfig { +export function mockModelConfig( + overrides: Partial = {}, +): ModelConfig { return { ref: "default/default-model-config", spec: { model: "gpt-4o", provider: "OpenAI" }, @@ -49,7 +54,9 @@ export function mockNamespace(overrides: Partial = {}): Namespace { return { name: "default", status: "Active", ...overrides }; } -export function mockToolServer(overrides: Partial = {}): ToolServerListEntry { +export function mockToolServer( + overrides: Partial = {}, +): ToolServerListEntry { return { ref: "default/e2e-tool-server", groupKind: "RemoteMCPServer.kagent.dev", @@ -58,7 +65,9 @@ export function mockToolServer(overrides: Partial = {}): To }; } -export function mockTool(overrides: Partial = {}): ToolsResponse { +export function mockTool( + overrides: Partial = {}, +): ToolsResponse { return { id: "e2e-tool", server_name: "e2e-tool-server", diff --git a/ui/playwright/tsconfig.json b/ui/playwright/tsconfig.json index fd1b9b7035..b84b7846f8 100644 --- a/ui/playwright/tsconfig.json +++ b/ui/playwright/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "types": ["node", "@playwright/test"] }, - "include": ["**/*.ts", "**/*.mjs"] + "include": ["**/*.ts"] } From 9b890531b8e4894a1c01dfc04f3abbafe4b01d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Mon, 13 Jul 2026 15:48:21 -0400 Subject: [PATCH 07/23] =?UTF-8?q?test(ui):=20address=20review=20=E2=80=94?= =?UTF-8?q?=20remove=20Cypress,=20use=20.nvmrc,=20enable=20Playwright=20vi?= =?UTF-8?q?deo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve Charlesthebird's review on PR #2180: - Playwright workflow now reads node-version-file: ui/.nvmrc instead of a hardcoded major version. - Remove Cypress entirely: delete cypress.config.ts and cypress/e2e/smoke.cy.ts, drop the cypress and now-unused start-server-and-test devDeps plus the test:e2e/test:e2e:cypress scripts, and remove the dead CYPRESS_INSTALL_BINARY env from the ui-playwright, ci, and ui-chromatic workflows and the Dockerfile. - Enable Playwright video recording (video: "on"); artifacts already upload via the existing test-results step. - De-Cypress the Playwright README (Jest, Vitest, Storybook untouched). data.ts is intentionally kept — it is consumed by the stacked stage-2 PR. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Carlos Logroño Guerrero --- .github/workflows/ci.yaml | 2 - .github/workflows/ui-chromatic.yaml | 2 - .github/workflows/ui-playwright.yaml | 4 +- ui/Dockerfile | 1 - ui/cypress.config.ts | 10 - ui/cypress/e2e/smoke.cy.ts | 73 - ui/package-lock.json | 2159 +++----------------------- ui/package.json | 4 - ui/playwright.config.ts | 2 +- ui/playwright/README.md | 8 +- 10 files changed, 193 insertions(+), 2072 deletions(-) delete mode 100644 ui/cypress.config.ts delete mode 100644 ui/cypress/e2e/smoke.cy.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8a435c1a9b..c7bbff56d6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -328,8 +328,6 @@ jobs: - name: Install dependencies working-directory: ./ui - env: - CYPRESS_INSTALL_BINARY: 0 # Skip Cypress binary - not needed for lint/unit tests run: npm ci - name: Run lint diff --git a/.github/workflows/ui-chromatic.yaml b/.github/workflows/ui-chromatic.yaml index 70f032da45..4a323b02dc 100644 --- a/.github/workflows/ui-chromatic.yaml +++ b/.github/workflows/ui-chromatic.yaml @@ -35,8 +35,6 @@ jobs: - name: Install dependencies working-directory: ./ui - env: - CYPRESS_INSTALL_BINARY: 0 run: npm ci - name: Build Storybook diff --git a/.github/workflows/ui-playwright.yaml b/.github/workflows/ui-playwright.yaml index 90b1db9e39..cad78fdac6 100644 --- a/.github/workflows/ui-playwright.yaml +++ b/.github/workflows/ui-playwright.yaml @@ -27,14 +27,12 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "24" + node-version-file: "ui/.nvmrc" cache: "npm" cache-dependency-path: ui/package-lock.json - name: Install dependencies working-directory: ./ui - env: - CYPRESS_INSTALL_BINARY: 0 run: npm ci - name: Install Playwright browser diff --git a/ui/Dockerfile b/ui/Dockerfile index 72c0507599..5e6708f0b2 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -15,7 +15,6 @@ RUN echo "Installing on $BUILDPLATFORM" \ ENV DO_NOT_TRACK=1 ENV NEXT_TELEMETRY_DISABLED=1 -ENV CYPRESS_INSTALL_BINARY=0 WORKDIR /app/ui diff --git a/ui/cypress.config.ts b/ui/cypress.config.ts deleted file mode 100644 index 93489c2d27..0000000000 --- a/ui/cypress.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'cypress' - -export default defineConfig({ - e2e: { - baseUrl: 'http://localhost:8001', - setupNodeEvents(_on, _config) { - }, - supportFile: false - }, -}) \ No newline at end of file diff --git a/ui/cypress/e2e/smoke.cy.ts b/ui/cypress/e2e/smoke.cy.ts deleted file mode 100644 index f295c3f45a..0000000000 --- a/ui/cypress/e2e/smoke.cy.ts +++ /dev/null @@ -1,73 +0,0 @@ -describe('Onboarding Wizard', () => { - it('successfully loads the first page of the onboarding wizard', () => { - cy.window().then((win) => { - win.localStorage.setItem('kagent-onboarding', 'false'); - }) - - cy.visit('/') - - cy.contains('p', "Let's get you started by creating your first agent") - cy.contains('button', "Let's Get Started").click(); - - - cy.contains('div', "Step 1: Configure AI Model").should('be.visible'); - cy.get('button[role="combobox"]').should('be.visible'); - cy.contains('button', 'Next: Agent Setup').should('be.visible'); - - - cy.contains('label', 'Create New').should('be.visible').click(); - cy.contains('label', "Provider & Model").should('be.visible'); - cy.contains('label', "Configuration Name").should('be.visible'); - }) -}) - -describe('Main page', () => { - it('successfully loads the main page', () => { - cy.window().then((win) => { - win.localStorage.setItem('kagent-onboarding', 'true'); - }) - - cy.visit('/') - cy.contains('h1', 'Agents').should('be.visible'); - - cy.wait(1000) - cy.visit('/agents') - cy.contains('h1', 'Agents').should('be.visible'); - - cy.visit('/agents/new') - cy.contains('h1', 'New Agent').should('be.visible'); - - cy.wait(1000) - cy.visit('/models') - cy.contains('h1', 'Models').should('be.visible'); - - cy.visit('/models/new') - cy.contains('h1', 'New Model').should('be.visible'); - - cy.wait(1000) - cy.visit('/mcp') - cy.contains('h1', 'MCP & tools').should('be.visible'); - cy.get('#mcp-search').should('be.visible'); - }) -}) - - -describe('Regressions', () => { - it('model edit page should load correctly', () => { - cy.window().then((win) => { - win.localStorage.setItem('kagent-onboarding', 'true'); - }) - - cy.visit('/models') - cy.contains('h1', 'Models').should('be.visible'); - - // `model.ref` (e.g. default/default-model-config) is embedded in data-test; use prefix to avoid exact-ref coupling - cy.get('[data-test^="edit-model-"]') - .first() - .should('be.visible') - .click(); - - cy.contains('h1', 'Edit Model').should('be.visible'); - cy.get('[data-test="edit-model-name-button"]').should('be.visible').click(); - }) -}) \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index 0a11de0827..6d2cdfcee2 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -78,7 +78,6 @@ "autoprefixer": "^10.5.2", "babel-plugin-react-compiler": "^1.0.0", "chromatic": "^16.10.1", - "cypress": "^15.18.1", "eslint": "^9.39.5", "eslint-config-next": "16.2.10", "eslint-plugin-storybook": "^10.5.0", @@ -87,7 +86,6 @@ "msw": "^2.15.0", "playwright": "^1.61.1", "postcss": "^8.5.18", - "start-server-and-test": "^3.0.11", "storybook": "^10.2.10", "tailwindcss": "^3.4.17", "ts-jest": "^29.4.11", @@ -921,56 +919,6 @@ "node": ">=18" } }, - "node_modules/@cypress/request": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-4.0.0.tgz", - "integrity": "sha512-wGTQfwDMMMiz/muFw4YbCLwTh0uZsXKK+6zWBzftADpitSi6iM62C8GzEhNcng2srUiGPksOriQkA8zakW2R0g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~4.0.4", - "http-signature": "~1.4.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "performance-now": "^2.1.0", - "qs": "~6.14.1", - "safe-buffer": "^5.1.2", - "tough-cookie": "^5.0.0", - "tunnel-agent": "^0.6.0" - }, - "engines": { - "node": ">= 14.17.0" - } - }, - "node_modules/@cypress/xvfb": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", - "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.1.0", - "lodash.once": "^4.1.1" - } - }, - "node_modules/@cypress/xvfb/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", @@ -1613,60 +1561,6 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, - "node_modules/@hapi/address": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", - "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^11.0.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@hapi/formula": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", - "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/hoek": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", - "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/pinpoint": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", - "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/tlds": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", - "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@hapi/topo": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", - "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^11.0.2" - } - }, "node_modules/@hono/node-server": { "version": "1.19.14", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", @@ -6369,20 +6263,6 @@ "@types/node": "*" } }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", - "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sizzle": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", - "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -6397,13 +6277,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/tmp": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", - "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -7517,27 +7390,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -7733,26 +7585,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -7817,23 +7649,6 @@ "node": ">= 0.4" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/autoprefixer": { "version": "10.5.2", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", @@ -7887,23 +7702,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", - "dev": true, - "license": "MIT" - }, "node_modules/axe-core": { "version": "4.11.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", @@ -7914,56 +7712,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", - "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/axios/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/axios/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/axios/node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -8099,27 +7847,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.10.43", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", @@ -8132,16 +7859,6 @@ "node": ">=6.0.0" } }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -8154,20 +7871,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/blob-util": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", - "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true, - "license": "MIT" - }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -8301,31 +8004,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -8357,16 +8035,6 @@ "node": ">= 0.8" } }, - "node_modules/cachedir": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", - "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -8464,13 +8132,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -8573,16 +8234,6 @@ "node": ">= 16" } }, - "node_modules/check-more-types": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", - "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -8698,148 +8349,53 @@ "url": "https://polar.sh/cva" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, + "license": "ISC", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 12" } }, - "node_modules/cli-table3": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", - "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "string-width": "^4.2.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "colors": "1.4.0" + "node": ">=12" } }, - "node_modules/cli-truncate": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", - "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^8.0.0", - "string-width": "^8.2.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=20" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", - "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/clsx": { @@ -8905,37 +8461,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -8946,26 +8471,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -9025,13 +8530,6 @@ "node": ">=6.6.0" } }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -9109,84 +8607,6 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/cypress": { - "version": "15.18.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.18.1.tgz", - "integrity": "sha512-JtkTVtUE2lvLYgZCaug+Uai0H9IqsJirlBO49c87QwG0bJUGvAUVBz1EJve0b0oaYP244Ew9M0BkrHpcqkYxmw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@cypress/request": "^4.0.0", - "@cypress/xvfb": "^1.2.4", - "@types/sinonjs__fake-timers": "8.1.1", - "@types/sizzle": "^2.3.2", - "@types/tmp": "^0.2.3", - "arch": "^2.2.0", - "blob-util": "^2.0.2", - "bluebird": "^3.7.2", - "buffer": "^5.7.1", - "cachedir": "^2.4.0", - "chalk": "^4.1.0", - "ci-info": "^4.1.0", - "cli-table3": "0.6.1", - "commander": "^6.2.1", - "common-tags": "^1.8.0", - "dayjs": "^1.10.4", - "debug": "^4.3.4", - "eventemitter2": "6.4.7", - "execa": "4.1.0", - "executable": "^4.1.1", - "fs-extra": "^9.1.0", - "hasha": "5.2.2", - "is-installed-globally": "~0.4.0", - "listr2": "^9.0.5", - "lodash": "^4.17.23", - "log-symbols": "^4.0.0", - "minimist": "^1.2.8", - "ospath": "^1.2.2", - "pretty-bytes": "^5.6.0", - "process": "^0.11.10", - "proxy-from-env": "1.0.0", - "request-progress": "^3.0.0", - "supports-color": "^8.1.1", - "systeminformation": "^5.31.1", - "tmp": "~0.2.4", - "tree-kill": "1.2.2", - "tslib": "1.14.1", - "untildify": "^4.0.0", - "yauzl": "^3.3.1" - }, - "bin": { - "cypress": "bin/cypress" - }, - "engines": { - "node": "^20.1.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/cypress/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/cypress/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -9194,19 +8614,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -9285,13 +8692,6 @@ "url": "https://github.com/sponsors/kossnocorp" } }, - "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "dev": true, - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -9446,16 +8846,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -9576,17 +8966,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -9639,16 +9018,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -9662,19 +9031,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -10430,20 +9786,6 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter2": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", - "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -10465,43 +9807,6 @@ "node": ">=18.0.0" } }, - "node_modules/execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/executable": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", - "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.2.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/exit-x": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", @@ -10640,16 +9945,6 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -10863,27 +10158,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -10930,46 +10204,19 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "dev": true, - "license": "Apache-2.0", + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { - "node": "*" + "node": ">= 0.6" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, "license": "MIT", "engines": { @@ -10989,22 +10236,6 @@ "node": ">= 0.8" } }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -11096,19 +10327,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -11165,22 +10383,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -11212,16 +10414,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -11282,22 +10474,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -11466,23 +10642,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasha": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -11649,21 +10808,6 @@ "node": ">= 14" } }, - "node_modules/http-signature": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", - "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^2.0.2", - "sshpk": "^1.18.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -11678,16 +10822,6 @@ "node": ">= 14" } }, - "node_modules/human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8.12.0" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -11701,27 +10835,6 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -11820,16 +10933,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -12211,23 +11314,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -12287,16 +11373,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -12434,26 +11510,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -12528,13 +11584,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true, - "license": "MIT" - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -13703,25 +12752,6 @@ "jiti": "bin/jiti.js" } }, - "node_modules/joi": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", - "integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/address": "^5.1.1", - "@hapi/formula": "^3.0.2", - "@hapi/hoek": "^11.0.7", - "@hapi/pinpoint": "^2.0.1", - "@hapi/tlds": "^1.1.1", - "@hapi/topo": "^6.0.2", - "@standard-schema/spec": "^1.1.0" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/jose": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", @@ -13761,13 +12791,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true, - "license": "MIT" - }, "node_modules/jsdom": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", @@ -13835,13 +12858,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true, - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -13862,13 +12878,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -13902,22 +12911,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsprim": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", - "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -13964,16 +12957,6 @@ "node": ">=0.10" } }, - "node_modules/lazy-ass": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-2.0.3.tgz", - "integrity": "sha512-/O3/DoQmI1XAhklDvF1dAjFf/epE8u3lzOZegQfLZ8G7Ud5bTRSZiFOpukHCu6jODrCA4gtIdwUCC7htxcDACA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "> 0.8" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -14098,503 +13081,215 @@ "cpu": [ "x64" ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=10" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, "engines": { - "node": ">=12" + "node": ">=14" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -15645,29 +14340,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -15678,19 +14350,6 @@ "node": ">=6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -16305,13 +14964,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ospath": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", - "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", - "dev": true, - "license": "MIT" - }, "node_modules/outvariant": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", @@ -16616,20 +15268,6 @@ "node": ">= 14.16" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -16988,19 +15626,6 @@ "node": ">= 0.8.0" } }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -17029,16 +15654,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -17081,24 +15696,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", - "dev": true, - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -17607,16 +16204,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "throttleit": "^1.0.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -17699,52 +16286,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rettime": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", @@ -17762,13 +16303,6 @@ "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, "node_modules/rolldown": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", @@ -17881,16 +16415,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -17911,27 +16435,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -18323,52 +16826,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -18426,32 +16883,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -18488,77 +16919,6 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, - "node_modules/start-server-and-test": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-3.0.11.tgz", - "integrity": "sha512-NRapOwJl6jr1DNSaQ+SRukHI2OKcFZA2Iv2tfTW9fI/S+6YmJGiwacR+0MG3o5p39lY4xWUOE5JFkKJBZUjxuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "arg": "5.0.2", - "check-more-types": "2.24.0", - "debug": "4.4.3", - "execa": "5.1.1", - "lazy-ass": "2.0.3", - "tree-kill": "1.2.2", - "wait-on": "9.0.10" - }, - "bin": { - "server-test": "src/bin/start.js", - "start-server-and-test": "src/bin/start.js", - "start-test": "src/bin/start.js" - }, - "engines": { - "node": "^22 || >=24" - } - }, - "node_modules/start-server-and-test/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/start-server-and-test/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/start-server-and-test/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -19047,33 +17407,6 @@ "url": "https://opencollective.com/synckit" } }, - "node_modules/systeminformation": { - "version": "5.31.4", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.4.tgz", - "integrity": "sha512-lZppDyQx91VdS5zJvAyGkmwe+Mq6xY978BDUG2wRkWE+jkmUF5ti8cvOovFQoN5bvSFKCXVkyKEaU5ec3SJiRg==", - "dev": true, - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32", - "freebsd", - "openbsd", - "netbsd", - "sunos", - "android" - ], - "bin": { - "systeminformation": "lib/cli.js" - }, - "engines": { - "node": ">=8.0.0" - }, - "funding": { - "type": "Buy me a coffee", - "url": "https://www.buymeacoffee.com/systeminfo" - } - }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -19242,16 +17575,6 @@ "node": ">=0.8" } }, - "node_modules/throttleit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", - "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -19327,16 +17650,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -19401,16 +17714,6 @@ "node": ">=18" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -19652,26 +17955,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true, - "license": "Unlicense" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -19695,16 +17978,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, "node_modules/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", @@ -20083,16 +18356,6 @@ "url": "https://github.com/sponsors/kettanaito" } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -20236,21 +18499,6 @@ "node": ">= 0.8" } }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -20535,26 +18783,6 @@ "node": ">=18" } }, - "node_modules/wait-on": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.10.tgz", - "integrity": "sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "axios": "^1.16.0", - "joi": "^18.2.1", - "lodash": "^4.18.1", - "minimist": "^1.2.8", - "rxjs": "^7.8.2" - }, - "bin": { - "wait-on": "bin/wait-on" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -20986,19 +19214,6 @@ "node": ">=12" } }, - "node_modules/yauzl": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.2.tgz", - "integrity": "sha512-Md9ankxxN23wncAN8s7+Tn3Co52zLUPMtnrLAbVCnfG5d2tKBFfmygYSgXlqFgXObtzIgqkx7aNgDBpso9+4qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/ui/package.json b/ui/package.json index 43cf0e31f7..f92bd065e2 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,8 +10,6 @@ "test": "jest", "test:vitest": "vitest run", "test:watch": "jest --watch", - "test:e2e:cypress": "cypress run --e2e", - "test:e2e": "start-server-and-test dev http://localhost:8001 test:e2e:cypress", "test:pw": "playwright test", "test:pw:ui": "playwright test --ui", "test:pw:debug": "playwright test --debug", @@ -98,7 +96,6 @@ "autoprefixer": "^10.5.2", "babel-plugin-react-compiler": "^1.0.0", "chromatic": "^16.10.1", - "cypress": "^15.18.1", "eslint": "^9.39.5", "eslint-config-next": "16.2.10", "eslint-plugin-storybook": "^10.5.0", @@ -107,7 +104,6 @@ "msw": "^2.15.0", "playwright": "^1.61.1", "postcss": "^8.5.18", - "start-server-and-test": "^3.0.11", "storybook": "^10.2.10", "tailwindcss": "^3.4.17", "ts-jest": "^29.4.11", diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index ac77c405b7..ebfe768505 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -36,7 +36,7 @@ export default defineConfig({ baseURL: APP_URL, screenshot: "only-on-failure", trace: "retain-on-failure", - video: "off", + video: "on", }, projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], webServer: [ diff --git a/ui/playwright/README.md b/ui/playwright/README.md index 96e8f55a1b..1d4dde1abb 100644 --- a/ui/playwright/README.md +++ b/ui/playwright/README.md @@ -11,10 +11,10 @@ flows across components**. | Atoms (`src/components/ui/*`) | shadcn primitives | skip | | Visual / render states | Storybook + Vitest-browser + Chromatic | skip | | Unit / logic | Jest (`*.test.ts(x)`) | skip | -| Page-load smoke (`h1` renders, page reachable, onboarding steps 1–2, model-edit page opens) | Cypress (`cypress/e2e/smoke.cy.ts`) | skip | +| Page-load smoke (`h1` renders, page reachable) | Playwright (`tests/smoke.spec.ts`, Stage 0) | — | | **Multi-step flows: form submission, payload correctness, streaming, wizard completion, error/edge states** | **Playwright (this suite)** | — | -Rule: if Cypress / Chromatic / Jest already assert it, Playwright does not. +Rule: if Chromatic / Jest already assert it, Playwright does not. ## How mocking works (important) @@ -73,8 +73,8 @@ BACKEND_INTERNAL_URL=http://127.0.0.1:8899/api npm run dev - **`data-testid` policy:** prefer `getByRole` / `getByLabel`. Add `data-testid` only where role/text is ambiguous or unstable (list rows, per-item action buttons, wizard steps, combobox options). Add incrementally — no upfront sweep. - Do **not** remove/rename the existing `data-test` model-edit hooks; Cypress - depends on them. + Keep the existing `data-test` model-edit hooks; the Stage 2 Models flow relies + on them. ## Roadmap From b6cbb1bbb69015f19875e8a64dcf6f3d35812d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Tue, 14 Jul 2026 12:47:58 -0400 Subject: [PATCH 08/23] test(ui): add test:e2e script alias for playwright MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore a conventional test:e2e entrypoint pointing at the Playwright suite, per review feedback on #2180. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Carlos Logroño Guerrero --- ui/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/package.json b/ui/package.json index f92bd065e2..bbba6b50e0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,6 +11,7 @@ "test:vitest": "vitest run", "test:watch": "jest --watch", "test:pw": "playwright test", + "test:e2e": "npm run test:pw", "test:pw:ui": "playwright test --ui", "test:pw:debug": "playwright test --debug", "storybook": "storybook dev -p 6006", From f50fd490921718682e55b5efcf65cfd12a66a6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Wed, 15 Jul 2026 10:23:02 -0400 Subject: [PATCH 09/23] fix(ui): disable rules-of-hooks false positive on Playwright use fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Carlos Logroño Guerrero --- ui/playwright/fixtures/test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/playwright/fixtures/test.ts b/ui/playwright/fixtures/test.ts index f3f95cb347..07cba2d1fb 100644 --- a/ui/playwright/fixtures/test.ts +++ b/ui/playwright/fixtures/test.ts @@ -22,6 +22,8 @@ export const test = base.extend({ await page.addInitScript(() => { window.localStorage.setItem("kagent-onboarding", "true"); }); + // `use` here is Playwright's fixture callback, not the React `use` hook. + // eslint-disable-next-line react-hooks/rules-of-hooks await use(page); }, }); From bf34b51320791e005a0072fd15e782b2bc028e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Wed, 15 Jul 2026 10:50:17 -0400 Subject: [PATCH 10/23] fix(ui): resolve Copilot review comments on Playwright foundation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rename fixture callback param to `provide` (drops the rules-of-hooks eslint-disable) - record video only on failure to match screenshot/trace - pin @playwright/test and playwright to aligned 1.61.1 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Carlos Logroño Guerrero --- ui/package-lock.json | 4 ++-- ui/package.json | 4 ++-- ui/playwright.config.ts | 2 +- ui/playwright/fixtures/test.ts | 6 ++---- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 6d2cdfcee2..59c5020f64 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -58,7 +58,7 @@ "@chromatic-com/storybook": "^5.2.1", "@eslint/eslintrc": "^3.3.6", "@jest/globals": "^30.4.1", - "@playwright/test": "^1.61.0", + "@playwright/test": "1.61.1", "@storybook/addon-a11y": "^10.5.0", "@storybook/addon-docs": "^10.5.0", "@storybook/addon-onboarding": "^10.5.0", @@ -84,7 +84,7 @@ "jest": "^30.4.2", "jest-environment-jsdom": "^30.4.1", "msw": "^2.15.0", - "playwright": "^1.61.1", + "playwright": "1.61.1", "postcss": "^8.5.18", "storybook": "^10.2.10", "tailwindcss": "^3.4.17", diff --git a/ui/package.json b/ui/package.json index bbba6b50e0..3dbb92a0c9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -77,7 +77,7 @@ "@chromatic-com/storybook": "^5.2.1", "@eslint/eslintrc": "^3.3.6", "@jest/globals": "^30.4.1", - "@playwright/test": "^1.61.0", + "@playwright/test": "1.61.1", "@storybook/addon-a11y": "^10.5.0", "@storybook/addon-docs": "^10.5.0", "@storybook/addon-onboarding": "^10.5.0", @@ -103,7 +103,7 @@ "jest": "^30.4.2", "jest-environment-jsdom": "^30.4.1", "msw": "^2.15.0", - "playwright": "^1.61.1", + "playwright": "1.61.1", "postcss": "^8.5.18", "storybook": "^10.2.10", "tailwindcss": "^3.4.17", diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index ebfe768505..f1206fe384 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -36,7 +36,7 @@ export default defineConfig({ baseURL: APP_URL, screenshot: "only-on-failure", trace: "retain-on-failure", - video: "on", + video: "retain-on-failure", }, projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], webServer: [ diff --git a/ui/playwright/fixtures/test.ts b/ui/playwright/fixtures/test.ts index 07cba2d1fb..c9f1659ef7 100644 --- a/ui/playwright/fixtures/test.ts +++ b/ui/playwright/fixtures/test.ts @@ -10,7 +10,7 @@ import { test as base, expect } from "@playwright/test"; const STUB_URL = "http://127.0.0.1:8899"; export const test = base.extend({ - page: async ({ page, request }, use) => { + page: async ({ page, request }, provide) => { try { await request.post(`${STUB_URL}/__mock/reset`); } catch (err) { @@ -22,9 +22,7 @@ export const test = base.extend({ await page.addInitScript(() => { window.localStorage.setItem("kagent-onboarding", "true"); }); - // `use` here is Playwright's fixture callback, not the React `use` hook. - // eslint-disable-next-line react-hooks/rules-of-hooks - await use(page); + await provide(page); }, }); From 8632b2c1f1c40ced1fbb58911e1e81acab5c1e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Wed, 15 Jul 2026 13:13:17 -0400 Subject: [PATCH 11/23] ci: re-trigger build matrix (transient cgr.dev token fetch flake) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Carlos Logroño Guerrero From ea4bc96acc260387eb4c26427c8da660638e5376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Thu, 9 Jul 2026 13:29:15 -0400 Subject: [PATCH 12/23] test(ui): Playwright helpers + per-test scenario overrides (stage 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds on the Stage 0 foundation to unlock non-happy-path testing and give specs a small, proven driver surface: - Scenario-override engine in the stub backend: POST /__mock/scenario sets a per-endpoint { status, body } override; /__mock/reset clears them. Default stays the happy path. - Typed control seam (mocks/control.ts) surfaced as a `mock` fixture (mock.noAgents(), mock.agentsError(), ...); the fixture resets the stub before every test so scenarios never leak between tests. - page.ts (loadPage, expectNoErrors, expectToast) and nav.ts (dropdown-aware gotoView/gotoCreate) driver helpers. - home spec (happy / empty / error) proving the engine, and a nav spec proving dropdown routing. Form/select/dialog helpers are deferred to Stage 2 (built demand-driven with the create-agent flow). Co-Authored-By: Claude Opus 4.8 Signed-off-by: Carlos Logroño Guerrero --- ui/playwright/README.md | 23 ++++---- ui/playwright/fixtures/test.ts | 33 +++++++---- ui/playwright/helpers/nav.ts | 41 ++++++++++++++ ui/playwright/helpers/page.ts | 48 ++++++++++++++++ ui/playwright/mocks/control.ts | 73 ++++++++++++++++++++++++ ui/playwright/mocks/data.ts | 14 +++-- ui/playwright/mocks/server.mjs | 91 ++++++++++++++++++++++++------ ui/playwright/tests/home.spec.ts | 26 +++++++++ ui/playwright/tests/nav.spec.ts | 34 +++++++++++ ui/src/components/LoadingState.tsx | 5 +- 10 files changed, 343 insertions(+), 45 deletions(-) create mode 100644 ui/playwright/helpers/nav.ts create mode 100644 ui/playwright/helpers/page.ts create mode 100644 ui/playwright/mocks/control.ts create mode 100644 ui/playwright/tests/home.spec.ts create mode 100644 ui/playwright/tests/nav.spec.ts diff --git a/ui/playwright/README.md b/ui/playwright/README.md index 1d4dde1abb..481634f36f 100644 --- a/ui/playwright/README.md +++ b/ui/playwright/README.md @@ -36,12 +36,13 @@ browser-originated and `page.route`-able — used in the chat spec (Stage 2). ``` playwright/ tests/ # *.spec.ts, one per feature area - helpers/ # (Stage 1) reusable drivers: page, forms, select, dialog, nav + helpers/ # reusable drivers: page, nav (forms/select/dialog land with Stage 2) mocks/ - server.mjs # stub backend (runtime source of happy-path data) - data.ts # typed spec-side builders (mirror server.mjs shapes) + server.mjs # stub backend (happy-path data + /__mock/scenario overrides) + data.ts # typed spec-side builders + ok() envelope (mirror server.mjs shapes) + control.ts # semantic mock seam: mock.noAgents(), mock.agentsError(), … fixtures/ - test.ts # import { test, expect } from here in every spec + test.ts # import { test, expect } from here; provides the `mock` fixture tsconfig.json ``` @@ -79,9 +80,11 @@ BACKEND_INTERNAL_URL=http://127.0.0.1:8899/api npm run dev ## Roadmap - **Stage 0 (done):** foundation — config, stub backend, CI, one smoke test. -- **Stage 1:** helper/driver library (`helpers/*`) + per-test scenario overrides - via the stub's `/__mock/scenario` endpoint. Prefer stateless scenario selection - keyed by request content (e.g. `?namespace=empty-ns` → `[]`); fall back to the - control endpoint with `workers: 1` for endpoints lacking a discriminator. -- **Stage 2:** feature flows (gap-scoped), ordered by importance — - Create Agent → Chat/session (A2A SSE mock) → Models → MCP → Onboarding completion. +- **Stage 1 (done):** page/nav helpers (`helpers/*`) + per-test scenario overrides — + the `mock` fixture drives the stub's `/__mock/scenario` endpoint via `mocks/control.ts` + (e.g. `mock.noAgents()`, `mock.agentsError()`), verified by the home + nav specs. + Runs serially (`workers: 1`) against the shared stub; raising the worker count later + needs per-worker servers or stateless request-keyed scenarios. +- **Stage 2:** feature flows (gap-scoped), ordered by importance — Create Agent → + Chat/session (A2A SSE mock) → Models → MCP → Onboarding completion. Adds the + `forms`/`select`/`dialog` helpers demand-driven against the create-agent form. diff --git a/ui/playwright/fixtures/test.ts b/ui/playwright/fixtures/test.ts index c9f1659ef7..10969e7de1 100644 --- a/ui/playwright/fixtures/test.ts +++ b/ui/playwright/fixtures/test.ts @@ -1,28 +1,37 @@ // Shared test fixture. Import { test, expect } from here in every spec (not -// directly from @playwright/test) so all specs get the same setup. -// -// For now it resets the stub backend before each test. Reset is a no-op until -// Stage 1 adds per-test scenarios, but establishing the pattern now means specs -// don't change when scenarios land. +// directly from @playwright/test) so all specs get the same setup: +// - `mock`: the stub-backend control seam (mock.noAgents(), mock.agentsError(), …) +// - the stub is reset before every test (page depends on mock), so scenarios +// set by one test never leak into the next. +// - onboarding wizard is bypassed on every navigation. import { test as base, expect } from "@playwright/test"; +import { makeMock, type MockBackend } from "../mocks/control"; -const STUB_URL = "http://127.0.0.1:8899"; - -export const test = base.extend({ - page: async ({ page, request }, provide) => { +export const test = base.extend<{ mock: MockBackend }>({ + // Param is named `run` (not the Playwright-idiomatic `use`) to avoid the + // react-hooks lint rule mistaking it for React 19's use() hook. + mock: async ({ request }, run) => { + const mock = makeMock(request); + // Reset scenario state before each test. The stub is managed by Playwright's + // webServer, so in CI an unreachable stub is a real failure — fail fast. try { - await request.post(`${STUB_URL}/__mock/reset`); + await mock.reset(); } catch (err) { - // Stub should always be reachable in CI (managed via webServer). Fail fast. if (process.env.CI) throw err; } + await run(mock); + }, + // Depend on `mock` so the reset above runs before every test that uses a page, + // even specs that don't touch scenarios (e.g. smoke). + page: async ({ page, mock }, run) => { + void mock; // Bypass the first-run onboarding wizard (AppInitializer redirects to it when // this key is unset). Runs before any page script, on every navigation. await page.addInitScript(() => { window.localStorage.setItem("kagent-onboarding", "true"); }); - await provide(page); + await run(page); }, }); diff --git a/ui/playwright/helpers/nav.ts b/ui/playwright/helpers/nav.ts new file mode 100644 index 0000000000..212c8550b4 --- /dev/null +++ b/ui/playwright/helpers/nav.ts @@ -0,0 +1,41 @@ +// Navigation helpers for the persistent header (src/components/Header.tsx). +// +// Routes live inside two Radix dropdown menus ("Create" and "View"), not flat +// links. Menu items only enter the DOM (as role="menuitem") once the menu is +// open. Header markup is duplicated for desktop/mobile, but the hidden block is +// out of the accessibility tree, so role-based locators resolve to the visible +// one on a desktop viewport. + +import { type Page } from "@playwright/test"; + +async function openMenu(page: Page, trigger: "Create" | "View"): Promise { + await page.getByRole("button", { name: trigger, exact: true }).click(); +} + +async function chooseFrom( + page: Page, + trigger: "Create" | "View", + item: string, + urlGlob?: string | RegExp, +): Promise { + await openMenu(page, trigger); + // Exact match: "New Agent" is a substring of "New Agent Harness". + await page.getByRole("menuitem", { name: item, exact: true }).click(); + if (urlGlob) await page.waitForURL(urlGlob); +} + +/** Open the "View" menu and go to a listing page, e.g. gotoView(page, "Models", "**\/models"). */ +export function gotoView(page: Page, item: string, urlGlob?: string | RegExp): Promise { + return chooseFrom(page, "View", item, urlGlob); +} + +/** Open the "Create" menu and go to a creation page, e.g. gotoCreate(page, "New Agent", "**\/agents/new"). */ +export function gotoCreate(page: Page, item: string, urlGlob?: string | RegExp): Promise { + return chooseFrom(page, "Create", item, urlGlob); +} + +/** Click the direct "Home" link in the header. */ +export async function gotoHome(page: Page): Promise { + await page.getByRole("link", { name: "Home" }).first().click(); + await page.waitForURL(/\/(agents)?$/); +} diff --git a/ui/playwright/helpers/page.ts b/ui/playwright/helpers/page.ts new file mode 100644 index 0000000000..0bffb83cd3 --- /dev/null +++ b/ui/playwright/helpers/page.ts @@ -0,0 +1,48 @@ +// Page-level driver helpers. Backend-agnostic — they assert on rendered DOM +// (roles/text), so they work regardless of how data is mocked. + +import { expect, type Page } from "@playwright/test"; + +/** + * Wait for the full-screen LoadingState overlay to clear. It sits on top of the + * page (z-10, backdrop-blur) while data loads, so content behind it counts as + * "visible" to Playwright even though a user can't see/use it yet. Call this + * before asserting on or interacting with page content. + */ +export async function waitForAppReady(page: Page): Promise { + await expect(page.getByTestId("loading-overlay")).toHaveCount(0); +} + +/** Navigate to a path, wait for loading to finish, and optionally assert the page's

. */ +export async function loadPage( + page: Page, + path: string, + opts: { heading?: string } = {}, +): Promise { + await page.goto(path); + await waitForAppReady(page); + if (opts.heading) { + await expect(page.getByRole("heading", { level: 1, name: opts.heading })).toBeVisible(); + } +} + +/** Assert the ErrorState component ("Error Encountered") is not on the page. */ +export async function expectNoErrors(page: Page): Promise { + await expect(page.getByText("Error Encountered")).toHaveCount(0); +} + +type ToastType = "success" | "error" | "warning" | "info"; + +/** + * Assert a sonner toast with the given text is visible. Toasts auto-dismiss, so + * call this promptly after the triggering action. Pass `type` to also assert the + * severity (data-type on the toast
  • ). + */ +export async function expectToast( + page: Page, + text: string | RegExp, + opts: { type?: ToastType } = {}, +): Promise { + const selector = opts.type ? `[data-sonner-toast][data-type="${opts.type}"]` : "[data-sonner-toast]"; + await expect(page.locator(selector).filter({ hasText: text })).toBeVisible(); +} diff --git a/ui/playwright/mocks/control.ts b/ui/playwright/mocks/control.ts new file mode 100644 index 0000000000..19368c525b --- /dev/null +++ b/ui/playwright/mocks/control.ts @@ -0,0 +1,73 @@ +// Semantic control seam over the stub's /__mock/* HTTP endpoints. Specs express +// intent (mock.noAgents()) instead of transport (POST /__mock/scenario ...), so +// if the mock mechanism ever changes, only this file does. +// +// Used via the `mock` fixture in playwright/fixtures/test.ts. Set scenarios +// BEFORE page.goto — fetch is cache:"no-store" and each test gets a fresh +// context, so a fresh navigation re-fetches and picks up the override. + +import type { APIRequestContext } from "@playwright/test"; +import { ok } from "./data"; +import type { AgentResponse } from "@/types"; + +const STUB_URL = "http://127.0.0.1:8899"; + +type EndpointSlug = + | "agents" + | "models" + | "modelconfigs" + | "namespaces" + | "toolservers" + | "tools" + | "substrate"; + +interface ScenarioOptions { + status?: number; + body?: unknown; +} + +export interface MockBackend { + /** Clear all scenario overrides (back to the happy path). */ + reset(): Promise; + /** Low-level override for any endpoint. */ + setScenario(endpoint: EndpointSlug, opts: ScenarioOptions): Promise; + + setAgents(agents: AgentResponse[]): Promise; + noAgents(): Promise; + agentsError(status?: number): Promise; + + noModelConfigs(): Promise; + modelConfigsError(status?: number): Promise; + + noToolServers(): Promise; + toolServersError(status?: number): Promise; +} + +export function makeMock(request: APIRequestContext): MockBackend { + const setScenario: MockBackend["setScenario"] = async (endpoint, opts) => { + await request.post(`${STUB_URL}/__mock/scenario`, { + data: { endpoint, status: opts.status, body: opts.body }, + }); + }; + + const errorBody = (endpoint: EndpointSlug) => ({ error: `stubbed ${endpoint} error` }); + + return { + reset: async () => { + await request.post(`${STUB_URL}/__mock/reset`); + }, + setScenario, + + setAgents: (agents) => setScenario("agents", { body: ok(agents) }), + noAgents: () => setScenario("agents", { body: ok([]) }), + agentsError: (status = 500) => setScenario("agents", { status, body: errorBody("agents") }), + + noModelConfigs: () => setScenario("modelconfigs", { body: ok([]) }), + modelConfigsError: (status = 500) => + setScenario("modelconfigs", { status, body: errorBody("modelconfigs") }), + + noToolServers: () => setScenario("toolservers", { body: ok([]) }), + toolServersError: (status = 500) => + setScenario("toolservers", { status, body: errorBody("toolservers") }), + }; +} diff --git a/ui/playwright/mocks/data.ts b/ui/playwright/mocks/data.ts index 3fa60f7418..405da30eab 100644 --- a/ui/playwright/mocks/data.ts +++ b/ui/playwright/mocks/data.ts @@ -1,13 +1,14 @@ -// Typed, spec-side mock builders. Use these in specs to construct payloads for -// assertions or (from Stage 1) to POST scenarios to the stub's /__mock/scenario -// endpoint. The stub backend's runtime happy-path lives in server.mjs; keep the -// shapes here in sync with it. +// Typed, spec-side mock builders. Use these in specs (and the control seam in +// control.ts) to construct payloads for assertions or to POST scenarios to the +// stub's /__mock/scenario endpoint. The stub backend's runtime happy-path lives +// in server.mjs; keep the shapes here in sync with it. // // Typed against the app's own types (via the @/ alias, see playwright/tsconfig.json) // so drift from the real API surface fails at compile time. import type { AgentResponse, + BaseResponse, ModelConfig, ProviderModelsResponse, RemoteMCPServerResponse, @@ -17,6 +18,11 @@ import type { export type Namespace = { name: string; status: string }; export type ToolServerListEntry = RemoteMCPServerResponse; +/** Wrap data in the backend's success envelope: `{ message, data }`. */ +export function ok(data: T, message = "OK"): BaseResponse { + return { message, data }; +} + export function mockAgentResponse( overrides: Partial = {}, ): AgentResponse { diff --git a/ui/playwright/mocks/server.mjs b/ui/playwright/mocks/server.mjs index 8181dc6dcd..b9445d200c 100644 --- a/ui/playwright/mocks/server.mjs +++ b/ui/playwright/mocks/server.mjs @@ -9,8 +9,13 @@ // // Dependency-free (Node built-in http only) so it runs without a TS/build step. // Payloads mirror the shapes in src/types/index.ts; the typed spec-side builders -// live in playwright/mocks/data.ts. Keep the two in sync until Stage 1 unifies -// them behind the /__mock/scenario control endpoint. +// live in playwright/mocks/data.ts and the semantic control seam in control.ts. +// +// Scenarios: default is the happy path below. A test can override any endpoint +// via POST /__mock/scenario { endpoint, status?, body? } (used by control.ts to +// force empty/error/custom responses) and clear all overrides via POST +// /__mock/reset. State is a single in-memory map — correct only while the runner +// is serial (workers: 1); see playwright/README.md. import { createServer } from "node:http"; @@ -64,19 +69,37 @@ const substrateStatus = { workers: [], }; -// GET route table keyed by pathname (query string stripped before lookup). -const GET_ROUTES = { - "/api/agents": () => ok([agent], "Successfully fetched agents"), - "/api/models": () => ok({ openai: [{ name: "gpt-4o", function_calling: true }] }), - "/api/modelconfigs": () => ok([modelConfig]), - "/api/namespaces": () => ok([{ name: "default", status: "Active" }], "Namespaces fetched successfully"), - "/api/toolservers": () => ok([toolServer]), - "/api/tools": () => ok([tool]), - "/api/substrate/status": () => ok(substrateStatus, "Substrate status fetched"), +// Default happy-path body per endpoint slug. +const DEFAULTS = { + agents: () => ok([agent], "Successfully fetched agents"), + models: () => ok({ openai: [{ name: "gpt-4o", function_calling: true }] }), + modelconfigs: () => ok([modelConfig]), + namespaces: () => ok([{ name: "default", status: "Active" }], "Namespaces fetched successfully"), + toolservers: () => ok([toolServer]), + tools: () => ok([tool]), + substrate: () => ok(substrateStatus, "Substrate status fetched"), +}; + +// GET pathname -> endpoint slug (query string stripped before lookup). +const PATH_TO_SLUG = { + "/api/agents": "agents", + "/api/models": "models", + "/api/modelconfigs": "modelconfigs", + "/api/namespaces": "namespaces", + "/api/toolservers": "toolservers", + "/api/tools": "tools", + "/api/substrate/status": "substrate", }; // endregion +// region Scenario state + +// slug -> { status, body } override set by POST /__mock/scenario. Empty = happy path. +let overrides = {}; + +// endregion + // region Server const json = (res, status, body) => { @@ -88,7 +111,24 @@ const json = (res, status, body) => { res.end(payload); }; -const server = createServer((req, res) => { +const readJsonBody = (req) => + new Promise((resolve) => { + let raw = ""; + req.on("data", (chunk) => { + raw += chunk; + }); + req.on("end", () => { + if (!raw) return resolve({}); + try { + resolve(JSON.parse(raw)); + } catch { + resolve(null); + } + }); + req.on("error", () => resolve(null)); + }); + +const server = createServer(async (req, res) => { // req.method/url are typed string|undefined; default them so a malformed // request can't throw on the split below. const method = req.method ?? "GET"; @@ -97,18 +137,33 @@ const server = createServer((req, res) => { // Control + health endpoints. if (pathname === "/__mock/health") return json(res, 200, { status: "ok" }); + if (method === "POST" && pathname === "/__mock/reset") { - // No scenario state yet — happy path only. Wired up in Stage 1. + overrides = {}; return json(res, 200, { status: "reset" }); } + if (method === "POST" && pathname === "/__mock/scenario") { - // Placeholder for per-test overrides (Stage 1). - return json(res, 200, { status: "noop" }); + const body = await readJsonBody(req); + if (!body || typeof body.endpoint !== "string") { + return json(res, 400, { error: "scenario requires { endpoint: string }" }); + } + overrides[body.endpoint] = { status: body.status ?? 200, body: body.body }; + console.log(`[stub] scenario set: ${body.endpoint} -> ${body.status ?? 200}`); + return json(res, 200, { status: "scenario-set", endpoint: body.endpoint }); } - if (method === "GET" && GET_ROUTES[pathname]) { - console.log(`[stub] ${method} ${url} -> 200`); - return json(res, 200, GET_ROUTES[pathname]()); + if (method === "GET") { + const slug = PATH_TO_SLUG[pathname]; + if (slug) { + const override = overrides[slug]; + if (override) { + console.log(`[stub] ${method} ${url} -> ${override.status} (override)`); + return json(res, override.status, override.body ?? {}); + } + console.log(`[stub] ${method} ${url} -> 200`); + return json(res, 200, DEFAULTS[slug]()); + } } // Anything unmocked is a real gap — make it loud so we notice leaks. diff --git a/ui/playwright/tests/home.spec.ts b/ui/playwright/tests/home.spec.ts new file mode 100644 index 0000000000..6570116df9 --- /dev/null +++ b/ui/playwright/tests/home.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from "../fixtures/test"; +import { loadPage, expectNoErrors } from "../helpers/page"; + +// Exercises the scenario-override engine (via the `mock` fixture) across the +// three states of the Agents list. Also the first real consumer of page.ts. +test.describe("home / agents list", () => { + test("renders the agent (happy path)", async ({ page }) => { + await loadPage(page, "/", { heading: "Agents" }); + await expect(page.getByText("e2e-agent")).toBeVisible(); + await expectNoErrors(page); + }); + + test("shows the empty state when there are no agents", async ({ page, mock }) => { + await mock.noAgents(); + await loadPage(page, "/", { heading: "Agents" }); + await expect(page.getByRole("heading", { level: 2, name: "No agents yet" })).toBeVisible(); + }); + + test("shows the error state when the agents request fails", async ({ page, mock }) => { + await mock.agentsError(); + await page.goto("/"); + await expect(page.getByText("Error Encountered")).toBeVisible(); + // ErrorState early-returns, so the page

    never renders. + await expect(page.getByRole("heading", { level: 1, name: "Agents" })).toHaveCount(0); + }); +}); diff --git a/ui/playwright/tests/nav.spec.ts b/ui/playwright/tests/nav.spec.ts new file mode 100644 index 0000000000..9a6aff14a9 --- /dev/null +++ b/ui/playwright/tests/nav.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from "../fixtures/test"; +import { loadPage } from "../helpers/page"; +import { gotoView, gotoCreate } from "../helpers/nav"; + +// Exercises nav.ts: dropdown-based navigation between listing pages, asserting +// each destination's

    . Covers real client-side routing (not just page loads). +test("navigates between sections via the View menu", async ({ page }) => { + await loadPage(page, "/", { heading: "Agents" }); + + await gotoView(page, "Models", "**/models"); + await expect(page.getByRole("heading", { level: 1, name: "Models" })).toBeVisible(); + + await gotoView(page, "MCP & tools", "**/mcp"); + await expect(page.getByRole("heading", { level: 1, name: "MCP & tools" })).toBeVisible(); +}); + +test("navigates to create pages via the Create menu", async ({ page }) => { + await loadPage(page, "/", { heading: "Agents" }); + + await gotoCreate(page, "New Agent", "**/agents/new"); + await expect(page.getByRole("heading", { level: 1, name: "New Agent", exact: true })).toBeVisible(); + + await gotoCreate(page, "New Agent Harness", "**/agents/new-harness"); + await expect(page.getByRole("heading", { level: 1, name: "New Agent Harness" })).toBeVisible(); + + await gotoCreate(page, "New Model", "**/models/new"); + await expect(page.getByRole("heading", { level: 1, name: "New Model" })).toBeVisible(); + + await gotoCreate(page, "New MCP Server", "**/mcp/new"); + await expect(page.getByRole("heading", { level: 1, name: "New MCP server" })).toBeVisible(); + + await gotoCreate(page, "New prompt library", "**/prompts/new"); + await expect(page.getByRole("heading", { level: 1, name: "New Prompt Library" })).toBeVisible(); +}); diff --git a/ui/src/components/LoadingState.tsx b/ui/src/components/LoadingState.tsx index 2f60abdfd4..7bad1785e0 100644 --- a/ui/src/components/LoadingState.tsx +++ b/ui/src/components/LoadingState.tsx @@ -6,7 +6,10 @@ export function LoadingState() { return (
    -
    +
    From 150a62d117357d1aa2abeb279f5fcf0e22642fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Thu, 9 Jul 2026 16:58:22 -0400 Subject: [PATCH 13/23] test(ui): Playwright create-agent declarative flow (stage 2.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First Stage 2 feature spec — the declarative create-agent flow: - happy path: fill name/description, pick the model, submit -> asserts the redirect to /agents and the captured POST /api/agents payload - validation: empty required fields block submit (no request sent) - failure: forced create error surfaces an error toast, no redirect Groundwork introduced here (demand-driven, reused by later sub-stages): - stub captures mutation (POST/PUT/DELETE) bodies and exposes them via GET /__mock/requests; supports per-mutation error overrides; reset clears both - control.ts: setMutation, agentsCreateError, capturedRequests, lastRequest - helpers/select.ts: Radix (used by the agent model/type pickers, model form, +// etc.). Options are portaled and only exist once the trigger is opened. + +import { expect, type Locator, type Page } from "@playwright/test"; + +/** + * Pick an option from a Radix Select. + * @param trigger the SelectTrigger — a Locator or a selector string (e.g. "#agent-field-model"). + * @param optionName the visible option text (exact or RegExp). + */ +export async function selectOption( + page: Page, + trigger: Locator | string, + optionName: string | RegExp, +): Promise { + const triggerLocator = typeof trigger === "string" ? page.locator(trigger) : trigger; + await expect(triggerLocator).toBeEnabled(); + await triggerLocator.click(); + await page.getByRole("option", { name: optionName }).click(); +} diff --git a/ui/playwright/mocks/control.ts b/ui/playwright/mocks/control.ts index 19368c525b..d3300bfb8d 100644 --- a/ui/playwright/mocks/control.ts +++ b/ui/playwright/mocks/control.ts @@ -5,6 +5,10 @@ // Used via the `mock` fixture in playwright/fixtures/test.ts. Set scenarios // BEFORE page.goto — fetch is cache:"no-store" and each test gets a fresh // context, so a fresh navigation re-fetches and picks up the override. +// +// Mutations (POST/PUT/DELETE) run server-side too, so page.route can't see them: +// the stub captures their bodies and exposes them via capturedRequests()/ +// lastRequest() for payload assertions. import type { APIRequestContext } from "@playwright/test"; import { ok } from "./data"; @@ -21,20 +25,38 @@ type EndpointSlug = | "tools" | "substrate"; +type MutationMethod = "POST" | "PUT" | "DELETE"; + interface ScenarioOptions { status?: number; body?: unknown; } +export interface CapturedRequest { + method: string; + path: string; + body: T; +} + export interface MockBackend { - /** Clear all scenario overrides (back to the happy path). */ + /** Clear all scenario overrides and captured requests (back to the happy path). */ reset(): Promise; - /** Low-level override for any endpoint. */ + + /** Low-level GET override for an endpoint. */ setScenario(endpoint: EndpointSlug, opts: ScenarioOptions): Promise; + /** Low-level mutation override, keyed by method + path (e.g. "POST", "/api/agents"). */ + setMutation(method: MutationMethod, path: string, opts: ScenarioOptions): Promise; + + /** All captured mutation requests, in order. */ + capturedRequests(): Promise; + /** The most recent captured mutation matching method + path substring, or null. */ + lastRequest(method: MutationMethod, pathIncludes: string): Promise | null>; setAgents(agents: AgentResponse[]): Promise; noAgents(): Promise; agentsError(status?: number): Promise; + /** Force POST /api/agents (create) to fail. */ + agentsCreateError(status?: number): Promise; noModelConfigs(): Promise; modelConfigsError(status?: number): Promise; @@ -50,17 +72,39 @@ export function makeMock(request: APIRequestContext): MockBackend { }); }; - const errorBody = (endpoint: EndpointSlug) => ({ error: `stubbed ${endpoint} error` }); + const setMutation: MockBackend["setMutation"] = async (method, path, opts) => { + await request.post(`${STUB_URL}/__mock/scenario`, { + data: { method, path, status: opts.status, body: opts.body }, + }); + }; + + const capturedRequests: MockBackend["capturedRequests"] = async () => { + const res = await request.get(`${STUB_URL}/__mock/requests`); + const json = (await res.json()) as { data: CapturedRequest[] }; + return json.data; + }; + + const errorBody = (label: string) => ({ error: `stubbed ${label} error` }); return { reset: async () => { await request.post(`${STUB_URL}/__mock/reset`); }, setScenario, + setMutation, + capturedRequests, + + lastRequest: async (method: MutationMethod, pathIncludes: string) => { + const all = await capturedRequests(); + const matches = all.filter((r) => r.method === method && r.path.includes(pathIncludes)); + return (matches[matches.length - 1] as CapturedRequest | undefined) ?? null; + }, setAgents: (agents) => setScenario("agents", { body: ok(agents) }), noAgents: () => setScenario("agents", { body: ok([]) }), agentsError: (status = 500) => setScenario("agents", { status, body: errorBody("agents") }), + agentsCreateError: (status = 500) => + setMutation("POST", "/api/agents", { status, body: errorBody("create-agent") }), noModelConfigs: () => setScenario("modelconfigs", { body: ok([]) }), modelConfigsError: (status = 500) => diff --git a/ui/playwright/mocks/server.mjs b/ui/playwright/mocks/server.mjs index b9445d200c..4c9c363dd0 100644 --- a/ui/playwright/mocks/server.mjs +++ b/ui/playwright/mocks/server.mjs @@ -95,9 +95,16 @@ const PATH_TO_SLUG = { // region Scenario state -// slug -> { status, body } override set by POST /__mock/scenario. Empty = happy path. +// Overrides set by POST /__mock/scenario. Empty = happy path. +// - GET reads keyed by endpoint slug ("agents", "models", …) +// - mutations keyed by " " (e.g. "POST /api/agents") let overrides = {}; +// Captured mutation requests (POST/PUT/DELETE) so specs can assert payloads — +// the app fetches server-side, so page.route can't see them. Read via +// GET /__mock/requests; cleared by /__mock/reset. +let requests = []; + // endregion // region Server @@ -138,19 +145,32 @@ const server = createServer(async (req, res) => { // Control + health endpoints. if (pathname === "/__mock/health") return json(res, 200, { status: "ok" }); + if (method === "GET" && pathname === "/__mock/requests") { + return json(res, 200, { data: requests }); + } + if (method === "POST" && pathname === "/__mock/reset") { overrides = {}; + requests = []; return json(res, 200, { status: "reset" }); } if (method === "POST" && pathname === "/__mock/scenario") { const body = await readJsonBody(req); - if (!body || typeof body.endpoint !== "string") { - return json(res, 400, { error: "scenario requires { endpoint: string }" }); + // Mutation override: { method, path, status?, body? } keyed by " ". + if (body && typeof body.method === "string" && typeof body.path === "string") { + const key = `${body.method} ${body.path}`; + overrides[key] = { status: body.status ?? 200, body: body.body }; + console.log(`[stub] scenario set: ${key} -> ${body.status ?? 200}`); + return json(res, 200, { status: "scenario-set", key }); + } + // GET override: { endpoint, status?, body? } keyed by endpoint slug. + if (body && typeof body.endpoint === "string") { + overrides[body.endpoint] = { status: body.status ?? 200, body: body.body }; + console.log(`[stub] scenario set: ${body.endpoint} -> ${body.status ?? 200}`); + return json(res, 200, { status: "scenario-set", endpoint: body.endpoint }); } - overrides[body.endpoint] = { status: body.status ?? 200, body: body.body }; - console.log(`[stub] scenario set: ${body.endpoint} -> ${body.status ?? 200}`); - return json(res, 200, { status: "scenario-set", endpoint: body.endpoint }); + return json(res, 400, { error: "scenario requires { endpoint } or { method, path }" }); } if (method === "GET") { @@ -166,6 +186,21 @@ const server = createServer(async (req, res) => { } } + // Mutations to /api/*: capture the body (for payload assertions) and respond. + // Default is 200 echoing the sent body in the success envelope; an override can + // force an error (e.g. 500) for failure-path tests. + if ((method === "POST" || method === "PUT" || method === "DELETE") && pathname.startsWith("/api/")) { + const body = await readJsonBody(req); + requests.push({ method, path: url, body }); + const override = overrides[`${method} ${pathname}`]; + if (override) { + console.log(`[stub] ${method} ${url} -> ${override.status} (override)`); + return json(res, override.status, override.body ?? {}); + } + console.log(`[stub] ${method} ${url} -> 200 (captured)`); + return json(res, 200, ok(body)); + } + // Anything unmocked is a real gap — make it loud so we notice leaks. console.warn(`[stub] UNHANDLED ${method} ${url} -> 404`); return json(res, 404, { error: `No stub for ${method} ${pathname}` }); diff --git a/ui/playwright/tests/agents/create-agent.spec.ts b/ui/playwright/tests/agents/create-agent.spec.ts new file mode 100644 index 0000000000..ff0eebc0f7 --- /dev/null +++ b/ui/playwright/tests/agents/create-agent.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from "../../fixtures/test"; +import { loadPage } from "../../helpers/page"; +import { selectOption } from "../../helpers/select"; + +// Sub-stage 2.1 — the declarative create-agent flow (single-page form). +// The model dropdown reads /api/modelconfigs (seeded), and submit does a +// server-side POST /api/agents, which the stub captures for payload assertions. + +// Model option label = `${spec.model} (${ref})` for the seeded modelconfig. +const MODEL_OPTION = "gpt-4o (default/default-model-config)"; +const AGENT_NAME = "e2e-new-agent"; +const AGENT_DESC = "Created by the Playwright e2e suite"; + +test.describe("create agent (declarative)", () => { + test("creates an agent and POSTs the expected payload", async ({ page, mock }) => { + await loadPage(page, "/agents/new", { heading: "New Agent" }); + + await page.getByLabel("Agent name").fill(AGENT_NAME); + await page.getByLabel("Description").fill(AGENT_DESC); + // System prompt is pre-filled with the default; namespace/type default to + // "default"/"Declarative" — only the model must be picked. + await selectOption(page, "#agent-field-model", MODEL_OPTION); + + await page.getByRole("button", { name: "Create Agent" }).click(); + + // Success = redirect to the agents list (no toast). + await expect(page).toHaveURL(/\/agents(\?|$)/); + await expect(page.getByRole("heading", { level: 1, name: "Agents" })).toBeVisible(); + + // Assert the payload the stub captured server-side. + const req = await mock.lastRequest<{ + metadata: { name: string; namespace: string }; + spec: { type: string; description: string; declarative: { modelConfig: string } }; + }>("POST", "/api/agents"); + expect(req, "expected a captured POST /api/agents").not.toBeNull(); + expect(req!.body.metadata.name).toBe(AGENT_NAME); + expect(req!.body.spec.type).toBe("Declarative"); + expect(req!.body.spec.description).toBe(AGENT_DESC); + // modelConfig is the name only — the "default/" namespace is stripped. + expect(req!.body.spec.declarative.modelConfig).toBe("default-model-config"); + }); + + test("blocks submit and shows a validation error when required fields are empty", async ({ page, mock }) => { + await loadPage(page, "/agents/new", { heading: "New Agent" }); + + await page.getByRole("button", { name: "Create Agent" }).click(); + + // Client-side validation blocks the submit: required-field errors render and + // the model field is flagged invalid — no navigation, no request sent. + await expect(page.getByText("Description is required")).toBeVisible(); + await expect(page.getByText("Please select a model")).toBeVisible(); + await expect(page).toHaveURL(/\/agents\/new/); + expect(await mock.capturedRequests()).toHaveLength(0); + }); + + test("shows an error toast when the create request fails", async ({ page, mock }) => { + await mock.agentsCreateError(); + await loadPage(page, "/agents/new", { heading: "New Agent" }); + + await page.getByLabel("Agent name").fill("e2e-fail-agent"); + await page.getByLabel("Description").fill("will fail"); + await selectOption(page, "#agent-field-model", MODEL_OPTION); + await page.getByRole("button", { name: "Create Agent" }).click(); + + await expect(page.locator('[data-sonner-toast][data-type="error"]')).toBeVisible(); + await expect(page).toHaveURL(/\/agents\/new/); + }); + + // Exercises the remaining form sections (Tools, Long-term memory, Context, + // Skills) — none of which the minimal path or Storybook/Chromatic covers — and + // asserts each lands in the submitted CR. + test("creates a fully-configured agent (tools, memory, context, skills)", async ({ page, mock }) => { + await loadPage(page, "/agents/new", { heading: "New Agent" }); + + await page.getByLabel("Agent name").fill("e2e-full-agent"); + await page.getByLabel("Description").fill("Fully configured e2e agent"); + await selectOption(page, "#agent-field-model", MODEL_OPTION); + + // Tools — open the dialog, pick the seeded tool, confirm. + await page.getByRole("button", { name: "Add Tools & Agents" }).click(); + await page.getByTestId("tool-row-e2e-tool-server-e2e-tool").click(); + await page.getByRole("button", { name: /Save Selection/ }).click(); + + // Long-term memory — embedding model + TTL (memory only emits when a model is set). + await selectOption(page, "#agent-field-memory-model", MODEL_OPTION); + await page.locator("#agent-field-memory-ttl").fill("30"); + + // Context — enable event compaction. + await page.getByTestId("context-compaction-switch").click(); + + // Skills — one OCI image ref. + await page.locator("#agent-oci-skill-0").fill("ghcr.io/example/python-skill:v1.0.0"); + + await page.getByRole("button", { name: "Create Agent" }).click(); + await expect(page).toHaveURL(/\/agents(\?|$)/); + + const req = await mock.lastRequest<{ + spec: { + declarative: { + tools: Array<{ type: string; mcpServer?: { toolNames: string[] } }>; + memory?: { modelConfig: string; ttlDays?: number }; + context?: { compaction?: unknown }; + }; + skills?: { refs?: string[] }; + }; + }>("POST", "/api/agents"); + expect(req, "expected a captured POST /api/agents").not.toBeNull(); + const spec = req!.body.spec; + + // Tools → spec.declarative.tools[] + expect(spec.declarative.tools?.[0]?.type).toBe("McpServer"); + expect(spec.declarative.tools[0].mcpServer?.toolNames).toContain("e2e-tool"); + // Memory → spec.declarative.memory + expect(spec.declarative.memory?.modelConfig).toBe("default-model-config"); + expect(spec.declarative.memory?.ttlDays).toBe(30); + // Context → spec.declarative.context + expect(spec.declarative.context?.compaction).toBeTruthy(); + // Skills → spec.skills (top-level) + expect(spec.skills?.refs).toContain("ghcr.io/example/python-skill:v1.0.0"); + }); +}); diff --git a/ui/src/components/create/ContextSection.tsx b/ui/src/components/create/ContextSection.tsx index 722f96965a..4dc0622024 100644 --- a/ui/src/components/create/ContextSection.tsx +++ b/ui/src/components/create/ContextSection.tsx @@ -39,6 +39,7 @@ export function ContextSection({

    = ({ return (
    !isDisabled && From 7e6aab9e1cd7ded68c81640130caadeacca8fb33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Logro=C3=B1o=20Guerrero?= Date: Thu, 9 Jul 2026 18:13:25 -0400 Subject: [PATCH 14/23] test(ui): Playwright chat/session flow (stage 2.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second Stage 2 feature (commit 2/8) — the chat streaming flow: - happy path: send a message, mock the A2A SSE reply -> asserts the user message, the streamed agent text, and a rendered tool-call block - empty state: a new chat shows "Start a conversation" - failure: an aborted stream surfaces an error toast, no agent reply Groundwork introduced here: - helpers/a2a.ts: mockAgentReply / mockAgentStreamError — page.route mock of the browser-side A2A SSE call (text/event-stream JSON-RPC frames) - stub: dynamic GET routes (agent detail, sessions-for-agent) + POST /api/sessions returning a session with an id (needed for the streamed contextId) - data-testid hooks: chat-input, chat-send (ChatInterface), tool-call (ToolDisplay) Co-Authored-By: Claude Opus 4.8 Signed-off-by: Carlos Logroño Guerrero --- ui/playwright/README.md | 6 +- ui/playwright/helpers/a2a.ts | 95 +++++++++++++++++++ ui/playwright/mocks/server.mjs | 58 +++++++++++ ui/playwright/tests/chat/chat-session.spec.ts | 91 ++++++++++++++++++ ui/src/components/ToolDisplay.tsx | 2 +- ui/src/components/chat/ChatInterface.tsx | 3 +- 6 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 ui/playwright/helpers/a2a.ts create mode 100644 ui/playwright/tests/chat/chat-session.spec.ts diff --git a/ui/playwright/README.md b/ui/playwright/README.md index 5011a0ef91..4edd7ef3b7 100644 --- a/ui/playwright/README.md +++ b/ui/playwright/README.md @@ -85,12 +85,12 @@ BACKEND_INTERNAL_URL=http://127.0.0.1:8899/api npm run dev (e.g. `mock.noAgents()`, `mock.agentsError()`), verified by the home + nav specs. Runs serially (`workers: 1`) against the shared stub; raising the worker count later needs per-worker servers or stateless request-keyed scenarios. -- **Stage 2:** feature flows (gap-scoped), split into 8 sub-stages — one feature per - branch/PR (linear stack), so each is reviewable in isolation with +- **Stage 2:** feature flows (gap-scoped), split into 8 sub-stages on one branch — **one + commit per feature**, so each is reviewable in isolation with `npm run test:pw -- playwright/tests/`. Shared infra (POST-capture, A2A SSE mock, `forms`/`select`/`dialog` helpers) is demand-driven — introduced by the first sub-stage that needs it. Ordered by importance: - - [ ] 2.1 Create Agent (declarative) — `tests/agents/create-agent.spec.ts` + - [x] 2.1 Create Agent (declarative) — `tests/agents/create-agent.spec.ts` - [ ] 2.2 Chat / session (A2A SSE mock) — `tests/chat/chat-session.spec.ts` - [ ] 2.3 Models / providers — `tests/models/models.spec.ts` - [ ] 2.4 MCP servers & tools — `tests/mcp/mcp-server.spec.ts` diff --git a/ui/playwright/helpers/a2a.ts b/ui/playwright/helpers/a2a.ts new file mode 100644 index 0000000000..754f11b6c3 --- /dev/null +++ b/ui/playwright/helpers/a2a.ts @@ -0,0 +1,95 @@ +// A2A chat streaming mock. Unlike the /api data calls (server-side → stub), the +// chat SSE call `POST /a2a//` is a real browser fetch, so page.route +// CAN intercept it. We fulfill it with a text/event-stream body of JSON-RPC +// frames (see a2aClient.ts processSSEStream): frames are "\n\n"-delimited, each +// line prefixed "data: ", each payload unwrapped as `.result`. + +import { type Page } from "@playwright/test"; + +// contextId must match the session id the stub's POST /api/sessions returns. +const CONTEXT_ID = "e2e-session"; +const TASK_ID = "e2e-task"; + +interface ToolCall { + name: string; + args?: Record; + result?: string; +} + +function frame(event: unknown): string { + return `data: ${JSON.stringify({ jsonrpc: "2.0", id: "1", result: event })}\n\n`; +} + +function statusUpdate(status: unknown, final = false): unknown { + return { kind: "status-update", taskId: TASK_ID, contextId: CONTEXT_ID, final, status }; +} + +function agentMessage(parts: unknown[], messageId?: number): unknown { + // A numeric messageId on the terminal text message makes the feedback (thumbs) + // buttons render on the agent reply. + return { kind: "message", role: "agent", parts, contextId: CONTEXT_ID, taskId: TASK_ID, messageId, metadata: {} }; +} + +/** Build the SSE body: optional tool call (request + response), then a final agent text. */ +function buildStream({ text, tool }: { text: string; tool?: ToolCall }): string { + const frames: string[] = []; + if (tool) { + frames.push( + frame( + statusUpdate({ + state: "working", + message: agentMessage([ + { + kind: "data", + data: { id: "call-1", name: tool.name, args: tool.args ?? {} }, + metadata: { adk_type: "function_call" }, + }, + ]), + }), + ), + ); + frames.push( + frame( + statusUpdate({ + state: "working", + message: agentMessage([ + { + kind: "data", + data: { id: "call-1", name: tool.name, response: { result: tool.result ?? "ok", isError: false } }, + metadata: { adk_type: "function_response" }, + }, + ]), + }), + ), + ); + } + // Terminal frame settles the turn and carries the agent's text reply. + frames.push( + frame( + statusUpdate( + { state: "completed", message: agentMessage([{ kind: "text", text }], 1) }, + true, + ), + ), + ); + return frames.join("") + "data: [DONE]\n\n"; +} + +/** Intercept the chat SSE call and reply with an agent message (and optional tool call). */ +export async function mockAgentReply( + page: Page, + opts: { text: string; tool?: ToolCall }, +): Promise { + const body = buildStream(opts); + const handler = (route: import("@playwright/test").Route) => + route.fulfill({ status: 200, contentType: "text/event-stream", body }); + await page.route("**/a2a/**", handler); + await page.route("**/a2a-sandboxes/**", handler); +} + +/** Intercept the chat SSE call and fail it (network error), for the failure path. */ +export async function mockAgentStreamError(page: Page): Promise { + const handler = (route: import("@playwright/test").Route) => route.abort("failed"); + await page.route("**/a2a/**", handler); + await page.route("**/a2a-sandboxes/**", handler); +} diff --git a/ui/playwright/mocks/server.mjs b/ui/playwright/mocks/server.mjs index 4c9c363dd0..6045380452 100644 --- a/ui/playwright/mocks/server.mjs +++ b/ui/playwright/mocks/server.mjs @@ -69,6 +69,32 @@ const substrateStatus = { workers: [], }; +// Returned by POST /api/sessions (createSession). The fixed id is what the chat +// UI uses for the new session and the streamed A2A contextId. +const session = { + id: "e2e-session", + name: "e2e chat", + agent_id: "default/e2e-agent", + user_id: "admin@kagent.dev", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + deleted_at: "", +}; + +// Prior conversation returned by GET /api/sessions//tasks (existing-chat load). +// Agent messages need metadata.displaySource:"assistant" to render; state must not +// be submitted/working (that would trigger stream resubscribe). +const task = { + id: "e2e-task", + contextId: "e2e-session", + kind: "task", + status: { state: "completed" }, + history: [ + { kind: "message", messageId: "t1-u", role: "user", parts: [{ kind: "text", text: "Prior question" }], metadata: { timestamp: 1 } }, + { kind: "message", messageId: "t1-a", role: "agent", parts: [{ kind: "text", text: "Prior answer" }], metadata: { displaySource: "assistant", timestamp: 2 } }, + ], +}; + // Default happy-path body per endpoint slug. const DEFAULTS = { agents: () => ok([agent], "Successfully fetched agents"), @@ -184,6 +210,32 @@ const server = createServer(async (req, res) => { console.log(`[stub] ${method} ${url} -> 200`); return json(res, 200, DEFAULTS[slug]()); } + + // Dynamic GET routes (parameterized paths not in PATH_TO_SLUG). + if (/^\/api\/agents\/[^/]+\/[^/]+$/.test(pathname)) { + console.log(`[stub] ${method} ${url} -> 200 (agent detail)`); + return json(res, 200, ok(agent)); + } + if (/^\/api\/sessions\/agent\/[^/]+\/[^/]+$/.test(pathname)) { + console.log(`[stub] ${method} ${url} -> 200 (sessions for agent)`); + return json(res, 200, ok([])); + } + // Session tasks (existing-chat history): /api/sessions//tasks. + if (/^\/api\/sessions\/[^/]+\/tasks$/.test(pathname)) { + console.log(`[stub] ${method} ${url} -> 200 (session tasks)`); + return json(res, 200, ok([task])); + } + // Single session (checkSessionExists): truthy for the seeded id, else 404 so + // the "Session not found" screen is reachable. + const sessionDetail = pathname.match(/^\/api\/sessions\/([^/]+)$/); + if (sessionDetail) { + if (sessionDetail[1] === "e2e-session") { + console.log(`[stub] ${method} ${url} -> 200 (session detail)`); + return json(res, 200, ok({ session })); + } + console.log(`[stub] ${method} ${url} -> 404 (session not found)`); + return json(res, 404, { error: "Session not found" }); + } } // Mutations to /api/*: capture the body (for payload assertions) and respond. @@ -197,6 +249,12 @@ const server = createServer(async (req, res) => { console.log(`[stub] ${method} ${url} -> ${override.status} (override)`); return json(res, override.status, override.body ?? {}); } + // createSession must return a session WITH an id (the UI uses it for the new + // chat's id + streamed contextId); the generic echo below wouldn't have one. + if (method === "POST" && pathname === "/api/sessions") { + console.log(`[stub] ${method} ${url} -> 200 (session created)`); + return json(res, 200, ok(session)); + } console.log(`[stub] ${method} ${url} -> 200 (captured)`); return json(res, 200, ok(body)); } diff --git a/ui/playwright/tests/chat/chat-session.spec.ts b/ui/playwright/tests/chat/chat-session.spec.ts new file mode 100644 index 0000000000..161e8d43d7 --- /dev/null +++ b/ui/playwright/tests/chat/chat-session.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from "../../fixtures/test"; +import { waitForAppReady, expectToast } from "../../helpers/page"; +import { mockAgentReply, mockAgentStreamError } from "../../helpers/a2a"; + +// Sub-stage 2.2 — the chat/session flow. Data calls (agent resolve, sessions, +// createSession) are server-side (stub); the A2A stream is a browser fetch +// mocked via page.route. A brand-new chat is the cleanest path (no session GETs +// on load); createSession fires on send and the stub returns a session with id +// "e2e-session", which the mocked SSE frames use as their contextId. + +const CHAT_URL = "/agents/default/e2e-agent/chat"; +const USER_MESSAGE = "List the pods please"; +const AGENT_REPLY = "Hello from the agent"; +const TOOL_NAME = "get_pods"; + +test.describe("chat / session", () => { + test("sends a message and renders the agent reply + tool call", async ({ page }) => { + await mockAgentReply(page, { + text: AGENT_REPLY, + tool: { name: TOOL_NAME, args: { ns: "default" }, result: "pod-a Running" }, + }); + + await page.goto(CHAT_URL); + await waitForAppReady(page); + const input = page.getByTestId("chat-input"); + await expect(input).toBeEnabled(); + + await input.fill(USER_MESSAGE); + await page.getByTestId("chat-send").click(); + + // User's own message, the agent's streamed reply, and the tool-call block. + await expect(page.getByText(USER_MESSAGE)).toBeVisible(); + await expect(page.getByText(AGENT_REPLY)).toBeVisible(); + await expect(page.getByTestId("tool-call")).toContainText(TOOL_NAME); + }); + + test("shows the empty state before any message", async ({ page }) => { + await page.goto(CHAT_URL); + await waitForAppReady(page); + await expect(page.getByRole("heading", { name: "Start a conversation" })).toBeVisible(); + await expect(page.getByTestId("chat-input")).toBeVisible(); + }); + + test("surfaces an error when the stream fails", async ({ page }) => { + await mockAgentStreamError(page); + + await page.goto(CHAT_URL); + await waitForAppReady(page); + const input = page.getByTestId("chat-input"); + await expect(input).toBeEnabled(); + + await input.fill(USER_MESSAGE); + await page.getByTestId("chat-send").click(); + + await expect(page.locator('[data-sonner-toast][data-type="error"]')).toBeVisible(); + // No agent reply rendered. + await expect(page.getByText(AGENT_REPLY)).toHaveCount(0); + }); + + test("loads an existing session and renders prior messages", async ({ page }) => { + await page.goto("/agents/default/e2e-agent/chat/e2e-session"); + await waitForAppReady(page); + await expect(page.getByText("Prior question")).toBeVisible(); + await expect(page.getByText("Prior answer")).toBeVisible(); + }); + + test("shows session-not-found for a missing session", async ({ page }) => { + await page.goto("/agents/default/e2e-agent/chat/missing"); + await expect(page.getByRole("heading", { name: "Session not found" })).toBeVisible(); + }); + + test("submits feedback on an agent reply", async ({ page, mock }) => { + await mockAgentReply(page, { text: AGENT_REPLY }); + await page.goto(CHAT_URL); + await waitForAppReady(page); + const input = page.getByTestId("chat-input"); + await expect(input).toBeEnabled(); + await input.fill(USER_MESSAGE); + await page.getByTestId("chat-send").click(); + await expect(page.getByText(AGENT_REPLY)).toBeVisible(); + + await page.getByRole("button", { name: "Thumbs up" }).click(); + const dialog = page.getByRole("dialog"); + await dialog.getByRole("textbox").fill("Great answer"); + await dialog.getByRole("button", { name: /submit/i }).click(); + + await expectToast(page, /thank you/i, { type: "success" }); + expect(await mock.lastRequest("POST", "/api/feedback"), "expected POST /api/feedback").not.toBeNull(); + }); + +}); diff --git a/ui/src/components/ToolDisplay.tsx b/ui/src/components/ToolDisplay.tsx index 5964abad8b..86fadd795f 100644 --- a/ui/src/components/ToolDisplay.tsx +++ b/ui/src/components/ToolDisplay.tsx @@ -166,7 +166,7 @@ const ToolDisplay = ({ call, result, status = "requested", isError = false, isDe : ''; return ( - +
    diff --git a/ui/src/components/chat/ChatInterface.tsx b/ui/src/components/chat/ChatInterface.tsx index 0402512f98..ed7aff6630 100644 --- a/ui/src/components/chat/ChatInterface.tsx +++ b/ui/src/components/chat/ChatInterface.tsx @@ -1163,6 +1163,7 @@ export default function ChatInterface({ selectedAgentName, selectedNamespace, se